创建一个 C++ 的 Slate Widget 类,地址为 /Public/UI/Widget,取名 SlAiEnemyHPWidget,作为敌人渲染在场景中的血条 Widget。
添加一个 UMG 的模块依赖。
SlAiCourse.Bulid.cs
PublicDependencyModuleNames.AddRange(new string[] {
"Core", "CoreUObject",
"Engine", "InputCore",
"Slate", "SlateCore",
"Json", "JsonUtilities",
"UMG" // 添加 UMG 模块依赖
});
给敌人血条界面添加一个进度条控件,血条的颜色变量,以及一个血条长度变更的方法。
SSlAiEnemyHPWidget.h
public:
// 提供给敌人角色来调用
void ChangeHP(float HP);
private:
TSharedPtr<class SProgressBar> HPBar;
// 结果颜色
FLinearColor ResultColor;
SSlAiEnemyHPWidget.cpp
// 引入头文件
#include "SProgressBar.h"
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlAiEnemyHPWidget::Construct(const FArguments& InArgs)
{
ChildSlot
[
SAssignNew(HPBar, SProgressBar)
];
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlAiEnemyHPWidget::ChangeHP(float HP)
{
HP = FMath::Clamp<float>(HP, 0.f, 1.f);
HPBar->SetPercent(HP);
ResultColor = FLinearColor(1.f - HP, HP, 0.f, 1.f); // 生命值高则是绿色,生命值低则是红色,跟 RGB 色值的 RG 有关
HPBar->SetFillColorAndOpacity(FSlateColor(ResultColor));
}
给敌人类添加一个 Widget 组件和感知组件。再添加一个敌人血条界面类的指针,用来放到 Widget 组件中,并且可以通过这个指针更改里面进度条组件的表现。
添加一个 float 类型的变量作为敌人的生命值。生命值会跟敌人血条界面类的进度条控件挂钩。
再添加一个看见玩家时敌人作出反应的方法。
最后为其添加一个 AI 控制器的引用,现在暂时用不上。
SlAiEnemyCharacter.h
class SLAICOURSE_API ASlAiEnemyCharacter : public ACharacter
{
public:
// 实时更新血条的朝向,由 Controller 调用,传入玩家位置
void UpdateHPBarRotation(FVector SPLocation);
protected:
// 血条
UPROPERTY(EditAnywhere, Category = "Mesh")
class UWidgetComponent* HPBar;
// 敌人感知
UPROPERTY(EditAnywhere, Category = "Mesh")
class UPawnSensingComponent* EnemySense;
private:
// 绑定到敌人感知的方法
UFUNCTION()
void OnSeePlayer(APawn* PlayerChar);
// 血条 UI 引用
TSharedPtr<class SSlAiEnemyHPWidget> HPBarWidget;
// 控制器引用
class ASlAiEnemyController* SEController;
// 生命值
float HP;
}
SlAiEnemyCharacter.cpp
#include "Perception/PawnSensingComponent.h"
// 添加头文件
#include "WidgetComponent.h"
#include "SSlAiEnemyHPWidget.h"
#include "SlAiHelper.h"
ASlAiEnemyCharacter::ASlAiEnemyCharacter()
{
// 实例化血条
HPBar = CreateDefaultSubobject<UWidgetComponent>(TEXT("HPBar"));
// Attach To 在 4.26 已经过时了,替换成如下方法
//HPBar->AttachTo(RootComponent);
HPBar->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform);
// 实例化敌人感知组件
EnemySense = CreateDefaultSubobject<UPawnSensingComponent>(TEXT("EnemySense"));
}
void ASlAiEnemyCharacter::BeginPlay()
{
// 设置血条 Widget
SAssignNew(HPBarWidget, SSlAiEnemyHPWidget);
HPBar->SetSlateWidget(HPBarWidget);
HPBar->SetRelativeLocation(FVector(0.f, 0.f, 100.f));
HPBar->SetDrawSize(FVector2D(100.f, 10.f));
// 设置初始生命值
HP = 100.f;
HPBarWidget->ChangeHP(HP / 200.f);
// 敌人感知参数设置
EnemySense->HearingThreshold = 0.f;
EnemySense->LOSHearingThreshold = 0.f;
EnemySense->SightRadius = 1000.f;
EnemySense->SetPeripheralVisionAngle(55.f);
EnemySense->bHearNoises = false;
// 绑定看到玩家的方法
FScriptDelegate OnSeePlayerDele;
OnSeePlayerDele.BindUFunction(this, "OnSeePlayer");
EnemySense->OnSeePawn.Add(OnSeePlayerDele);
}
void ASlAiEnemyCharacter::UpdateHPBarRotation(FVector SPLocation)
{
FVector StartPos(GetActorLocation().X, GetActorLocation().Y, 0);
FVector TargetPos(SPLocation.X, SPLocation.Y, 0.f);
HPBar->SetWorldRotation(FRotationMatrix::MakeFromX(TargetPos - StartPos).Rotator());
}
void ASlAiEnemyCharacter::OnSeePlayer(APawn* PlayerChar)
{
if (Cast<ASlAiPlayerCharacter>(PlayerChar)) {
SlAiHelper::Debug(FString("I See Player !"), 3.f);
}
}
给敌人的 AI 控制器添加一个玩家的指针和一个指向敌人自己的指针。添加这个类自身的构造函数(用于开启 Tick()),重写一下它的 Tick() 和 BeginPlay()。
再添加一个获取玩家位置的方法。
SlAiEnemyController.h
class SLAICOURSE_API ASlAiEnemyController : public AAIController
{
GENERATED_BODY()
public:
ASlAiEnemyController();
virtual void Tick(float DeltaTime) override;
// 获取玩家的位置
FVector GetPlayerLocation() const;
protected:
virtual void BeginPlay() override;
private:
// 玩家的指针
class ASlAiPlayerCharacter* SPCharacter;
// 敌人角色指针
class ASlAiEnemyCharacter* SECharacter;
}
SlAiEnemyController.cpp
// 添加头文件
#include "Kismet/GameplayStatics.h"
#include "SlAiPlayerCharacter.h"
#include "SlAiEnemyCharacter.h"
ASlAiEnemyController::ASlAiEnemyController()
{
PrimaryActorTick.bCanEverTick = true;
}
void ASlAiEnemyController::BeginPlay()
{
// 如果不调用父类函数,行为树就没法正常运作
Super::BeginPlay();
// 初始化一下玩家指针,这个指针会一直存在
SPCharacter = Cast<ASlAiPlayerCharacter>(UGameplayStatics::GetPlayerCharacter(GWorld, 0));
// 如果角色没有初始化
if (!SECharacter) SECharacter = Cast<ASlAiEnemyCharacter>(GetPawn());
}
void ASlAiEnemyController::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// 如果玩家指针和角色指针存在,一直修改角色血条朝向,老师偷懒没有区分视角
if (SECharacter && SPCharacter) SECharacter->UpdateHPBarRotation(SPCharacter->GetActorLocation());
}
FVector ASlAiEnemyController::GetPlayerLocation() const
{
// 如果玩家指针存在,返回玩家位置
if (SPCharacter) return SPCharacter->GetActorLocation();
return FVector::ZeroVector;
}
此时运行游戏,在敌人面前走动,可以在左上角看见输出的 Debug 语句,说明敌人的视觉感知模块添加成功。并且敌人头上的血条始终朝向玩家的角色。
在开始之前,建议对 UE4 行为树没有概念的读者先学习一下行为树。虚幻文档行为树相关入口
读者也可以在网上搜索一下 UE4 行为树相关的蓝图视频教程。
在搭建行为树之前,先添加 AI 的模块依赖。
SlAiCourse.Bulid.cs
PublicDependencyModuleNames.AddRange(new string[] {
"Core", "CoreUObject",
"Engine", "InputCore",
"Slate", "SlateCore",
"Json", "JsonUtilities",
"UMG",
"AIModule", "GameplayTasks" // 添加 AI 相关的模块依赖
});
在 /Blueprint/Enemy 目录下新建一个蓝图的 Behavior Tree(行为树)。命名为 EnemyBehaviorTree,作为敌人的行为树。
在 Public/AI 路径下创建以下 C++ 类:
创建一个 BlackboardData 类,命名为 SlAiEnemyBlackboard,作为敌人行为树用的黑板;
创建一个 BTDecorator 类,命名为 SlAiBTDecoratorBase,作为敌人行为树节点的装饰器基类;
创建一个 BTService 类,命名为 SlAiBTServiceBase,作为敌人行为树节点的服务基类;
创建一个 BTTaskNode 类,命名为 SlAiEnemyTaskBase,作为敌人行为树的任务节点基类。
以 SlAiEnemyTaskBase 为父类创建一个子类,名为 SlAiEnemyTaskWander,作为敌人的巡逻任务节点。
再在 /Blueprint/Enemy 下新建一个 Data Asset,以 SlAiEnemyBlackboard 为目标类,命名为 EnemyBlackboard,作为黑板类的数据资产实例(没有办法直接将黑板类指定为行为树目标黑板)。
在 EnemyBehaviorTree 里面指定 Blackboard Asset 为 EnemyBlackboard。
在数据控制类添加敌人 AI 状态的枚举,敌人包括以下 7 种状态
SlAiTypes.h
// 敌人 AI 状态
UENUM(BlueprintType)
enum class EEnemyAIState : uint8
{
ES_Patrol UMETA(DisplayName = "Patrol"),
ES_Chase UMETA(DisplayName = "Chase"),
ES_Escape UMETA(DisplayName = "Escape"),
ES_Attack UMETA(DisplayName = "Attack"),
ES_Hurt UMETA(DisplayName = "Hurt"),
ES_Defence UMETA(DisplayName = "Defence"),
ES_Dead UMETA(DisplayName = "Dead")
};
黑板类重写这个 PostLoad() 方法,用于注册黑板的属性并加入到黑板中。
SlAiEnemyBlackboard.h
public:
virtual void PostLoad() override;
如果从引擎的黑板界面直接创建属性,看起来就是先指定类型再定义 Entry;这里从代码来看则是先声明 Entry,再指定它的类型等其他细节。当然代码这里 Entry 定义好以后,还要加入到 Keys 这个容器里面。
SlAiEnemyBlackboard.cpp
// 引入头文件
#include "SlAiTypes.h"
#include "BehaviorTree/Blackboard/BlackboardKeyType_Enum.h"
#include "BehaviorTree/Blackboard/BlackboardKeyType_Vector.h"
#include "BehaviorTree/Blackboard/BlackboardKeyType_Float.h"
#include "BehaviorTree/Blackboard/BlackboardKeyType_Object.h"
#include "BehaviorTree/Blackboard/BlackboardKeyType_Bool.h"
void USlAiEnemyBlackboard::PostLoad()
{
Super::PostLoad();
// 目的地
FBlackboardEntry Destination;
Destination.EntryName = FName("Destination");
UBlackboardKeyType_Vector* DestinationKeyType = NewObject<UBlackboardKeyType_Vector>();
Destination.KeyType = DestinationKeyType;
// 敌人状态
FBlackboardEntry EnemyState;
EnemyState.EntryName = FName("EnemyState");
UBlackboardKeyType_Enum* EnemyStateKeyType = NewObject<UBlackboardKeyType_Enum>();
EnemyStateKeyType->EnumType = FindObject<UEnum>(ANY_PACKAGE, *FString("EEnemyAIState"), true);
// 上面这条语句其实可以不需要,下面这条语句已经提供了名字,引擎会自动搜索到这个名字的枚举并给 EnumType 赋值
EnemyStateKeyType->EnumName = FString("EEnemyAIState");
EnemyState.KeyType = EnemyStateKeyType;
Keys.Add(Destination);
Keys.Add(EnemyState);
}
实际上这个教程案例里面不会用到自己写的 Service 和 Decorator,我们这里只简单带过一下。
服务类重写它的特殊帧率执行方法。一般用来更新黑板数据。
SlAiBTServiceBase.h
protected:
// 按 AI 系统的特殊帧率进行更新,可以把数据更新放在这里
virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
SlAiBTServiceBase.cpp
void USlAiBTServiceBase::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
}
装饰器类在行为树中充当 If 来判断行为树的执行路线。所以重写它这个返回 bool 值的方法。
SlAiBTDecoratorBase.h
private:
// 对应的就是蓝图节点的 PerformConditionCheck
virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override;
SlAiBTDecoratorBase.cpp
bool USlAiBTDecoratorBase::CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const
{
return true;
}
Task 任务节点会执行相应的逻辑,一般作为行为树的叶节点。在 Task 基类添加一个初始化敌人控制器和敌人角色的方法。
SlAiEnemyTaskBase.h
protected:
// 初始化控制器和角色,不成功就返回 false
bool InitEnemyElement(UBehaviorTreeComponent& OwnerComp);
protected:
class ASlAiEnemyController* SEController;
class ASlAiEnemyCharacter* SECharacter;
SlAiEnemyTaskBase.cpp
// 添加头文件
#include "SlAiEnemyController.h"
bool USlAiEnemyTaskBase::InitEnemyElement(UBehaviorTreeComponent& OwnerComp)
{
// 如果已经初始化了,直接 return,避免重复调用
if (SEController && SECharacter) return true;
// 进行赋值
SEController = Cast<ASlAiEnemyController>(OwnerComp.GetAIOwner());
if (SEController) {
SECharacter = Cast<ASlAiEnemyCharacter>(SEController->GetPawn());
if (SECharacter) return true;
}
return false;
}
运行游戏,在行为树添加节点结构如下:(对应敌人的 7 种状态,注意,全部装饰器节点都要选 Observer aborts 为 self)
梁迪老师在这个教程里面把行为树当成状态机来用,所以读者仅需学会如何使用行为树就好了。。不用把老师的行为树结构奉为标准做法。
将导航网格添加到场景中,调整其位置和大小。然后把场景里的圆球树叶的树删掉,按 P 可以查看导航网格如下。
如果你的引擎版本是较新版本的话要在这里先加一个导航系统的依赖,笔者用的版本是 4.26.2。
SlAiCourse.Bulid.cs
PublicDependencyModuleNames.AddRange(new string[] {
"Core", "CoreUObject",
"Engine", "InputCore",
"Slate", "SlateCore",
"Json", "JsonUtilities",
"UMG",
"AIModule", "GameplayTasks",
"NavigationSystem" // 添加依赖
});
声明三个要用到的闲置动画指针,用于获取动画。再声明一个方法设置敌人当前的闲置动画是哪一个,并返回对应动画的时长。
SlAiEnemyAnim.h
// 声明一下类
class UAnimSequence;
class UAnimMontage;
UCLASS()
class SLAICOURSE_API USlAiEnemyAnim : public UAnimInstance
{
GENERATED_BODY()
public:
// 设置 Idle 模式,返回动作时长(老师的方法名拼写错了,我这里改正)
float SetIdleType(int NewType);
protected:
// 等待动作指针
UAnimSequence* AnimIdle_I;
UAnimSequence* AnimIdle_II;
UAnimSequence* AnimIdle_III;
}
SlAiEnemyAnim.cpp
USlAiEnemyAnim::USlAiEnemyAnim()
{
// 获取动作
static ConstructorHelpers::FObjectFinder<UAnimSequence> StaticAnimIdle_I(TEXT("AnimSequence'/Game/Res/PolygonAdventure/Mannequin/Enemy/Animation/MoveGroup/Enemy_Idle_I.Enemy_Idle_I'"));
AnimIdle_I = StaticAnimIdle_I.Object;
static ConstructorHelpers::FObjectFinder<UAnimSequence> StaticAnimIdle_II(TEXT("AnimSequence'/Game/Res/PolygonAdventure/Mannequin/Enemy/Animation/MoveGroup/Enemy_Idle_II.Enemy_Idle_II'"));
AnimIdle_II = StaticAnimIdle_II.Object;
static ConstructorHelpers::FObjectFinder<UAnimSequence> StaticAnimIdle_III(TEXT("AnimSequence'/Game/Res/PolygonAdventure/Mannequin/Enemy/Animation/MoveGroup/Enemy_Idle_III.Enemy_Idle_III'"));
AnimIdle_III = StaticAnimIdle_III.Object;
Speed = 0.f;
IdleType = 0.f;
}
float USlAiEnemyAnim::SetIdleType(int NewType)
{
IdleType = FMath::Clamp<float>((float)NewType, 0.f, 2.f);
switch (NewType) {
case 0:
return AnimIdle_I->GetPlayLength();
case 1:
return AnimIdle_II->GetPlayLength();
case 2:
return AnimIdle_III->GetPlayLength();
}
return AnimIdle_I->GetPlayLength();
}
给敌人角色声明一个设置移动速度的方法。敌人在巡逻时和追逐玩家等不同状态的时候移动速度是不一样的,而且将改变速度的逻辑写在方法里有利于解耦。
再添加一个方法用于通过 Anim 的 SetIdleType() 方法来获取闲置动画的总时长。
SlAiEnemyCharacter.h
public:
// 修改移动速度
void SetMaxSpeed(float Speed);
// 获取 Idle 等待时长
float GetIdleWaitTime();
private:
// 动作蓝图引用
class USlAiEnemyAnim* SEAnim;
SlAiEnemyCharacter.cpp
#include "SlAiEnemyController.h"
#include "SlAiEnemyAnim.h" // 引入头文件
void ASlAiEnemyCharacter::BeginPlay()
{
Super::BeginPlay();
// 获取动作引用
SEAnim = Cast<USlAiEnemyAnim>(GetMesh()->GetAnimInstance());
}
void ASlAiEnemyCharacter::SetMaxSpeed(float Speed)
{
// 设置最大运动速度
GetCharacterMovement()->MaxWalkSpeed = Speed;
}
float ASlAiEnemyCharacter::GetIdleWaitTime()
{
// 如果动作引用不存在直接返回 3 秒
if (!SEAnim) return 3.f;
// 创建随机流
FRandomStream Stream;
Stream.GenerateNewSeed();
int IdleType = Stream.RandRange(0, 2);
float AnimLength = SEAnim->SetIdleType(IdleType);
// 更新种子
Stream.GenerateNewSeed();
// 产生动作次数
int AnimCount = Stream.RandRange(1, 3);
// 返回全部的动画时长(这里老师将 IdleType 放进去了,这里就先改了)
return AnimLength * AnimCount;
}
void ASlAiEnemyCharacter::OnSeePlayer(APawn* PlayChar)
{
if (Cast<ASlAiPlayerCharacter>(PlayerChar)) {
// 先注释掉
//SlAiHelper::Debug(FString("I See Player !"), 3.f);
}
}
重写敌人 AI 控制器的支配和取消支配方法。支配方法主要是获取并运行行为树,以及初始化敌人或行为树的一些参数的;取消支配方法则停止行为树。
SlAiEnemyController.h
public:
ASlAiEnemyController();
// 添加支配和取消支配方法(4.26 版本这俩方法已经被 final 修饰了,但是有另外两个替代的方法,就是在前面加 On)
//virtual void Possess(APawn* InPawn) override;
//virtual void UnPossess() override;
virtual void OnPossess(APawn* InPawn) override;
virtual void OnUnPossess() override;
private:
// 添加黑板组件和行为树组件
class UBlackboardComponent* BlackboardComp;
class UBehaviorTreeComponent* BehaviorComp;
SlAiEnemyController.cpp
// 引入所有的头文件
#include "SlAiEnemyBlackboard.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "BehaviorTree/BehaviorTreeComponent.h"
#include "Kismet/GameplayStatics.h"
#include "TimerManager.h"
void ASlAiEnemyController::OnPossess(APawn* InPawn)
{
// 不调用父类函数,绑定行为树框架后也不会有任何效果
Super::OnPossess(InPawn);
// 在这里实例化一下角色,以免获取不到
SECharacter = Cast<ASlAiEnemyCharacter>(InPawn);
// 获取行为树资源
UBehaviorTree* StaticBehaviorTreeObject = LoadObject<UBehaviorTree>(NULL, TEXT("BehaviorTree'/Game/Blueprint/Enemy/EnemyBehaviorTree.EnemyBehaviorTree'"));
// 老师说如果只靠上面获取行为树的代码只能让一个敌人运行行为树,通过下面这里复制行为树才能让场景中所有敌人都得到行为树
UBehaviorTree* BehaviorTreeObject = DuplicateObject<UBehaviorTree>(StaticBehaviorTreeObject, NULL);
// 如果资源不存在,直接返回
if (!BehaviorTreeObject) return;
// 黑板也复制一份
BehaviorTreeObject->BlackboardAsset = DuplicateObject<USlAiEnemyBlackboard>((USlAiEnemyBlackboard*)StaticBehaviorTreeObject->BlackboardAsset, NULL);
// Blackboard 存在于父类,把它赋给头文件声明的黑板组件指针
BlackboardComp = Blackboard;
bool IsSuccess = true;
if (BehaviorTreeObject->BlackboardAsset && (Blackboard == nullptr || Blackboard->IsCompatibleWith(BehaviorTreeObject->BlackboardAsset) == false)) {
IsSuccess = UseBlackboard(BehaviorTreeObject->BlackboardAsset, BlackboardComp);
}
if (IsSuccess) {
BehaviorComp = Cast<UBehaviorTreeComponent>(BrainComponent);
if (!BehaviorComp) {
BehaviorComp = NewObject<UBehaviorTreeComponent>(this, TEXT("BehaviorComp"));
BehaviorComp->RegisterComponent();
}
BrainComponent = BehaviorComp;
check(BehaviorComp != NULL);
BehaviorComp->StartTree(*BehaviorTreeObject, EBTExecutionMode::Looped);
// 上面的代码其实就等同于这个方法
//RunBehaviorTree(BehaviorTreeObject);
// 设置预状态为巡逻
BlackboardComp->SetValueAsEnum("EnemyState", (uint8)EEnemyAIState::ES_Patrol);
// 上面这条语句也可用下面这里代替
/*
int32 EnemyStateIndex = BlackboardComp->GetKeyID("EnemyState");
BlackboardComp->SetValue(EnemyStateIndex, (UBlackboardKeyType_Enum::FDataType)EEnemyAIState::ES_Patrol);
*/
// 修改敌人的初始速度为 100
SECharacter->SetMaxSpeed(100.f);
}
}
void ASlAiEnemyController::OnUnPossess()
{
Super::OnUnPossess();
// 停止行为树
if (BehaviorComp) BehaviorComp->StopTree();
}
在黑板创建一个等待时间的 float 类型变量。
SlAiEnemyBlackboard.cpp
void USlAiEnemyBlackboard::PostLoad()
{
// ... 省略
// 等待时间
FBlackboardEntry WaitTime;
WaitTime.EntryName = FName("WaitTime");
WaitTime.KeyType = NewObject<UBlackboardKeyType_Float>();
Keys.Add(Destination);
Keys.Add(EnemyState);
// 添加变量
Keys.Add(WaitTime);
}
做好了以上的准备工作,这里开始写 AI 的巡逻任务。声明两个黑板值,一个是目的地,一个是等候时间;并且再重写任务的执行方法。
SlAiEnemyTaskWander.h
protected:
UPROPERTY(EditAnywhere, Category = "Blackboard")
struct FBlackboardKeySelector Destination;
// 修改等待时间
UPROPERTY(EditAnywhere, Category = "Blackboard")
struct FBlackboardKeySelector WaitTime;
private:
// 重写执行函数
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent & OwnerComp, uint8* NodeMemory) override;
SlAiEnemyTaskWander.cpp
// 引入头文件
#include "SlAiEnemyCharacter.h"
#include "SlAiEnemyController.h"
// 4.26 不用这个
//#include "AI/Navigation/NavigationSystem.h"
#include "NavigationSystem.h"
#include "BehaviorTree/BehaviorTreeComponent.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "BehaviorTree/Blackboard/BlackboardKeyType_Vector.h"
EBTNodeResult::Type USlAiEnemyTaskWander::ExecuteTask(UBehaviorTreeComponent & OwnerComp, uint8* NodeMemory)
{
// 如果这里初始化敌人参数不成功,直接返回失败
if (!InitEnemyElement(OwnerComp)) return EBTNodeResult::Failed;
// 闲逛范围是 1000
const float WanderRadius = 1000.f;
// 闲逛起点是自己的位置
const FVector WanderOrigin = SECharacter->GetActorLocation();
// 保存随机的位置
FVector DesLoc(0.f);
// 使用导航系统获取随机点(4.26 已经将 UNavigationSystem 改名为 UNavigationSystemV1,这里我就先改了)
// 如果 4.26 版本前面没加导航系统的依赖这里会无法识别这个方法
UNavigationSystemV1::K2_GetRandomReachablePointInRadius(SEController, WanderOrigin, DesLoc, WanderRadius);
// 当距离小于 500 时,重新找点
while(FVector::Distance(WanderOrigin, DesLoc) < 500.f) {
// 使用导航系统重新获取随机点
UNavigationSystemV1::K2_GetRandomReachablePointInRadius(SEController, WanderOrigin, DesLoc, WanderRadius);
}
// 修改速度为 100
SECharacter->SetMaxSpeed(100.f);
// 修改目的地
OwnerComp.GetBlackboardComponent()->SetValueAsVector(Destination.SelectedKeyName, DesLoc);
// 获取停顿时长
float TotalWaitTime = SECharacter->GetIdleWaitTime();
// 修改等待时长
OwnerComp.GetBlackboardComponent()->SetValueAsFloat(WaitTime.SelectedKeyName, TotalWaitTime);
// 返回成功
return EBTNodeResult::Succeeded;
}
编译后打开引擎,为了添加刚刚写好的巡逻逻辑,将行为树局部调整如下:
将原来场景中的敌人删掉,放入一个新的敌人类。运行后可见敌人正在场景中四处走动,走到目的地后会有等待时间,然后再进行下一次移动。