AI_Perception
前言
AI感知是UE自带的一个用于AI发现目标的功能, 使用起来不算麻烦, 本文简单记录一下此模块的内容, 并研究一部分源码看看是否有啥坑 /手动狗头
感知模块最核心的就是感知组件3个事件, 能返回检测到的Actor以及部分参数

整个模块基本围绕着这一点展开
类之间的关系
- AISystem: AI系统的总管理者, 负责创建
UAIPerceptionSystem和UBehaviorTreeManager等等一切AI相关的管理者(单例); - AIPerceptionSystem: AI感知的管理者, 保存了所有Source, Listener和一些重要数据, 另外是作为感知模块的Tick的发起者
- AIScense: 感知逻辑的处理对象, 继承出来包括Sight,Hearing等等不同的感知类, 感知的核心逻辑处理都此类里
- AISenseConfig: 作为数据配置的类, 主要服务于AIScense类
- AIPerceptionStimuliSourceComponent: 用于注册Source的组件, 默认情况下Pawn类型的Actor会自动注册
- AIPerceptionComponent: 感知组件, 通过
AISenseConfig配置感知类型和对应参数, 挂载了此组件的类会自动注册为Listener
流程简述
- AISystem创建AIPerceptionSystem, 并且绑定了ActorSpawn事件,默认情况下如果是Pawn会自动将其注册为Source
- 在AIPerceptionSystem启动之前, 所有挂载AIPerceptionStimuliSourceComponent和AIPerceptionComponent对象会将自身注册到AIPerceptionSystem系统中
- AIPerceptionSystem执行Tick
- 遍历所有Sense对象处理对应的感知逻辑
- 遍历所有Listener(AIPerceptionComponent), 符合条件就广播告知感知目标信息变化
检测方法
在运行时, 按下标点符号的上引号键可以开启AI Debug, 按小键盘4可以开启Perception的调试
如下

Sight

最基础的视线检测, 比较容易理解, 在视锥体内符合条件的都会被检测到
过程全自动
- AutoSuccessRangeFromLastSeenLocation: 下面的
UAISense_Sight::Update()中有详解 - DetectionByAffilliation: 检测的阵营, 看下面的阵营判定
检测事件的2个位置参数说明

Hearing

听觉, 需要事件触发
Sence_Hearing类在update的时候会刷选所有收到的NoiseEvent, 符合条件的就观察成功

- 如果设置的MaxAge大于0 , 那么过一段时间后会自动遗忘掉这个Source目标
- 阵营判定同Sight
位置数据类似Sight
Damage


更直接粗暴, 通过事件直接触发, 类似Hearing, 但是没有距离限制
传递的2个FVector关系如下

其他
前面三种是最常用的, 一般能够涵盖绝大多数需求, UE还给我们加了几种现成的但是不算很完整的检测

蓝图没有任何API与此相关, cpp也查不到相关的使用, 看上去就是让我们自己扩展的
以Touch为例, 先看源码, 一般只需要去看Update方法内的实现即可

比较简单, 就是需要我们手动的注册一个事件, 然后下一次Update的时候会通知到Listener

封装一个蓝图函数库即可, 需要注意的是2个Actor变量的含义, FAITouchEvent中的TouchReceiver是AI感知组件的拥有者, OtherActor是最终被检测出来的Actor目标(有点奇怪), 所以如果Perception组件是放在AIcontroller上的, 那传递参数就是这样的

此外, 还可以用蓝图扩展, 分别是UAISense_Blueprint和UAISenseConfig_Blueprint两个类

Config类里可以随便加变量

同样的, 逻辑处理可以重写上述几个方法, 笔者没有具体尝试, 感觉应该少了一些API, 需要自己扩展
部分代码细节拆分
Source注册
注册有几种方式, 之前有提到的Pawn可以自动注册, 还有就是挂载了AIPerceptionStimuliSourceComponent组件的对象, 可以手动调用组件的API注册或者让组件自动注册


注册后会先存在TArray<FPerceptionSourceRegistration> SourcesToRegister中, 另外Source对应的SenceConfig配置也会新创建对应的Sense类保存在TArray<UAISense*> Senses中

Listener注册

在AIPerceptionComponent组件初始化的时候会先注册SenseConfig(同Source), 这一步默认是自动的
如果关闭了bStartEnabled, 那么需要手动调用ConfigureSense()重新设置config来启动
接下来刷新Subsystem中的Listener容器, 同样会通知到对应的Sense对象
AIPerceptionSystem::Tick

首先会将Source注册进来的原始数据TArray<FPerceptionSourceRegistration> SourcesToRegister转换成
TMap<const AActor*, FPerceptionStimuliSource> RegisteredStimuliSources;, 后续使用的都是后者
此过程中会绑定Actor销毁事件用于移除Source数据,
另外也执行了Sense类的虚函数RegisterTarget()
目前RegisterTarget()函数只在Sight类型里实现
然后推进所有Sense对象的时间线, 用于控制刷新频率
执行所有Sense对象的Tick, 最终会执行每个Sense对象的Update()虚函数
最后是执行所有Listener对象的UAIPerceptionComponent组件的ProcessStimuli()函数
UAISense_Sight::RegisterSource
主要做了一件事, 初始化或者刷新保存的观察对象的重要性
这里发现这里获取了一下观察目标的队伍ID, 但是并没有使用

UAISense_Sight::Update()
首先遍历所有在实现内和实现外对象的Score

Score由注册时的重要性和Age决定, 下图大致看一下这些数据

然后开始检测逻辑
自上而下if/else
- 自动检测
如果AutoSuccessRangeFromLastSeenLocation的值大约0, 那么这一步会开启;
意思是如果当前目标在发现他的位置一定范围内, 那么就不会重新查找, 继续保持这个位置, 算是性能优化的一部分
具体实现可以去看虚函数ShouldAutomaticallySeeTarget()
- 视锥检测
顾名思义的AI的视锥范围检测, 这里有射线检测来判断是否有遮挡物的存在
这里有一个扩展选项, 如果Source单位继承了
IAISightTargetInterface接口可以重写一些视线检测方法
- 已经记住的目标
之前的目标如果没有被遗忘, 那么继续把上一次的目标作为当前目标返回
最后会让减少生命周期从而降低前面提到过的Score
UAIPerceptionComponent::ProcessStimuli()
这里就是对前面传入的StimuliToProcess对象数组进行遍历处理, 把合适的数据广播出去, 重点不多,
主要是对检测生命周期的处理和判断, 然后广播对应事件以及处理Forgot对象

阵营判定
阵营判定在几个地方有用到, 如在sight和hearing中有检测阵营的选项, 默认勾选了Enemies的检测, 如果什么都不做, 那么是只能检测到中立单位(查找目标也是中立, 所有source也是中立)

Sense类里的阵营判断就在上图中的2条, 在RegisterTarget()中会进行判定
仔细看源码会发现用了很多的位运算, 一眼看去还是不太容易理解
这个要结合几个地方一起看

首先Source对象actor需要继承IGenericTeamAgentInterface, 否则都会被当做中立
然后重写其中的方法, 将TeamID传递进去, 如下

然后需要关注FGenericTeamId中几个全局静态方法

可以看到有一个解算类, 如果不进行什么操作会使用默认的方法, 如下
1 | ETeamAttitude::Type DefaultTeamAttitudeSolver(FGenericTeamId A, FGenericTeamId B) |
意味着只要不相等,那都是敌对, 基本也符合, 那如果我们要把中立怪也当做非敌对关系, 就需要自己写一个方法再设置一下, 如下
1 | namespace |
在合适的实际设置一下即可
1 | FGenericTeamId::SetAttitudeSolver(&NextTeamAttitudeSolver); |
这样Perception中的阵营判定就能正常的作用了