UE4引擎中阵营关系浅谈
前言
如果你没有使用虚幻引擎的感知系统,那么本篇文章对于你的作用并不大。首先虚幻系统中的阵营关系被应用于感知系统。在感知系统中,感知源与刺激源之间的关联是通过阵营关系进行检查。
蓝图并不支持使用阵营关系设置
这样的设计手段可以帮助我们更好的处理怪物AI之间的关系,从设计手段上划分了逻辑目标,但是又没有针对特定目标编写设计逻辑。简而言之,我们可以通过阵营判定,判断目标是否是我需要关注或是忽略的对象。
虚幻引擎中,阵营关系被划分为三种(阵营种类可以根据设计需求随意添加),总的来说,你编写的AI对象和目标的关系就三种:敌对,中立,友好。
UENUM()
namespace ETeamAttitude
{
enum Type
{
Friendly,
Neutral,
Hostile,
};
}
那么如何引擎是如何处理阵营关系呢?我们继续往下看。
AI感知系统
感知系统,是为了提供AI行为反应数据来源的主要方法。目前初去旧有系统(视觉,听觉)在新的系统中做了大方向的调整,以至于将旧有组件逐渐放弃,转向新的系统中。新系统提供了不光是视觉和听觉能力,还包括感知,触碰,伤害,团队等。并且支持自定义感知逻辑。
系统在游戏启动后,将会为场景中的对象(仅处理Pawn类型及其子类)处理阵营关系。并且处理完毕后,将会构建监听逻辑关系,如果阵营发生变化,则需要重新再次注册对象到感知系统。
注意:只有当场景中的AIController添加感知组件,并添加感知源配置信息才会进行阵营关系检查。为什么要在AIController中添加组件,稍后会说明。
在UAIPerceptionSystem类中,当场景启动生成新的Pawn对象会通过调用函数OnNewPawn整理阵营关系,将对象与感知源逐一处理,查询是否是感知源关心的阵营对象。
//以下源码来自源代码 类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完成阵营数据获取的,通过查看源文件,可以看到三个重要的接口函数。
//源码内容摘自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添加到视觉测试中,进行测试,并进行阵营过滤器检查。
//摘自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的继承关系如下
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值
//获取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虚函数即可,然后在函数内完成比较逻辑,例如下面的代码
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 函数时,阵营检查将直接通过接口函数的默认逻辑完成。如下
//源码内容摘自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完成,继续向上翻看源码
//源码来自FGenericTeamId内,文件GenericTeamAgentInterface.h
static ETeamAttitude::Type GetAttitude(FGenericTeamId TeamA, FGenericTeamId TeamB)
{
return AttitudeSolverImpl ? (*AttitudeSolverImpl)(TeamA, TeamB) : ETeamAttitude::Neutral;
}
AttitudeSolverImpl是在FGenericTeamId结构体内定义的函数指针对象,函数原型是
//源码来自FGenericTeamId内,文件GenericTeamAgentInterface.h
typedef ETeamAttitude::Type FAttitudeSolverFunction(FGenericTeamId, FGenericTeamId);
static FAttitudeSolverFunction* AttitudeSolverImpl;
并且引擎提供了函数指针AttitudeSolverImpl设置入口
//源码来自FGenericTeamId内,文件GenericTeamAgentInterface.h
//函数声明
static void SetAttitudeSolver(FAttitudeSolverFunction* Solver);
//函数定义
void FGenericTeamId::SetAttitudeSolver(FGenericTeamId::FAttitudeSolverFunction* Solver)
{
AttitudeSolverImpl = Solver;
}
可以看到所有操作函数都是静态函数,如果你愿意,你可以写这样的匿名函数,来管理阵营规则
FGenericTeamId::SetAttitudeSolver([](FGenericTeamId A, FGenericTeamId B)->ETeamAttitude::Type{
//这里写你要用来比较的逻辑
return ETeamAttitude::Neutral;
});
可能看起来有点乱,虚幻引擎在阵营设计上做了很多处理,对于使用者来说,你只需要知道如何重写比较规则,和设置阵营ID即可。如果有兴趣可以尝试调试下上面的函数,可以帮助你尽可能的了解设计机制。
祝各位在虚幻中玩的愉快~
虚幻版本V4.21.2
强哥好久没更新了啊
最近确实很忙。。。
现在才发现这个宝藏博客,多谢博主分享了。以后会持续关注
强哥这波更新牛皮
纯蓝图好像改不了。。
是的纯蓝图是不支持此操作的~
哦豁张强老师!
哈喽~
哇塞,我竟然找到组织了