UE4引擎中阵营关系浅谈
前言
如果你没有使用虚幻引擎的感知系统,那么本篇文章对于你的作用并不大。首先虚幻系统中的阵营关系被应用于感知系统。在感知系统中,感知源与刺激源之间的关联是通过阵营关系进行检查。
蓝图并不支持使用阵营关系设置
这样的设计手段可以帮助我们更好的处理怪物AI之间的关系,从设计手段上划分了逻辑目标,但是又没有针对特定目标编写设计逻辑。简而言之,我们可以通过阵营判定,判断目标是否是我需要关注或是忽略的对象。
虚幻引擎中,阵营关系被划分为三种(阵营种类可以根据设计需求随意添加),总的来说,你编写的AI对象和目标的关系就三种:敌对,中立,友好。
1 2 3 4 5 6 7 8 9 10 | UENUM() namespace ETeamAttitude { enum Type { Friendly, Neutral, Hostile, }; } |
那么如何引擎是如何处理阵营关系呢?我们继续往下看。
AI感知系统
感知系统,是为了提供AI行为反应数据来源的主要方法。目前初去旧有系统(视觉,听觉)在新的系统中做了大方向的调整,以至于将旧有组件逐渐放弃,转向新的系统中。新系统提供了不光是视觉和听觉能力,还包括感知,触碰,伤害,团队等。并且支持自定义感知逻辑。
系统在游戏启动后,将会为场景中的对象(仅处理Pawn类型及其子类)处理阵营关系。并且处理完毕后,将会构建监听逻辑关系,如果阵营发生变化,则需要重新再次注册对象到感知系统。
注意:只有当场景中的AIController添加感知组件,并添加感知源配置信息才会进行阵营关系检查。为什么要在AIController中添加组件,稍后会说明。
在UAIPerceptionSystem类中,当场景启动生成新的Pawn对象会通过调用函数OnNewPawn整理阵营关系,将对象与感知源逐一处理,查询是否是感知源关心的阵营对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | //以下源码来自源代码 类UAIPerceptionSystem成员函数 void UAIPerceptionSystem::OnNewPawn(APawn& Pawn) { if (bHandlePawnNotification == false ) { return ; } //Senses for (UAISense* Sense : Senses) { if (Sense == nullptr ) { continue ; } if (Sense->WantsNewPawnNotification()) { Sense->OnNewPawn(Pawn); } if (Sense->ShouldAutoRegisterAllPawnsAsSources()) { FAISenseID SenseID = Sense->GetSenseID(); check(IsSenseInstantiated(SenseID)); RegisterSource(SenseID, Pawn); } } } |
从以上代码可以看到,当生成新的对象(Pawn类型),则会与感知系统中注册的感知源进行检查处理。
总的来说,感知系统,管理了所有的感知源。感知源通过阵营关系,将关心的阵营(关心哪些阵营是可选的)加入到观测询问容器中。
阵营接口类IGenericTeamAgentInterface
引擎在处理阵营关系时,是通过接口IGenericTeamAgentInterface完成阵营数据获取的,通过查看源文件,可以看到三个重要的接口函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 | //源码内容摘自IGenericTeamAgentInterface接口类 /** Assigns Team Agent to given TeamID */ virtual void SetGenericTeamId( const FGenericTeamId& TeamID) {} /** Retrieve team identifier in form of FGenericTeamId */ virtual FGenericTeamId GetGenericTeamId() const { return FGenericTeamId::NoTeam; } /** Retrieved owner attitude toward given Other object */ virtual ETeamAttitude::Type GetTeamAttitudeTowards( const AActor& Other) const { const IGenericTeamAgentInterface* OtherTeamAgent = Cast< const IGenericTeamAgentInterface>(&Other); return OtherTeamAgent ? FGenericTeamId::GetAttitude(GetGenericTeamId(), OtherTeamAgent->GetGenericTeamId()) : ETeamAttitude::Neutral; } |
接口函数中包含了设置FGenericTeamId,以及获取FGenericTeamId和比较队伍结果
对象FGenericTeamId就是用来判断阵营的数据对象
从接口函数GetTeamAttitudeTowards不难看出,阵营关系是通过从给定对象(参数Actor)身上调用接口函数GetGenericTeamId来完成比较的,并返回阵营结果ETeamAttitude枚举值。至于如何比较,我们稍后再说。
那么问题就在于是谁继承接口类,并完成函数调用的呢?
在前面已经说过如果想要使用阵营,需要先添加感知组件并配置感知源信息。我在工程内使用了感知组件,组件添加到了AIController上,并添加了视觉感知源配置。所以通过调试UAISense_Sight类内函数RegisterTarget可以得知,场景启动后,会将对象Actor添加到视觉测试中,进行测试,并进行阵营过滤器检查。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | //摘自UAISense_Sight类RegisterTarget函数部分源码 // generate all pairs and add them to current Sight Queries bool bNewQueriesAdded = false ; AIPerception::FListenerMap& ListenersMap = *GetListeners(); const FVector TargetLocation = TargetActor.GetActorLocation(); for (AIPerception::FListenerMap::TConstIterator ItListener(ListenersMap); ItListener; ++ItListener) { const FPerceptionListener& Listener = ItListener->Value; //GetTeamAgent函数获取监听器上的感知组件的所在Actor,并转为接口对象(我的感知组件添加在了AIController,所以此处堆栈对象是AIController类型) const IGenericTeamAgentInterface* ListenersTeamAgent = Listener.GetTeamAgent(); if (Listener.HasSense(GetSenseID()) && Listener.GetBodyActor() != &TargetActor) { const FDigestedSightProperties& PropDigest = DigestedProperties[Listener.GetListenerID()]; if (FAISenseAffiliationFilter::ShouldSenseTeam(ListenersTeamAgent, TargetActor, PropDigest.AffiliationFlags)) { // create a sight query FAISightQuery SightQuery(ItListener->Key, SightTarget->TargetId); SightQuery.Importance = CalcQueryImportance(ItListener->Value, TargetLocation, PropDigest.SightRadiusSq); SightQueryQueue.Add(SightQuery); bNewQueriesAdded = true ; } } } |
从上面源码中可以得知,阵营关系是通过检查是否继承IGenericTeamAgentInterface接口类来完成的(查阅GetTeamAgent函数可知)。由于我添加感知组件是在AIController身上,所以ListenersTeamAgent变量在堆栈中查看是AIController类型
为什么感知组件添加在AIController中
翻阅源码,我们可以看到AIController的继承关系如下
1 | class AIMODULE_API AAIController : public AController, public IAIPerceptionListenerInterface, public IGameplayTaskOwnerInterface, public IGenericTeamAgentInterface, public IVisualLoggerDebugSnapshotInterface |
在引擎框架设计中,AIController从开始就已经继承接口IGenericTeamAgentInterface。从引擎给与的信息来看,添加感知组件在AIController上是最正确的选择。
从设计逻辑上来将,AI控制器用来控制不同的角色,但是感知逻辑应该适用于不同的角色
但是判定阵营关系中,刺激源的阵营接口应该添加在Pawn对象身上,这也是为了迎合引擎设计思路。从上面翻查的源码可以获知:角色Panw会被添加到感知系统检查阵营,阵营检查是由感知配置对象调用感知组件的Actor完成。由于感知组件添加在AIController上,所以才得到这样的结果。
这也是讲得通的,控制器(Controller)控制的角色(Pawn)有很多种,有好有坏。那么不管好坏,对于控制器来说都是需要检查的。
如何动态更改阵营关系?
如果希望在运行时修改阵营关系,可以主动调用以下函数完成,记住要调整阵营ID值
1 2 3 4 5 6 7 8 9 10 11 | //获取AI系统 UAISystem* AiSystem = Cast<UAISystem>(GetWorld()->GetAISystem()); //获取感知系统 if (AiSystem && AiSystem->GetPerceptionSystem()) { ANPlayerCharacter* Player = Cast<ANPlayerCharacter>(UGameplayStatics::GetPlayerCharacter( this , 0)); //解除已注册对象 AiSystem->GetPerceptionSystem()->UnregisterSource(*UGameplayStatics::GetPlayerCharacter( this , 0)); //重新注册对象到刺激源 重新注册时记得修改阵营数据 AiSystem->GetPerceptionSystem()->RegisterSource(*UGameplayStatics::GetPlayerCharacter( this , 0)); } |
如何自定义阵营检测逻辑
如果你希望使用自己的阵营检测逻辑,可以有两种途径完成
- 修改引擎默认的阵营检查规则
- 修改某个AIController的阵营检查规则
其实最简单的就是修改某个AIController的,我们只需要在继承AIController的类里重写GetTeamAttitudeTowards虚函数即可,然后在函数内完成比较逻辑,例如下面的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | ETeamAttitude::Type AEnemyAIController::GetTeamAttitudeTowards( const AActor& Other) const { //检查对象是否继承了阵营数据接口 const IGenericTeamAgentInterface* OtherTeamAgent = Cast< const ANPlayerCharacter>(&Other); //没有继承接口则返回中立关系 if (!OtherTeamAgent) return ETeamAttitude::Neutral; //继承接口调用接口函数获取队伍ID数据 FGenericTeamId TeamId = OtherTeamAgent->GetGenericTeamId(); //如果对象的队伍ID和我的一样 if (TeamId.GetId() == GetGenericTeamId().GetId()) { return ETeamAttitude::Friendly; } return ETeamAttitude::Hostile; } |
修改全局阵营检查逻辑
当在AI控制器中如果没有重写 GetTeamAttitudeTowards 函数时,阵营检查将直接通过接口函数的默认逻辑完成。如下
1 2 3 4 5 6 7 | //源码内容摘自IGenericTeamAgentInterface接口类 virtual ETeamAttitude::Type GetTeamAttitudeTowards( const AActor& Other) const { const IGenericTeamAgentInterface* OtherTeamAgent = Cast< const IGenericTeamAgentInterface>(&Other); return OtherTeamAgent ? FGenericTeamId::GetAttitude(GetGenericTeamId(), OtherTeamAgent->GetGenericTeamId()) : ETeamAttitude::Neutral; } |
观察代码我们可以看到,阵营检查是通过FGenericTeamId类中的静态函数GetAttitude完成,继续向上翻看源码
1 2 3 4 5 | //源码来自FGenericTeamId内,文件GenericTeamAgentInterface.h static ETeamAttitude::Type GetAttitude(FGenericTeamId TeamA, FGenericTeamId TeamB) { return AttitudeSolverImpl ? (*AttitudeSolverImpl)(TeamA, TeamB) : ETeamAttitude::Neutral; } |
AttitudeSolverImpl是在FGenericTeamId结构体内定义的函数指针对象,函数原型是
1 2 3 4 | //源码来自FGenericTeamId内,文件GenericTeamAgentInterface.h typedef ETeamAttitude::Type FAttitudeSolverFunction(FGenericTeamId, FGenericTeamId); static FAttitudeSolverFunction* AttitudeSolverImpl; |
并且引擎提供了函数指针AttitudeSolverImpl设置入口
1 2 3 4 5 6 7 8 | //源码来自FGenericTeamId内,文件GenericTeamAgentInterface.h //函数声明 static void SetAttitudeSolver(FAttitudeSolverFunction* Solver); //函数定义 void FGenericTeamId::SetAttitudeSolver(FGenericTeamId::FAttitudeSolverFunction* Solver) { AttitudeSolverImpl = Solver; } |
可以看到所有操作函数都是静态函数,如果你愿意,你可以写这样的匿名函数,来管理阵营规则
1 2 3 4 | FGenericTeamId::SetAttitudeSolver([](FGenericTeamId A, FGenericTeamId B)->ETeamAttitude::Type{ //这里写你要用来比较的逻辑 return ETeamAttitude::Neutral; }); |
可能看起来有点乱,虚幻引擎在阵营设计上做了很多处理,对于使用者来说,你只需要知道如何重写比较规则,和设置阵营ID即可。如果有兴趣可以尝试调试下上面的函数,可以帮助你尽可能的了解设计机制。
祝各位在虚幻中玩的愉快~
虚幻版本V4.21.2
强哥好久没更新了啊
最近确实很忙。。。
现在才发现这个宝藏博客,多谢博主分享了。以后会持续关注
强哥这波更新牛皮
纯蓝图好像改不了。。
是的纯蓝图是不支持此操作的~
哦豁张强老师!
哈喽~
哇塞,我竟然找到组织了