程序员求职经验分享与学习资料整理平台

网站首页 > 文章精选 正文

AFSim_实用工具_行为树状态实时监视插件

balukai 2025-08-02 17:29:59 文章精选 5 ℃

_^

AFSim-实用工具:




行为树状态实时监视插件


1

监视效果

这个插件工具主要是为了 可视化展示 afsim的warlock在运行过程中指定平台的 行为树状态 。是通过将mystic的行为树分析工具插件的代码进行移植实现的,warlock运行 demos下的behavior_tree 效果视频如下:
前半段是mystic自带的插件,后半段是开发的warolck插件)

2

行为树基本概念

行为树概念在网上有很多文章可以翻阅:

1)行为树的结构是倒置的树

2) 行为树节点类型

3) 行为树逻辑节点表示

4) 行为树主要规则

3


afsim中的behaviortree

afsim在核心代码wsf中定义了两种类来实现行为树的功能, 基础型WsfBehaviorTree和WsfBehaviorTreeNode 高级型WsfAdvancedBehaviorTree和
WsfAdvancedBehaviorTreeNode,
同时在observer中也定义了这两种类型的监听对象 WsfBehaviorObserver和
WsfAdvancedBehaviorObserver。
官方文档 AdvancedBehaviorTree 是比 BehaviorTree 更高级的行为树对象,它支持了三种节点状态(Running、True和False)这也是目前使用最广泛的方式,并且mystic的插件也只能加载 AdvancedBehaviorTree 的数据来展示,因此本文也仅对AdvancedBehaviorTree处理。


上面mystic加载的行为树的定义在run_tree.txt中,代码如下:
我们要通过插件对行为树的执行情况进行展示,那么首先需要获取到场景中哪些平台上具有行为树,然后监听行为节点状态的变化来更新。
1) 获取行为树结构
通过查看源代码,我们知道afsim加载脚本定义的行为树是通过 WsfScriptProcessor 的ProcessInput处理的,同时Processor作为 processor组件会 挂载到指定的平台上。在WsfScriptProcessor对象中提供了获取行为树对象的方法,再通过行为树对象就能够遍历获取到所有行为节点。

因此,这里就能可以通过平台->处理器组件->处理器->WsfScriptProcessor来获取到某平台上关联的行为树,然后获取树节点。(其实还可以通过 AdvancedBehaviorTree事件 来获取行为树,这是后话了^_^ )。 所有树节点类型在
WsfAdvancedBehaviorTreeNode
中定义,继承关系图如下:

2) 监听行为树状态

绑定
WsfAdvancedBehaviorObserver的回调函数即可:

AdvancedBehaviorTree ,通过它可以获取行为树^_^

AdvancedBehaviorTreeState ,通过它可以更新树节点状态


4

mystic中的behaviortree

afsim的mystic工具提供了一个在 回放过程中查看行为树 (或 状态机,本文不涉及 )的插件 (工程
ResultBehaviorAnalysisTool)
下图是加载run_tree.aer的行为树展示效果 注: 这里是用我改造后支持中文脚本的wizard将示例中的节点名改为了中文,你们的wizrd如果不支持中文脚本的话就直接用英文的吧,对插件功能来说没有差别)

要使这个插件有数据,需要先通过mission.exe将demos/behavior_tree下的run_tree.txt跑一遍,然后他 会生成run_tree.aer ,再用mystic来加载,从Views菜单打开行为树展示界面。大家去用用吧,有点不好用,很容易就把图搞到再也找不到的地方去了^_^, 下面来分析mystic的行为树显示流程。

mystic的行为树(或状态机)插件界面是 DockWindow ,它会在 周期函数Update 中更新行为树状态,这个函数传入的参数是回放框架读取的 回放数据rv::ResultData ,它里面就包含了行为树的结构和状态数据。

查看它的代码实现逻辑:

在这个处理中,如果 mViewPtr 已经加载了,就直接更新界面,否则先根据数据初始化这个对象。

界面构造的逻辑是从mDataInterfacePtr对象中获取行为树或状态机结构数据,然后绘制到界面上等待下一次的DockWindow的Update,然后也是从mDataInterfacePtr中获取状态数据更新。在更新状态时,它有一个判断,就是当前的行为树状态数据的回放数据索引如果没有更新的话,就表示没有新的状态变化,就不更新界面。

5

实现

有了上面的基础,我们就可以来实现了,我们的目标很简单: 通过移植mystic的插件代码将其改造为warlock插件,能够列表展示场景中与平台关联的行为树,能根据平台进行切换(其他功能暂时不做) ,下面来分步实现:

1) warlock插件搭建

这个在之前的文章《 天线方向图实时绘制插件 》中已经有相应的描述,这里不赘述了。首先创建一个列表来罗列当前场景中的所有平台(后续会修改为只加载有行为树的平台),这个比较简单,读取平台数据添加到列表即可,界面效果如下:


2) 行为树结构获取

warlock加载场景完成后,通过 重载的SimulationStarting 函数获取行为树的相关节点数据。在run_tree.txt这个示例场景中,通过mystic查看到蓝_飞机1这个平台的行为树,除根节点外包含18个节点:2个并行节点、4个顺序节点、1个选择节点、3个条件节点、8个动作节点。

通过调试代码,可看到获取的节点数量:

注:ActionNode调用getChildren会抛异常,因为叶子节点不会有子节点,需要做try_catch。


3)行为树界面框架

既然mystic自带有行为树(状态机)显示插件,那么我的想法就是基于它来做改造, 一是将它改造为warlock的插件,二是对它的界面做些修改来适配界面 。那么我们需要思考怎么将这个插件的东西集成到warlock中实时展示行为树或状态机的状态了。

首先,mystic的数据都是从aer文件中获取的,且定义了一套Msg结构来保存aer文件中的数据。这些结构 cmake时会动态生成到build/include下的RvEventPipeClasses.hpp和RvEventPipeClasses.cpp中 ,然后它们依赖mystic中的 RvEventPipeMessages 。因此这里的第一个改造就是 将这几个文件复制 到我们的工程来编译:

有了数据结构,现在就来集成界面。用于展示行为树的几个界面如下图,其中主界面是 RvBAT_MovableView (我这里把黑板和状态机相关的文件都不用,因为我这里只关注行为树,虽然黑板数据也是行为树相关的,但暂时先不处理)

先将上述文件拷贝到我们的工程,并将RvBAT_MovableView集成到我们之前的界面右侧。然后将mystic回放相关的代码删除。由于 RvBAT_Interface是保存数据的关键类 ,只要把行为树的结构数据以及状态变化数据给他,后面的界面流程就可以保持不变,就是下图的两个关键对象。
这个类跟mystic的关联较大,包括 ResultPlatform、ResultDb、ResultData 等,因此需要进行删除和修改(只要是把RvBAT_Interface的AdvanceTimeRead的代码全注释),再把resources资源也拷贝过去进行编译,一些小问题修改后就能编译成功。

编译不报错就完成了框架的改造,下面来处理初始数据和实时状态变化数据。


4)初始数据设置到界面

上面说了在RvBAT_Interface对象中的两个重要的成员变量mTrees和mFSMs, 我这里只关心mTrees这个变量 。在原本的AdvanceTimeRead中有两处是对这个对象的操作,一处是初始化,一处是更新状态。

先来处理行为树结构的初始化。在前面介绍过在SimInterface的SimulationStarting方法中可以获取平台的行为树结构的数据,那么既然RvBAT_Interface的成员变量是mTrees,那么将 AdvanceTimeRead的参数改为这个变量的类型传入,数据在SimulationStarting构造 ,AdvanceTimeRead的初始化实现就很简单了:

void RvBAT::Interface::AdvanceTimeRead(    QMap<size_t, QMap<size_t, RvBAT::BehaviorTree>> trees){    QMutexLocker locker(&mMutex);     // 初始化mTrees变量    if (!trees.isEmpty())    {        mTrees.swap(trees);        if (mTrees.size() > 0)        {            // We are only loaded if we set data.            mBehaviorToolLoaded = true;        }    }}

然后仿照mystic的RvBAT_DockWindow的写法, 在调用RvBAT_MovableView的Update方法前,先调用RvBAT_Interface的AdvanceTimeRead方法,并传入我们创建好的trees数据 ,如下:

void BehaviorTreeDockWidget::update(){    // Only update when our window is visible.    if (isVisible() && m_dataInterfacePtr != nullptr)    {        m_dataInterfacePtr->AdvanceTimeRead(m_trees);         if (m_dataInterfacePtr->IsLoaded()) {            ui->btView->Update(m_trees);        }        // TODO    }}

最后在SimulationStarting中构造trees并设置 BehaviorTreeDockWidget 中,显示出行为树初始结 构,如下图:

完整行为树节点数据获取代码(包括初始状态设置):

void BehaviorTreeMonitorSimInterface::SimulationStarting(const WsfSimulation& aSimulation){    m_simulation = &const_cast(aSimulation);    mCallbacks.Add(WsfObserver::AdvancedBehaviorTreeState(m_simulation)        .Connect(&BehaviorTreeMonitorSimInterface::AdvancedBehaviorTreeState, this));     auto platformCount = aSimulation.GetPlatformCount();    // 获取平台    for (int i = 0; i < platformCount; ++i) {        WsfPlatform* platform = aSimulation.GetPlatformEntry(i);        if (platform == nullptr) continue;        auto platformIndex = platform->GetIndex();        QString platformName = platform->GetName().c_str();        auto processorCount = platform->GetComponentCount();        for (int j = 0; j < processorCount; ++j) {            WsfProcessor* processor = platform->GetComponentEntry(j);            if (processor == nullptr) continue;            WsfScriptProcessor* scriptProcessor = dynamic_cast(processor);            if (scriptProcessor == nullptr) continue;            // 获取WsfAdvancedBehaviorTree            WsfAdvancedBehaviorTree* bt = scriptProcessor->AdvancedBehaviorTree();            if (bt == nullptr) continue;            int treeId = bt->GetTreeId();            // 只添加有行为树的平台到列表            m_platforms.insert(platformIndex, platform);            // 递归获取行为树初始结构数据            auto rootNode = bt->RootNode();            getChildTree(rootNode.get(), true, bt, platform, m_trees);            m_trees[platformIndex][treeId].mName = bt->GetName();            m_trees[platformIndex][treeId].mState = new rv::MsgBehaviorTreeState;            m_trees[platformIndex][treeId].mState->platformIndex(platformIndex);            m_trees[platformIndex][treeId].mState->treeId(treeId);        }    }    m_behaviorTreeDockWidget->setPlatformAndTree(m_platforms, m_trees);}

其中构造trees时需要采用递归的方式来获取数据,代码如下:

void getChildTree(    WsfAdvancedBehaviorTreeNode* node,    bool isRoot,    WsfAdvancedBehaviorTree* bt,    WsfPlatform* platform,    QMap<size_t, QMap<size_t, RvBAT::BehaviorTree>>& trees){    auto treeId = bt->GetTreeId();    auto platformIndex = platform->GetIndex();     // 节点本身的数据    rv::BehaviorTreeNode treeNode;    treeNode.nodeId(node->Id());    treeNode.treeId(treeId);    treeNode.nodeType(node->GetType());    treeNode.nodeName(node->GetName());    treeNode.nodeDesc(node->GetDescription());    treeNode.isRootNode(isRoot);    // 节点状态    rv::BehaviorTreeNodeExec nodeExec;    nodeExec.execState(node->GetNodeStatus());    nodeExec.executeTooltip(node->GetExecuteTooltip());    nodeExec.nodeId(node->Id());    nodeExec.nodeName(node->GetName());    nodeExec.preconditionTooltip(node->GetPreconditionTooltip());    trees[platformIndex][treeId].mState->execList().push_back(nodeExec);    // 节点的children数据    try {        auto children = node->GetChildren();            treeNode.numChildren(children.size());            for (auto child : children)            {                treeNode.childrenIds().push_back(child->Id());                getChildTree(child.get(), false, bt, platform, trees);            }    }    catch (...) {    }    trees[platformIndex][treeId].mChildren.insert(node->Id(), treeNode);}

另外, 在集成后,会报Qt的sendEvent跨线程了,原因是afsim的SimulationStarting是另外的线程中触发的,而我们调用RvBAT_MovableView的Update方法后才会创建RvBAT::ABTScene,而这个对象又是在另外的线程创建的,所以会报错:

我这里的处理方法是,将数据 先设置到界面对象中,再通过信号触发界面的Update 方法,这样就能正常执行了。

5) behavior相关事件监听

上面完成了初始化工作,现在就来实现实时更新行为树状态。这里需要注意一点,因为afsim通知插件更新数据是在多线程中完成的,因此在 更新行为树状态时需加个锁来置换最新的数据

有两种方式来更新行为树的状态数据:一种是在SimInterface的 SimulationClockRead函数中获取行为树的状态数据 ,然后更新到插件;另一种是 监听behavior相关事件通知。

第一种方法与SimulationStarting的处理相似,就是遍历平台上的行为树状态数据,然后设置到trees中,再传给界面更新(可能效率较低,因为不管节点状态有没有变化都会执行)。我这里只说明第二种方式。

前面说过可以通过 绑定
WsfAdvancedBehaviorObserver的回调函数来获取树节点的状态:

这里我们只需要监听第二个就行了。

首先在插件的SimInterface实现类中 添加WsfSimulation和UtCallbackHolder变量 ,并在SimulationStarting中将变量初始化,并绑定上面的回调函数:(记得添加
WsfAdvancedBehaviorObserver.hpp头文件。

调试时,一旦行为树状态变化了,就会触发回调函数, 这里的node是根节点root,也需要通过递归思想来更新子节点的状态。

先来处理一个效率问题,因为m_trees[platformIndex][treeId].mState.execList()返回的是一个列表,要更新某个节点的状态时,需要遍历并比对nodeId,节点一多,为了更新某个节点的状态,需要每次都遍历整个列表,这样效率比较低。

为了提高效率,我这里 将nodeId和execList的迭代器做了映射。

在初始化时设置

最后在 AdvancedBehaviorTreeState实现状态更新 的关键代码如下:

void updateNodeState(    WsfAdvancedBehaviorTreeNode* node,     QMap<size_t, rv::BehaviorTreeNodeExecList::iterator>& nodeMap){    nodeMap[node->Id()]->execState(node->GetNodeStatus());    // 节点的children数据    try {        auto children = node->GetChildren();        for (auto child : children)        {            updateNodeState(child.get(), nodeMap);        }    }    catch (...) {    }} void BehaviorTreeMonitorSimInterface::AdvancedBehaviorTreeState(    double aSimTime,    WsfAdvancedBehaviorTreeNode* root){    WsfPlatform* platform = root->GetOwningPlatform();    int platformIndex = platform->GetIndex();     WsfAdvancedBehaviorTree* tree = root->GetOwningTree();    int treeId = tree->GetTreeId();     int nodeId = root->Id();    std::string nodeName = root->GetName();     rv::MsgBehaviorTreeState* state = m_trees[platformIndex][treeId].mState;    if (state == nullptr) return;    state->simTime(aSimTime);     updateNodeState(root, m_nodeIterMap[platformIndex][treeId]);    m_behaviorTreeDockWidget->updateTree(m_trees);}

6) 地图选中某平台切换

这个是想在 地图或平台浏览器点击某个平台时,行为树自动切换到对应平台去 。要达到这个目的,可以通过连接WkfEnviroment的 PlatformSelectionChanged 信号来实现:

connect(&wkfEnv,    &wkf::Environment::PlatformSelectionChanged,    this,    &BehaviorTreeDockWidget::platformSelectionChanged);

然后到槽函数中处理就行了:

void BehaviorTreeDockWidget::platformSelectionChanged(    const wkf::PlatformList& aSelectedPlatforms,     const wkf::PlatformList& aUnselectedPlatforms){    if (aSelectedPlatforms.isEmpty()) return;    auto platform = aSelectedPlatforms.last();    auto index = platform->GetIndex();
int count = ui->listWidgetPlatform->count(); for (int i = 0; i < count; ++i) { auto item = ui->listWidgetPlatform->item(i); auto platformIndex = item->data(Qt::UserRole).toInt(); if (platformIndex == index) { ui->listWidgetPlatform->setCurrentRow(i); on_listWidgetPlatform_itemClicked(item); return; } }}

为了实现切换效果,我这里在多添加了一个蓝-飞机2,使用上述相同的行为树定义,只是删除它执行交战这个节点以示区别:

完成后,编译插件并启动warlock加载run_tree.txt的执行效果如开篇视频所展示的。

6

总结

至此,实时行为树状态监视插件框架移植和搭建完成,移植的过程中,还是有许多代码逻辑需要修改,主要修改的两个文件是 RvBAT_Interface和RvBAT_MovableView,其他文件(如Scene相关的)基本不用修改 。只要理清mystic的实现逻辑对应去修改应该问题不是太大。

另外,由于改动的逻辑还是比较多,我也有点迷糊上面的过程,到底有没有讲明白,大家对着实现的时候应该会遇到很多问题,请尽量试着解决吧。

然后,这篇文章只把行为树做了, 状态机和黑板并没有做 。但基本可以按着上面的思路来实现。afsim是将这三者集成在一块完成的,这个就需要在完成行为树后,根据它的mystic的实现代码再次移植一下就可以了。

最后,这个只是把东西整通了,其他功能比如: 点击节点显示详细;节点变化的日志列表;节点的状态动态化;节点连线的动态化 等等可根据需要自由发挥 ,我呢继续去做下一个想法的实现去了.....

插件源码在本公众号同名淘宝店:


天线方向图实时绘制插件

wkf界面开发和插件扩展

服务端仿真引擎框架

DIS分布式仿真


Tags:

最近发表
标签列表