[UE4入门笔记(17)] 51.C++搭建行为树框架 52.实现巡逻AI --梁迪老师UE4纯C++&Slate开发沙盒游戏

前言:

笔者目前在校本科大三,目标方向是人工智能、计算机视觉。上一个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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值