Reactive control patterns

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:

Behavior trees win when:

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

Suggested reading

← Custom node types Interop with BehaviorTree.CPP →