行为树节点类型介绍
- 控制节点
- 条件节点
- 动作节点
- 装饰节点
不同的人可能设计得会有点小出入,一般来说前面三大类是必须的,有的可能业务没有涉及到装饰节点,但有时候装饰节点的存在会使行为树看起来更自然,也更益于策划的配置。
每个节点都会有成功(success) 失败(failed) 运行中(running) 三种状态。对于条件节点来说一般只存在失败或者成功两个状态。下面分别细说上述的几大类节点
条件节点
条件节点就是具体执行的条件判断,一般只会返回成功或失败。在行为树中它跟动作节点一样一般都只出现在叶子节点上。比如说:周围是否有敌人?现在是白天吗?是否死亡?等等。
动作节点
动作节点是执行某一项具体的行为,一般只出现在叶子节点上。比如说:攻击、 移向某个目标等等,对于一个持续性的动作,在执行的过程中会返回running状态。这里穿插一些个人的想法,动作节点的返回状态一定要明确清楚,这样策划配置的时候才好配。为什么这么说呢?因为动作节点不像是条件节点有很明确的是否判断,有时候动作节点的是否判断不是很好界定。就比如巡逻(随机行走),它根本就没有所谓的失败。我自己在写行为节点的时候对于返回状态我都会写出具体说明。在巡逻的过程中返回running,否则到达目标点返回success
装饰节点
装饰节点就是修饰自己的子节点,由于其本身没有选择哪个子节点执行的逻辑,所以装饰节点有且仅有一个子节点,并且不会出现在叶子节点上。常见的装饰节点有:结果取反,子节点执行多少次,子节点运行的概率,运行直到成功,等等
控制节点
控制节点之所以放在最后面,是因为它在行为树中占据着至高的地位,是它将行为树中的条件节点,动作节点,装饰节点联系起来并控制着行为树逻辑的扭转,具体的来说是根据当前子节点返回的状态来决定下一步该执行哪个节点,下面以几种常见的控制节点来做一下详细说明
1、选择节点
选择节点可以说是行为树中常客了,根据不同的逻辑需求,又可以对选择节点进行分类,下面分别介绍几种经常用到的选择节点:
1.1、普通非优先级选择节点
正常情况下都是从左到右依次遍历,根据子节点的返回状态,存在如下的几种可能:
- 当找到一个成功运行的子节点,则当前选择节点也返回成功,并终止后续子节点的遍历
- 当所有的子节点都运行失败,则当前选择节点也返回失败
- 当某一个子节点返回运行中(running),则当前选择节点也返回运行中状态,并终止后续子节点的遍历,与前两种情况不同,下一帧再运行到该选择节点时,并不是头开始遍历,而是从当前运行中的节点开始往后遍历
下面伪代码说明
#define INVALID_INDEX -1
enum NodeStatus
{
SUCCESS = 0x01,
FAILED = 0x02,
RUNNING = 0x04
}
class BaseNode
{
public:
virtual NodeStatus Update() = 0;
protected:
int m_running_index = INVALID_INDEX;
std::vector<BaseNode*> m_childs;
}
class NonePrioritySelectorNode : public BaseNode
{
NodeStatus Update()
{
NodeStatus ret_status = FAILED;
int index = (m_running_index != INVALID_INDEX?m_running_index:0);
for (begin_index < m_childs.size(); begin_index++)
{
ret_status = m_childs[i]->Update();
if (ret_status & 0x05)
break;
}
SetRunningIndex(ret_status&RUNNING?begin_index:INVALID_INDEX);
return ret_status;
}
}
1.2、优先级选择节点
跟非优先级选择节点唯一的区别就是。不管上一帧该节点返回什么状态,每一帧都是从左到右遍历。所以就会存在靠左边的节点有优先执行的权利,一旦靠左边的节点满足执行条件就会终止上一帧没有执行完的节点的逻辑 伪代码如下:
class PrioritySelectorNode : public BaseNode
{
NodeStatus Update()
{
for (int index = 0;begin_index < m_childs.size(); begin_index++)
{
ret_status = m_childs[i]->Update();
if (ret_status & 0x05)
break;
}
return ret_status;
}
}
1.3、权重选择节点
权重选择节点就是给每个子节点设置一个权重值。权重值越大被选中的概率也就越大,=所有子节点的权重都一样的时候,就是随机选择节点了,这时候你的节点要支持权重值得设置。伪代码如下:
class WeightSelectorNode : public BaseNode
{
NodeStatus Update()
{
int index = m_running_index;
if (index == INVALID_INDEX)
{
index = GetNextChildByWeight();
}
NodeStatus status = m_childs[index]->Update();
SetRunningIndex(ret_status&RUNNING?begin_index:INVALID_INDEX);
return ret_status;
}
}
一般的行为树选择节点也就分成这三种,这里是把权重选择和随机选择统一了,当然也可以分开,这样随机选择也就少去了设置权重的过程
2、顺序节点
顺序节点是从左到右依次执行子节点,主要有如下几种情况
- 只有当前一个子节点执行成功,才会执行下一个子节点,直至所有的子节点都执行成功时该顺序节点才返回成功。
- 如果有一个子节点执行失败时,该顺序节点也返回失败。
- 如果有一个子节点返回运行中(running),该顺序节点也返回running,并且下一帧会从当前运行中的子节点往后继续执行。
伪代码如下:
class SeqenceNode : public BaseNode
{
NodeStatus Update()
{
NodeStatus ret_status = SUCCESS;
int index = (m_running_index!=INVALID_INDEX?m_running_index:0);
for (;index < m_childs.size();index++)
{
ret_status = m_childs[index]->Update();
if (ret_status & 0 x06)//running or failed
{
break;
}
}
SetRunningIndex(ret_status&RUNNING?index:INVALID_INDEX);
return ret_status;
}
}
3、并行节点
不同于选择节点和顺序节点依次执行,并行节点是“同时”执行所有的节点。这里的同时并不是指我们在程序常用多线程或者多进程实现的并发机制,而是在同一帧都执行一次,因为大多数情况下我们都是单线程的。
选择节点和顺序节点的返回状态跟子节点返回状态有密切的关系。在并行节点中,他的返回情况通常是用于手动设置的,常见的几种返回状态如下:
- 当全部节点都返回成功时退出;
- 当某一个节点返回成功时退出;
- 当全部节点都返回成功或失败时退出;
- 当某一个节点返回成功或失败时退出;
- 当全部节点都返回失败时退出;
- 当某一个节点返回失败时退出;
结束语
这一章主要是介绍行为树常用节点的概念和原理,下一章会结合behavior designer举几个具体的实例在说明行为树的节点在实际中的运用。
待续~~