Reactive control patterns
A short essay on when behavior trees shine, when state machines are a better fit, and the architectural patterns that separate working reactive systems from the kind that wake you up at 3am.
What problem behavior trees solve
A reactive agent has to choose what to do next, in real time, based on what just happened and what the world looks like right now. The two classical ways to express that choice are:
- Finite state machines — explicit "I'm in state X, so I do Y" rules. Easy to draw, hard to scale past a dozen states.
- Behavior trees — a tree of small decisions, where each leaf does one thing and each branch encodes a fallback or a sequence.
Behavior trees win when:
- You have more than a handful of "modes" the agent can be in.
- The fallback structure is the interesting part (try A, then B, then C).
- You want designers — not just engineers — to author behavior.
States vs. ticks
A common mistake is to mix up ticking with
being in a state. A behavior tree doesn't have a persistent
"current state" the way an FSM does. Each call to
tree.tick() walks the tree from the root, runs children,
and returns a status. RUNNING means "I'm in the middle of
this — call me again next cycle". SUCCESS /
FAILURE mean "I'm done with this branch, you can move
on."
If you want a stateful phase (e.g. "robot is in
Charging phase"), model that as a Sequence of nodes that
returns RUNNING until the phase ends:
Selector
├── Sequence ← "Charging" phase
│ ├── IsBatteryBelow 0.95
│ ├── Wait 1.0
│ └── ChargeTick
└── ReadyPhase ← otherwise, go to "ready"
The "fallback ladder" pattern
For most real robots, the highest-level structure looks like a Selector with one branch per high-level goal, ordered from "most important" to "least important":
Selector
├── EmergencyHalt ← safety wins, always
├── LowBatteryReturn ← run out of juice
├── CurrentTask ← whatever the user assigned
└── Idle ← nothing to do, wait
The trick is that the CurrentTask branch itself is usually a huge subtree, not a single node. That's how you keep the top-level structure readable while still having hundreds of behaviors under it.
Blackboards: how nodes share state
A behavior tree is a tree, but the nodes need to talk to each other
beyond the parent-child relationship. The standard pattern is a
blackboard — a key/value dict attached to the tree
runtime. The editor doesn't ship one directly, but the BTCPP
Tree::setBlackboard
pattern is the canonical way: a node writes a value, another node
reads it.
A common mistake is using the blackboard as a global variable and losing track of who writes what. Treat it as an explicit data flow: name your keys, document them in a header comment in your tree, and review them in code review.
Subtree composition
For large trees, factor repeated logic into a
subtree (a named Sequence or Selector that you can
re-use). Most engines support this as a separate
<SubTree> or <Tree>
node. The editor can edit subtrees in isolation and copy them across
trees via JSON.
Common pitfalls
Pitfall 1: too many roots
Each Selector with multiple children is a "soft" state machine. If you have 20 root-level branches, you have 20 implicit states and 20×20 = 400 implicit transitions. That's a state machine wearing a tree costume. Either prune the branches or use an explicit FSM at the top and a tree per state.
Pitfall 2: state in nodes
Don't store state on the node object. The tree is supposed to be
re-entrant: you should be able to instantiate it twice with no
cross-contamination. State belongs in the blackboard or in the world
(the robot's actual battery level, not a flag on the
IsBatteryBelow node).
Pitfall 3: deep nested Selectors
Selectors nested five levels deep are a sign that you should be using a sub-tree or refactoring into a Sequence with a parallel sensing block. The tree should be readable as a paragraph.
When NOT to use behavior trees
- Your system has a small, fixed number of modes (3–5). Use a state machine — it's easier to draw and reason about.
- You need strong compile-time guarantees about the call graph. Use a typed ECS or an explicit scheduler.
- You're modeling a data pipeline, not a control flow. Use a DAG library.
Suggested reading
- Marzinotto et al., "A Unified Activity Recognition Framework", 2014 — academic framing of BTs vs. FSMs.
- BehaviorTree.CPP documentation (behaviortree.dev) — the most production-tested open-source BT engine.
- COLLADA / OpenRAVE planning docs — earlier-generation BT uses in robotics.