行为树节点基类必须支持状态机语义,每个节点tick()须返回Running、Success或Failure三种状态之一,并通过可继承的BTNode::Status类型封装;Composite节点需显式控制子节点遍历与中断逻辑,Decorator须接管子节点生命周期,所有节点须显式管理运行时上下文以支持跨帧恢复。

行为树节点基类必须支持状态机语义
行为树不是简单递归调用,每个节点执行后要返回 Running、Success 或 Failure,否则黑板(Blackboard)数据无法被正确复用,子树重入时会跳过中间状态。C++ 里别用 enum class 硬编码三个值就完事——得封装成可继承的 BTNode::Status 类型,并让所有节点(Sequence、Selector、Decorator、Leaf)都实现 tick() 接口。
常见错误是把 tick() 写成纯虚函数但没处理重入逻辑:比如一个 Wait 节点在第 1 帧返回 Running,第 2 帧却重新初始化计时器。正确做法是在节点内部缓存运行时上下文(如 m_elapsed),或把上下文抽到独立的 BTInstance 中与节点实例解耦。
Composite 节点的子节点遍历顺序和中断机制要显式控制
Sequence 和 Selector 的执行不是“for 循环走一遍”,而是每帧只 tick 当前活跃子节点,并根据其返回状态决定是否继续、跳过或中止。比如 Sequence 在某个子节点返回 Failure 后,后续子节点必须跳过;而 Selector 在某子节点返回 Success 后,其余子节点不能执行。
容易踩的坑:
- 用
std::vector<:unique_ptr>>存子节点,但每次 tick 都从索引 0 开始——这会导致Running节点被重置 - 没区分“首次进入子节点”和“从中断恢复”,导致条件节点(如
IsPlayerInSight)重复查询或漏查 - 在多线程环境直接共享同一棵行为树,没做
BTInstance实例隔离,造成状态错乱
Decorator 节点必须能修改子节点返回值或拦截执行
Inverter、Repeat、UntilFail 这类装饰器不是包装器,它们要接管子节点的生命周期。例如 Repeat(3) 必须记录已执行次数,并在子节点返回 Success 后主动再次 tick 它——而不是等上层 Composite 来调度。
关键设计点:
- 装饰器的
tick()必须调用m_child->tick(),且不能无条件转发返回值 -
Repeat类型需持有自己的计数器,且该计数器属于运行时实例,不能存在节点类静态成员里 - 像
ForceSuccess这种装饰器,即使子节点返回Failure,也要覆盖为Success并结束 tick 流程
避免用 std::function + lambda 构建叶子节点,优先用策略模式
写个 MoveToTarget 叶子节点时,如果用 std::function 存逻辑,看似灵活,实则带来三重问题:无法序列化、无法调试具体哪一行出错、每次 tick 都有虚调用+闭包捕获开销。更实际的做法是定义抽象基类 BTAIAction,再派生具体行为(MoveToTargetAction、AttackAction),由叶子节点持有一个 std::unique_ptr。
这样做的好处:
- 行为逻辑可单独单元测试,不依赖树结构
- 黑板访问统一走
m_blackboard.get,而非在 lambda 里散落多处("distance") - 编辑器导出时,只需序列化派生类名和参数字段,不用解析任意代码
真正难的不是写通基本流程,而是让每个节点在 Running 状态下能安全暂停、保存上下文、跨帧恢复——这要求所有节点不依赖栈变量,所有临时状态必须显式管理。很多人卡在这里,不是语法不会,是没想清楚“行为”本质上是一组带状态的协程。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











