网站首页 > 文章精选 正文
_^
AFSim-实用工具:
行为树状态实时监视插件
1
监视效果
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处理。
因此,这里就能可以通过平台->处理器组件->处理器->WsfScriptProcessor来获取到某平台上关联的行为树,然后获取树节点。(其实还可以通过 AdvancedBehaviorTree事件 来获取行为树,这是后话了^_^ )。 所有树节点类型在
WsfAdvancedBehaviorTreeNode 中定义,继承关系图如下:
2) 监听行为树状态
绑定
WsfAdvancedBehaviorObserver的回调函数即可:
AdvancedBehaviorTree ,通过它可以获取行为树^_^
AdvancedBehaviorTreeState ,通过它可以更新树节点状态
4
mystic中的behaviortree
afsim的mystic工具提供了一个在 回放过程中查看行为树 (或 状态机,本文不涉及 )的插件 (工程
ResultBehaviorAnalysisTool) 。 下图是加载run_tree.aer的行为树展示效果 ( 注: 这里是用我改造后支持中文脚本的wizard将示例中的节点名改为了中文,你们的wizrd如果不支持中文脚本的话就直接用英文的吧,对插件功能来说没有差别)
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 (我这里把黑板和状态机相关的文件都不用,因为我这里只关注行为树,虽然黑板数据也是行为树相关的,但暂时先不处理)
编译不报错就完成了框架的改造,下面来处理初始数据和实时状态变化数据。
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,而这个对象又是在另外的线程创建的,所以会报错:
上面完成了初始化工作,现在就来实现实时更新行为树状态。这里需要注意一点,因为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,使用上述相同的行为树定义,只是删除它执行交战这个节点以示区别:
6
总结
至此,实时行为树状态监视插件框架移植和搭建完成,移植的过程中,还是有许多代码逻辑需要修改,主要修改的两个文件是 RvBAT_Interface和RvBAT_MovableView,其他文件(如Scene相关的)基本不用修改 。只要理清mystic的实现逻辑对应去修改应该问题不是太大。
另外,由于改动的逻辑还是比较多,我也有点迷糊上面的过程,到底有没有讲明白,大家对着实现的时候应该会遇到很多问题,请尽量试着解决吧。
然后,这篇文章只把行为树做了, 状态机和黑板并没有做 。但基本可以按着上面的思路来实现。afsim是将这三者集成在一块完成的,这个就需要在完成行为树后,根据它的mystic的实现代码再次移植一下就可以了。
最后,这个只是把东西整通了,其他功能比如: 点击节点显示详细;节点变化的日志列表;节点的状态动态化;节点连线的动态化 等等可根据需要自由发挥 ,我呢继续去做下一个想法的实现去了.....
插件源码在本公众号同名淘宝店:
- 上一篇: Sliero VAD:高精度、轻量级的语音活动检测模型
- 下一篇: 大白话讲nnvm
猜你喜欢
- 2025-08-02 C++开发者都应该使用的十个C++11特性(上)
- 2025-08-02 如何实现自己的C++ unique_ptr?
- 2025-08-02 刚学会C++的小白用这个开源框架,做个 RPC 服务要多久?
- 2025-08-02 C++11+ 泛型编程(模板)
- 2025-08-02 abelkhan中的rpc框架
- 2025-08-02 C++设计模式:用代码演绎武侠世界的绝世神功
- 2025-08-02 视频分析与对象跟踪-扩展模块的单目标和多目标跟踪
- 2025-08-02 ROS2开发实践:ROS核心(节点、话题、服务、DDS通信协议等)
- 2025-08-02 C++语言程序员编程必收藏的20个经典实战案例(附完整源码)
- 2025-08-02 C# 控制电脑睡眠,休眠,关机以及唤醒
- 最近发表
- 标签列表
-
- newcoder (56)
- 字符串的长度是指 (45)
- drawcontours()参数说明 (60)
- unsignedshortint (59)
- postman并发请求 (47)
- python列表删除 (50)
- 左程云什么水平 (56)
- 编程题 (64)
- postgresql默认端口 (66)
- 数据库的概念模型独立于 (48)
- 产生系统死锁的原因可能是由于 (51)
- 数据库中只存放视图的 (62)
- 在vi中退出不保存的命令是 (53)
- 哪个命令可以将普通用户转换成超级用户 (49)
- noscript标签的作用 (48)
- 联合利华网申 (49)
- swagger和postman (46)
- 结构化程序设计主要强调 (53)
- 172.1 (57)
- apipostwebsocket (47)
- 唯品会后台 (61)
- 简历助手 (56)
- offshow (61)
- mysql数据库面试题 (57)
- fmt.println (52)