UE4中SendAiMessage节点逻辑剖析与应用(一)

前言

在蓝图中我们会看到这个节点“Send AIMessage”。他在AI分类项中,但是没有任何与之相关的描述介绍和注释(V4.21.2版本)。从字面意思可以理解为“发送一个AI消息”,但是这个消息发送到了哪里?由谁接收呢?下面我来剖析下这个节点的执行流程!

思考

由于“Send AIMessage”(以下简称SA节点)节点在蓝图中,我们是无法从蓝图中追踪到作者的设计逻辑与意图的,我们肯定是要回到C++中着手寻找应用逻辑!蓝图作为与C++相辅相成的逻辑操作语言,本身的优势和劣势非常明显!大部分蓝图逻辑核心均存在于C++中,所以我们需要先找到SA节点在C++中的位置!这很简单!

在蓝图时间图表中将SA节点拽出来,鼠标悬停即可找到SA所在的蓝图静态函数库(遵循基本静态函数特点),如图一

图一

这样就找到的入手点,开始着手阅读源码的设计思路!

分析

  • 寻找 SendAIMessage 定义

找到静态蓝图函数库UAIBlueprintHelperLibrary类,直接在源码中搜寻即可!找到后寻找SA函数!(技巧:在任意源文件的逻辑操作域中加入代码UAIBlueprintHelperLibrary::SendAIMessage(),然后F12跳转到生命可快速找到函数声明,前提是使用的VS版本智能感知系统没有任何问题,我使用的是VS2015配合VA插件

void UAIBlueprintHelperLibrary::SendAIMessage(APawn* Target, FName Message, UObject* MessageSource, bool bSuccess)
{
	//FAIMessage负责分发消息
	FAIMessage::Send(Target, FAIMessage(Message, MessageSource, bSuccess));
}

我们找到了FAIMessage类,这是一个工具类,主要负责查找给定目标身上是否存在有效的消息接收结构组件,参照一下源码。

void FAIMessage::Send(APawn* Pawn, const FAIMessage& Message)
{
	UBrainComponent* BrainComp = FindBrainComponentHelper(Pawn);
	Send(BrainComp, Message);
}

从源码看,从给定过来的对象Pawn身上要查找一个UBrainComponent类型对象指针。大家可以继续阅读源码,我不在赘述,源码中明确指出需要从对象身上获取,或是从对象的控制器上获取!我们得到一个结论!如果希望接收Message的对象必须自己或是控制器携带 UBrainComponent 组件

  • UBrainComponent 组件

其实 UBrainComponent (以下简称UB组件)组件你不认识,但是UBehaviorTreeComponent组件你一定不会陌生!是的UBehaviorTreeComponent是 UBrainComponent组件的子类!

class AIMODULE_API UBehaviorTreeComponent : public UBrainComponent

我们继续往下看,当我们的敌人具备携带 UBrainComponent组件,那么他就可以接收AIMessage消息!那么我们应该如何响应消息呢?从Send函数我们可以找到另外的一个Send函数,源码如下!

void FAIMessage::Send(UBrainComponent* BrainComp, const FAIMessage& Message)
{
	if (BrainComp)
	{
		BrainComp->HandleMessage(Message);
	}
}
void UBrainComponent::HandleMessage(const FAIMessage& Message)
{
	MessagesToProcess.Add(Message);
}

从上面的代码一路跟下来,最后发现,程序将构建的FAIMessage对象添加到了UBrainComponent组件的MessagesToProcess对象数组中!我们只要继续查找MessagesToProcess对象的应用逻辑即可!

  • MessagesToProcess

MessagesToProcess是一个数组容器,里面装填的数据类型是FAIMessage。

通过查找引用,我们寻找了一下 MessagesToProcess在程序中的逻辑应用,找到如下源码

void UBrainComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
{
	if (MessagesToProcess.Num() > 0)
	{
		const int32 NumMessages = MessagesToProcess.Num();
		for (int32 Idx = 0; Idx < NumMessages; Idx++)
		{
			// create a copy of message in case MessagesToProcess is changed during loop
			const FAIMessage MessageCopy(MessagesToProcess[Idx]);

			for (int32 ObserverIndex = 0; ObserverIndex < MessageObservers.Num(); ObserverIndex++)
			{
				MessageObservers[ObserverIndex]->OnMessage(MessageCopy);
			}
		}
		MessagesToProcess.RemoveAt(0, NumMessages, false);
	}
}

从代码可以看到,当我们发送一个消息,会被添加到 MessagesToProcess数组中,然后在组件轮询函数Tick中会执行消息的分发!这里就是发送消息的根了!当我们找到这里,基本上SA节点的逻辑就已经被清晰的拆解完成了!

消息分发时,会通过MessageObservers数组容器(消息监听对象数组)进行消息的分发!

  • MessageObservers

我们再看看 MessageObservers数组容器,首先容器只能添加FAIMessageObserver类型对象指针(源码不粘了)。FAIMessageObserver对象中存在一个单播代理,对是单播,并且非动态!也就是表明,SA节点发出的事件只能广播到C++中!

接下来寻找如何添加 FAIMessageObserver到观察队列数组中,基本上工作就算完成了!我们可以查询MessageObservers的调用痕迹!找到如下源码。

void FAIMessageObserver::Register(UBrainComponent* OwnerComp)
{
	OwnerComp->MessageObservers.Add(this);
	Owner = OwnerComp;
}

MessageObservers是UB类的成员对象,但是上面的Register函数是归属FAIMessageObserver类的!这种设计手法是虚幻中典型的应用手法,包括像定时器这种广播型行为设计都使用了此种方案!优点就是更清晰的将对象和对象之间的耦合降到最低!有点中介设计模式的味道!

  • FAIMessageObserver::Register的应用

其实到这里,我们已经剖析的差不多了!基本流程就是构建一个FAIMessageObserver然后添加到UB的MessageObservers数组中。在 FAIMessageObserver中的单播代理上绑定回调函数即可

通过FAIMessageObserver::Register跟踪,我们找到了下面的代码

FAIMessageObserverHandle FAIMessageObserver::Create(UBrainComponent* BrainComp, FName MessageType, FAIRequestID MessageID, FOnAIMessage const& Delegate)
{
	FAIMessageObserverHandle ObserverHandle;
	if (BrainComp)
	{
		FAIMessageObserver* NewObserver = new FAIMessageObserver();
		NewObserver->MessageType = MessageType;
		NewObserver->MessageID = MessageID;
		NewObserver->bFilterByID = true;
		NewObserver->ObserverDelegate = Delegate;
		NewObserver->Register(BrainComp);

		ObserverHandle = MakeShareable(NewObserver);
	}

	return ObserverHandle;
}

注意,这是一个静态的成员函数,并且返回了一个共享指针(切记外部必须持有有效的共享指针,否则将导致绑定FAIMessageObserver指针被释放,无法获得事件通知)。我们希望获得AI的消息通知,只需要使用此函数创建绑定即可!

到了这里基本上的代码剖析就完成了!当我们要开始程序阅读时,最好的办法是调试断点看堆栈!而对于简单的设计,其实看看引用关系就能推断出设计的意图!程序是最讲道理的!在下一篇文章我们讲讲如何使用SA节点接收消息吧!

版本:V4.21.2

添加评论

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