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

9 条评论

回复 疯奇奇 取消回复

您的电子邮箱地址不会被公开。 必填项已用*标注