• UEC++ day7


    敌人NPC机制

    敌人机制分析与需求

    • 新建一个character类来作为敌人,直接建蓝图设置骨骼网格,因为敌人可能多种就不规定死,然后这个敌人肯定需要两个触发器,一个用于大范围巡逻,一个用于是否达到主角近点进行攻击
      在这里插入图片描述
    • 注意我们要避免摄像机被敌人阻挡
    • BaseEnemy.h
    // Fill out your copyright notice in the Description page of Project Settings.
    
    #pragma once
    
    #include "CoreMinimal.h"
    #include "GameFramework/Character.h"
    #include "BaseEnemy.generated.h"
    
    UCLASS()
    class UEGAME_API ABaseEnemy : public ACharacter
    {
    	GENERATED_BODY()
    
    public:
    	// Sets default values for this character's properties
    	ABaseEnemy();
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI")
    	class USphereComponent* ChaseVolume;
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI")
    	USphereComponent* AttackVolume;
    protected:
    	// Called when the game starts or when spawned
    	virtual void BeginPlay() override;
    
    public:	
    	// Called every frame
    	virtual void Tick(float DeltaTime) override;
    
    	// Called to bind functionality to input
    	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
    
    	UFUNCTION()
    	virtual void OnChaseVolumeOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
    	UFUNCTION()
    	virtual void OnChaseVolumeOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
    	UFUNCTION()
    	virtual void OnAttackVolumeOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
    	UFUNCTION()
    	virtual void OnAttackVolumeOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • BaseEnemy.cpp
    // Fill out your copyright notice in the Description page of Project Settings.
    
    
    #include "BaseEnemy.h"
    #include "Components/SphereComponent.h"
    #include "Components/SkeletalMeshComponent.h"
    #include "Components/CapsuleComponent.h"
    // Sets default values
    ABaseEnemy::ABaseEnemy()
    {
     	// Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    	PrimaryActorTick.bCanEverTick = true;
    	
    	//避免摄像机被敌人给阻挡
    	GetMesh()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
    	GetCapsuleComponent()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
    
    	ChaseVolume = CreateDefaultSubobject<USphereComponent>(TEXT("ChaseVolume"));
    	ChaseVolume->SetupAttachment(GetRootComponent());
    	ChaseVolume->InitSphereRadius(800.f);
    	ChaseVolume->SetCollisionObjectType(ECollisionChannel::ECC_WorldDynamic);
    	ChaseVolume->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
    	ChaseVolume->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Overlap);
    
    	AttackVolume = CreateDefaultSubobject<USphereComponent>(TEXT("AttackVolume"));
    	AttackVolume->SetupAttachment(GetRootComponent());
    	AttackVolume->InitSphereRadius(100.f);
    	AttackVolume->SetCollisionObjectType(ECollisionChannel::ECC_WorldDynamic);
    	AttackVolume->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
    	AttackVolume->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Overlap);
    }
    
    // Called when the game starts or when spawned
    void ABaseEnemy::BeginPlay()
    {
    	Super::BeginPlay();
    
    	ChaseVolume->OnComponentBeginOverlap.AddDynamic(this, &ABaseEnemy::OnChaseVolumeOverlapBegin);
    	ChaseVolume->OnComponentEndOverlap.AddDynamic(this, &ABaseEnemy::OnChaseVolumeOverlapEnd);
    
    	AttackVolume->OnComponentBeginOverlap.AddDynamic(this, &ABaseEnemy::OnAttackVolumeOverlapBegin);
    	AttackVolume->OnComponentEndOverlap.AddDynamic(this, &ABaseEnemy::OnAttackVolumeOverlapEnd);
    	
    }
    
    // Called every frame
    void ABaseEnemy::Tick(float DeltaTime)
    {
    	Super::Tick(DeltaTime);
    
    }
    
    // Called to bind functionality to input
    void ABaseEnemy::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
    {
    	Super::SetupPlayerInputComponent(PlayerInputComponent);
    
    }
    
    void ABaseEnemy::OnChaseVolumeOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
    {
    }
    
    void ABaseEnemy::OnChaseVolumeOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
    {
    }
    
    void ABaseEnemy::OnAttackVolumeOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
    {
    }
    
    void ABaseEnemy::OnAttackVolumeOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
    {
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 注意虚幻的visibility与camera默认是block
      在这里插入图片描述

    部署导航网格

    ++ Nav Mesh Bounds Volume:导航网格
    在这里插入图片描述

    • Nav Modifier Volume:修改导航网格

    添加AI模块与创建敌人移动状态枚举

    • 有些组件的使用是需要添加依赖项的就像之前的UMG需要添加到自己的工程目录下的工程名.Build.cs里面,调用AI的模块就需要添加AIModule
      在这里插入图片描述
    • 添加敌人移动状态枚举变量与接近主角的函数
    UENUM(BlueprintType)
    enum class EEnemyMovementStatus :uint8
    {
    	EEMS_Idle			UMETA(DisplayName="Idle"),
    	EEMS_MoveToTarget	UMETA(DisPlayName="MoveToTarget"),
    	EEMS_Attacking		UMETA(DisPlayName="Attacking"),
    	EEMS_Dead			UMETA(DisPlayName="Dead")
    };
    
    //--------------------------------------------------------------
    	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Enemy Stats")
    	EEnemyMovementStatus EnemyMovementStatus;
    //--------------------------------------------------------------
    	void MoveToTarget(class AMainPlayer* Player);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    获取AIController并持有敌人

    • AAIcontroller* AIController所需头文件:#include "AIController.h"
    • 声明AAIController类的指针
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI")
    class AAIController* AIController;
    
    • 1
    • 2
    • 设置持有属性
    // Sets default values
    ABaseEnemy::ABaseEnemy()
    {
     	// Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    	PrimaryActorTick.bCanEverTick = true;
    //-----------------省略----------------------------------------------
    	//设置持有属性
    	AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
    	//初始化默认移动状态
    	EnemyMovementStatus = EEnemyMovementStatus::EEMS_Idle;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 获取到Controller属性
    // Called when the game starts or when spawned
    void ABaseEnemy::BeginPlay()
    {
    	Super::BeginPlay();
    //-----------------省略----------------------------------------------	
    	//拿到Controller
    	AIController = Cast<AAIController>(GetController());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    调用MoveTo去追逐Player

    • 逻辑(一):首先在追逐事件中去判断是不是Player如果是就调用追逐函数MoveToTarget
    void ABaseEnemy::OnChaseVolumeOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
    {
    	if (OtherActor)
    	{
    		AMainPlayer* Player = Cast<AMainPlayer>(OtherActor);
    		if (Player)
    		{
    			MoveToTarget(Player);
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 逻辑(二):MoveToTarget函数中,首先将敌人的枚举移动状态变换为EEMS_MoveToTarget,然后判断AIController是否获取成功,成功就去调用MoveTo去追逐主角
    • FAIMoveRequest 是UE中的一个结构体,主要用于指定人工智能(AI)代理的移动请求。其内部包含了一系列参数,如目标位置、到达速度、抵达距离、是否允许短路等等,这些参数都可以用来控制 AI 代理的移动行为。
      当调用 AAIController::MoveTo() 或者 AAIController::SimpleMoveTo() 等函数时,会创建并返回一个 FAIMoveRequest 结构体实例。然后你可以设置它的参数,并将它传递给 AAIController::UpdateMoveStatus() 函数,从而让 AI 代理按照你的要求进行移动。
      总的来说,FAIMoveRequest 提供了一种方便的方式来指定和调整 AI 代理的移动请求,使得 AI 的行为更加灵活多样。
      FAIMoveRequest MoveRequest;
      MoveRequest.SetGoalActor(Player);//设置移动请求目标
      MoveRequest.SetAcceptanceRadius(10.f);	//设置移动半径
      
      • 1
      • 2
      • 3
    • 在UE中,FNavPathSharedPtr NavPath 表示一个智能指针,用于存储和管理导航路径。智能指针是一个类模板,可以自动管理所指向对象的生命周期,避免了因忘记释放内存而导致的内存泄漏问题。
      具体来说,FNavPathSharedPtr 是一个 shared_ptr 类型的智能指针,它指向的是一个 FNavPath 对象。FNavPath 是 Unreal Engine 中的一个类,用于表示从起点到终点的一条导航路径。
      因此,在虚幻引擎中,NavPath 可以用来保存和操作导航路径。例如,可以使用它来获取路径的距离、方向等信息,或者更新路径以适应场景的变化。
      FNavPathSharedPtr NavPath;//会返回路径
      
      • 1
    • 调用MoveTo去追逐主角
    void ABaseEnemy::MoveToTarget(AMainPlayer* Player)
    {
    	EnemyMovementStatus = EEnemyMovementStatus::EEMS_MoveToTarget;
    	if (AIController)
    	{
    		FAIMoveRequest MoveRequest;
    		MoveRequest.SetGoalActor(Player);//设置移动请求目标
    		MoveRequest.SetAcceptanceRadius(10.f);	//设置移动半径
    
    		FNavPathSharedPtr NavPath;//会返回路径
    
    		AIController->MoveTo(MoveRequest, &NavPath);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 逻辑(三):当主角出了追逐移动事件就停止移动
    void ABaseEnemy::OnChaseVolumeOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
    {
    	if (OtherActor)
    	{
    		AMainPlayer* Player = Cast<AMainPlayer>(OtherActor);
    		if (Player)
    		{
    			if (AIController)
    			{
    				//停止移动
    				AIController->StopMovement();
    			}
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    创建敌人动画蓝图

    • 基本与创建MainPlayer动画蓝图差不多
    • EnemyAnimInstance,h
    // Fill out your copyright notice in the Description page of Project Settings.
    
    #pragma once
    
    #include "CoreMinimal.h"
    #include "Animation/AnimInstance.h"
    #include "EnemyAnimInstance.generated.h"
    
    /**
     * 
     */
    UCLASS()
    class UEGAME_API UEnemyAnimInstance : public UAnimInstance
    {
    	GENERATED_BODY()
    public:
    	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Animation Properties")
    	float Speed;
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Animation Properties")
    	class ABaseEnemy* Enemy;
    
    	virtual void NativeInitializeAnimation() override;
    
    	UFUNCTION(BlueprintCallable, Category = "Animaion Properties")
    	void UpDataAnimationProperties();
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • EnemyAnimInstance.cpp
    // Fill out your copyright notice in the Description page of Project Settings.
    
    
    #include "EnemyAnimInstance.h"
    #include "Animation/AnimInstance.h"
    #include "Characters/Enemy/BaseEnemy.h"
    
    void UEnemyAnimInstance::NativeInitializeAnimation()
    {
    	Enemy = Cast<ABaseEnemy>(TryGetPawnOwner());
    }
    
    void UEnemyAnimInstance::UpDataAnimationProperties()
    {
    	if (Enemy)
    	{
    		Enemy = Cast<ABaseEnemy>(TryGetPawnOwner());
    	}
    	if (Enemy)
    	{
    		FVector SpeedVector = Enemy->GetVelocity();
    		FVector PlanarSpeed = FVector(SpeedVector.X, SpeedVector.Y, 0.f);
    		Speed = PlanarSpeed.Size();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 创建动画蓝图
      在这里插入图片描述

    敌人行走的混合空间

    • 基本和当时创建Player的混合空间差不多
      在这里插入图片描述
    • 编写动画蓝图
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
    • 然后设置到蓝图上即可
      在这里插入图片描述

    创建蒙太奇以及攻击编码

    • 创建蒙太奇
      在这里插入图片描述
    • 攻击逻辑:老规矩引用一个bool变量用于检测是否在攻击范围,新建Montage引用后续方便调用Montage功能,与两个函数用来攻击和攻击结束的逻辑编写,攻击结束要在蓝图中调用加上反射,在重叠事件中写是否检测到主角进入攻击范围如果是就执行攻击函数,攻击函数中首先关闭移动,然后判断是不是正在攻击(默认肯定没有攻击,所以判断完要设定为正在攻击),然后获取AnimInstance进行片段播放,攻击结束函数逻辑就先把状态变为待机状态,然后判断是否在攻击范围bool变量,如果为真就继续执行攻击函数形成闭环,最后离开了重叠事件范围就先把bool检测攻击变为false,判断攻击状态是否结束,如果结束就继续执行追逐主角函数,可能这个逻辑会导致bug,所以我们把追逐主角的函数MoveToTarget添加反射到时候在蓝图中完善
    • 需要的变量与函数
    	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Attack")
    	bool bAttackVolumeOverlap;
    
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attack")
    	class UAnimMontage* AttackMontage;
    	
    UFUNCTION(BlueprintCallable)
    void MoveToTarget(class AMainPlayer* Player);
    void AttackBegin();
    UFUNCTION(BlueprintCallable)
    void AttackEnd();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 函数逻辑
    void ABaseEnemy::OnChaseVolumeOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
    {
    	if (OtherActor)
    	{
    		AMainPlayer* Player = Cast<AMainPlayer>(OtherActor);
    		if (Player)
    		{
    			MoveToTarget(Player);
    		}
    	}
    }
    
    void ABaseEnemy::OnChaseVolumeOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
    {
    	if (OtherActor)
    	{
    		AMainPlayer* Player = Cast<AMainPlayer>(OtherActor);
    		if (Player)
    		{
    			if (AIController)
    			{
    				//停止移动
    				AIController->StopMovement();
    			}
    		}
    	}
    }
    
    void ABaseEnemy::OnAttackVolumeOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
    {
    	if (OtherActor)
    	{
    		AMainPlayer* Player = Cast<AMainPlayer>(OtherActor);
    		if (Player)
    		{
    			bAttackVolumeOverlap = true;
    			AttackBegin();
    		}
    	}
    }
    
    void ABaseEnemy::OnAttackVolumeOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
    {
    	if (OtherActor)
    	{
    		AMainPlayer* Player = Cast<AMainPlayer>(OtherActor);
    		if (Player)
    		{
    			bAttackVolumeOverlap = false;
    			if (EnemyMovementStatus!=EEnemyMovementStatus::EEMS_Attacking)
    			{
    				MoveToTarget(Player);
    			}
    		}
    	}
    }
    
    void ABaseEnemy::MoveToTarget(AMainPlayer* Player)
    {
    	EnemyMovementStatus = EEnemyMovementStatus::EEMS_MoveToTarget;
    	if (AIController)
    	{
    		FAIMoveRequest MoveRequest;
    		MoveRequest.SetGoalActor(Player);//设置移动请求目标
    		MoveRequest.SetAcceptanceRadius(10.f);	//设置移动半径
    
    		FNavPathSharedPtr NavPath;//会返回路径
    
    		AIController->MoveTo(MoveRequest, &NavPath);
    	}
    }
    
    void ABaseEnemy::AttackBegin()
    {
    	//攻击中关闭移动
    	if (AIController)
    	{
    		AIController->StopMovement();
    	}
    	if (EnemyMovementStatus != EEnemyMovementStatus::EEMS_Attacking)
    	{
    		EnemyMovementStatus = EEnemyMovementStatus::EEMS_Attacking;
    		UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
    		if (AnimInstance && AttackMontage)
    		{
    			float PlayRate = FMath::RandRange(0.9f, 1.1f);
    			FString SectionName = FString::FromInt(FMath::RandRange(1, 3));
    			AnimInstance->Montage_Play(AttackMontage, PlayRate);
    			AnimInstance->Montage_JumpToSection(FName(*SectionName), AttackMontage);
    		}
    	}
    }
    
    void ABaseEnemy::AttackEnd()
    {
    	EnemyMovementStatus = EEnemyMovementStatus::EEMS_Idle;
    	if (bAttackVolumeOverlap)
    	{
    		AttackBegin();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101

    攻击动画的编写以及连续追逐

    • 首先把Montage的攻击通知加上,然后在动画蓝图里面添加蒙太奇
      在这里插入图片描述
      在这里插入图片描述
    • 然后在事件图表里面进行编辑,调用通知AttackEnd事件执行AttackEnd函数,然后进行判断主角离开了攻击重叠事件的判断,如果离开了,就又继续执行追逐主角函数
      在这里插入图片描述

    敌人更新攻击目标

    • 思想:近点跟随,谁离得近就先攻击谁
    • 在MainPlayer中新建一个敌人类的指针引用与一个模版敌人类,一个更新攻击目标的函数
    	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Attack")
    	class ABaseEnemy* AttackTarget;
    		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attack")
    	TSubclassOf<ABaseEnemy> EnemyFilter;
    //-----------------------------------------------------------------
    void UpdataAttackTarget();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 定义一个模版敌人类就是因为要使用这个函数GetOverlappingActors:它能够获取与指定组件相交的所有 Actors 列表,在使用时记得加上敌人类头文件,避免不知道EnemyFilter
    void AMainPlayer::UpdataAttackTarget()
    {
    
    	TArray<AActor*> OVerlappingActors;
    	GetOverlappingActors(OVerlappingActors,EnemyFilter);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 在返回的相交所有Actor列表里面进行选择最近的那个,逻辑是:遍历OverlapingActors数组进行比较,近的就替换远的。新建一个用于交换的敌人类引用,新建一个最小范围,然后获取当前位置,然后开始变量数组,将数组里面的单位全部转换为敌人类,判断敌人是否存在是否死亡,如果存在无死亡就当前距离主角的位置记录下来,如果距离主角位置小于最小范围那么就将距离主角位置赋值最小范围,将当前敌人类给交换敌人的引用,循环结束就将当前距离主角位置赋值给AttackTarget
    void AMainPlayer::UpdataAttackTarget()
    {
    
    	TArray<AActor*> OVerlappingActors;
    	GetOverlappingActors(OVerlappingActors,EnemyFilter);
    
    	//判断列表里面是否为空,为空就无攻击目标
    	if (OVerlappingActors.Num() == 0)
    	{
    		AttackTarget = nullptr;
    		return;
    	}
    
    	ABaseEnemy* ClosestDistance = nullptr;
    	float MinDistance = 1000.f;
    	FVector Loation = GetActorLocation();
    
    	for (auto Actor : OVerlappingActors)
    	{
    		ABaseEnemy* Enemy = Cast<ABaseEnemy>(Actor);
    		if (Enemy && Enemy->EnemyMovementStatus != EEnemyMovementStatus::EEMS_Dead)
    		{
    			float DistanceToActor = (Enemy->GetActorLocation() - Loation).Size();//记录当前位置Enemy距离MainPlayer位置
    			if (DistanceToActor < MinDistance)
    			{
    				MinDistance = DistanceToActor;
    				ClosestDistance = Enemy;
    			}
    		}
    	}
    	AttackTarget = ClosestDistance;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 然后在BaseEnemy类的攻击碰撞事件中调用此函数
    void ABaseEnemy::OnAttackVolumeOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
    {
    	if (OtherActor)
    	{
    		AMainPlayer* Player = Cast<AMainPlayer>(OtherActor);
    		if (Player)
    		{
    			Player->UpdataAttackTarget();
    			bAttackVolumeOverlap = true;
    			AttackBegin();
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    玩家自动面向攻击目标

    • 采用插值的思想方法,当我们需要攻击转向时就进行插值转向
    • 在MainPlayer类中新建一个插值速度变量与一个bool是否进行插值变量并赋初值
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attack")
    	float InterpSpeed;
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Attack")
    	bool bInterpToEnemy;
    
    
    InterpSpeed = 15.f;
    bInterpToEnemy = false;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 然后在攻击函数中将bool插值变量赋为true,攻击结束函数中设为false
    void AMainPlayer::AttackBegin()
    {
    	if (!bIsAttacking)
    	{
    		bIsAttacking = true;
    		bInterpToEnemy = true;
    		//拿到动画
    		UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
    
    		if (AnimInstance && AttackMontage)
    		{
    			float PlayRate = FMath::RandRange(1.25f, 1.75f);
    			FString SectionName = FString::FromInt(FMath::RandRange(1, 2));
    			//指定片段播放
    			AnimInstance->Montage_Play(AttackMontage, PlayRate);
    			AnimInstance->Montage_JumpToSection(FName(*SectionName), AttackMontage);
    		}
    	}
    }
    
    void AMainPlayer::AttackEnd()
    {
    	bIsAttacking = false;
    	bInterpToEnemy = false;
    	//形成闭环
    	if (bAttackKeyDown)
    	{
    		AttackKeyDown();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 然后去Tick里面进行转向逻辑编写,使用RInterpTo要头文件:#include “Kismet/KismetMathLibrary.h”
    //进行转向插值
    	if (bInterpToEnemy && AttackTarget)
    	{
    		//只需要AttackTarget的Yaw转向
    		FRotator LookAtYaw(0.f, UKismetMathLibrary::FindLookAtRotation(GetActorLocation(), AttackTarget->GetActorLocation()).Yaw, 0.f);
    		FRotator InterpRotation = FMath::RInterpTo(GetActorRotation(), LookAtYaw, DeltaTime, InterpSpeed);
    		SetActorRotation(InterpRotation);
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 将Enemy给过滤器,必须是C++类
      在这里插入图片描述

    敌人自动面向玩家攻击目标

    • 基本与玩家面向攻击目标的编写差不多
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attack")
    	float InterpSpeed;
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Attack")
    	bool bInterpToPlayer;
    
    
    InterpSpeed = 15.f;
    bInterpToPlayer = false;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 然后在攻击函数中将bool插值变量赋为true,攻击结束函数中设为false
    void ABaseEnemy::AttackBegin()
    {
    	//攻击中关闭移动
    	if (AIController)
    	{
    		AIController->StopMovement();
    	}
    	if (EnemyMovementStatus != EEnemyMovementStatus::EEMS_Attacking)
    	{
    		EnemyMovementStatus = EEnemyMovementStatus::EEMS_Attacking;
    
    		bInterpToPlayer = true;
    
    		UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
    		if (AnimInstance && AttackMontage)
    		{
    			float PlayRate = FMath::RandRange(0.9f, 1.1f);
    			FString SectionName = FString::FromInt(FMath::RandRange(1, 3));
    			AnimInstance->Montage_Play(AttackMontage, PlayRate);
    			AnimInstance->Montage_JumpToSection(FName(*SectionName), AttackMontage);
    		}
    	}
    }
    
    void ABaseEnemy::AttackEnd()
    {
    	EnemyMovementStatus = EEnemyMovementStatus::EEMS_Idle;
    	bInterpToPlayer = false;
    	if (bAttackVolumeOverlap)
    	{
    		AttackBegin();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 然后去Tick里面进行转向逻辑编写,注意的事FindLookAtRotation中的目标位置是Player,得去获取位置,获取位置要加头文件:#include "Kismet/GameplayStatics.h",使用RInterpTo也要头文件:#include "Kismet/KismetMathLibrary.h"
    void ABaseEnemy::Tick(float DeltaTime)
    {
    	Super::Tick(DeltaTime);
    
    	if (bInterpToPlayer)
    	{
    		FRotator LookYaw(0.f, UKismetMathLibrary::FindLookAtRotation(GetActorLocation(), UGameplayStatics::GetPlayerPawn(this, 0)->GetActorLocation()).Yaw, 0.f);
    		FRotator InterpRotation = FMath::RInterpTo(GetActorRotation(), LookYaw, DeltaTime, InterpSpeed);
    		SetActorRotation(InterpRotation);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    BaseEnemy.h

    // Fill out your copyright notice in the Description page of Project Settings.
    
    #pragma once
    
    #include "CoreMinimal.h"
    #include "GameFramework/Character.h"
    #include "BaseEnemy.generated.h"
    
    UENUM(BlueprintType)
    enum class EEnemyMovementStatus :uint8
    {
    	EEMS_Idle			UMETA(DisplayName="Idle"),
    	EEMS_MoveToTarget	UMETA(DisPlayName="MoveToTarget"),
    	EEMS_Attacking		UMETA(DisPlayName="Attacking"),
    	EEMS_Dead			UMETA(DisPlayName="Dead")
    };
    
    UCLASS()
    class UEGAME_API ABaseEnemy : public ACharacter
    {
    	GENERATED_BODY()
    
    public:
    	// Sets default values for this character's properties
    	ABaseEnemy();
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI")
    	class USphereComponent* ChaseVolume;
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI")
    	USphereComponent* AttackVolume;
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI")
    	class AAIController* AIController;
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Enemy Stats")
    	EEnemyMovementStatus EnemyMovementStatus;
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Attack")
    	bool bAttackVolumeOverlap;
    
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attack")
    	class UAnimMontage* AttackMontage;
    
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attack")
    	float InterpSpeed;
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Attack")
    	bool bInterpToPlayer;
    protected:
    	// Called when the game starts or when spawned
    	virtual void BeginPlay() override;
    
    public:	
    	// Called every frame
    	virtual void Tick(float DeltaTime) override;
    
    	// Called to bind functionality to input
    	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
    
    	UFUNCTION()
    	virtual void OnChaseVolumeOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
    	UFUNCTION()
    	virtual void OnChaseVolumeOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
    	UFUNCTION()
    	virtual void OnAttackVolumeOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
    	UFUNCTION()
    	virtual void OnAttackVolumeOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
    	
    	UFUNCTION(BlueprintCallable)
    	void MoveToTarget(class AMainPlayer* Player);
    
    	void AttackBegin();
    
    	UFUNCTION(BlueprintCallable)
    	void AttackEnd();
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78

    BaseEnemy.cpp

    // Fill out your copyright notice in the Description page of Project Settings.
    
    
    #include "BaseEnemy.h"
    #include "Components/SphereComponent.h"
    #include "Components/SkeletalMeshComponent.h"
    #include "Components/CapsuleComponent.h"
    #include "AIController.h"
    #include "Characters/Player/MainPlayer.h"
    #include "Animation/AnimInstance.h"
    #include "Kismet/KismetMathLibrary.h"
    #include "Kismet/GameplayStatics.h"
    // Sets default values
    ABaseEnemy::ABaseEnemy()
    {
     	// Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    	PrimaryActorTick.bCanEverTick = true;
    	
    	ChaseVolume = CreateDefaultSubobject<USphereComponent>(TEXT("ChaseVolume"));
    	ChaseVolume->SetupAttachment(GetRootComponent());
    	ChaseVolume->InitSphereRadius(800.f);
    	ChaseVolume->SetCollisionObjectType(ECollisionChannel::ECC_WorldDynamic);
    	ChaseVolume->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
    	ChaseVolume->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Overlap);
    
    	AttackVolume = CreateDefaultSubobject<USphereComponent>(TEXT("AttackVolume"));
    	AttackVolume->SetupAttachment(GetRootComponent());
    	AttackVolume->InitSphereRadius(100.f);
    	AttackVolume->SetCollisionObjectType(ECollisionChannel::ECC_WorldDynamic);
    	AttackVolume->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
    	AttackVolume->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Overlap);
    	
    	//避免摄像机被敌人给阻挡
    	GetMesh()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
    	GetCapsuleComponent()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
    	//设置持有属性
    	AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
    
    	//初始化默认移动状态
    	EnemyMovementStatus = EEnemyMovementStatus::EEMS_Idle;
    
    	InterpSpeed = 15.f;
    	bInterpToPlayer = false;
    }
    
    // Called when the game starts or when spawned
    void ABaseEnemy::BeginPlay()
    {
    	Super::BeginPlay();
    
    	ChaseVolume->OnComponentBeginOverlap.AddDynamic(this, &ABaseEnemy::OnChaseVolumeOverlapBegin);
    	ChaseVolume->OnComponentEndOverlap.AddDynamic(this, &ABaseEnemy::OnChaseVolumeOverlapEnd);
    
    	AttackVolume->OnComponentBeginOverlap.AddDynamic(this, &ABaseEnemy::OnAttackVolumeOverlapBegin);
    	AttackVolume->OnComponentEndOverlap.AddDynamic(this, &ABaseEnemy::OnAttackVolumeOverlapEnd);
    	
    	//拿到Controller
    	AIController = Cast<AAIController>(GetController());
    }
    
    // Called every frame
    void ABaseEnemy::Tick(float DeltaTime)
    {
    	Super::Tick(DeltaTime);
    
    	if (bInterpToPlayer)
    	{
    		FRotator LookYaw(0.f, UKismetMathLibrary::FindLookAtRotation(GetActorLocation(), UGameplayStatics::GetPlayerPawn(this, 0)->GetActorLocation()).Yaw, 0.f);
    		FRotator InterpRotation = FMath::RInterpTo(GetActorRotation(), LookYaw, DeltaTime, InterpSpeed);
    		SetActorRotation(InterpRotation);
    	}
    }
    
    // Called to bind functionality to input
    void ABaseEnemy::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
    {
    	Super::SetupPlayerInputComponent(PlayerInputComponent);
    
    }
    
    void ABaseEnemy::OnChaseVolumeOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
    {
    	if (OtherActor)
    	{
    		AMainPlayer* Player = Cast<AMainPlayer>(OtherActor);
    		if (Player)
    		{
    			MoveToTarget(Player);
    		}
    	}
    }
    
    void ABaseEnemy::OnChaseVolumeOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
    {
    	if (OtherActor)
    	{
    		AMainPlayer* Player = Cast<AMainPlayer>(OtherActor);
    		if (Player)
    		{
    			if (AIController)
    			{
    				//停止移动
    				AIController->StopMovement();
    			}
    		}
    	}
    }
    
    void ABaseEnemy::OnAttackVolumeOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
    {
    	if (OtherActor)
    	{
    		AMainPlayer* Player = Cast<AMainPlayer>(OtherActor);
    		if (Player)
    		{
    			Player->UpdataAttackTarget();
    			bAttackVolumeOverlap = true;
    			AttackBegin();
    		}
    	}
    }
    
    void ABaseEnemy::OnAttackVolumeOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
    {
    	if (OtherActor)
    	{
    		AMainPlayer* Player = Cast<AMainPlayer>(OtherActor);
    		if (Player)
    		{
    			bAttackVolumeOverlap = false;
    			if (EnemyMovementStatus!=EEnemyMovementStatus::EEMS_Attacking)
    			{
    				MoveToTarget(Player);
    			}
    		}
    	}
    }
    
    void ABaseEnemy::MoveToTarget(AMainPlayer* Player)
    {
    	EnemyMovementStatus = EEnemyMovementStatus::EEMS_MoveToTarget;
    	if (AIController)
    	{
    		FAIMoveRequest MoveRequest;
    		MoveRequest.SetGoalActor(Player);//设置移动请求目标
    		MoveRequest.SetAcceptanceRadius(10.f);	//设置移动半径
    
    		FNavPathSharedPtr NavPath;//会返回路径
    
    		AIController->MoveTo(MoveRequest, &NavPath);
    	}
    }
    
    void ABaseEnemy::AttackBegin()
    {
    	//攻击中关闭移动
    	if (AIController)
    	{
    		AIController->StopMovement();
    	}
    	if (EnemyMovementStatus != EEnemyMovementStatus::EEMS_Attacking)
    	{
    		EnemyMovementStatus = EEnemyMovementStatus::EEMS_Attacking;
    
    		bInterpToPlayer = true;
    
    		UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
    		if (AnimInstance && AttackMontage)
    		{
    			float PlayRate = FMath::RandRange(0.9f, 1.1f);
    			FString SectionName = FString::FromInt(FMath::RandRange(1, 3));
    			AnimInstance->Montage_Play(AttackMontage, PlayRate);
    			AnimInstance->Montage_JumpToSection(FName(*SectionName), AttackMontage);
    		}
    	}
    }
    
    void ABaseEnemy::AttackEnd()
    {
    	EnemyMovementStatus = EEnemyMovementStatus::EEMS_Idle;
    	bInterpToPlayer = false;
    	if (bAttackVolumeOverlap)
    	{
    		AttackBegin();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186

    MainPlayer.h

    // Fill out your copyright notice in the Description page of Project Settings.
    
    #pragma once
    
    #include "CoreMinimal.h"
    #include "GameFramework/Character.h"
    #include "MainPlayer.generated.h"
    
    //声明移动状态枚举
    UENUM(BlueprintType)
    enum class EPlayerMovementStatus :uint8
    {
    	EPMS_Normal UMETA(DisplayName = "Normal"),
    	EPMS_Sprinting UMETA(DisplayName = "Sprinting"),
    	EPMS_Dead UMETA(DisplayName = "Dead")
    };
    
    UENUM(BlueprintType)
    enum class EPlayerStaminaStatus :uint8
    {
    	EPSS_Normal UMETA(DisplayName = "Normal"),
    	EPSS_Exhausted UMETA(DisplayName = "Exhausted"),
    	EPSS_ExhaustedRecovering UMETA(DisplayName = "ExhaustedRecovering")
    };
    
    
    
    UCLASS()
    class UEGAME_API AMainPlayer : public ACharacter
    {
    	GENERATED_BODY()
    
    public:
    	// Sets default values for this character's properties
    	AMainPlayer();
    
    	//新建一个SpringArm
    	UPROPERTY(visibleAnywhere,BlueprintReadOnly)
    	class USpringArmComponent* SpringArm;
    	//新建一个Camera
    	UPROPERTY(visibleAnywhere, BlueprintReadOnly)
    	class UCameraComponent* FollowCamera;
    
    	
    	float BaseTurnRate;		//使用键盘X转向的速率
    	float BaseLookUpRate;	//使用键盘Y转向的速率
    
    	//主角状态
    	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Playe State")
    	float Health;
    	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playe State")
    	float MaxHealth;
    	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Playe State")
    	float Stamina;
    	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playe State")
    	float MaxStamina;
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Playe State")
    	float StaminaConsumeRate;
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Playe State", meta = (ClampMin = 0, ClampMax = 1))
    	float ExhaustedStamina;
    	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playe State")
    	int Coins;
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Player State")
    	float RunningSpeed;
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Player State")
    	float SprintSpeed;
    	UPROPERTY(VisibleAnywhere,BlueprintReadWrite,Category="Player State")
    	EPlayerMovementStatus MovementStatus;
    	UPROPERTY(VisibleAnywhere,BlueprintReadWrite,Category="Player State")
    	EPlayerStaminaStatus StaminaStatus;
    
    	bool bLeftShiftDown;
    	
    	//检测是否持剑
    	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Weapon")
    	bool bIsWeapon;
    
    	//正在装备的武器
    	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Weapon")
    	class AWeaponItem* EquipWeapon;
    
    	//正在重叠的武器
    	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Weapon")
    	AWeaponItem* OverlapWeapon;
    
    	bool bAttackKeyDown;//是否按下攻击键
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Animation")
    	bool bIsAttacking;
    
    	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Animation")
    	class UAnimMontage* AttackMontage;
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Attack")
    	class ABaseEnemy* AttackTarget;
    
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attack")
    	TSubclassOf<ABaseEnemy> EnemyFilter;
    
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attack")
    	float InterpSpeed;
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Attack")
    	bool bInterpToEnemy;
    protected:
    	// Called when the game starts or when spawned
    	virtual void BeginPlay() override;
    
    public:	
    	// Called every frame
    	virtual void Tick(float DeltaTime) override;
    
    	// Called to bind functionality to input
    	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
    
    	//重新Character类中的Jump方法
    	void Jump() override;
    
    	void MoveForward(float value);
    	void MoveRight(float value);
    
    	void Turn(float Value);
    	void LookUp(float Value);
    
    	void TurnRate(float Rate);
    	void LookUpRate(float Rate);
    
    	//改变状态
    	UFUNCTION(BlueprintCallable,Category="Player|State")
    	void AddHealth(float value);
    	UFUNCTION(BlueprintCallable, Category = "Player|State")
    	void AddStamina(float value);
    	UFUNCTION(BlueprintCallable, Category = "Player|State")
    	void AddCoin(float value);
    
    	//重写TakeDamage方法
    	float TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;
    
    	//短小精悍
    	FORCEINLINE void LeftShiftDown() { bLeftShiftDown = true; }
    	FORCEINLINE void LeftShiftUp() { bLeftShiftDown = false; }
    
    	void SetMovementStatus(EPlayerMovementStatus Status);
    
    	void InteractKeyDown();
    
    	void AttackKeyDown();
    
    	FORCEINLINE void AttackKeyUp() { bAttackKeyDown = false; }
    
    	void AttackBegin();
    	
    	UFUNCTION(BlueprintCallable)
    	void AttackEnd();
    
    	void UpdataAttackTarget();
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157

    MainPlayer.cpp

    // Fill out your copyright notice in the Description page of Project Settings.
    
    
    #include "MainPlayer.h"
    #include "GameFramework/SpringArmComponent.h"
    #include "Camera/CameraComponent.h"
    #include "Components/CapsuleComponent.h"
    #include "Components/InputComponent.h"
    #include "GameFramework/PlayerController.h"
    #include "GameFramework/CharacterMovementComponent.h"
    #include "GamePlay/WeaponItem.h"
    #include "Animation/AnimInstance.h"
    #include "Characters/Enemy/BaseEnemy.h"
    #include "Kismet/KismetMathLibrary.h"
    // Sets default values
    AMainPlayer::AMainPlayer()
    {
     	// Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    	PrimaryActorTick.bCanEverTick = true;
    
    	SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
    	SpringArm->SetupAttachment(GetRootComponent());
    	//设置SPringArm无碰撞臂长
    	SpringArm->TargetArmLength = 600.f;
    	SpringArm->bUsePawnControlRotation = true;//硬编码SpringArm继承controlller旋转为真
    
    
    	FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
    	FollowCamera->SetupAttachment(SpringArm, NAME_None);
    	FollowCamera->bUsePawnControlRotation = false;//硬编码FollowCamera继承controlller旋转为假
    
    	//设置胶囊体的默认宽高
    	GetCapsuleComponent()->SetCapsuleSize(35.f, 100.f);
    
    	//对Character的Pawn进行硬编码
    	bUseControllerRotationPitch = false;
    	bUseControllerRotationYaw = false;
    	bUseControllerRotationRoll = false;
    
    	//硬编码orient Rotation to Movement,给个默认转向速率
    	GetCharacterMovement()->bOrientRotationToMovement = true;
    	GetCharacterMovement()->RotationRate = FRotator(0.f, 500.f, 0.f);
    
    	//设置跳跃初始值与在空中的坠落时横向运动控制量
    	GetCharacterMovement()->JumpZVelocity = 400.f;
    	GetCharacterMovement()->AirControl = 0.15f;
    
    	//给键盘控制转向的速率变量赋初值
    	BaseTurnRate = 21.f;
    	BaseLookUpRate = 21.f;
    
    	//初始化角色状态
    	MaxHealth = 100.f;
    	Health = MaxHealth;
    	MaxStamina = 200.f;
    	Stamina = MaxStamina;
    	StaminaConsumeRate = 20.f;
    	ExhaustedStamina = 0.167f;
    	Coins = 0;
    	RunningSpeed = 600.f;
    	SprintSpeed = 900.f;
    	MovementStatus = EPlayerMovementStatus::EPMS_Normal;
    	StaminaStatus = EPlayerStaminaStatus::EPSS_Normal;
    
    	//默认没有按下shift
    	bLeftShiftDown = false;
    
    
    	InterpSpeed = 15.f;
    	bInterpToEnemy = false;
    }
    
    // Called when the game starts or when spawned
    void AMainPlayer::BeginPlay()
    {
    	Super::BeginPlay();
    }
    
    // Called every frame
    void AMainPlayer::Tick(float DeltaTime)
    {
    	Super::Tick(DeltaTime);
    	switch (StaminaStatus)
    	{
    	case EPlayerStaminaStatus::EPSS_Normal:
    		//当Shift按下
    		if (bLeftShiftDown)
    		{
    			if (Stamina - StaminaConsumeRate * DeltaTime <= MaxStamina * ExhaustedStamina)
    			{
    				StaminaStatus = EPlayerStaminaStatus::EPSS_Exhausted;
    			}
    			//无论是不是精疲力尽状态都要减去当前帧冲刺消耗的耐力
    			Stamina -= StaminaConsumeRate * DeltaTime;
    			SetMovementStatus(EPlayerMovementStatus::EPMS_Sprinting);
    		}
    		else
    		{
    			//当Shift没有按下,恢复耐力
    			Stamina = FMath::Clamp(Stamina + StaminaConsumeRate * DeltaTime, 0.f, MaxStamina);
    			SetMovementStatus(EPlayerMovementStatus::EPMS_Normal);
    		}
    		break;
    	case EPlayerStaminaStatus::EPSS_Exhausted:
    		if (bLeftShiftDown)
    		{
    			//如果耐力已经为0
    			if (Stamina - StaminaConsumeRate * DeltaTime <= 0.f)
    			{
    				//么我们需要内部编码把shift抬起,此时StaminaStatus状态转换为ExhaustedRecovering状态,然后设置移动状态为Normal
    				LeftShiftUp();
    				StaminaStatus = EPlayerStaminaStatus::EPSS_ExhaustedRecovering;	
    				SetMovementStatus(EPlayerMovementStatus::EPMS_Normal);
    			}
    			else
    			{
    				Stamina -= StaminaConsumeRate * DeltaTime;
    			}
    		}
    		else
    		{
    			StaminaStatus = EPlayerStaminaStatus::EPSS_ExhaustedRecovering;
    			Stamina = FMath::Clamp(Stamina + StaminaConsumeRate * DeltaTime, 0.f, MaxStamina);
    			SetMovementStatus(EPlayerMovementStatus::EPMS_Normal);
    		}
    		break;
    	case EPlayerStaminaStatus::EPSS_ExhaustedRecovering:
    		//当恢复大于疲劳区时,StaminaStatus状态为Normal
    		if (Stamina + StaminaConsumeRate * DeltaTime >= MaxStamina * ExhaustedStamina)
    		{
    			StaminaStatus = EPlayerStaminaStatus::EPSS_Normal;
    		}
    		//这状态值肯定是加定了
    		Stamina += StaminaConsumeRate * DeltaTime;
    
    		//抬起shift
    		LeftShiftUp();
    		SetMovementStatus(EPlayerMovementStatus::EPMS_Normal);
    		break;
    	default:
    		break;
    	}
    
    	//进行转向插值
    	if (bInterpToEnemy && AttackTarget)
    	{
    		//只需要AttackTarget的Yaw转向
    		FRotator LookAtYaw(0.f, UKismetMathLibrary::FindLookAtRotation(GetActorLocation(), AttackTarget->GetActorLocation()).Yaw, 0.f);
    		FRotator InterpRotation = FMath::RInterpTo(GetActorRotation(), LookAtYaw, DeltaTime, InterpSpeed);
    		SetActorRotation(InterpRotation);
    	}
    }
    
    // Called to bind functionality to input
    void AMainPlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
    {
    	Super::SetupPlayerInputComponent(PlayerInputComponent);
    	//检查PlayerInputComponent指针,check函数只能在这使用
    	check(PlayerInputComponent);
    
    	//绑定跳跃轴映射事件
    	PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &AMainPlayer::Jump);//按下空格
    	PlayerInputComponent->BindAction("Jump", IE_Released, this, &ACharacter::StopJumping);//抬起空格
    
    	PlayerInputComponent->BindAction("Sprint", IE_Pressed, this, &AMainPlayer::LeftShiftDown);//按下shift
    	PlayerInputComponent->BindAction("Sprint", IE_Released, this, &AMainPlayer::LeftShiftUp);//抬起shift
    
    	//拾取剑
    	PlayerInputComponent->BindAction("Interact", IE_Pressed, this, &AMainPlayer::InteractKeyDown);//按下F
    
    	//攻击
    	PlayerInputComponent->BindAction("Attack", IE_Pressed, this, &AMainPlayer::AttackKeyDown);
    	PlayerInputComponent->BindAction("Attack", IE_Released, this, &AMainPlayer::AttackKeyUp);
    
    	//绑定移动轴映射事件
    	PlayerInputComponent->BindAxis("MoveForward", this, &AMainPlayer::MoveForward);
    	PlayerInputComponent->BindAxis("MoveRight", this, &AMainPlayer::MoveRight);
    
    	//绑定Controller控制器去管理视角旋转
    	PlayerInputComponent->BindAxis("Turn", this, &AMainPlayer::Turn);
    	PlayerInputComponent->BindAxis("LookUp", this, &AMainPlayer::LookUp);
    
    	//绑定键盘鼠标轴映射事件
    	PlayerInputComponent->BindAxis("TurnRate", this, &AMainPlayer::TurnRate);
    	PlayerInputComponent->BindAxis("LookUpRate", this, &AMainPlayer::LookUpRate);
    
    }
    
    void AMainPlayer::Jump()
    {
    	//继承父类的方法
    	Super::Jump();
    }
    
    void AMainPlayer::MoveForward(float value)
    {
    	if (Controller != nullptr && value != 0.f && !(bIsAttacking))
    	{
    		//获取到Control旋转
    		FRotator Rotation = Controller->GetControlRotation();
    		//转向只关注水平Yaw方向,因此置0防止影响
    		FRotator YowRotation = FRotator(0.0f, Rotation.Yaw, 0.0f);
    		//获取相机(鼠标控制器的朝向),并且朝这个轴的方向移动
    		FVector Direction = FRotationMatrix(YowRotation).GetUnitAxis(EAxis::X);
    		AddMovementInput(Direction, value);
    	}
    
    }
    
    void AMainPlayer::MoveRight(float value)
    {
    	if (Controller != nullptr && value != 0.f && !(bIsAttacking))
    	{
    		//获取到Controller旋转
    		FRotator Rotation = Controller->GetControlRotation();
    		//转向只关注水平Yaw方向,因此置0防止影响
    		FRotator YowRotation = FRotator(0.0f, Rotation.Yaw, 0.0f);
    		//获取相机(鼠标控制器的朝向),并且朝这个轴的方向移动
    		FVector Direction = FRotationMatrix(YowRotation).GetUnitAxis(EAxis::Y);
    		AddMovementInput(Direction, value);
    	}
    }
    
    void AMainPlayer::Turn(float Value)
    {
    	if (Value != 0.f)
    	{
    		AddControllerYawInput(Value);
    	}
    	
    }
    
    void AMainPlayer::LookUp(float Value)
    {
    	//UE_LOG(LogTemp, Warning, TEXT("%f"), GetControlRotation().Pitch);
    
    	//控制视角
    	if (GetControlRotation().Pitch < 270.f && GetControlRotation().Pitch >180.f && Value > 0.f)
    	{
    		return;
    	}
    	else if (GetControlRotation().Pitch < 180.f && GetControlRotation().Pitch >45.f && Value < 0.f)
    	{
    		return;
    	}
    	AddControllerPitchInput(Value);
    }
    
    void AMainPlayer::TurnRate(float Rate)
    {
    	//要乘以一个DeltaTime这样就可以避免高帧底帧差值问题
    	float Value = Rate * BaseTurnRate * GetWorld()->GetDeltaSeconds();
    	if (Value != 0.f)
    	{
    		AddControllerYawInput(Value);
    	}
    }
    
    void AMainPlayer::LookUpRate(float Rate)
    {
    	//要乘以一个DeltaTime这样就可以避免高帧底帧差值问题
    	float Value = Rate * BaseLookUpRate * GetWorld()->GetDeltaSeconds();
    	//控制视角
    	if (GetControlRotation().Pitch < 270.f && GetControlRotation().Pitch >180.f && Value > 0.f)
    	{
    		return;
    	}
    	else if (GetControlRotation().Pitch < 180.f && GetControlRotation().Pitch >45.f && Value < 0.f)
    	{
    		return;
    	}
    	AddControllerPitchInput(Value);
    
    }
    
    void AMainPlayer::AddHealth(float value)
    {
    	Health = FMath::Clamp(Health + value, 0.f, MaxHealth);
    }
    
    void AMainPlayer::AddStamina(float value)
    {
    	Stamina = FMath::Clamp(Stamina + value, 0.f, MaxStamina);
    }
    
    void AMainPlayer::AddCoin(float value)
    {
    	Coins += value;
    }
    
    float AMainPlayer::TakeDamage(float Damage, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
    {
    	if (Health - Damage <= 0.f)
    	{
    		Health = FMath::Clamp(Health - Damage, 0.f, MaxHealth);
    		//TODO Die();
    	}
    	else
    	{
    		Health -= Damage;
    	}
    	return Health;
    }
    
    void AMainPlayer::SetMovementStatus(EPlayerMovementStatus Status)
    {
    	MovementStatus = Status;
    	//切换状态的时候改变移动速度
    	switch (MovementStatus)
    	{
    	case EPlayerMovementStatus::EPMS_Sprinting:
    		GetCharacterMovement()->MaxWalkSpeed = SprintSpeed;
    		break;
    	default:
    		GetCharacterMovement()->MaxWalkSpeed = RunningSpeed;
    		break;
    	}
    }
    
    void AMainPlayer::InteractKeyDown()
    {
    	if (OverlapWeapon)
    	{
    		if (EquipWeapon)
    		{
    			//交换武器
    			EquipWeapon->UnEuip(this);
    			OverlapWeapon->Equip(this);
    		}
    		else
    		{
    			//装备武器
    			OverlapWeapon->Equip(this);
    		}
    	}
    	else
    	{
    		if (EquipWeapon)
    		{
    			//卸载武器
    			EquipWeapon->UnEuip(this);
    		}
    	}
    }
    
    void AMainPlayer::AttackKeyDown()
    {
    	bAttackKeyDown = true;
    	if (bIsWeapon)
    	{
    		AttackBegin();
    	}
    
    }
    
    
    void AMainPlayer::AttackBegin()
    {
    	if (!bIsAttacking)
    	{
    		bIsAttacking = true;
    		bInterpToEnemy = true;
    		//拿到动画
    		UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
    
    		if (AnimInstance && AttackMontage)
    		{
    			float PlayRate = FMath::RandRange(1.25f, 1.75f);
    			FString SectionName = FString::FromInt(FMath::RandRange(1, 2));
    			//指定片段播放
    			AnimInstance->Montage_Play(AttackMontage, PlayRate);
    			AnimInstance->Montage_JumpToSection(FName(*SectionName), AttackMontage);
    		}
    	}
    }
    
    void AMainPlayer::AttackEnd()
    {
    	bIsAttacking = false;
    	bInterpToEnemy = false;
    	//形成闭环
    	if (bAttackKeyDown)
    	{
    		AttackKeyDown();
    	}
    }
    
    void AMainPlayer::UpdataAttackTarget()
    {
    
    	TArray<AActor*> OVerlappingActors;
    	GetOverlappingActors(OVerlappingActors,EnemyFilter);
    
    	//判断列表里面是否为空,为空就无攻击目标
    	if (OVerlappingActors.Num() == 0)
    	{
    		AttackTarget = nullptr;
    		return;
    	}
    
    	ABaseEnemy* ClosestDistance = nullptr;
    	float MinDistance = 1000.f;
    	FVector Loation = GetActorLocation();
    
    	for (auto Actor : OVerlappingActors)
    	{
    		ABaseEnemy* Enemy = Cast<ABaseEnemy>(Actor);
    		if (Enemy && Enemy->EnemyMovementStatus != EEnemyMovementStatus::EEMS_Dead)
    		{
    			float DistanceToActor = (Enemy->GetActorLocation() - Loation).Size();//记录当前位置Enemy距离MainPlayer位置
    			if (DistanceToActor < MinDistance)
    			{
    				MinDistance = DistanceToActor;
    				ClosestDistance = Enemy;
    			}
    		}
    	}
    	AttackTarget = ClosestDistance;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274
    • 275
    • 276
    • 277
    • 278
    • 279
    • 280
    • 281
    • 282
    • 283
    • 284
    • 285
    • 286
    • 287
    • 288
    • 289
    • 290
    • 291
    • 292
    • 293
    • 294
    • 295
    • 296
    • 297
    • 298
    • 299
    • 300
    • 301
    • 302
    • 303
    • 304
    • 305
    • 306
    • 307
    • 308
    • 309
    • 310
    • 311
    • 312
    • 313
    • 314
    • 315
    • 316
    • 317
    • 318
    • 319
    • 320
    • 321
    • 322
    • 323
    • 324
    • 325
    • 326
    • 327
    • 328
    • 329
    • 330
    • 331
    • 332
    • 333
    • 334
    • 335
    • 336
    • 337
    • 338
    • 339
    • 340
    • 341
    • 342
    • 343
    • 344
    • 345
    • 346
    • 347
    • 348
    • 349
    • 350
    • 351
    • 352
    • 353
    • 354
    • 355
    • 356
    • 357
    • 358
    • 359
    • 360
    • 361
    • 362
    • 363
    • 364
    • 365
    • 366
    • 367
    • 368
    • 369
    • 370
    • 371
    • 372
    • 373
    • 374
    • 375
    • 376
    • 377
    • 378
    • 379
    • 380
    • 381
    • 382
    • 383
    • 384
    • 385
    • 386
    • 387
    • 388
    • 389
    • 390
    • 391
    • 392
    • 393
    • 394
    • 395
    • 396
    • 397
    • 398
    • 399
    • 400
    • 401
    • 402
    • 403
    • 404
    • 405
    • 406
    • 407
    • 408
    • 409
    • 410
    • 411
    • 412
    • 413
    • 414
    • 415
    • 416
    • 417
    • 418
    • 419
  • 相关阅读:
    python图像处理 ——图像锐化
    FP独立站该怎么运营?斗篷黑科技教您找对方法引流获客
    【项目实战】Spring Boot项目抵御XSS攻击
    C# 把多个dll合成一个dll
    mysql面试题34:Hash索引和B+树区别是什么?在设计索引怎么选择?
    Spark新特性与核心概念
    安卓WebApp开发-项目MiliSetu
    idea 超实用的插件
    Linux实操篇-RPM 与 YUM
    TIA博途中通过数组实现批量处理模拟量的梯形图程序示例
  • 原文地址:https://blog.csdn.net/qq_44924388/article/details/134502122