前言:
笔者目前在校本科大三,目标方向是人工智能、计算机视觉。上一个OpenCV学习笔记专栏已完结,在学习完OpenCV后,我继续学习C++,并用纯C++做UE4项目的方式继续提升自己的水平。
梁迪老师的水平非常高,他的课程本来也无需笔记:课程本身即为最好的笔记。但由于我天赋有限,还是边看边记,以防遗忘——知识点太多,步骤太繁杂了。在学习过程中,我也偶有思考,思索为什么某个方法老师要这样做。所以,一是为了记录,二是为了分享,才有了这个专栏。
内容方面,由于我在开启这个专栏时,此项目已经做完很多了。所以,前期的一些大篇幅叙述的知识,可能在后期应用中一带而过。以及,前期的一些知识,后期会重新剖析,并加上我的个人理解。
另外,若有学术交流/学业交流意愿,可以邮件联系1246210283@qq.com,希望一齐进步。
本篇学习内容:
51.C++搭建行为树框架
52.实现巡逻AI
51.C++搭建行为树框架
(1)Build.cs中添加模块:“AIModule”、“GameplayTasks”,运行游戏,关掉,关掉工程,打开UE4编辑器
(2)创建行为树EnemyBehaviorTree、SlAiEnemyBlackboard(继承于BlackboardData)、SlAiBTDecorator(继承于BTDecorator)、SlAiBTServiceBase(继承于BTService)、SlAiEnemyTaskBase(继承于BTTaskNode)、SlAiEnemyTaskWander(继承于SlAiEnemyTaskBase)、创建Data Asset继承于SlAiEnemyBlackboard并在EnemyBehaviorTree设置。
(3)Types里准备枚举类
//敌人AI状态
UENUM(BlueprintType)
enum class EEnemyAIState : uint8
{
ES_Patrol UMETA(DiaplayName = "Patrol"),
ES_Chase UMETA(DiaplayName = "Chase"),
ES_Escape UMETA(DiaplayName = "Escape"),
ES_Attack UMETA(DiaplayName = "Attack"),
ES_Hurt UMETA(DiaplayName = "Hurt"),
ES_Defence UMETA(DiaplayName = "Defence"),
ES_Dead UMETA(DiaplayName = "Dead"),
};
(4)写Blackboard
注:Blackboard相当于内存,行为树相当于CPU
注:如果想给Blackboard添加变量,需要重写PostLoad方法
重写PostLoad方法:
#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); //在C++中把刚刚创建的枚举添加进去。也可以在UE4编辑器内进行
EnemyStateKeyType->EnumName = FString("EEnemyAIState"); //其实UE4运行之后 也会通过这个名字来获取实例指定到EnumType
EnemyState.KeyType = EnemyStateKeyType;
Keys.Add(Destination);
Keys.Add(EnemyState);
//这样,行为树就可以得到这些变量,来作为行为树节点的判断、调用 等
}
(5)写TaskBase
注:Service,服务器,可以有一个帧执行(AI特殊帧率)的节点,一般用于更新黑板数据
注:Decorator,选择器,判定节点要不要往下运行
注:Task,写人物的动作、行为。TaskBase在本项目是所有任务节点的基类
bool USlAiEnemyTaskBase::InitEnemyElement(UBehaviorTreeComponent& OwnerComp)
{
//如果已经初始化了,直接return,避免重复调用
if (SECharacter && SEController) return true;
//进行赋值
SEController = Cast<ASlAiEnemyController>(OwnerComp.GetAIOwner());
if (SEController) {
SECharacter = Cast<ASlAiEnemyCharacter>(SEController->GetPawn());
if (SECharacter) return true;
}
return false;
}
(6)设置行为树
最终的行为树是纯粹的分支方法,当然,这是一种较为简单的行为树框架。
(7)添加寻路组件 Nav Mesh Bounds
按P查看寻路渲染
52.实现巡逻AI
(1)与Controller绑定
virtual void Possess(APawn* InPawn) override;
virtual void UnPossess() override;
class UBlackboardComponent* BlackboardComp;
class UBehaviorTreeComponent* BehaviorComp;
void ASlAiEnemyController::Possess(APawn * InPawn)
{
//绝对要调用父类函数,否则绑定行为树不会有任何效果
Super::Possess(InPawn);
//Possess会在BeginPlay之前运行,所以在这里实例化一下角色
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;
//获取行为树资源的BlackboardAsset,强转为USlAiEnemyBlackboard,然后复制一份。否则多个行为树的BlackboardAsset会指向同一个资源
BehaviorTreeObject->BlackboardAsset = DuplicateObject<USlAiEnemyBlackboard>((USlAiEnemyBlackboard*)StaticBehaviorTreeObject->BlackboardAsset, NULL);
//接下来进行资源绑定
BlackboardComp = Blackboard; //父类里面有的 赋值给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);//启动行为树
//设置预状态为巡逻
BlackboardComp->SetValueAsEnum("EnemyState", (uint8)EEnemyAIState::ES_Patrol);//把这个行为树绑定的黑板下叫EnemyState的变量改为了巡逻
//第二种写法:
//int32 EnemyStateIndex = BlackboardComp->GetKeyID("EnemyState");
//BlackboardComp->SetValue<UBlackboardKeyType_Enum>(EnemyStateIndex, (UBlackboardKeyType_Enum::FDataType)EEnemyAIState::ES_Patrol);
//设置初始速度
SECharacter->SetMaxSpeed(100.f);
}
//上述代码来源于 RunBehaviorTree(BehaviorTreeObject)
}
(2)Anim中准备等待动作指针
实现目标:敌人巡逻到随机地点,等待一段时间,播放等待的动作。
UAnimSequence* AnimIdle_I;
UAnimSequence* AnimIdle_II;
UAnimSequence* AnimIdle_III;
(3)Character中准备获取Idle等待时长函数,函数中随机选取动作,并播放1-4次
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();
}
float ASlAiEnemyCharacter::GetIdleWaitTime()
{
/*
产生随机种子,随机播放某个动作1-4次。由于产生随机动画的种子和产生随机次数的种子都是一个随机流Stream,所以中间要更新种子。
*/
//如果动作蓝图不存在,直接返回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, 4);
//返回全部的动画时长
return AnimLength * AnimCount;
}
(4)写巡逻任务TaskWander
重写执行函数,创建变量目的地和等待时间
注:执行函数的重写是在之前创建的SlAiEnemyTaskWander
文件中。
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);
//使用导航系统获取随机点 参数:一个在世界内的物体、起始点、保存的随机目的地、半径
UNavigationSystem::K2_GetRandomReachablePointInRadius(SEController, WanderOrigin, DesLoc, WanderRadius);
//当距离小于500时,重新找点
while (FVector::Distance(WanderOrigin, DesLoc) < 500.f) {
UNavigationSystem::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;
}
注:还要在Blackboard中添加等待时长变量WaitTime