When FSMs get tangled, behavior trees scale. Behavior is a tree of composable nodes — sequences, selectors, and leaves — evaluated each tick. They power the AI in most modern action games.
A behavior tree organizes AI as a tree of nodes evaluated from the root each tick, where each node reports success, failure, or running. Unlike an FSM’s web of transitions, a tree is modular — you compose behavior from reusable sub-trees and add new branches without rewiring everything. This composability is why behavior trees became the standard for complex AI in modern games.
A behavior tree ticks from the ROOT each frame; every node returns:
SUCCESS · FAILURE · RUNNING (still working, e.g. walking a path)
Composite nodes control flow over their children:
SEQUENCE run children in order; FAIL if any fails ("do A AND B AND C")
SELECTOR try children in order; SUCCEED at the first that works
("do A OR B OR C" — a priority fallback list)
Leaf nodes do the work:
ACTION "move to cover", "shoot" (has an effect)
CONDITION "is player visible?" (just checks)
Behavior is COMPOSED from reusable sub-trees -> scales far past FSMs.The two core composite nodes give you AND and OR logic. A Sequence runs its children in order and fails if any child fails — "walk to the door AND open it AND go through." A Selector tries its children until one succeeds — "attack the player, OR if you can’t, chase, OR patrol." Nesting these two builds prioritized, conditional behavior with remarkable clarity.
SUCCESS, FAILURE, RUNNING = "success", "failure", "running"
def sequence(children, agent):
for child in children: # AND: all must succeed, in order
result = child(agent)
if result != SUCCESS:
return result # stop at the first non-success
return SUCCESS
def selector(children, agent):
for child in children: # OR: succeed at the first that works
result = child(agent)
if result != FAILURE:
return result # a success or "running" wins
return FAILURE
# "attack if possible, else chase, else patrol" = a selector of behaviors:
def enemy_ai(agent):
return selector([try_attack, try_chase, patrol], agent)Actions in games take time — walking a path is not instant. The RUNNING result lets a node say "I’m not done yet," so the tree remembers it is mid-action and resumes there next tick instead of restarting from the top. This is what lets a behavior tree drive time-consuming actions smoothly, and it is the key difference from a simple decision tree that resolves in one pass.
SUCCESS, FAILURE, RUNNING = "success", "failure", "running"
def move_to(target):
def action(agent):
agent.step_toward(target) # move a little this tick
if agent.reached(target):
return SUCCESS # done
return RUNNING # still going -> resume here next tick
return action
# a sequence that walks to cover, THEN shoots — the walk takes many ticks:
def take_cover_and_fire(agent):
return sequence([move_to(agent.cover), shoot], agent)
# while move_to returns RUNNING, the tree waits on it (doesn't restart 'shoot').Two additions make behavior trees practical at scale. Decorator nodes wrap a child to modify it — invert its result, repeat it, add a cooldown, or gate it behind a condition. And a blackboard is shared memory (the player’s last known position, current target) that nodes read and write, keeping the tree stateless while giving actions the context they need. Together they keep large trees clean and data-driven.
DECORATORS wrap a single child to modify its behavior:
Inverter flip SUCCESS <-> FAILURE ("is the player NOT visible?")
Repeater run the child N times / until it fails
Cooldown only allow the child every X seconds (rate-limit an attack)
Guard run the child only if a condition holds
BLACKBOARD = shared memory the tree reads/writes:
{ "target": player, "last_seen_pos": (10, 4), "health": 80 }
-> a "chase" action reads blackboard["last_seen_pos"]; a "spot player"
condition writes it. Nodes stay reusable + stateless; data lives on the
blackboard. This is how engines (Unreal's Behavior Trees) structure AI.