--- title: Ue4 AI相关功能笔记 date: 2021-09-07  19:57:53 tags: AI rating: ⭐️ --- ## 其他文档 UE4场景询问系统浅析(EQS与行为树 http://www.uejoy.com/?p=500 UE4 AIModule源码阅读之AI感知系统 https://zhuanlan.zhihu.com/p/356716030 ## AI感知系统(AI Perception System) 文档:https://docs.unrealengine.com/4.26/zh-CN/InteractiveExperiences/ArtificialIntelligence/AIPerception/ 给角色类添加AIPerception组件并且添加所需要类型的感知,即可实现感知效果。 - AI伤害:对Event Any Damage、Event Point Damage或Event Radial Damage作出反应 - AI听觉:可用于检测由 报告噪点事件(Report Noise Event) 产生的声音 - AI感知报告:在PredictionTime秒内向请求者提供PredictedActor的预计位置。 - AI视觉:当一个Actor进入视觉半径后,AI感知系统将发出更新信号,并穿过被看到的Actor(举例而言,一个玩家进入该半径,并被具备视觉感知的AI所察觉)。 - AI团队:这会通知感知组件的拥有者同团队中有人处在附近(发送该事件的游戏代码也会发送半径距离)。 - AI触觉:配置能够检测到AI与物体发生主动碰撞、,或是与物体发生被动碰撞。。举例而言,在潜入类型的游戏中,您可能希望玩家在不接触敌方AI的情况下偷偷绕过他们。使用此感官可以确定玩家与AI发生接触,并能用不同逻辑做出响应。 除了可以使用绑定FActorPerceptionUpdatedDelegate委托来实现获取目标Actor外,还可以使用GetHostileActorsBySense()(蓝图版本GetCurrentlyPerceivedActors()) 处理函数ProcessStimuli()是从UAIPerceptionSystem::Tick()调用的,所以只需要修改Tick频率即可。PrimaryActorTick.TickInterval即可。 UAISense_Prediction一般与UAISense_Sight连用,在UAISense_Sight丢失目标后若干秒(使用计时器),调用RequestControllerPredictionEvent,其中PredictionTime为预测时间长短。之后在处理函数中将事件Location传递给黑板变量。 ### UPawnSensingComponent 除了UAIPerceptionComponent还可以使用UPawnSensingComponent,有OnSeePawn与OnHearNoise委托。不过UPawnSensingComponent有个问题即它只能“看到”或者“听到”Pawn。 案例代码: ``` ARPGAICharacter::ARPGAICharacter(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer.SetDefaultSubobjectClass(ACharacter::CharacterMovementComponentName)) { AIControllerClass = ARPGAIController::StaticClass(); bUseControllerRotationYaw = true; SensingComponent = CreateDefaultSubobject(TEXT("SensingComponent")); SensingComponent->SetPeripheralVisionAngle(90.f); } void ARPGAICharacter::PossessedBy(AController* NewController) { Super::PossessedBy(NewController); if (SensingComponent) { SensingComponent->OnSeePawn.AddDynamic(this, &ARPGAICharacter::OnSeeTarget); SensingComponent->OnHearNoise.AddDynamic(this, &ARPGAICharacter::OnHearNoise); } } void ARPGAICharacter::OnSeeTarget(APawn* Pawn) { ARPGAIController* AIController = Cast(GetController()); //Set the seen target on the blackboard if (AIController) { GLog->Log("Oh hello there"); AIController->SetSeenTarget(Pawn); } } void ARPGAICharacter::OnHearNoise(APawn* Pawn,const FVector& Location, float Volume) { ARPGAIController* AIController = Cast(GetController()); //Set the seen target on the blackboard if (AIController) { GLog->Log(FString::Printf(TEXT("Hear Noise:Vec3(%f,%f,%f) Volume:%f"),Location.X,Location.Y,Location.Z,Volume)); AIController->SetHearTarget(Pawn,Location,Volume); } } ``` 控制类中一般是将这些数据添加到黑板中。对应HearNoise则需要在角色中添加UPawnNoiseEmitterComponent ,之后调用MakeNoise实现。 ``` PawnNoiseEmitterComponent->MakeNoise(this, 1.f, GetActorLocation()); ``` ## 刺激源(Stimuli Source) 该组件封装了场景中可以感知的内容以及可以感知的内容。它通常添加在AI需要感知的Actor中。 ## 调试方法 您可以使用AI调试工具调试AI感知。操作方法是在游戏运行时按下撇号(')键,然后按数字键4调出感知信息。 ## 设计思路 对于ARPG游戏来说我们只需AI听觉、AI感知报告、AI视觉与AI团队。 AI听觉:让AI确定TargetLocation,让AI去最后听到声音的位置查看。 AI视觉:让AI发现TargetActor。 AI团队:让AI将自己的TargetActor发送给其他AI。 ## UE4 AI阻挡AI解决策略 CharacterMovement->bUseRVOAvoidance = true ## RAMA的插件 ### nav修改Volume https://nerivec.github.io/old-ue4-wiki/pages/ai-custom-pathing-how-to-use-nav-modifiers-query-filters.html ## 随机Task实现(蓝图实现) https://answers.unrealengine.com/questions/658501/random-task-in-ai.html 使用Service节点随机给黑板随机生成一个数字,在下面连接Tasks上加上一个BlackboardBasedCondition限制节点,来实现。 ## BT节点 ### 黑板相关 可以在构造函数中给FBlackboardKeySelector设置过滤 ``` //筛选Actor类型 BlackboardKey.AddObjectFilter(this, GET_MEMBER_NAME_CHECKED(UBTService_DefaultFocus, BlackboardKey), AActor::StaticClass()); //筛选Vector类型 BlackboardKey.AddVectorFilter(this, GET_MEMBER_NAME_CHECKED(UBTService_DefaultFocus, BlackboardKey)); MyVectorKey.AddVectorFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyVectorKey)); MyObjectKey.AddObjectFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyObjectKey), AActor::StaticClass()); MySpecialActorClassKey.AddClassFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MySpecialActorClassKey), AMySpecialActor::StaticClass()); MyEnumKey.AddEnumFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyEnumKey), StaticEnum()); MyEnumKey.AddNativeEnumFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyEnumKey), "MyEnum"); MyIntKey.AddIntFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyIntKey)); MyFloatKey.AddFloatFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyFloatKey)); MyBoolKey.AddBoolFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyBoolKey)); MyRotatorKey.AddRotatorFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyRotatorKey)); MyStringKey.AddStringFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyStringKey)); MyNameKey.AddNameFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyNameKey)); ``` ``` //通过行为树组件获取到黑板组件 UBlackboardComponent* MyBlackboard = OwnerComp.GetBlackboardComponent(); //获取黑板键值 UObject* KeyValue = MyBlackboard->GetValue(BlackboardKey.GetSelectedKeyID()); //设置黑板键值 UBlackboardComponent->SetValueAsObject(TargetActor.SelectedKeyName, nullptr); ``` 如果设置了FBlackboardKeySelector变量,还需要在重写InitializeFromAsset(UBehaviorTree& Asset): ``` void UMyBTTask::InitializeFromAsset(UBehaviorTree& Asset) { Super::InitializeFromAsset(Asset); UBlackboardData* BBAsset = GetBlackboardAsset(); if (ensure(BBAsset)) { MySpecialKey.ResolveSelectedKey(*BBAsset); } } ``` ### 实例化 Service节点是默认不被实例化的,这代表这个节点会共用内存。实例化Service的两种方法: #### 第一种: 使用内存块. 优点: 更加节省,不需要实例化节点,小巧方便. 缺点: 内存块的大小是固定的,如果涉及到动态分配内存的类型,例如数组,则无法使用此方法. 在源码BTService.h文件中,可以看到一段二十行的英文注释,关于实例化的描述翻译如下。 >由于其中一些可以针对特定的AI进行实例化,因此以下虚拟函数未标记为const: OnBecomeRelevant(来自UBTAuxiliaryNode) OnCeaseRelevant(来自UBTAuxiliaryNode) TickNode(来自UBTAuxiliaryNode) OnSearchStart 如果未实例化您的节点(默认行为),请不要在这些函数内更改对象的任何属性! 模板节点使用相同的树资产在所有行为树组件之间共享,并且必须将其运行时属性存储在提供的NodeMemory块中(分配大小由GetInstanceMemorySize()确定) GetInstanceMemorySize()函数的返回值决定了Service节点可以被分配到用来存储自身属性的内存大小。 而此函数是在UBTAuxiliaryNode的父类UBTNode中声明的,并且默认返回值为0! >UBTNode不光是UBTAuxiliaryNode的父类,也是UBTTaskNode(任务节点)的父类。 也就是说,行为树中 Service(服务节点),Decorator(装饰器节点),Task(任务节点)都是默认不被实例化的! 至于如何实例化,我们完全可以直接参考源码提供的BTService_RunEQS的写法。 首先在自定义Service头文件中声明一个结构体,将需要的变量都放结构体里。 在类中声明一个此结构体的变量,并重写GetInstanceMemorySize函数返回结构体所需内存大小。在InitializeMemory函数中可以对结构体变量进行初始化。 #### 第二种: 直接实例化节点. 优点: 可以使用任何复杂类型的属性. 缺点: 会为每个节点创建一个新对象. 这种只要在构造函数中设置标志位 bCreateNodeInstance = true; ,你就可以在头文件中声明任意变量使用了. ### 更改描述及命名 重写GetStaticDescription函数。 NodeName可以从Details面板修改,也可以在类构造函数中直接 NodeName = “ ” 但这里的NodeName并不会修改在AddService时列表中显示的名称。 列表中的名称是由类名决定的,除了修改类名,也可以用类说明符DisplayName修改。 ``` UClass(DisplayName="xxxx") ``` ### 蓝图节点为啥会是双本版设计 源码逻辑: 当AIOwner为AIController时,优先调用带AI后缀的函数。(若只实现了不带AI后缀的,也会调用成功) 当AIOwner不为AIController时,只调用不带AI后缀的函数。 ``` void UBTService_BlueprintBase::SetOwner(AActor* InActorOwner) { ActorOwner = InActorOwner; AIOwner = Cast(InActorOwner); } void UBTService_BlueprintBase::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) { Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds); //判断是否Owner为AIController并且有任一版本函数被实现 if (AIOwner != nullptr && (ReceiveTickImplementations & FBTNodeBPImplementationHelper::AISpecific)) { ReceiveTickAI(AIOwner, AIOwner->GetPawn(), DeltaSeconds); } //是否实现了不带AI后缀的函数 else if (ReceiveTickImplementations & FBTNodeBPImplementationHelper::Generic) { ReceiveTick(ActorOwner, DeltaSeconds); } } ``` ## 只有附加在Composites(合成节点)上的Service,才会调用OnSearchStart。 Composites(合成节点)也就是指Sequence,Selector,SimpleParallel节点。 还有种特殊情况:当Service附加在与Root相连的第一个合成节点上时,SearchStart会被触发两次。 ## Compisiton 复合节点:此类节点定义分支的根以及执行该分支的基本规则。 ## Decorator ![](https://docs.unrealengine.com/4.26/Images/InteractiveExperiences/ArtificialIntelligence/BehaviorTrees/BehaviorTreeNodeReference/BehaviorTreeNodeReferenceDecorators/CustomDecorator.webp) 可以理解为条件节点,连接Composite或者Task节点,决定树中的分支、甚至单个节点能否被执行。 c++实现可以参考: - UBTDecorator_BlueprintBase - UBTDecorator_BlackboardBase - UBTDecorator_CheckGameplayTagsOnActor - UBTDecorator_Loop 蓝图节点中可重写的函数有: - ReceiveTick:通用版本的tick函数。如果条件符合会在TickNode()中调用。 - ReceiveTickAI:AI版本的tick函数。如果条件符合会在TickNode()中调用。 - ReceiveExecutionStart:通用版本的Start函数。如果条件符合会在OnNodeActivation()中调用。 - ReceiveExecutionStartAI:AI版本的Start函数。如果条件符合会在OnNodeActivation()中调用。 - ReceiveExecutionFinish:通用版本的Finish函数。如果条件符合会在OnNodeDeactivation()中调用。 - ReceiveExecutionFinishAI:AI版本的Finish函数。如果条件符合会在OnNodeDeactivation()中调用。 - ReceiveObserverActivated:通用版本的Activated函数。如果条件符合会在OnBecomeRelevant()中调用。 - ReceiveObserverActivatedAI:AI版本的Activated函数。如果条件符合会在OnBecomeRelevant()中调用。 - ReceiveObserverDeactivated:通用版本的Deactivated函数。如果条件符合会在OnCeaseRelevant()中调用。 - ReceiveObserverDeactivatedAI:AI版本的Deactivated函数。如果条件符合会在OnCeaseRelevant()中调用。 - PerformConditionCheck:通用版本的ConditionCheck函数。如果条件符合会在CalculateRawConditionValueImpl()中调用。 - PerformConditionCheckAI:AI版本的ConditionCheck函数。如果条件符合会在CalculateRawConditionValueImpl()中调用。 ## Service ![](https://docs.unrealengine.com/4.26/Images/InteractiveExperiences/ArtificialIntelligence/BehaviorTrees/BehaviorTreeNodeReference/BehaviorTreeNodeReferenceServices/NewCustomService_01.webp) 可以理解为并行逻辑节点,连接连接Composite或者Task节点,运行到当前分支时,Service节点将按照之前设定的频率执行。通常是使用这些Service节点检查、更新黑板数据。 c++可以参考: - UBTService_BlueprintBase - UBTService_BlackboardBase - UBTService_RunEQS 蓝图节点中可重写的函数有: - ReceiveTick - ReceiveTickAI - ReceiveSearchStart - ReceiveSearchStartAI - ReceiveActivation - ReceiveActivationAI - ReceiveDeactivation - ReceiveDeactivationAI ### 相关事件 - OnSearchStart:所挂靠的合成节点被激活时调用的函数。 - TickNode:Tick执行的函数(间隔自定义)。可在构造函数设置 bNotifyTick 来决定是否调用。 - OnBecomeRelevant:节点被激活时调用(等同于蓝图Service里的Receive Activation) 需要在构造函数中设置 bNotifyBecomeRelevant = true来开启调用。 - OnCeaseRelevant:节点被激活时调用(等同于蓝图Service里的Receive Deactivation) 需要在构造函数中设置 bNotifyCeaseRelevant = true来开启调用。 - TickNode,OnBecomeRelevant和OnCeaseRelevant并不是UBTService类的虚函数,而是UBTService的父类UBTAuxiliaryNode的。装饰器UBTDecorator也继承此类。 - GetInstanceMemorySize:当节点被实例化时分配的内存大小,关于实例化后面会详谈。 - Tick函数执行间隔 = Random(Interval-RandomDeviation,Interval+RandomDeviation) 在这也就是 = Random(0.5-0.1 , 0.5+0.1), 每次调用都会计算一次。 - CallTickonSearchStart:若为True,则会在SearchStart函数触发时,调用一次Tick函数。 若为False,则即使节点已被激活,也要等待一次间隔才能执行第一次Tick函数。 - RestartTimeronEachActivation:若为True,则每次节点被激活都会从0开始计算间隔。 若为False,则会记录下上一次已经等待的间隔,在下一次激活时沿用。 ## Task 可以理解为具体执行的任务(逻辑)。此类节点是行为树的叶。它们是可执行的操作,没有输出连接。 UBTTask_BlueprintBase与UBTTask的区别在于 以UBTTask_PlaySound这种简单UBTTask为例,主要实现了ExecuteTask(),其他是显示图标与描述的GetStaticDescription()与GetNodeIconName() 蓝图节点中可重写的函数有: - ReceiveTick:通用版本的tick函数。如果条件符合会在TickTask()中调用。 - ReceiveTickAI:AI版本的tick函数。如果条件符合会在TickTask()中调用。 - ReceiveExecute:通用版本的Execute函数。如果条件符合会在ExecuteTask()中调用。 - ReceiveAbort:通用版本的Abort函数。如果条件符合会在AbortTask()中调用。 - ReceiveExecuteAI:AI版本的Execute函数。如果条件符合会在ExecuteTask()中调用。 - ReceiveAbortAI:AI版本的Abort函数。如果条件符合会在AbortTask()中调用。 ### RunBehavior 与 RunBehaviorDynamic 可以加载其他行为树并且运行。 - RunBehavior的一个限制是在运行时不能改变该子树资源。存在该限制的原因是给子树的根等级装饰器节点提供支持,这些装饰器节点将注入到父树中。此外,在运行时不能修改运行树的结构。 - RunBehaviorDynamic使用行为树组件上的SetDynamicSubtree 函数即可以在运行时分配子树资源。本函数不会为子树的根等级装饰器节点提供支持。 ## EQS 大致流程: 通过生成器在场景生成检测点获取当前位置的各种数据,之后再通过条件判断获取想要的查询结果。EQS的一些使用范例包括:找到最近的回复剂或弹药、判断出威胁最大的敌人,或者找到能看到玩家的视线(下面就会显示这样的一个示例)。 ### 在Actor类中使用EQS 结合原生代码使用EQS 虽然EQS查询通常是在行为树中运行,但也可以直接从原生代码使用它。以下示例展示了一个虚构的查询,要在指定区域内为角色或物品寻找安全生成地点: ``` // 以下名称必须与查询中使用的变量名一致 static const FName SafeZoneIndexName = FName(TEXT("SafeZoneIndex")); static const FName SafeZoneRadiusName = FName(TEXT("SafeZoneRadius")); // 运行查询,根据区域索引和安全半径寻找安全的生成点 bool AMyActor::RunPlacementQuery(const UEnvQuery* PlacementQuery) { if (PlacementQuery) { // 设置查询请求 FEnvQueryRequest QueryRequest(PlacementQuery, this); // 设置查询参数 QueryRequest.SetIntParam(SafeZoneIndexName, SafeZoneIndexValue); QueryRequest.SetFloatParam(SafeZoneRadiusName, SafeZoneRadius); // 执行查询 QueryRequest.Execute(EEnvQueryRunMode::RandomBest25Pct, this, &AFortAthenaMutator_SpawningPolicyBase::OnEQSSpawnLocationFinished); // 返回true说明查询已开始 return true; } // 返回false说明查询未能开始 return false; } ```