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
About Author
Zack
UEjoy博客发起者,旨在分享更多虚幻引擎技术!如果您有兴趣加入一起维护这个博客,请联系我~邮箱zack#ccvs.cc