UE4开发C++沙盒游戏教程笔记(十六)(对应教程集数 49 ~ 51)

48. 敌人追逐 AI

提前写好黑板要创建的变量

添加敌人的攻击状态的枚举。

SlAiTypes.h

// 敌人攻击状态
UENUM(BlueprintType)
enum class EEnemyAttackType : uint8
{
	EA_Normal	UMETA(DisplayName = "Normal"),	// 正常攻击
	EA_Dash		UMETA(DisplayName = "Dash"),	// 冲刺攻击
	EA_Pursuit	UMETA(DisplayName = "Pursuit")	// 位移攻击(大招)
};

由于老师遇到莫名的行为树崩溃问题,所以他在这集先把剩余的所有黑板值都添加上去。

SlAiEnemyBlackboard.cpp

// 添加头文件
#include "SlAiPlayerCharacter.h"

void USlAiEnemyBlackboard::PostLoad()
{
	// ... 省略

	// 攻击类型
	FBlackboardEntry AttackType;
	AttackType.EntryName = FName("AttackType");
	UBlackboardKeyType_Enum* AttackTypeKeyType = NewObject<UBlackboardKeyType_Enum>();
	AttackTypeKeyType->EnumType = FindObject<UEnum>(ANY_PACKAGE, *FString("EEnemyAttackType"), true);
	AttackTypeKeyType->EnumName = FString("EEnemyAttackType");
	AttackType.KeyType = AttackTypeKeyType;

	// 玩家指针
	FBlackboardEntry PlayerPawn;
	PlayerPawn.EntryName = FName("PlayerPawn");
	UBlackboardKeyType_Object* PlayerPawnKeyType = NewObject<UBlackboardKeyType_Object>();
	PlayerPawnKeyType->BaseClass = ASlAiPlayerCharacter::StaticClass();
	PlayerPawn.KeyType = PlayerPawnKeyType;

	// 某一个动作是否完成
	FBlackboardEntry ProcessFinish;
	ProcessFinish.EntryName = FName("ProcessFinish");
	ProcessFinish.KeyType = NewObject<UBlackboardKeyType_Bool>();

	Keys.Add(Destination);
	Keys.Add(EnemyState);
	Keys.Add(WaitTime);
	// 添加变量
	Keys.Add(AttackType);
	Keys.Add(PlayerPawn);
	Keys.Add(ProcessFinish);
	
}

此时运行游戏,能看到黑板已经添加了上面的三个变量,并且已经配置好相应的参数。

完善敌人的追逐逻辑

在 Public/AI 下以 SlAiEnemyTaskBase 为基类创建两个子类:
一个命名为 SlAiEnemyTaskChaseSwitch,负责敌人的追逐、攻击、放弃追逐回到巡逻的三种状态的逻辑切换;
另一个命名为 SlAiEnemyTaskLocaSP,负责更新敌人的追逐目的地点。

在 PlayerState 里声明一个方法,返回角色是否已经挂了的布尔值。

SlAiPlayerState.h

public:

	// 是否已经死亡
	bool IsPlayerDead();

SlAiPlayerState.cpp

bool ASlAiPlayerState::IsPlayerDead()
{
	return HP <= 0.f;
}

玩家角色类也声明这个方法,通过 PlayerState 的同名方法获取。

SlAiPlayerCharacter.h

public:

	// 获取是否已经死亡
	bool IsPlayerDead();

SlAiPlayerCharacter.cpp

bool ASlAiPlayerCharacter::IsPlayerDead()
{
	if (SPController->SPState) return SPController->SPState->IsPlayerDead();
	return false;
}

在敌人 AI 控制器声明 3 个方法和 1 个 bool 值,都是与追逐有关的。

SlAiEnemyController.h

public:

	// 玩家是否已经死亡
	bool IsPlayerDead();

	// 看到了玩家,由 Character 的 OnSeePlayer 调用
	void OnSeePlayer();

	// 丢失玩家定位
	void LoosePlayer();

public:

	// 是否锁定了玩家
	bool IsLockPlayer;

SlAiEnemyController.cpp

void ASlAiEnemyController::BeginPlay()
{

	// 是否锁定了玩家
	IsLockPlayer = false;
}

bool ASlAiEnemyController::IsPlayerDead()
{
	if (SPCharacter) return SPCharacter->IsPlayerDead();
	return false;
}

void ASlAiEnemyController::OnSeePlayer()
{
	// 如果已经锁定了玩家或者玩家已经死了,不再执行下面的函数
	if (IsLockPlayer || IsPlayerDead()) return;

	// 设置锁定了玩家
	IsLockPlayer = true;
	// 修改预状态为追逐
	BlackboardComp->SetValueAsEnum("EnemyState", (uint8)EEnemyAIState::ES_Chase);
	// 修改最大速度为 300
	SECharacter->SetMaxSpeed(300.f);
}

void ASlAiEnemyController::LoosePlayer()
{
	// 设置丢失玩家的锁定
	IsLockPlayer = false;
}

追逐切换任务,其实 Task 类要做的都是执行任务和任务结束后要执行什么,以及修改一些黑板值。本质都都差不多,就不多解释了。

SlAiEnemyTaskChaseSwitch.h

protected:

	UPROPERTY(EditAnywhere, Category = "Blackboard")
	struct FBlackboardKeySelector EnemyState;

private:

	// 重写执行函数
	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent & OwnerComp, uint8* NodeMemory) override;

SlAiEnemyTaskChaseSwitch.cpp

// 引入头文件
#include "SlAiEnemyController.h"
#include "SlAiEnemyCharacter.h"
#include "NavigationSystem.h"	// 依旧是 4.26 用这个
#include "BehaviorTree/BehaviorTreeComponent.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "SlAiTypes.h"

EBTNodeResult::Type USlAiEnemyTaskChaseSwitch::ExecuteTask(UBehaviorTreeComponent & OwnerComp, uint8* NodeMemory)
{
	// 如果初始化敌人参数不成功,直接返回失败
	if (!InitEnemyElement(OwnerComp)) return EBTNodeResult::Failed;
	// 获取与玩家的距离
	float EPDistance = FVector::Distance(SECharacter->GetActorLocation(), SEController->GetPlayerLocation());
	// 如果距离小于 300 了,状态设置为攻击,跳出追逐状态
	if (EPDistance < 300.f)
	{
		OwnerComp.GetBlackboardComponent()->SetValueAsEnum(EnemyState.SelectedKeyName, (uint8)EEnemyAIState::ES_Attack);
		return EBTNodeResult::Failed;
	}
	// 如果大于 1500 了,跳到巡逻状态,并且设置没有锁定玩家
	if (EPDistance > 1500.f) {
		// 告诉控制器丢失了玩家
		SEController->LoosePlayer();
		// 修改状态为巡逻
		OwnerComp.GetBlackboardComponent()->SetValueAsEnum(EnemyState.SelectedKeyName, (uint8)EEnemyAIState::ES_Patrol);
		// 跳出追逐状态
		return EBTNodeResult::Failed;
	}
	return EBTNodeResult::Succeeded;
}

这里是追逐时锁定玩家位置任务。

SlAiEnemyTaskLocaSP.h

protected:

	UPROPERTY(EditAnywhere, Category = "Blackboard")
	struct FBlackboardKeySelector Destination;

private:

	// 重写执行函数
	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent & OwnerComp, uint8* NodeMemory) override;

SlAiEnemyTaskLocaSP.cpp

// 引入头文件
#include "SlAiEnemyController.h"
#include "SlAiEnemyCharacter.h"
#include "NavigationSystem.h"	// 依旧是 4.26 用这个
#include "BehaviorTree/BehaviorTreeComponent.h"
#include "BehaviorTree/BlackboardComponent.h"

EBTNodeResult::Type USlAiEnemyTaskLocaSP::ExecuteTask(UBehaviorTreeComponent & OwnerComp, uint8* NodeMemory)
{
	// 如果初始化敌人参数不成功,直接返回失败
	if (!InitEnemyElement(OwnerComp)) return EBTNodeResult::Failed;

	// 范围是 20 以内
	const float ChaseRadius = 20.f;
	// 获取玩家到敌人之间的单位向量
	FVector SPToSE = SEController->GetPlayerLocation() - SECharacter->GetActorLocation();
	SPToSE.Normalize();
	// 探索起点是玩家位置减去与敌人之间距离的一点点
	const FVector ChaseOrigin = SEController->GetPlayerLocation() - 100.f * SPToSE;
	// 保存随机的位置
	FVector DesLoc(0.f);
	// 使用导航系统获取随机点(依旧是类后缀加上 V1)
	UNavigationSystemV1::K2_GetRandomReachablePointInRadius(SEController, ChaseOrigin, DesLoc, ChaseRadius);
	// 修改目的地
	OwnerComp.GetBlackboardComponent()->SetValueAsVector(Destination.SelectedKeyName, DesLoc);
	// 修改速度为 300
	SECharacter->SetMaxSpeed(300.f);
	return EBTNodeResult::Succeeded;
}

最后在敌人角色类的 BeginPlay() 这里初始化它的控制器指针,并且在看到玩家的方法里调用控制的 OnSeePlayer()。

SlAiEnemyCharacter.cpp


void ASlAiEnemyCharacter::BeginPlay()
{
	Super::BeginPlay();

	SEAnim = Cast<USlAiEnemyAnim>(GetMesh()->GetAnimInstance());
	// 初始化敌人控制器
	SEController = Cast<ASlAiEnemyController>(GetController());


}

void ASlAiEnemyCharacter::OnSeePlayer(APawn* PlayerChar)
{
	if (Cast<ASlAiPlayerCharacter>(PlayerChar)) {
		// 取消注释
		SlAiHelper::Debug(FString("I See Player !"), 3.f);
		// 告诉控制器我看到玩家了
		if (SEController) SEController->OnSeePlayer();
	}
}

编译代码,打开项目后调整敌人的行为树局部如下:

行为树追逐逻辑
运行游戏,在敌人面前时,屏幕左上角会弹出 Debug 信息,并且敌人会开始追逐玩家。玩家跑到离敌人 1500 单位远的距离后,敌人不会再继续追逐,而是重新回到巡逻状态。如果靠太近的话敌人不会再追逐,因为他进入了攻击状态,而我们还没有添加攻击逻辑。

在这里插入图片描述

49. C++ 动画 RootMotion

在添加敌人的攻击逻辑之前先来看看敌人的攻击动画。由于部分攻击动画有向前位移的要素,但是我们并不希望敌人能够在攻击的时候向前位移,而引擎提供的 EnableRootMotion 勾选框的效果又不是我们想要的(它把旋转也固定了),所以我们准备通过代码来控制骨骼的位移情况。

声明两个变量,一个记录根骨骼的位置,一个记录根骨骼的权重;再声明两个 float 变量,也用于控制动画播放时调整骨骼。

SlAiEnemyAnim.h

public:

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "EnemyAnim")
	FVector RootBonePos;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "EnemyAnim")
	float RootBoneAlpha;

protected:

	// 攻击的蒙太奇动画指针
	UAnimMontage* AnimAttack_I;
	UAnimMontage* AnimAttack_II;
	UAnimMontage* AnimAttack_III;
	UAnimMontage* AnimAttack_IV;

	// 攻击动画的指针
	UAnimSequence* AnimAttackSeq_III;
	UAnimSequence* AnimAttackSeq_IV;

	// 动作计时器
	float CurrentPlayTime;
	// 动作第一帧 Y 轴位置
	float StartYPos;
	

首先初始化头文件声明的那些参数。

在更改参数的帧函数里修改攻击动画 3 和 4 在播放时的骨骼位置。其实在这里的逻辑都是为了更新参数而已,实际控制动画的逻辑都放在了动画蓝图的节点那里,它会运用到暴露给蓝图的参数。

SlAiEnemyAnim.cpp

USlAiEnemyAnim::USlAiEnemyAnim()
{
	// ... 省略

	// 获取攻击的蒙太奇动画
	static ConstructorHelpers::FObjectFinder<UAnimMontage> StaticAnimAttack_I(TEXT("AnimMontage'/Game/Res/PolygonAdventure/Mannequin/Enemy/Animation/FightGroup/MonEnemy_Attack_I.MonEnemy_Attack_I'"));
	AnimAttack_I = StaticAnimAttack_I.Object;
	static ConstructorHelpers::FObjectFinder<UAnimMontage> StaticAnimAttack_II(TEXT("AnimMontage'/Game/Res/PolygonAdventure/Mannequin/Enemy/Animation/FightGroup/MonEnemy_Attack_II.MonEnemy_Attack_II'"));
	AnimAttack_II = StaticAnimAttack_II.Object;
	static ConstructorHelpers::FObjectFinder<UAnimMontage> StaticAnimAttack_III(TEXT("AnimMontage'/Game/Res/PolygonAdventure/Mannequin/Enemy/Animation/FightGroup/MonEnemy_Attack_III.MonEnemy_Attack_III'"));
	AnimAttack_III = StaticAnimAttack_III.Object;
	static ConstructorHelpers::FObjectFinder<UAnimMontage> StaticAnimAttack_IV(TEXT("AnimMontage'/Game/Res/PolygonAdventure/Mannequin/Enemy/Animation/FightGroup/MonEnemy_Attack_IV.MonEnemy_Attack_IV'"));
	AnimAttack_IV = StaticAnimAttack_IV.Object;

	// 获取攻击动画资源
	static ConstructorHelpers::FObjectFinder<UAnimSequence> StaticAnimAttackSeq_III(TEXT("AnimSequence'/Game/Res/PolygonAdventure/Mannequin/Enemy/Animation/FightGroup/Enemy_Attack_III.Enemy_Attack_III'"));
	AnimAttackSeq_III = StaticAnimAttackSeq_III.Object;
	static ConstructorHelpers::FObjectFinder<UAnimSequence> StaticAnimAttackSeq_IV(TEXT("AnimSequence'/Game/Res/PolygonAdventure/Mannequin/Enemy/Animation/FightGroup/Enemy_Attack_IV.Enemy_Attack_IV'"));
	AnimAttackSeq_IV = StaticAnimAttackSeq_IV.Object;
	

	Speed = 0.f;
	IdleType = 0.f;
	// 初始化参数
	RootBonePos = FVector::ZeroVector;
	RootBoneAlpha = 0.f;
	CurrentPlayTime = 0.f;
	StartYPos = 0.f;
}

void USlAiEnemyAnim::NativeUpdateAnimation(float DeltaSeconds)
{
	
	
	//定义 Transform 变量
	FTransform OutputTrans;
	// 如果正在播放攻击动画三,实时修改根骨骼位置
	if (Montage_IsPlaying(AnimAttack_III)) {
		CurrentPlayTime += DeltaSeconds;
		CurrentPlayTime = FMath::Clamp<float>(CurrentPlayTime, 0.f, AnimAttackSeq_III->GetPlayLength());
		// GetBoneTransform() 获取指定时间刻的动画中,骨骼的位置
		AnimAttackSeq_III->GetBoneTransform(OutputTrans, 0.f, CurrentPlayTime, true);
		RootBonePos = FVector(OutputTrans.GetLocation().X, StartYPos, OutputTrans.GetLocation().Z);
	}
	// 下面这段 else 只是测试代码,等下会注释掉
	else {
		Montage_Play(AnimAttack_III);
		CurrentPlayTime = 0.f;
		RootBonePos = FVector::ZeroVector;
		RootBoneAlpha = 1.f;
		// 获取攻击动画三的第一帧的 Y 值
		AnimAttackSeq_III->GetBoneTransform(OutputTrans, 0, 0.f, true);
		StartYPos = OutputTrans.GetLocation().Y;
	}
}

SlAiEnemyController.cpp

void ASlAiEnemyController::OnPossess(APawn* InPawn)
{
		
		// 把这句注释掉
		//BehaviorComp->StartTree(*BehaviorTreeObject, EBTExecutionMode::Looped);

}

编译后打开项目,给敌人的动画蓝图调整如下:

敌人动画蓝图调整

运行游戏,可看见敌人在原地不断播放第三种攻击动画,并且没有位移。

敌人攻击动画三

接下来给第四种攻击动画也做同样的操作;并且完善攻击动画的播放逻辑。

声明一个播放攻击动画并返回动画时长的方法。

SlAiEnemyAnim.h

#include "Animation/AnimInstance.h"
#include "SlAiTypes.h"	// 引入头文件

public:

	// 播放攻击动画,返回动画时长
	float PlayAttackAction(EEnemyAttackType AttackType);
	

SlAiEnemyAnim.cpp

void USlAiEnemyAnim::NativeUpdateAnimation(float DeltaSeconds)
{
	
	
	// 移走 Transform 的定义
	if (Montage_IsPlaying(AnimAttack_III)) {
		CurrentPlayTime += DeltaSeconds;
		CurrentPlayTime = FMath::Clamp<float>(CurrentPlayTime, 0.f, AnimAttackSeq_III->GetPlayLength());
		FTransform OutputTrans;		// 移到这里
		AnimAttackSeq_III->GetBoneTransform(OutputTrans, 0.f, CurrentPlayTime, true);
		RootBonePos = FVector(OutputTrans.GetLocation().X, StartYPos, OutputTrans.GetLocation().Z);
	}
	
	// 测试用的,现在注释掉或者删掉
	/*
	else {
		Montage_Play(AnimAttack_III);
		CurrentPlayTime = 0.f;
		RootBonePos = FVector::ZeroVector;
		RootBoneAlpha = 1.f;
		AnimAttackSeq_III->GetBoneTransform(OutputTrans, 0, 0.f, true);
		StartYPos = OutputTrans.GetLocation().Y;
	}
	*/

	// 如果正在播放攻击动画四,实时修改根骨骼位置
	if (Montage_IsPlaying(AnimAttack_IV)) {
		CurrentPlayTime += DeltaSeconds;
		CurrentPlayTime = FMath::Clamp<float>(CurrentPlayTime, 0.f, AnimAttackSeq_IV->GetPlayLength());
		FTransform OutputTrans;
		AnimAttackSeq_IV->GetBoneTransform(OutputTrans, 0.f, CurrentPlayTime, true);
		RootBonePos = FVector(OutputTrans.GetLocation().X, StartYPos, OutputTrans.GetLocation().Z);
	}

	// 如果已经不再播放有位移的攻击动画,但是权重还大于 0,在 0.3 秒内重置权重
	if (!Montage_IsPlaying(AnimAttack_III) && !Montage_IsPlaying(AnimAttack_IV) && RootBoneAlpha > 0.f) {
		RootBoneAlpha -= DeltaSeconds * 3;
		if (RootBoneAlpha < 0.f) RootBoneAlpha = 0.f;
	}
}


float USlAiEnemyAnim::PlayAttackAction(EEnemyAttackType AttackType)
{
	FRandomStream Stream;
	Stream.GenerateNewSeed();
	int SelectIndex = Stream.RandRange(0, 1);
	FTransform OutputTrans;
	switch (AttackType)
	{
	case EEnemyAttackType::EA_Normal:
		// 选择随机播放普通攻击动作
		if (SelectIndex == 0) {
			Montage_Play(AnimAttack_I);
			return AnimAttack_I->GetPlayLength();
		}
		else {
			Montage_Play(AnimAttack_II);
			return AnimAttack_II->GetPlayLength();
		}
		break;
	case EEnemyAttackType::EA_Dash:
		// 播放攻击三
		Montage_Play(AnimAttack_III);
		// 开始播放蒙太奇时依旧会执行下面这些逻辑,而不是等蒙太奇播放完了再执行
		CurrentPlayTime = 0.f;
		RootBonePos = FVector::ZeroVector;
		RootBoneAlpha = 1.f;
		// 获取攻击动画三的第一帧的 Y 值
		AnimAttackSeq_III->GetBoneTransform(OutputTrans, 0, 0.f, true);
		StartYPos = OutputTrans.GetLocation().Y;
		// 返回时长
		return AnimAttack_III->GetPlayLength();
		break;
	case EEnemyAttackType::EA_Pursuit:
		// 开始播放动画
		Montage_Play(AnimAttack_IV);
		CurrentPlayTime = 0.f;
		RootBonePos = FVector::ZeroVector;
		RootBoneAlpha = 1.f;
		// 获取攻击动画四的第一帧的 Y 值
		AnimAttackSeq_IV->GetBoneTransform(OutputTrans, 0, 0.f, true);
		StartYPos = OutputTrans.GetLocation().Y;
		return AnimAttack_IV->GetPlayLength();
		break;
	}
	return 0;
}

回到敌人 AI 控制器这里取消行为树运行的注释。

SlAiEnemyController.cpp

void ASlAiEnemyController::OnPossess(APawn* InPawn)
{
		
		// 取消注释
		BehaviorComp->StartTree(*BehaviorTreeObject, EBTExecutionMode::Looped);

}

给敌人类也添加一个播放攻击动画的方法,它会调用动画类的同名方法。

SlAiEnemyCharacter.h

#include "GameFramework/Character.h"
#include "SlAiTypes.h"	// 引入头文件

public:

	// 播放攻击动画,返回动画时长
	float PlayAttackAction(EEnemyAttackType AttackType);

SlAiEnemyCharacter.cpp

float ASlAiEnemyCharacter::PlayAttackAction(EEnemyAttackType AttackType)
{
	// 如果动作蓝图不存在直接返回 0 秒
	if (!SEAnim) return 0.f;
	// 返回攻击时长
	return SEAnim->PlayAttackAction(AttackType);
}

攻击动画的播放逻辑已经写好了,接下来创建一些 Task,下节课再开始写。

基于 SlAiEnemyTaskBase 创建以下敌人攻击逻辑要用到的子类:

  1. SlAiEnemyTaskAttackSwitch:切换敌人使用的攻击方式
  2. SlAiEnemyTaskAttackDash:冲刺攻击(攻击动画三)
  3. SlAiEnemyTaskAttackNormal:普通攻击
  4. SlAiEnemyTaskAttackPursuit:追逐攻击(攻击动画四)
  5. SlAiEnemyTaskAttackFollow:敌人使用追逐攻击时跟随玩家
  6. SlAiEnemyTaskRotate:敌人旋转朝向玩家

50. 敌人攻击 AI (上)

在编写敌人攻击逻辑之前,我们先准备一些方法用于判断玩家是否在远离敌人,这个判断会决定敌人是否采取攻击动作 4(位移比较长的攻击动画)。

首先需要有一个获取玩家指针的方法,用于在普通攻击时指定玩家。
然后声明一个 float 类型的数组来存放一段连续的时间内,敌人与玩家的距离。
既然与时间挂钩,那么声明一个计时器,并且提供一个方法给计时器绑定,用于更新这个距离数组的元素。
最后声明一个方法来封装这些逻辑,直接返回 bool 值,判断玩家是否在远离敌人。

SlAiEnemyController.h

public:

	// 判定玩家是否在远离
	bool IsPlayerAway();

	// 获取玩家指针
	UObject* GetPlayerPawn();

private:

	// 更新状态序列
	void UpdateStatePama();

private:

	// 保存与玩家的距离序列,保存 8 个,每 0.3 秒更新一次
	TArray<float> EPDisList;

	// 时间委托句柄
	FTimerHandle EPDisHandle;

SlAiEnemyController.cpp

void ASlAiEnemyController::BeginPlay()
{
	
	// 进行委托绑定
	FTimerDelegate EPDisDele = FTimerDelegate::CreateUObject(this, &ASlAiEnemyController::UpdateStatePama);
	GetWorld()->GetTimerManager().SetTimer(EPDisHandle, EPDisDele, 0.3f, true);
}


bool ASlAiEnemyController::IsPlayerAway()
{
	if (!IsLockPlayer || !SPCharacter || EPDisList.Num() < 6 || IsPlayerDead()) return false;
	int BiggerNum = 0;
	float LastDis = -1.f;
	// 只要有三个比前面的大,就判定远离了
	for (TArray<float>::TIterator It(EPDisList); It; ++It) {
		if (*It > LastDis) BiggerNum += 1;
		LastDis = *It;
	}
	return BiggerNum > 3;
}

UObject* ASlAiEnemyController::GetPlayerPawn()
{
	return SPCharacter;
}

void ASlAiEnemyController::UpdateStatePama()
{
	// 更新与玩家的距离序列
	if (EPDisList.Num() < 6) {
		EPDisList.Push(FVector::Distance(SECharacter->GetActorLocation(), GetPlayerLocation()));
	}
	else {
		EPDisList.RemoveAt(0);
		EPDisList.Push(FVector::Distance(SECharacter->GetActorLocation(), GetPlayerLocation()));
	}
}

攻击状态切换 Task,其实就是根据距离来判断敌人该用哪种攻击动作;如果玩家离开一定距离,敌人会用位移攻击;如果正在远离,则会使用位移较远的攻击。

SlAiEnemyTaskAttackSwitch.h

protected:

	UPROPERTY(EditAnywhere, Category = "Blackboard")
	struct FBlackboardKeySelector AttackType;
	
	UPROPERTY(EditAnywhere, Category = "Blackboard")
	struct FBlackboardKeySelector EnemyState;

private:

	// 重写执行函数
	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent & OwnerComp, uint8* NodeMemory) override;

SlAiEnemyTaskAttackSwitch.cpp

#include "SlAiEnemyController.h"
#include "SlAiEnemyCharacter.h"
#include "NavigationSystem.h"
#include "BehaviorTree/BehaviorTreeComponent.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "SlAiTypes.h"

EBTNodeResult::Type USlAiEnemyTaskAttackSwitch::ExecuteTask(UBehaviorTreeComponent & OwnerComp, uint8* NodeMemory)
{
	// 如果初始化敌人参数不成功,直接返回失败
	if (!InitEnemyElement(OwnerComp)) return EBTNodeResult::Failed;
	// 如果玩家已经死亡
	if (SEController->IsPlayerDead()) {
		// 告诉控制器丢失了玩家
		SEController->LoosePlayer();
		// 修改状态为巡逻
		OwnerComp.GetBlackboardComponent()->SetValueAsEnum(EnemyState.SelectedKeyName, (uint8)EEnemyAIState::ES_Patrol);
		// 跳出攻击状态
		return EBTNodeResult::Failed;
	}
	// 获取与玩家的距离
	float EPDistance = FVector::Distance(SECharacter->GetActorLocation(), SEController->GetPlayerLocation());
	// 如果距离小于 200
	if (EPDistance < 200.f) {
		// 修改攻击状态为普攻
		OwnerComp.GetBlackboardComponent()->SetValueAsEnum(AttackType.SelectedKeyName, (uint8)EEnemyAttackType::EA_Normal);
		return EBTNodeResult::Succeeded;
	}
	// 如果距离小于250并且判定到玩家在远离
	if (EPDistance < 300.f && SEController->IsPlayerAway()) {
		// 修改状态为追逐攻击
		OwnerComp.GetBlackboardComponent()->SetValueAsEnum(AttackType.SelectedKeyName, (uint8)EEnemyAttackType::EA_Pursuit);
		return EBTNodeResult::Succeeded;
	}
	if (EPDistance > 200.f && EPDistance < 300.f) {
		OwnerComp.GetBlackboardComponent()->SetValueAsEnum(AttackType.SelectedKeyName, (uint8)EEnemyAttackType::EA_Dash);
		return EBTNodeResult::Succeeded;
	}
	// 如果大于 300 
	if (EPDistance > 300.f) {
		// 修改攻击状态为追逐
		OwnerComp.GetBlackboardComponent()->SetValueAsEnum(EnemyState.SelectedKeyName, (uint8)EEnemyAIState::ES_Chase);
		// 跳出攻击状态
		return EBTNodeResult::Failed;
	}
	return EBTNodeResult::Failed;
}

普通攻击 Task,由于没有位移,所以要获得玩家指针以及声明一下等待时间的黑板值,让敌人在播放动作的时候面向玩家并在原地等待普通攻击动画结束。

SlAiEnemyTaskAttackNormal.h

protected:

	UPROPERTY(EditAnywhere, Category = "Blackboard")
	struct FBlackboardKeySelector PlayerPawn;
	
	UPROPERTY(EditAnywhere, Category = "Blackboard")
	struct FBlackboardKeySelector WaitTime;

private:

	// 重写执行函数
	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent & OwnerComp, uint8* NodeMemory) override;

SlAiEnemyTaskAttackNormal.cpp

// 引入头文件
#include "SlAiEnemyController.h"
#include "SlAiEnemyCharacter.h"
#include "NavigationSystem.h"
#include "BehaviorTree/BehaviorTreeComponent.h"
#include "BehaviorTree/BlackboardComponent.h"

EBTNodeResult::Type USlAiEnemyTaskAttackNormal::ExecuteTask(UBehaviorTreeComponent & OwnerComp, uint8* NodeMemory)
{
	// 如果初始化敌人参数不成功,直接返回失败
	if (!InitEnemyElement(OwnerComp)) return EBTNodeResult::Failed;
	// 播放普通攻击动画
	float AttackDuration = SECharacter->PlayAttackAction(EEnemyAttackType::EA_Normal);
	// 设置参数
	OwnerComp.GetBlackboardComponent()->SetValueAsObject(PlayerPawn.SelectedKeyName, SEController->GetPlayerPawn());
	OwnerComp.GetBlackboardComponent()->SetValueAsFloat(WaitTime.SelectedKeyName, AttackDuration);
	// 返回成功
	return EBTNodeResult::Succeeded;
}

冲刺攻击(攻击动画三) Task,相比之下就复杂了些许。播放攻击动画三时我们需要动态调节敌人的速度以及目标地点,确保敌人能以一个恰当的速度往玩家所在的地方进行位移攻击。而动画结束后敌人应该以原来的追逐速度追逐玩家。所以我们额外声明一个方法,用于恢复敌人的移动速度。并且运用一个计时器来绑定这个方法,在动画播放完后调用它。

SlAiEnemyTaskAttackDash.h

protected:

	// 动作结束后事件
	void OnAnimationTimerDone();

protected:
	
	UPROPERTY(EditAnywhere, Category = "Blackboard")
	struct FBlackboardKeySelector WaitTime;

	UPROPERTY(EditAnywhere, Category = "Blackboard")
	struct FBlackboardKeySelector Destination;

	// 攻击动作结束后的委托
	FTimerHandle TimerHandle;

private:

	// 重写执行函数
	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent & OwnerComp, uint8* NodeMemory) override;
	// 重写任务终止函数
	virtual EBTNodeResult::Type AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;

SlAiEnemyTaskAttackDash.cpp

// 引入头文件
#include "SlAiEnemyController.h"
#include "SlAiEnemyCharacter.h"
#include "NavigationSystem.h"
#include "BehaviorTree/BehaviorTreeComponent.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "TimerManager.h"


EBTNodeResult::Type USlAiEnemyTaskAttackDash::ExecuteTask(UBehaviorTreeComponent & OwnerComp, uint8* NodeMemory)
{
	// 如果初始化敌人参数不成功,直接返回失败
	if (!InitEnemyElement(OwnerComp)) return EBTNodeResult::Failed;

	// 播放突击动画
	float AttackDuration = SECharacter->PlayAttackAction(EEnemyAttackType::EA_Dash);
	// 范围是 5
	const float ChaseRadius = 5.f;
	// 获取玩家到敌人之间的单位向量
	FVector SPToSE = SEController->GetPlayerLocation() - SECharacter->GetActorLocation();
	SPToSE.Normalize();
	// 探索起点是玩家位置减去与敌人之间距离的一点点
	const FVector ChaseOrigin = SEController->GetPlayerLocation() - 20.f * SPToSE;
	// 保存随机的位置
	FVector DesLoc(0.f);
	// 使用导航系统获取随机点
	UNavigationSystemV1::K2_GetRandomReachablePointInRadius(SEController, ChaseOrigin, DesLoc, ChaseRadius);
	// 角色速度
	float Speed = (FVector::Distance(SECharacter->GetActorLocation(), DesLoc))	/ AttackDuration + 30.f;
	// 修改敌人速度
	SECharacter->SetMaxSpeed(Speed);
	// 修改目的地
	OwnerComp.GetBlackboardComponent()->SetValueAsVector(Destination.SelectedKeyName, DesLoc);
	// 设置参数
	OwnerComp.GetBlackboardComponent()->SetValueAsFloat(WaitTime.SelectedKeyName, AttackDuration);
	// 添加事件委托
	FTimerDelegate TimerDelegate = FTimerDelegate::CreateUObject(this, &USlAiEnemyTaskAttackDash::OnAnimationTimerDone);
	// 注册到事件管理器
	SEController->GetWorld()->GetTimerManager().SetTimer(TimerHandle, TimerDelegate, AttackDuration, false);
	// 继续执行
	return EBTNodeResult::Succeeded;
}

EBTNodeResult::Type USlAiEnemyTaskAttackDash::AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	// 如果初始化敌人参数不成功或者事件句柄没有激活,直接返回
	if (!InitEnemyElement(OwnerComp) || !TimerHandle.IsValid()) return EBTNodeResult::Aborted;
	// 卸载时间委托
	SEController->GetWorld()->GetTimerManager().ClearTimer(TimerHandle);
	// 返回
	return EBTNodeResult::Aborted;
}

void USlAiEnemyTaskAttackDash::OnAnimationTimerDone()
{
	// 重新设置速度为 300
	if (SECharacter) SECharacter->SetMaxSpeed(300.f);
}

戛然而止 = =💧

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值