行为树入门工具包
偶然看到一个2012年 Alex Champandard 的演讲1,本想看看有什么可以借鉴,看到一半后,发现就是 Game AI Pro 的文章 《The Behavior Tree Starter Kit2》的演示版。结合代码3,对照着看一遍,有助于理解。
Motivation
行为树广泛应用于各个系统:角色,策略,小队,动画,镜头……
使用案例:
- Rockstar Games: R.A.G.E.
- Guerrilla Games:Killzone 2 & 3
- Uncharted 2
- Halo 3
- NBA ‘09
- Metro 2033
- Crysis: Warhead
- League of Legends
The Basics
v1:原型
一个基本的行为树实现:
enum Status
/**
* Return values of and valid states for behaviors.
*/
{
BH_INVALID,
BH_SUCCESS,
BH_FAILURE,
BH_RUNNING,
BH_ABORTED,
};
class Behavior
/**
* Base class for actions, conditions and composites.
*/
{
public:
virtual Status update() = 0;
virtual void onInitialize() {}
virtual void onTerminate(Status) {}
Behavior()
: m_eStatus(BH_INVALID)
{
}
virtual ~Behavior()
{
}
Status tick()
{
if (m_eStatus != BH_RUNNING)
{
onInitialize();
}
m_eStatus = update();
if (m_eStatus != BH_RUNNING)
{
onTerminate(m_eStatus);
}
return m_eStatus;
}
void reset()
{
m_eStatus = BH_INVALID;
}
void abort()
{
onTerminate(BH_ABORTED);
m_eStatus = BH_ABORTED;
}
bool isTerminated() const
{
return m_eStatus == BH_SUCCESS || m_eStatus == BH_FAILURE;
}
bool isRunning() const
{
return m_eStatus == BH_RUNNING;
}
Status getStatus() const
{
return m_eStatus;
}
private:
Status m_eStatus;
};
复合节点
class Composite : public Behavior
{
public:
void addChild(Behavior* child) { m_Children.push_back(child); }
void removeChild(Behavior*);
void clearChildren();
protected:
typedef std::vector<Behavior*> Behaviors;
Behaviors m_Children;
};
Sequence
class Sequence : public Composite
{
protected:
virtual ~Sequence()
{
}
virtual void onInitialize()
{
m_CurrentChild = m_Children.begin();
}
virtual Status update()
{
// Keep going until a child behavior says it's running.
for (;;)
{
Status s = (*m_CurrentChild)->tick();
// If the child fails, or keeps running, do the same.
if (s != BH_SUCCESS)
{
return s;
}
// Hit the end of the array, job done!
if (++m_CurrentChild == m_Children.end())
{
return BH_SUCCESS;
}
}
}
Behaviors::iterator m_CurrentChild;
};
Selector
class Selector : public Composite
{
protected:
virtual ~Selector()
{
}
virtual void onInitialize()
{
m_Current = m_Children.begin();
}
virtual Status update()
{
// Keep going until a child behavior says its running.
for (;;)
{
Status s = (*m_Current)->tick();
// If the child succeeds, or keeps running, do the same.
if (s != BH_FAILURE)
{
return s;
}
// Hit the end of the array, it didn't end well...
if (++m_Current == m_Children.end())
{
return BH_FAILURE;
}
}
}
Behaviors::iterator m_Current;
};
v2:共享结构
一个简单的行为树原型很容易实现,但有内存占用的问题:即使它们使用同一个行为树,使用它的每个 Agent 都要创建各自的行为树实例。解决的办法就是分离行为数据。
节点
- 所有行为树的公共数据
- 树的基本结构
- 所有参数和其它设置
任务
- 运行时实例的特定数据
- 指针,计数器……
基本节点
class Node;
class Task;
enum Status
{
BH_INVALID,
BH_SUCCESS,
BH_FAILURE,
BH_RUNNING,
};
class Node
{
public:
virtual Task* create() = 0;
virtual void destroy(Task*) = 0;
virtual ~Node() {}
};
class Task
{
public:
Task(Node& node)
: m_pNode(&node)
{
}
virtual ~Task() {}
virtual Status update() = 0;
virtual void onInitialize() {}
virtual void onTerminate(Status) {}
protected:
Node* m_pNode;
};
class Behavior
{
protected:
Task* m_pTask;
Node* m_pNode;
Status m_eStatus;
public:
Behavior()
: m_pTask(NULL)
, m_pNode(NULL)
, m_eStatus(BH_INVALID)
{
}
Behavior(Node& node)
: m_pTask(NULL)
, m_pNode(NULL)
, m_eStatus(BH_INVALID)
{
setup(node);
}
~Behavior()
{
m_eStatus = BH_INVALID;
teardown();
}
void setup(Node& node)
{
teardown();
m_pNode = &node;
m_pTask = node.create();
}
void teardown()
{
if (m_pTask == NULL)
{
return;
}
ASSERT(m_eStatus != BH_RUNNING);
m_pNode->destroy(m_pTask);
m_pTask = NULL;
}
Status tick()
{
if (m_eStatus == BH_INVALID)
{
m_pTask->onInitialize();
}
m_eStatus = m_pTask->update();
if (m_eStatus != BH_RUNNING)
{
m_pTask->onTerminate(m_eStatus);
}
return m_eStatus;
}
template <class TASK>
TASK* get() const
{
// NOTE: It's safe to static_cast, the caller almost
// always knows what kind of task type it is.
return dynamic_cast<TASK*>(m_pTask);
}
};
复合节点
typedef std::vector<Node*> Nodes;
class Composite : public Node
{
public:
Nodes m_Children;
};
Sequence
class Sequence : public Task
{
public:
Sequence(Composite& node)
: Task(node)
{
}
Composite& getNode()
{
return *static_cast<Composite*>(m_pNode);
}
virtual void onInitialize()
{
m_CurrentChild = getNode().m_Children.begin();
m_CurrentBehavior.setup(**m_CurrentChild);
}
virtual Status update()
{
for (;;)
{
Status s = m_CurrentBehavior.tick();
if (s != BH_SUCCESS)
{
return s;
}
if (++m_CurrentChild == getNode().m_Children.end())
{
return BH_SUCCESS;
}
m_CurrentBehavior.setup(**m_CurrentChild);
}
}
protected:
Nodes::iterator m_CurrentChild;
Behavior m_CurrentBehavior;
};
Selector
class Selector : public Task
{
public:
Selector(Composite& node)
: Task(node)
{
}
Composite& getNode()
{
return *static_cast<Composite*>(m_pNode);
}
virtual void onInitialize()
{
m_CurrentChild = getNode().m_Children.begin();
m_Behavior.setup(**m_CurrentChild);
}
virtual Status update()
{
for (;;)
{
Status s = m_Behavior.tick();
if (s != BH_FAILURE)
{
return s;
}
if (++m_CurrentChild == getNode().m_Children.end())
{
return BH_FAILURE;
}
m_Behavior.setup(**m_CurrentChild);
}
}
protected:
Nodes::iterator m_CurrentChild;
Behavior m_Behavior;
};
特点
优点
- 易于实现
- 易于调试
- 易于理解 缺点
- 代码完全基于轮询
- 一系列的虚函数调用
- 遍历过程涉及大量内存访问
- 栈的最大大小取决于数据,潜在的栈溢出问题
Modern BTs
为了解决上述问题,可以编写一个专门的解释器,能够比C++更好地处理行为树。
优化策略:
- 使用直接办法
- 设计数据结构
- 减少函数调用
- 考量内存访问
- 使用领域知识
- 理解BT使用场景
- 优化架构/算法
- 简化BT结构
v3: Data-Oriented BT
优化内存分配
降低Cache miss,提高缓存一致性,内存分配采取以下方法:
- 使用栈分配器为整个行为树分配内存
- 连续分配行为树节点的内存
- 默认使用深度优先顺序存储节点
- 子节点紧随其父节点
代码实现
基础部分没有什么变化
enum Status
{
BH_INVALID,
BH_SUCCESS,
BH_FAILURE,
BH_RUNNING,
};
class Behavior
{
public:
Behavior()
: m_eStatus(BH_INVALID)
{
}
virtual ~Behavior()
{
}
Status tick()
{
if (m_eStatus == BH_INVALID)
onInitialize();
m_eStatus = update();
if (m_eStatus != BH_RUNNING)
onTerminate(m_eStatus);
return m_eStatus;
}
protected:
virtual Status update() = 0;
virtual void onInitialize() {}
virtual void onTerminate(Status) {}
Status m_eStatus;
};
typedef std::vector<Behavior*> Behaviors;
变化在这里:
const size_t k_MaxBehaviorTreeMemory = 8192;
class BehaviorTree
{
public:
BehaviorTree()
: m_pBuffer(new uint8_t[k_MaxBehaviorTreeMemory])
, m_iOffset(0)
{
}
~BehaviorTree()
{
delete [] m_pBuffer;
}
template <typename T>
T& allocate()
{
T* node = new ((void*)((uintptr_t)m_pBuffer + m_iOffset)) T;
m_iOffset += sizeof(T);
ASSERT(m_iOffset < k_MaxBehaviorTreeMemory);
return *node;
}
protected:
uint8_t* m_pBuffer;
size_t m_iOffset;
};
使用节点索引
降低内存消耗,可以使用以下方法:
- 不依赖指针
- 使用下标或相对偏移量
- 让复合节点的子节点更易查找
代码实现
const size_t k_MaxChildrenPerComposite = 7;
class Composite : public Behavior
{
public:
Composite()
: m_ChildCount(0)
{
}
void addChild(Behavior& child)
{
ASSERT(m_ChildCount < k_MaxChildrenPerComposite);
ptrdiff_t p = (uintptr_t)&child - (uintptr_t)this;
ASSERT(p < std::numeric_limits<uint16_t>::max());
m_Children[m_ChildCount++] = static_cast<uint16_t>(p);
}
Behavior& getChild(size_t index)
{
ASSERT(index < m_ChildCount);
return *(Behavior*)((uintptr_t)this + m_Children[index]);
}
size_t getChildCount() const
{
return m_ChildCount;
}
private:
uint16_t m_Children[k_MaxChildrenPerComposite];
uint16_t m_ChildCount;
};
class Sequence : public Composite
{
protected:
virtual ~Sequence()
{
}
virtual void onInitialize()
{
m_Current = 0;
}
virtual Status update()
{
for (;;)
{
Status s = getChild(m_Current).tick();
if (s != BH_SUCCESS)
{
return s;
}
ASSERT(m_Current < std::numeric_limits<uint16_t>::max() - 1);
if (++m_Current == getChildCount())
{
return BH_SUCCESS;
}
}
}
uint16_t m_Current;
};
class Selector : public Composite
{
protected:
virtual ~Selector()
{
}
virtual void onInitialize()
{
m_Current = 0;
}
virtual Status update()
{
for (;;)
{
Status s = getChild(m_Current).tick();
if (s != BH_FAILURE)
{
return s;
}
ASSERT(m_Current < std::numeric_limits<uint16_t>::max() - 1);
if (++m_Current == getChildCount())
{
return BH_FAILURE;
}
}
}
uint16_t m_Current;
};
特点
优点
- 实现难度不算太大
- 优化方法简单易行
缺点
- 不易调试,需要借助工具
- 代码不易阅读和理解
v4: Event-Driven BT
执行顺序
首次遍历
- 所有节点按深度优先顺序开始
- 一切照旧
后续遍历
- 不再从根节点遍历!
- 子节点会广播其状态变化
- 父节点做出响应并应用
- 只在最坏的情况下,重新从根节点开始
代码实现
enum Status
{
BH_INVALID,
BH_SUCCESS,
BH_FAILURE,
BH_RUNNING,
BH_SUSPENDED,
};
typedef std::function<void (Status)> BehaviorObserver;
class Behavior
{
public:
Behavior()
: m_eStatus(BH_INVALID)
{
}
virtual ~Behavior()
{
}
Status tick()
{
if (m_eStatus == BH_INVALID)
{
onInitialize();
}
m_eStatus = update();
if (m_eStatus != BH_RUNNING)
{
onTerminate(m_eStatus);
}
return m_eStatus;
}
virtual Status update() = 0;
virtual void onInitialize() {}
virtual void onTerminate(Status) {}
Status m_eStatus;
BehaviorObserver m_Observer;
};
class BehaviorTree
{
public:
void start(Behavior& bh, BehaviorObserver* observer = NULL)
{
if (observer != NULL)
{
bh.m_Observer = *observer;
}
m_Behaviors.push_front(&bh);
}
void stop(Behavior& bh, Status result)
{
ASSERT(result != BH_RUNNING);
bh.m_eStatus = result;
if (bh.m_Observer)
{
bh.m_Observer(result);
}
}
void tick()
{
// Insert an end-of-update marker into the list of tasks.
m_Behaviors.push_back(NULL);
// Keep going updating tasks until we encounter the marker.
while (step())
{
continue;
}
}
bool step()
{
Behavior* current = m_Behaviors.front();
m_Behaviors.pop_front();
// If this is the end-of-update marker, stop processing.
if (current == NULL)
{
return false;
}
// Perform the update on this individual task.
if (current->m_eStatus != Status::BH_SUSPENDED)
{
current->tick();
}
// Process the observer if the task terminated.
if (current->m_eStatus != BH_RUNNING && current->m_Observer)
{
current->m_Observer(current->m_eStatus);
}
else // Otherwise drop it into the queue for the next tick().
{
m_Behaviors.push_back(current);
}
return true;
}
protected:
std::deque<Behavior*> m_Behaviors;
};
// ============================================================================
class Composite : public Behavior
{
public:
typedef std::vector<Behavior*> Behaviors;
Behaviors m_Children;
};
class Sequence : public Composite
{
public:
Sequence(BehaviorTree& bt)
: m_pBehaviorTree(&bt)
{
}
protected:
BehaviorTree* m_pBehaviorTree;
virtual void onInitialize()
{
m_Current = m_Children.begin();
BehaviorObserver observer = std::bind(&Sequence::onChildComplete, this, std::placeholders::_1);
m_pBehaviorTree->start(**m_Current, &observer);
}
void onChildComplete(Status)
{
Behavior& child = **m_Current;
if (child.m_eStatus == BH_FAILURE)
{
m_pBehaviorTree->stop(*this, BH_FAILURE);
return;
}
ASSERT(child.m_eStatus == BH_SUCCESS);
if (++m_Current == m_Children.end())
{
m_pBehaviorTree->stop(*this, BH_SUCCESS);
}
else
{
BehaviorObserver observer = std::bind(&Sequence::onChildComplete, this, std::placeholders::_1);
m_pBehaviorTree->start(**m_Current, &observer);
}
}
virtual Status update()
{
return BH_RUNNING;
}
Behaviors::iterator m_Current;
};
特点
优点
- 保证最小计算量
- 容易实现一个单步调试工具
- 优雅简洁
缺点
- 使用回调函数,代码不直观,不易理解。
- 不只依赖于优化技巧,还需要其他方面的知识。
What Next?
尚未实现的部分
节点
- 装饰节点(Decorators)
- 并行节点(Parellel)
API
- 如何
abort()
行为以及整个行为树 - 如何支持不可中断的行为。