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中的阵营判定就能正常的作用了