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 创建以下敌人攻击逻辑要用到的子类:
- SlAiEnemyTaskAttackSwitch:切换敌人使用的攻击方式
- SlAiEnemyTaskAttackDash:冲刺攻击(攻击动画三)
- SlAiEnemyTaskAttackNormal:普通攻击
- SlAiEnemyTaskAttackPursuit:追逐攻击(攻击动画四)
- SlAiEnemyTaskAttackFollow:敌人使用追逐攻击时跟随玩家
- 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);
}
戛然而止 = =💧