AI_Perception

前言

AI感知是UE自带的一个用于AI发现目标的功能, 使用起来不算麻烦, 本文简单记录一下此模块的内容, 并研究一部分源码看看是否有啥坑 /手动狗头

感知模块最核心的就是感知组件3个事件, 能返回检测到的Actor以及部分参数

image-20230202141025532

整个模块基本围绕着这一点展开

类之间的关系

  • AISystem: AI系统的总管理者, 负责创建UAIPerceptionSystemUBehaviorTreeManager等等一切AI相关的管理者(单例);
  • AIPerceptionSystem: AI感知的管理者, 保存了所有Source, Listener和一些重要数据, 另外是作为感知模块的Tick的发起者
  • AIScense: 感知逻辑的处理对象, 继承出来包括Sight,Hearing等等不同的感知类, 感知的核心逻辑处理都此类里
  • AISenseConfig: 作为数据配置的类, 主要服务于AIScense类
  • AIPerceptionStimuliSourceComponent: 用于注册Source的组件, 默认情况下Pawn类型的Actor会自动注册
  • AIPerceptionComponent: 感知组件, 通过AISenseConfig配置感知类型和对应参数, 挂载了此组件的类会自动注册为Listener

流程简述

  1. AISystem创建AIPerceptionSystem, 并且绑定了ActorSpawn事件,默认情况下如果是Pawn会自动将其注册为Source
  2. 在AIPerceptionSystem启动之前, 所有挂载AIPerceptionStimuliSourceComponent和AIPerceptionComponent对象会将自身注册到AIPerceptionSystem系统中
  3. AIPerceptionSystem执行Tick
    1. 遍历所有Sense对象处理对应的感知逻辑
    2. 遍历所有Listener(AIPerceptionComponent), 符合条件就广播告知感知目标信息变化

检测方法

在运行时, 按下标点符号的上引号键可以开启AI Debug, 按小键盘4可以开启Perception的调试

如下

image-20230202105716134

Sight

image-20230202105443258

最基础的视线检测, 比较容易理解, 在视锥体内符合条件的都会被检测到

过程全自动

  • AutoSuccessRangeFromLastSeenLocation: 下面的UAISense_Sight::Update()中有详解
  • DetectionByAffilliation: 检测的阵营, 看下面的阵营判定

检测事件的2个位置参数说明

image-20230202114808869

Hearing

image-20230202111011684

听觉, 需要事件触发

Sence_Hearing类在update的时候会刷选所有收到的NoiseEvent, 符合条件的就观察成功

image-20230202110945914

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

位置数据类似Sight

Damage

image-20230202111534938

image-20230202111615498

更直接粗暴, 通过事件直接触发, 类似Hearing, 但是没有距离限制

传递的2个FVector关系如下

image-20230202114947819

其他

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

image-20230202135339159

蓝图没有任何API与此相关, cpp也查不到相关的使用, 看上去就是让我们自己扩展的

以Touch为例, 先看源码, 一般只需要去看Update方法内的实现即可

image-20230202135500021

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

image-20230202135558559

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

image-20230202135741529


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

image-20230202140143995

Config类里可以随便加变量

image-20230202140423849

同样的, 逻辑处理可以重写上述几个方法, 笔者没有具体尝试, 感觉应该少了一些API, 需要自己扩展

部分代码细节拆分

Source注册

注册有几种方式, 之前有提到的Pawn可以自动注册, 还有就是挂载了AIPerceptionStimuliSourceComponent组件的对象, 可以手动调用组件的API注册或者让组件自动注册

image-20230201192027072

image-20230201192037906

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

image-20230201192526913

Listener注册

image-20230201195939180

AIPerceptionComponent组件初始化的时候会先注册SenseConfig(同Source), 这一步默认是自动的

如果关闭了bStartEnabled, 那么需要手动调用ConfigureSense()重新设置config来启动

接下来刷新Subsystem中的Listener容器, 同样会通知到对应的Sense对象

AIPerceptionSystem::Tick

image-20230201200056378

首先会将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, 但是并没有使用

image-20230201164409501

UAISense_Sight::Update()

首先遍历所有在实现内和实现外对象的Score

image-20230201165536739

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

image-20230201165811137

然后开始检测逻辑

自上而下if/else

  • 自动检测

如果AutoSuccessRangeFromLastSeenLocation的值大约0, 那么这一步会开启;

意思是如果当前目标在发现他的位置一定范围内, 那么就不会重新查找, 继续保持这个位置, 算是性能优化的一部分

具体实现可以去看虚函数ShouldAutomaticallySeeTarget()

  • 视锥检测

顾名思义的AI的视锥范围检测, 这里有射线检测来判断是否有遮挡物的存在

这里有一个扩展选项, 如果Source单位继承了IAISightTargetInterface接口可以重写一些视线检测方法

  • 已经记住的目标

之前的目标如果没有被遗忘, 那么继续把上一次的目标作为当前目标返回


最后会让减少生命周期从而降低前面提到过的Score

UAIPerceptionComponent::ProcessStimuli()

这里就是对前面传入的StimuliToProcess对象数组进行遍历处理, 把合适的数据广播出去, 重点不多,

主要是对检测生命周期的处理和判断, 然后广播对应事件以及处理Forgot对象

image-20230201201528268

阵营判定

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

image-20230201205546756

Sense类里的阵营判断就在上图中的2条, 在RegisterTarget()中会进行判定

仔细看源码会发现用了很多的位运算, 一眼看去还是不太容易理解

这个要结合几个地方一起看

image-20230202093703946

首先Source对象actor需要继承IGenericTeamAgentInterface, 否则都会被当做中立

然后重写其中的方法, 将TeamID传递进去, 如下

image-20230202094305439

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

image-20230202094158984

可以看到有一个解算类, 如果不进行什么操作会使用默认的方法, 如下

1
2
3
4
ETeamAttitude::Type DefaultTeamAttitudeSolver(FGenericTeamId A, FGenericTeamId B)
{
return A != B ? ETeamAttitude::Hostile : ETeamAttitude::Friendly;
}

意味着只要不相等,那都是敌对, 基本也符合, 那如果我们要把中立怪也当做非敌对关系, 就需要自己写一个方法再设置一下, 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace
{
ETeamAttitude::Type NextTeamAttitudeSolver(FGenericTeamId A, FGenericTeamId B)
{
if (A == B || A == ETeamAttitude::Neutral || B == ETeamAttitude::Neutral)
{
return ETeamAttitude::Friendly;
}
else
{
return ETeamAttitude::Hostile;
}
//return A == B ? ETeamAttitude::Friendly : ETeamAttitude::Hostile;
}
}

在合适的实际设置一下即可

1
FGenericTeamId::SetAttitudeSolver(&NextTeamAttitudeSolver);

这样Perception中的阵营判定就能正常的作用了