在游戏领域,行为树是常用的AI解决方案,用行为树可以快速明了地描述AI的行为模型,而UE4提也供了非常完善的行为树解决方案,不仅有用户友好的界面,而且也有多样化的底层支持。在官网的行为树快速入门指南中,我们可以了解到UE4行为树编辑器的使用以及用蓝图创造行为树节点的方式,而在一些特定的需求当中,蓝图相对于C++并不会非常灵活。因此,笔者稍微研究了下行为树C++层次中的内容,简单分享下行为树里各种节点的C++写法
首先上一张行为树完整图,是基于场景查询系统(EQS)快速入门制作的:
在官网EQS入门的例子中,AI大致遵循这样的逻辑:
- 没看到玩家,特定范围随机某个点巡逻
- 视觉感知到玩家,会转向看着玩家
- 玩家离开视线,调用EQS找到当前时刻最好的能看到玩家的位置,走过去
- 看不到玩家,再回到随机巡逻
而在笔者的行为树完整图中,添加了以下的节点:
- 任务节点(Task):JumpForNTimes -> 跳N次
- 服务节点(Service):TraceDistance -> 监控AI到某个点的距离
- 装饰器节点(Decorator):CheckActorDistance -> 检查AI到某个Actor的距离
最终想要达到的AI目的:
- 没看到玩家,特定范围随机某个点巡逻
- 视觉感知到玩家,会转向看着玩家,然后跳N次(JumpForNTimes),并且会在屏幕实时打印AI跟玩家的距离(TraceDistance)
- 玩家离开视线,调用EQS找到当前时刻最好的能看到玩家的位置,走过去
- 看不到玩家,再回到随机巡逻。但如果玩家再次接近到一定距离,AI会“警觉”(CheckActorDistance),执行跳N次的操作
下面就一起来看下这三个节点具体的写法。在写这些行为树节点具体逻辑之前,首先需要在Build.cs
的PublicDependencyModuleNames
加上AIModule
跟GameplayTasks
,保证三种节点所需要的方法都能支持
Task节点:JumpForNTimes
task节点表示AI实际的一种操作,我们在UE4源码的Runtime/AIModule/Classes/BehaviorTree/Tasks
中能够看到预设的许多task节点的定义。JumpForNTimes
这类操作并不是瞬时的,需要跳完N次后才会执行后面的动作,因此在写法上,可以参考BTTask_Wait
的实现。
首先创建BTTask_JumpForNTimes
类,继承UBTTaskNode
1 | // BTTask_JumpForNTimes.h |
- UBTTask_JumpForNTimes:JumpForNTimes任务节点定义
- JumpTimes:跳跃次数,是需要我们在编辑器里设置的内容,因此需要标注
UPROPERTY
+EditAnywhere
- ExecuteTask:任务执行时的逻辑。
EBTNodeResult
是task节点执行的结果表示- 对于非瞬时完成的任务(比如Wait),可以在
ExecuteTask
接口中返回EBTNodeResult::InProgress
(任务进行中),并在后面的TickTask
中判断完成条件,执行FinishLatentTask(OwnerComp, EBTNodeResult)
来通知任务的完成结果
- TickTask:每个tick中该任务执行的逻辑
- GetInstanceMemorySize:获取节点实例自己的内存空间大小,用于预分配内存
- GetNodeIconName:编辑器里节点icon
- JumpTimes:跳跃次数,是需要我们在编辑器里设置的内容,因此需要标注
- FBTJumpForNTimesTaskMemory:我们任务节点自带的内存空间,放着节点私有的变量
- JumpTimesInternal:实际用来记录跳跃次数的计数器
- 在GetInstanceMemorySize返回sizeof结构体,这样引擎会预分配相应大小的内存块
- 在ExecuteTask、TickTask可以通过转换uint8* NodeMemory获得内存块对应结构体的实例
在cpp逻辑里,JumpForNTimes
可以这样实现:
1 | // BTTask_JumpForNTimes.cpp |
Service节点:TraceDistance
Service节点通常用于在某个行为节点/分支执行过程中,执行响应的检查逻辑更新黑板,亦或是作为sidecar式的逻辑监控节点/分支的运行情况。
TraceDistance只用于实时监控AI到某个点的距离,并且在屏幕上打印数据。在实现上可以参考已有的BTService_DefaultFocus
跟BTService_RunEQS
来写
1 | // BTService_TraceDistance.h |
在头文件里定义UBTService_TraceDistance
,继承UBTService_BlackboardBase
。UBTService_BlackboardBase
默认提供了一个可选黑板Key的属性,在编辑器里可以看到这个选项的。其他属性如下:
TickNode
是这个Service生命周期里的一个hook函数,表示每Tick的行为OnCeaseRelevant
也是这个Service生命周期里的hook函数,表示当行为树运行到和这个Service不相关(Service结束服务)时候的逻辑操作- 当然除了这两者还有一个
OnBecomeRelevant
钩子表示当运行到和Service相关的节点/分支时候的逻辑操作。
我们打算在每个tick时候在特定的LogKey
打印AI与玩家的距离,而在玩家没法再看到(TargetActor被清空)时候打印“追踪距离结束”的字样。因此cpp里可以这样写:
1 | // BTService_TraceDistance.cpp |
Decorator节点:CheckActorDistance
Decorator装饰器节点通常用来表示某种条件判断。条件判断成立后,AI的某些行为是否执行,或者执行优先级,都会有所变化。
在CheckActorDistance装饰器里,我们希望实现判断AI跟某个Actor距离在某个范围内,就优先执行装饰器装饰到的节点/分支的行为。我们可以参照其它预设装饰器的实现,来编写CheckActorDistance的逻辑
1 | // BTDecorator_CheckActorDistance.h |
- DistanceRadius:编辑器里需要设置的半径范围
- CalculateRawConditionValue:用来计算装饰器条件是否成立的接口
我们只需要在CalculateRawConditionValue
里判断距离是否在给定范围内就好。
1 | // BTDecorator_CheckActorDistance.cpp |
值得一提的是,在黑板里最好加个LastTargetActor
表示上一个看到的TargetActor
,然后在AIController里丢失视线的逻辑中,在清空TargetActor
之前,把LastTargetActor
设置为当前的TargetActor
,这样上一个TargetActor
在丢失视线后回来到一定范围,AI就会有“预警”效果了
总结
UE的AIModule非常的大,行为树只是冰山一角,还有很多需要细细研究。