All tutorials Mighty Professional
Build a Game Engine ยท AI

Behavior Trees

A guard NPC has to patrol, notice you, chase, attack and fall back when it's hurt, and next week a designer will ask for one more behavior. A behavior tree, a tree of small decision and action nodes that the game walks from the top on every AI update, makes that new behavior one new branch instead of a rewire of every existing one. This page builds one from scratch: the three return statuses, sequences and selectors, decorators, parallel nodes, the blackboard the nodes share, and how a tree notices an enemy halfway through a walk. It also covers where trees stop paying, with C++ and Rust code, six live widgets and case studies from Halo, Unreal, The Last of Us, Horizon Zero Dawn and Hitman.

Time~45 min LevelJunior to mid; review for senior PrereqsYou can read C++ or pseudocode. Trees and recursion. Basic game-loop awareness. HardwareNone.
โ—‚ Build a Game Engine Phase 11 ยท AI Next ยท Navmesh & Steering โ–ธ
What you'll be able to do by the end

01Why behavior trees

An NPC decides, many times a second, whether to patrol a route, chase a target, take cover, reload, call for backup or retreat. In the early 2000s those decisions commonly lived in finite state machines (FSMs): the NPC is in exactly one state, such as Patrol, and a hand-written transition moves it to another, such as Chase. That works at five states. Every state added after that has to be checked against every existing one for transitions in both directions (§3), and by a few dozen states the transition set is hard to review and easy to break.

A behavior tree drops the transitions. Each behavior is a branch, the order of the branches sets their priority, and the game walks the tree from the root on every tick (one AI update, often once per frame) to find the first branch that can run. The survey literature puts it structurally: in a BT "the state transition logic is not dispersed across the individual states, but organized in a hierarchical tree structure, with the states as leaves"[5]. Adding a behavior means grafting a subtree onto an existing branch, not wiring transitions to every other state. (Walking from the root every tick is the classic model; §11 covers the event-driven variants that keep running the active branch and re-check only when something they watch changes.)

You'll meet them in the major engines. Unreal ships a behavior tree editor[6] and, since 2022, StateTree, a hybrid of trees and state machines, alongside it[7]. Unity shipped its own first-party tool in September 2024, the com.unity.behavior package[8], which Unity calls a behavior graph rather than a tree, since branches may merge back. Studios that hand-roll their AI often land somewhere else entirely, as §14 shows.

What the tree buys you over an FSM:

02A short history

Why does an idea from a 2004 shooter now drive Unreal's AI editor and robot navigation software? Behavior trees came out of game AI and were formalized later by robotics researchers, and the textbook terms you'll meet in engine docs come from both sides.

Timeline: from Halo 2 (2004) to the first BT textbook (2018)
2004
Halo 2 ships with a behavior DAG. Damian Isla's AI for the Covenant, the alien alliance the player fights in Halo, is built from self-contained behaviors chosen by decision schemes such as prioritized lists and sequences, arranged in what he calls a "behavior DAG" because one behavior subtree can sit in several places in the graph[1].
2005
Isla presents "Handling Complexity in the Halo 2 AI" at GDC. The talk argues for behavior-centric architectures over state-centric ones: behaviors as self-contained modules, evaluation order encoding priority, no explicit state-to-state transitions[1]. Later BT writing, from the Game AI Pro starter kit to the robotics surveys, cites it as a starting point[10][5].
2007
Champandard writes "Understanding Behavior Trees" on AiGameDev.com.[2] The short article frames behaviors as HFSM states with their outgoing transitions removed, sequenced instead by the semantics of their parent, "the same method as most programming languages", with sequences and selectors as the standard composites and decorators layered on top. Champandard and Dunstan's 2013 Game AI Pro chapter[10] follows with an open-source reference implementation, the Behavior Tree Starter Kit, and describes the "second-generation" techniques that followed, event-driven traversal among them.
2014
Unreal Engine 4 ships a built-in behavior tree editor.[6] The system includes a visual graph editor, a paired blackboard asset, decorators that attach to composite or task nodes, and an event-driven evaluation model that avoids polling.
2014
Marzinotto, Colledanchise, Smith, and Ögren formalize BTs for robotics. "Towards a Unified Behavior Trees Framework for Robot Control," IEEE ICRA 2014 (the International Conference on Robotics and Automation)[4]. The paper reviews the inconsistent BT literature of the time, proposes a unified formulation, and relates BTs to controlled hybrid dynamical systems, the control-theory model for systems that mix continuous motion with discrete switches between controllers, giving the game-AI idea the rigor robotics and control applications need.
2018
Colledanchise and Ögren publish the first book-length treatment of BTs. Behavior Trees in Robotics and AI: An Introduction, CRC Press[3]. It covers the formal analysis of efficiency, safety and robustness, how BTs relate to FSMs and generalize decision trees, and BTs combined with planning and machine learning, and is the standard academic reference.

In short: game AI built the idea, starting with Isla's Halo 2 AI (shipped 2004, presented at GDC 2005)[1] and Champandard's 2007 article that set out sequences, selectors and decorators as the standard nodes[2]. Robotics formalized it, from the 2014 ICRA paper[4] to Colledanchise and Ögren's 2018 book[3]. A 2022 survey credits the key milestones to Michael Mateas and Andrew Stern's ABL behavior language and to Isla's talk[5]. Isla himself claims no priority, and calls his architecture a "hierarchical finite state machine (HFSM) or a behavior tree, or even more specifically, a behavior DAG"[1]: a directed acyclic graph, because one behavior subtree can sit in several places.

03The FSM problem

A designer asks for one more enemy state, Heal, in an FSM that already has seven: Idle, Patrol, Chase, Attack, Flee, Stunned and Search. Before anyone writes Heal, someone has to answer fourteen questions: can each of the seven existing states move to Heal, and can Heal move back to each of them? A wrong answer either way is a bug that playtesting finds later.

An with n states has up to n(n - 1) directed transitions, because each state can move to each of the other n - 1. Not every pair needs a transition in practice, but every new state raises the same two questions for every existing one. Missing a transition is a bug (the agent gets stuck in a state). Adding a redundant one is also a bug (the agent escapes a state it shouldn't leave). That's n - 1 pairs to check per new state, and the total grows with the square of the state count, O(n2).

Worked example

The seven-state FSM above has up to 7 × 6 = 42 transitions. Adding Heal makes it 8 × 7 = 56, so 14 new transitions to decide: 7 into Heal and 7 out of it. A behavior tree for the same eight behaviors, as a flat selector, has a root plus one leaf per behavior: 9 nodes and 8 edges. Adding a ninth behavior adds 1 node and 1 edge, and no existing branch changes.

Hierarchical FSMs (HFSMs) help by nesting a smaller state machine inside a state, so a transition every child shares, such as "any combat state can flee", is written once on the parent. The explosion still happens within each level. Pushdown automata, state machines with a stack of states, help with "go do X, then come back to what you were doing". Neither removes the core problem: the interaction between states lives in transitions, and transitions grow with the square of the state count.

The widget below makes this visible. Add states to the FSM on the left and the transition arrows multiply. The behavior tree on the right adds a leaf for each new behavior, and its edge count grows by one each time.

Live ยท FSM vs BT scaling
FSM states
3
FSM transitions
6
BT nodes
4
BT edges
3
Click + add state up to the maximum of eight and watch the two counts. The FSM on the left draws every possible transition, the worst case: 3 states make 6 arrows and 8 make 56. Real FSMs wire fewer, but whether each pair needs one is still a question someone has to answer, which is the design cost from the text. The BT on the right is a flat selector for simplicity, so 8 behaviors is 9 nodes and 8 edges. A real tree has nested composites, but its edge count still grows by one per behavior, and each new branch is isolated from its siblings.

04Tree structure and tick

A guard is halfway through a walk to its next waypoint when the AI update runs again. The tree can't block the game until the walk finishes, and it can't forget the walk either. So every node answers each tick with one of three words: done, couldn't, or still working.

A behavior tree is a rooted tree. Every node has zero or more children. The tree is evaluated from the root every tick (typically once per frame, though some engines tick AI at a lower rate to save CPU). Each node's tick() function returns one of three statuses:

Node types fall into three categories:

Evaluation is top-down, left-to-right. The root ticks its children according to its composite type, those children tick theirs, and the results propagate back up: one walk through the tree per tick.

Worked example

The widget below has 8 nodes: a root selector, an Attack sequence (EnemyClose? then Fire), a Chase sequence (EnemySeen? then MoveTo), and a Patrol action. With no enemy around, one tick visits 6 of them: Root, Attack, EnemyClose? (fails, so Fire is skipped), Chase, EnemySeen? (fails, so MoveTo is skipped), then Patrol, which succeeds. With an enemy in range it visits 4: Root, Attack, EnemyClose?, Fire. Chase and Patrol are never touched, because the selector stops at the first branch that succeeds.

The widget below shows this evaluation in action. A selector at the root tries three branches: Attack (sequence of condition + action), Chase (sequence of condition + action), and Patrol (single action). Switch the scenario to change which conditions pass, then step through or auto-play to watch the tick propagate.

Live ยท BT tick visualizer
step
0
total steps
0
scenario
patrol
Pick a scenario, then press step to light up one node result at a time, or auto to play the tick. Total steps counts every highlight, including each composite's entry and its result: 9 for patrol and chase, 6 for attack. In the "patrol" scenario, both conditions fail. The selector tries Attack (fails at EnemyClose?), then Chase (fails at EnemySeen?), then falls through to Patrol (succeeds). In "chase," EnemySeen passes, so the Chase sequence runs. In "attack," EnemyClose passes, so the selector never reaches Chase or Patrol. Priority is encoded by left-to-right order. Blue (EVAL) marks a composite still working through its children within this tick; it is not a return status.

05Sequences

A guard should fire only if it has ammo and has finished aiming. If any step fails, it shouldn't carry on to the next one. That "do these in order, stop at the first failure" pattern is a sequence.

A Sequence is AND logic. It executes its children left to right. If a child returns SUCCESS, the sequence moves to the next child. If a child returns FAILURE, the sequence stops and returns FAILURE. If all children return SUCCESS, the sequence returns SUCCESS. If any child returns RUNNING, the sequence returns RUNNING.

The mental model: "do A, then B, then C. If any step fails, abort." A patrol sequence might be: [HasWaypoint?, MoveToWaypoint, Wait(2s), NextWaypoint]. If HasWaypoint? fails, the entire patrol sequence fails and the parent selector tries something else.

Worked example

The patrol sequence on a frame where the guard is mid-walk: HasWaypoint? returns SUCCESS, so the sequence moves on; MoveToWaypoint returns RUNNING, so the sequence stops there and returns RUNNING. Wait(2s) and NextWaypoint aren't ticked this frame. Two frames later the walk ends: MoveToWaypoint returns SUCCESS and the sequence goes straight on to Wait(2s), which returns RUNNING for the next two seconds.

06Selectors (fallbacks)

A guard has three things it could be doing: attacking, chasing or patrolling. It should attack if it can, chase if it can't attack, and patrol only when neither applies. That "try these in priority order, stop at the first one that works" pattern is a selector.

A Selector (sometimes called Fallback or Priority) is OR logic. It executes children left to right. If a child returns FAILURE, the selector moves to the next child. If a child returns SUCCESS, the selector stops and returns SUCCESS. If all children fail, the selector returns FAILURE. If any child returns RUNNING, the selector returns RUNNING.

The mental model: "try A. If that fails, try B. If that fails, try C." The leftmost child has the highest priority. A top-level selector with [AttackSequence, ChaseSequence, PatrolAction] tries combat first, pursuit second, and only patrols if neither applies.

Worked example

Give a sequence and a selector the same three children, [HasAmmo?, Aim, Fire], returning SUCCESS, SUCCESS, FAILURE. The sequence ticks all three: HasAmmo? and Aim succeed, Fire fails, so it returns FAILURE. The selector ticks only HasAmmo?, which succeeds, so it returns SUCCESS and never looks at Aim or Fire. Same children, opposite answers, and a different number of nodes ticked.

The widget below puts a sequence and a selector side by side with the same three children. Toggle each child's result to see how the composite outcome differs.

Live ยท Sequence vs Selector
Sequence result
ยทยทยท
Selector result
ยทยทยท
Click a child's button to cycle its result through OK, FAIL and RUNNING; both composites read the same three results, and each panel shows how far its composite got. With the default (OK, OK, FAIL): the sequence stops at Fire and returns FAILURE (AND logic, one failure is enough to fail it). The selector stops at HasAmmo? and returns SUCCESS (OR logic, one success is enough). Click HasAmmo? once to make it FAIL and neither result changes, but both paths do: the sequence now stops at the first child, and the selector moves on and succeeds at Aim. Click again for RUNNING, which stops either composite at that child and propagates up unchanged.

07Decorators

A guard shouts "Over there!" when it spots you. Left alone, the bark's condition stays true while you're in view, so the guard starts a new bark the moment the last one ends. You don't want to edit the Bark action; you want to wrap it in "after each bark, stay quiet for ten seconds". That wrapper is a decorator.

A decorator wraps exactly one child and modifies its behavior or result. Common decorators:

Worked example

Wrap a three-second Bark in Cooldown(10). The guard spots you at t = 0 s and the bark plays until t = 3 s. From 3 s to 13 s the decorator returns FAILURE without ticking Bark, so the parent selector moves on to the next branch and the guard gets on with chasing you. At t = 13 s, if you're still in view, it barks again: one bark every 13 seconds instead of back-to-back barks.

Unreal uses decorators differently. They attach to a composite or task node rather than sitting in the tree as a parent of their own, and Epic recommends them in place of condition leaves: "In the standard model for Behavior Trees, conditionals are Task leaf nodes", whereas Unreal's conditional decorators gate a branch so that every leaf is an action[6][22]. Those decorators double as abort triggers: when a decorator's condition changes, it can abort its own subtree, the lower-priority branches to its right, or both (§11).

08Parallel nodes

A guard sprinting to cover should keep its gun pointed at you while it runs. A sequence can't do that: it would finish the run and only then start tracking you. You need a node that keeps both children going at once.

A Parallel node ticks all its children in the same tick rather than one after another, so several of them can be RUNNING at once. It then needs a rule for when it's done. Two success policies are common, and several implementations generalize both into a threshold (succeed once k children have):

Whether a parallel re-ticks a child that has already returned is an implementation choice rather than a property of the idea, and two widely used open-source runtimes disagree. BehaviorTree.CPP, a C++ library, keeps a set of completed children and skips them until the node resets[20]. py_trees, a Python library, does the opposite by default: "A parallel ticks every child every time the parallel is itself ticked", with skipping available as an opt-in synchronised setting on its SuccessOnAll and SuccessOnSelected policies[21]. Read your runtime before assuming either.

Failure rules vary more than the success policies. py_trees fails the parallel as soon as any child fails, under every policy[21]; BehaviorTree.CPP makes the failure count a second threshold (below). A typical use: [Parallel: MoveTo(cover), TrackTarget]. The agent moves and keeps its aim on the enemy at the same time, and under succeed-on-one, MoveTo finishing resolves the node as SUCCESS and the still-running TrackTarget is halted: stopped, with whatever progress it had thrown away.

BehaviorTree.CPP does not expose named policies at all. It exposes two integer ports, success_count (default -1, meaning all children) and failure_count (default 1), and states the completion rule in one line: "It is completed when either the SUCCESS or FAILURE threshold is reached. Any remaining running children are halted."[20] The source carries a second failure route the docs leave out: the node also fails the moment enough children have failed that the success threshold can no longer be reached[20]. And for the case where the halt is the part you do not want, there is a separate node: ParallelAll "always executes ALL children to completion. It never halts children early."[20]

Worked example

The sandbox below starts with four children: MoveTo succeeds after 3 ticks, PlayAnim after 5, TrackTarget after 2, and Reload fails after 6. With BehaviorTree.CPP's defaults (all four must succeed, one failure fails the node), successes arrive at ticks 2, 3 and 5, and at tick 6 Reload fails: the node returns FAILURE with nothing left to halt. Set success_count to 1 and TrackTarget's success at tick 2 ends it instead. MoveTo (2 of 3 ticks done), PlayAnim (2 of 5) and Reload (1 of 6, not ticked on tick 2 because it sits to the right of the deciding child) are all halted: 3 actions and 5 ticks of their work thrown away.

Live · Parallel policy sandbox
tick
0
resolved
0 ok / 0 fail
parallel returns
RUNNING
children halted
0
Each row is one child and each column one tick. Press run or step, then change the two thresholds or a child's duration and outcome and run it again. Each child has its own completion tick and outcome, and the node follows BehaviorTree.CPP's loop: it ticks the unresolved children in order, skips any child that has already returned (its row goes dim, where py_trees' default would tick it again), and tests both thresholds after every child. The moment one is met, anything still running is halted and loses its progress, marked x, including children to the right that were not ticked that tick. The defaults are BehaviorTree.CPP's: success_count = 4 (all children) and failure_count = 1 (any single failure). With them the node runs until tick 6 and fails when Reload fails. Set success_count to 1 and TrackTarget decides it at tick 2 instead, halting the other three mid-flight: same children and durations, a third of the time on the agent, and three actions thrown away.
Parallel does not mean multi-threaded

"Parallel" describes the evaluation policy: several children can be RUNNING at once, but they are ticked one after another on one thread, so there is no race between them. BehaviorTree.CPP's documentation says its parallel nodes run children "concurrently, but not in separate threads"[20], and py_trees calls the parallelism "merely conceptual"[21]. Real concurrency, where it exists, lives inside a leaf that hands its work to another thread or process. Unreal avoids the general node: its Simple Parallel pairs one main task with a background subtree[6].

09Blackboards

The Chase branch needs to know where the target is. The Attack branch needs the same target plus the ammo count. Perception found the target and the weapon system knows the ammo, and neither should have to know which nodes care.

Nodes in a behavior tree share that data through a : a key-value store, such as "target.distance" → 25, that any node can read from and write to. It holds the target entity, its last-known position, the agent's health, the ammo count, whether a perception query returned a result. Usually each agent gets its own blackboard instance, and some systems add shared parent scopes for squad-level data.

Unreal's blackboard is a first-class asset paired with the BT. You define typed keys (Object, Vector, Float, Bool and Enum, among others) in the blackboard definition. Decorators can observe specific keys and trigger re-evaluation when the value changes, which is the mechanism behind Unreal's event-driven BT model.[6]

Worked example

The widget's blackboard starts at target.visible = true, distance = 25, health = 80, ammo = 12. The root selector tries Retreat first: health < 30? is false at 80, so Retreat fails. Attack checks target.visible (true), then distance < 15 (false at 25), so Attack fails without checking ammo. Chase checks target.visible (true) and runs MoveTo, so the active behavior is Chase. Drop health to 29 and Retreat's first check passes, so it wins before Attack or Chase is looked at.

The widget below shows a behavior tree reading from a live blackboard. Adjust the sliders to change perception data; the tree re-evaluates on every change.

Live ยท Blackboard inspector
active behavior
ยทยทยท
Drag the sliders and toggle visibility; the tree on the left re-evaluates on every change, colors each node by its result, and the readout names the active behavior. Retreat sits at the top and fires whenever health drops below 30, whatever else is true. Attack requires target.visible AND distance below 15 AND ammo above 0. Chase requires target.visible only. Patrol is the fallback. Set distance to 10 and the tree switches from Chase to Attack; set ammo to 0 and it falls back to Chase; turn visibility off and it falls through to Patrol. Then drop health below 30 from any of those states and everything else stops mattering, which is what putting a branch first in a selector actually means.

10Actions and conditions

Everything so far decides which leaf runs. The leaves are where the guard actually checks the world or does something, and the hard case is an action that takes longer than one frame, such as walking across a room.

Leaf nodes are where the actual work happens. Two kinds:

The RUNNING status is what makes BTs handle multi-frame actions cleanly. A MoveTo action that takes 200 frames to complete returns RUNNING on every frame until the last. The parent sequence sees RUNNING, returns RUNNING itself, and the tree pauses evaluation of that branch until next tick. This propagation continues all the way up to the root.

Worked example

A guard walks 10 m to cover at 3 m/s. That takes 10 / 3 ≈ 3.33 s, which is 200 frames at 60 fps. MoveTo returns RUNNING on frames 1 to 199 and SUCCESS on frame 200. On each of those 199 frames, its sequence and every composite above it return RUNNING too, so the root reports RUNNING and the game loop carries on rendering the walk.

The composites below are reactive: every tick starts again at child 0, which is the default Colledanchise and Ögren build on[3] and the baseline §11 compares memory against. Each one still records which child was RUNNING, for one reason: when an earlier child ends the tick first (a condition fails, a higher-priority branch takes over), the child that was running has to be halted, or it keeps a half-finished action alive.

Code: base node, sequence, selector

Node hierarchy + composites
#include <memory>
#include <vector>

enum class Status { Success, Failure, Running };

// Base node. Every node implements tick(); halt() is optional.
class Node {
public:
    virtual ~Node() = default;
    virtual Status tick() = 0;

    // Called when a parent abandons this node while it was RUNNING.
    // Anything the node started and has not finished gets undone here.
    virtual void halt() {}
};

// Sequence: AND logic. Fail on first failure, succeed when all succeed.
// Reactive: every tick starts again at child 0.
class Sequence : public Node {
    std::vector<std::unique_ptr<Node>> children;
    int runningIndex = -1;  // child that returned RUNNING last tick, or -1
public:
    void addChild(std::unique_ptr<Node> child) {
        children.push_back(std::move(child));
    }

    // Abort is recursive: a composite that is dropped must drop whatever
    // it had running underneath, or that work leaks.
    void halt() override {
        for (auto& child : children) child->halt();
        runningIndex = -1;
    }

    Status tick() override {
        const int count = static_cast<int>(children.size());
        for (int i = 0; i < count; ++i) {
            Status result = children[i]->tick();
            if (result == Status::Success) continue;
            // An earlier child ended this tick before the one that was running
            // got ticked. Halt it, or its half-finished action lives on.
            if (runningIndex > i) children[runningIndex]->halt();
            runningIndex = (result == Status::Running) ? i : -1;
            return result;             // FAILURE or RUNNING propagates up
        }
        runningIndex = -1;
        return Status::Success;        // all children succeeded
    }
};

// Selector: OR logic. Succeed on first success, fail when all fail.
// Reactive: higher-priority children are re-checked every tick.
class Selector : public Node {
    std::vector<std::unique_ptr<Node>> children;
    int runningIndex = -1;  // child that returned RUNNING last tick, or -1
public:
    void addChild(std::unique_ptr<Node> child) {
        children.push_back(std::move(child));
    }

    void halt() override {
        for (auto& child : children) child->halt();
        runningIndex = -1;
    }

    Status tick() override {
        const int count = static_cast<int>(children.size());
        for (int i = 0; i < count; ++i) {
            Status result = children[i]->tick();
            if (result == Status::Failure) continue;
            // A higher-priority child took over from the one that was running.
            // That switch is the abort, and the loser must be halted.
            if (runningIndex > i) children[runningIndex]->halt();
            runningIndex = (result == Status::Running) ? i : -1;
            return result;             // SUCCESS or RUNNING propagates up
        }
        runningIndex = -1;
        return Status::Failure;        // all children failed
    }
};
// Stand-ins for the engine types. Blackboard gets its real definition
// in the blackboard pane below; NavAgent is whatever your pathfinding exposes.
pub struct Blackboard;
pub struct NavAgent;

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Status { Success, Failure, Running }

// Everything a node might touch travels in one place. The C++ pane gives
// MoveTo reference members instead; that does not port. Every action node
// needs &mut NavAgent, only one &mut can exist at a time, so the tree could
// hold at most one such node. Passing context per tick sidesteps that; the
// bonsai-bt crate does the same, handing the blackboard to a callback on each tick.
pub struct Context {
    pub blackboard: Blackboard,
    pub nav_agent: NavAgent,
}

// Base trait. Every node implements tick(); halt() is optional.
pub trait Node {
    fn tick(&mut self, context: &mut Context) -> Status;

    // Called when a parent abandons this node mid-RUNNING. Anything the
    // node started and has not finished gets undone here.
    fn halt(&mut self, _context: &mut Context) {}
}

// Sequence: AND logic. Fail on first failure, succeed when all succeed.
// Reactive: every tick starts again at child 0.
pub struct Sequence {
    children: Vec<Box<dyn Node>>,
    running_index: Option<usize>,  // child that returned Running last tick
}

impl Sequence {
    pub fn new(children: Vec<Box<dyn Node>>) -> Self {
        Sequence { children, running_index: None }
    }
}

impl Node for Sequence {
    fn tick(&mut self, context: &mut Context) -> Status {
        for i in 0..self.children.len() {
            let result = self.children[i].tick(context);
            if result == Status::Success {
                continue;
            }
            // An earlier child ended this tick before the one that was running
            // got ticked. Halt it, or its half-finished action lives on.
            if let Some(previous) = self.running_index.filter(|&previous| previous > i) {
                self.children[previous].halt(context);
            }
            self.running_index = if result == Status::Running { Some(i) } else { None };
            return result;  // FAILURE or RUNNING propagates up
        }
        self.running_index = None;
        Status::Success  // all children succeeded
    }

    fn halt(&mut self, context: &mut Context) {
        // Abort is recursive: a composite that is dropped must drop whatever
        // it had running underneath, or that work leaks.
        for child in &mut self.children { child.halt(context); }
        self.running_index = None;
    }
}

// Selector: OR logic. Succeed on first success, fail when all fail.
// Reactive: higher-priority children are re-checked every tick.
pub struct Selector {
    children: Vec<Box<dyn Node>>,
    running_index: Option<usize>,  // child that returned Running last tick
}

impl Selector {
    pub fn new(children: Vec<Box<dyn Node>>) -> Self {
        Selector { children, running_index: None }
    }
}

impl Node for Selector {
    fn tick(&mut self, context: &mut Context) -> Status {
        for i in 0..self.children.len() {
            let result = self.children[i].tick(context);
            if result == Status::Failure {
                continue;
            }
            // A higher-priority child took over from the one that was running.
            // That switch is the abort, and the loser must be halted.
            if let Some(previous) = self.running_index.filter(|&previous| previous > i) {
                self.children[previous].halt(context);
            }
            self.running_index = if result == Status::Running { Some(i) } else { None };
            return result;  // SUCCESS or RUNNING propagates up
        }
        self.running_index = None;
        Status::Failure  // all children failed
    }

    fn halt(&mut self, context: &mut Context) {
        for child in &mut self.children { child.halt(context); }
        self.running_index = None;
    }
}

Code: a concrete action (MoveTo with RUNNING)

Multi-frame action node
// Restated from the neighbouring panes so this one compiles on its own.
enum class Status { Success, Failure, Running };
struct Vec3 { float x, y, z; };
class Node { public: virtual ~Node() = default; virtual Status tick() = 0; virtual void halt() {} };
struct Blackboard { template <typename T> T get(const char*) const { return T{}; } };
struct NavAgent { bool requestPath(Vec3); bool hasArrived() const; void cancelPath(); };

// MoveTo: navigate to a target position read from the blackboard.
// Returns RUNNING while pathfinding/moving, SUCCESS on arrival,
// FAILURE if the path is invalid or the target is unreachable.
class MoveTo : public Node {
    Blackboard& blackboard;
    NavAgent&   navAgent;
    bool started = false;
public:
    MoveTo(Blackboard& blackboard, NavAgent& navAgent)
        : blackboard(blackboard), navAgent(navAgent) {}

    Status tick() override {
        if (!started) {
            Vec3 target = blackboard.get<Vec3>("target.position");
            if (!navAgent.requestPath(target))
                return Status::Failure;  // no valid path
            started = true;
        }
        if (navAgent.hasArrived()) {
            started = false;          // reset for next invocation
            return Status::Success;
        }
        return Status::Running;      // still moving
    }

    // Without this, a higher-priority branch that preempts us mid-move
    // leaves started == true. On re-entry we skip requestPath and poll
    // hasArrived() against a stale path. That is the abort bug §11 is about,
    // and it is invisible until a designer adds a higher-priority branch.
    void halt() override {
        if (started) {
            navAgent.cancelPath();
            started = false;
        }
    }
};
// Restated from the neighbouring panes so this one compiles on its own.
#[derive(Clone, Copy)]
pub struct Vec3 { pub x: f32, pub y: f32, pub z: f32 }
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Status { Success, Failure, Running }
pub struct Blackboard;
impl Blackboard { pub fn get<T>(&self, _key: &str) -> Option<T> { None } }
pub struct NavAgent;
impl NavAgent {
    pub fn request_path(&mut self, _target: Vec3) -> bool { true }
    pub fn has_arrived(&self) -> bool { true }
    pub fn cancel_path(&mut self) {}
}
pub struct Context { pub blackboard: Blackboard, pub nav_agent: NavAgent }
pub trait Node {
    fn tick(&mut self, context: &mut Context) -> Status;
    fn halt(&mut self, _context: &mut Context) {}
}

// MoveTo: navigate to a target position read from the blackboard.
// Returns Running while pathfinding/moving, Success on arrival,
// Failure if the path is invalid or the target is unreachable.
pub struct MoveTo {
    started: bool,
}

impl MoveTo {
    pub fn new() -> Self { MoveTo { started: false } }
}

impl Node for MoveTo {
    fn tick(&mut self, context: &mut Context) -> Status {
        if !self.started {
            // get returns Option here, unlike the C++ pane's by-value get:
            // a missing blackboard key is a failure, not a default-constructed
            // Vec3 that quietly sends the agent to the world origin.
            let Some(target) = context.blackboard.get::<Vec3>("target.position") else {
                return Status::Failure;  // no target set
            };
            if !context.nav_agent.request_path(target) {
                return Status::Failure;  // no valid path
            }
            self.started = true;
        }
        if context.nav_agent.has_arrived() {
            self.started = false;     // reset for next invocation
            return Status::Success;
        }
        Status::Running              // still moving
    }

    fn halt(&mut self, context: &mut Context) {
        if self.started {
            context.nav_agent.cancel_path();
            self.started = false;
        }
    }
}

Code: blackboard with typed access

Blackboard key-value store
#include <any>
#include <string>
#include <unordered_map>

// Blackboard: type-erased key-value store.
// Production code would use a fixed-layout struct for cache locality;
// the std::any version here prioritizes flexibility for prototyping.
class Blackboard {
    std::unordered_map<std::string, std::any> data;
public:
    template<typename T>
    void set(const std::string& key, T value) {
        data[key] = std::move(value);
    }

    template<typename T>
    T get(const std::string& key) const {
        auto it = data.find(key);
        if (it == data.end())
            return T{};               // default-constructed if missing
        return std::any_cast<T>(it->second);  // throws std::bad_any_cast on a type mismatch
    }

    bool has(const std::string& key) const {
        return data.count(key) > 0;
    }
};
use std::any::Any;
use std::collections::HashMap;

// Blackboard: type-erased key-value store.
// Production code would use a fixed-layout struct for cache locality;
// the Any version here prioritizes flexibility for prototyping.
pub struct Blackboard {
    data: HashMap<String, Box<dyn Any>>,
}

impl Blackboard {
    pub fn new() -> Self {
        Blackboard { data: HashMap::new() }
    }

    pub fn set<T: 'static>(&mut self, key: &str, value: T) {
        self.data.insert(key.to_string(), Box::new(value));
    }

    pub fn get<T: 'static + Clone>(&self, key: &str) -> Option<T> {
        self.data.get(key)?.downcast_ref::<T>().cloned()
    }

    pub fn has(&self, key: &str) -> bool {
        self.data.contains_key(key)
    }
}
What's intentionally missing

The code above skips: memory composites, Parallel and decorators (§7, §8, §11); thread safety (a blackboard read and written from a job system needs atomics or locks); key-change notifications (the observers behind Unreal's event-driven aborts); typed key definitions (catching "target.positon" typos at compile time); and scope hierarchies (per-entity vs per-squad vs global). A shipping blackboard also usually resolves string keys to integer IDs once, at load time, so a lookup is an array index rather than a string hash and compare.

11Memory and reactive behavior

A guard is three ticks into a five-tick walk to its next waypoint when you step into view. Does it turn and fight on the next tick, or finish the walk first? That depends on whether the composites above the walk re-check their earlier children every tick, and each choice has a cost.

Two evaluation strategies:

Two things this framing gets wrong if you read it too fast

First, these are properties of an individual composite, not modes of the whole tree. The literature and the reference implementations both spell this out as separate node types: BehaviorTree.CPP ships Sequence, ReactiveSequence and SequenceWithMemory as separate nodes that differ in what they re-tick after a child returns FAILURE or RUNNING, plus AsyncSequence, a Sequence that yields back to the tree after each child that succeeds[9]. A real tree mixes them.

Second, a composite with memory still ticks from the root. Every tick enters at the root and walks down through the composites on the active path; what memory changes is that a composite skips children it has already resolved, not that the traversal starts somewhere else. Colledanchise and Ögren make the same point from the other direction, noting that memory nodes "can be considered to be syntactic sugar" because their behavior is reproducible with a memoryless tree plus auxiliary conditions[3].

And the trap that follows from the first point: a reactive composite re-ticks its earlier children, so any action to the left of the running one runs again every frame. Fine for a condition check. Not fine for firing a weapon, spending ammo, or playing a bark. Non-idempotent actions under a reactive parent are a classic BT bug.

Worked example

Take the abort playground's tree below: a Root selector over Combat (EnemySpotted?, then a four-tick Engage) and Patrol (a five-tick GoToWaypoint, then a three-tick Scan that starts on the tick the walk ends). With no enemy, Patrol takes 7 ticks. A reactive root checks EnemySpotted? on all 7 of them. A memory root checks it once, on tick 1, then resumes Patrol for ticks 2 to 7. If the enemy appears just after tick 1, the reactive root starts Combat on tick 2, while the memory root keeps patrolling for 6 more ticks and starts Combat on tick 8. Seven condition checks buy a one-tick reaction; one check costs up to six ticks of delay.

Production systems tend to mix the two. Unreal's BT does not re-evaluate from the root every tick: the active task keeps running until it finishes or a decorator aborts it, which behaves like memory composites throughout. Each decorator carries an Observer Aborts setting, and Epic's node reference defines the four values exactly: None, "Do not abort anything"; Self, "Abort self and any subtrees running under this node"; Lower Priority, "Abort any nodes to the right of this node"; Both, "Abort self, any subtrees running under this node, and any nodes to the right of this node."[22] When the decorator's observed blackboard key changes, the configured abort fires and forces re-evaluation. That buys reactive behavior exactly where you asked for it, without re-checking every condition every frame.[6]

An abort is not free, and the widget's log is where that shows. BehaviorTree.CPP defines an asynchronous action as one that "May return RUNNING instead of SUCCESS or FAILURE, when ticked" and "Can be stopped as fast as possible when the method halt() is invoked", and notes that "Frequently, the method halt() must be implemented by the developer"[23]. When another node triggers a halt, "the onHalted() method is invoked"[23], and whatever the action had accumulated is yours to unwind: a path request in flight, a partially played animation, a reserved cover slot. Aborting a MoveTo three ticks into five throws away three ticks of pathing. A condition that flickers can trigger that every frame, which is one reason abort types are chosen per decorator rather than set once for the whole tree.

Interruption is where BTs are weakest. Anguelov's slide at GDC 2017 says behavior trees "are inherently bad at two things: Transitions/Interruptions, Behavior Prioritization"[11], and his written follow-up gives the mechanism for each[14]. For transitions: event-driven BTs stopped re-evaluating the tree from the root every update, which fixed the cost, but "behavioral switches will not occur until the currently selected branch completes or fails". The usual repair is monitor nodes that poll conditions outside the running branch and reset the tree when one fires, and "the deeper in the tree we are the more monitors we have registered and therefore the greater the evaluation cost of the tree for each update". For prioritization: forcing one behavior ahead of the tree's fixed order typically takes a global monitor that sets override flags every branch must check, which adds implicit transitions between unrelated branches. In his words, "we have implicitly converted the BTs into an expensive, unreadable and error-prone finite state machine". That is the case for StateTree and the other hybrids in §16.

Predict

Before touching the widget: the root is a selector with memory, the decorator's Observer Aborts is None, and the agent is three ticks into a five-tick GoToWaypoint when EnemySpotted becomes true. How many ticks pass before the Combat branch runs? Then check the two ways to make that number 1, and what each one costs.

The widget below is a real evaluator: actions carry progress across ticks and return RUNNING until they finish, composites remember which child was running, and halt() throws that progress away and says so in the log. The Root and Patrol modes are switchable; Combat is a memory sequence, as an Unreal branch gated by a decorator would be.

Live · Abort playground
tick
0
EnemySpotted
false
running branch
none
The tree shows each action's progress and each composite's mode, and the log underneath records every tick. Press tick three times, then toggle EnemySpotted, then keep ticking (or press run). The default setting is the one to try first. Root selector on memory with AbortType None: flipping EnemySpotted three ticks in changes nothing for four more ticks, until Patrol finishes both its actions, because a memory composite resumes at its running child and never looks left. Switch the root to reactive and the same flip is caught on the next tick: the selector re-ticks Combat, switches branches, and the log shows halt(GoToWaypoint) discarding its progress. Leave the root on memory and set AbortType to LowerPriority instead, and the decorator's observer fires the moment the key changes, which is Unreal's approach: reactivity where it was asked for rather than everywhere. Self and Both matter on the way out: with Engage running, clear EnemySpotted and only those two abort it; under None or LowerPriority the memory Combat sequence finishes Engage against an enemy that is gone. Finally, put Patrol in reactive mode: GoToWaypoint completes, Scan gets one tick, then the reactive sequence re-ticks GoToWaypoint, which starts a fresh walk and halts Scan, so Patrol never finishes. GoToWaypoint is not idempotent, and this is the §11 bug.

12Utility-based selection

A guard sees two enemies. A selector picks the first branch whose conditions hold, so the guard attacks whichever enemy the designer's tree happens to check first. You want it to weigh how close and how dangerous each enemy is, which takes a score rather than a fixed order.

Standard selectors pick children by fixed left-to-right priority. replaces that with a scoring function: each child is assigned a utility value (a float computed from threat level, distance, ammo count, health, cooldown timers), and the child with the highest score wins. Dave Mark's Infinite Axis Utility System, shown briefly in a shared GDC 2013 AI Summit talk and presented in full with Mike Lewis at GDC 2015 as the rebuilt AI for Guild Wars 2, is a widely cited formulation[17].

Worked example

Score each enemy on two considerations, each from 0 to 1. Closeness is 1 − distance / 50 m, and threat comes from the enemy type. Enemy A is 10 m away (closeness 0.8) with threat 0.5. Enemy B is 30 m away (closeness 0.4) with threat 0.9. A common way to combine considerations is to multiply them, so one score near 0 vetoes the option: A scores 0.8 × 0.5 = 0.40 and B scores 0.4 × 0.9 = 0.36, so the guard engages A. Move B to 20 m (closeness 0.6) and it scores 0.54 and wins. The curves here are made up; tuning them is the real work.

A common hybrid keeps the tree for the coarse decision hierarchy (combat vs exploration vs dialogue) and uses utility scoring for the fine-grained choice within a category (which target to engage, which cover point to use). That avoids scoring everything against everything while still getting context-sensitive decisions where they matter.

Implementation: replace the Selector's left-to-right loop with a "score all children, sort by score, evaluate in score order" loop. The child evaluation still follows the standard tick protocol (SUCCESS/FAILURE/RUNNING), so the rest of the tree machinery is unchanged.

13BT vs GOAP vs HTN

Sometimes the designer doesn't want to author each decision. They want to say "kill the player" and have the soldier work out "pick up a gun, reload, flank" on its own, in whatever order the level allows. That's what a planner does, and two planning-based alternatives to behavior trees ship in production.

(Goal-Oriented Action Planning) gives every action preconditions and effects, such as "Reload needs ammo in the bag and leaves the gun loaded", and lets a search find a chain of actions that reaches the goal. (Hierarchical Task Network) planning has designers write how each big task breaks into smaller ones, and a planner picks the breakdown that fits the current world state.

PropertyBehavior TreeGOAPHTN
Behavior authored by Designer builds tree structure Designer defines actions + preconditions; planner finds the sequence Designer defines task decompositions; planner picks decomposition path
Emergent behavior Low. The tree encodes the decision space explicitly. High. The planner can combine actions in ways the designer didn't anticipate. Medium. Constrained by decomposition hierarchy but can still surprise.
Debuggability High. One path through a tree. Visual debuggers show the active branch. Low. Plans are generated at runtime. Hard to explain why the planner chose a specific action sequence. Medium. The decomposition tree is readable, but plan selection can be opaque.
Shipped in Halo 2[1], Hitman (2016)[13], games built on Unreal's BT F.E.A.R.[18] Transformers: Fall of Cybertron[19]
CPU cost per tick Low. One tree walk: about the depth of the active path for memory BTs, up to every node for reactive ones. Variable. Planning is a search, potentially expensive. Usually amortized by caching plans. Low to medium. The method hierarchy prunes the search; High Moon measured its HTN planner as considerably faster than its earlier GOAP one[19].

F.E.A.R. (Monolith, 2005) is the best-known shipped GOAP game. Jeff Orkin's GDC 2006 talk[18] describes a system where the FSM has only three states (Goto, Animate, UseSmartObject) and A* plans over the action space to decide what to do. The soldiers flank, lay suppression fire and flush the player out with grenades; the cost lands on debugging, where "why did the AI throw a grenade?" means reconstructing the planner's search.

HTN planning (Humphreys, Game AI Pro[19]) sits between BTs and GOAP. High-level tasks decompose into subtasks according to designer-authored rules. The planner picks decomposition paths based on world state. High Moon Studios shipped HTN in Transformers: Fall of Cybertron and reported it "considerably faster" than the GOAP system in the previous game, War for Cybertron, because the task hierarchy culls large parts of the search and the planner needs no heuristic sort.

Guerrilla's Horizon Zero Dawn uses a different hybrid for its machines: HTN planning combined with utility-based decisions, plus a group layer that coordinates herds of different machine classes (§14)[16].

14Case studies

Shipped games show where the trade-offs above land in practice. Two of them, The Last of Us and Horizon Zero Dawn, don't drive their enemies with a behavior tree at all, and Hitman uses one only for the characters gameplay depends on.

Halo 2 / Halo 3 (Bungie)

Isla's GDC 2005 talk[1] describes the Covenant AI as a behavior DAG (a directed acyclic graph rather than a strict tree, because one behavior subtree can occupy several places). Behaviors are self-contained modules picked by decision schemes such as prioritized lists, and extra machinery keeps the structure tractable: impulses that jump straight to a behavior, stimulus behaviors that event handlers insert at runtime, and behavior tagging that skips irrelevant relevancy checks. For Halo 3, Isla's GDC 2008 talk applies the prioritized-list idea one level up: encounter objectives form a tree of prioritized tasks that squads are poured into and filter down through, pulled up when a higher-priority task activates[24].

Unreal Engine BT system

Epic describes Unreal's BTs as "event-driven to avoid doing unnecessary work every frame": instead of polling, the tree listens for events, and conditional decorators observe blackboard keys and abort when their values change[6]. The editor's debugger highlights the active branch while the game runs, which makes it quick to see why an NPC chose a behavior. In place of a general Parallel composite, Unreal offers Simple Parallel, which runs a single main task alongside a background subtree; its Finish Mode decides whether the node ends as soon as the main task does, aborting the background, or waits for the background to finish[22].

The Last of Us (Naughty Dog)

McIntosh's account of the human enemies[15] describes an architecture built on FSMs rather than behavior trees. An NPC's top-level states, called skills (panic, advance, melee, gun combat, hide, investigate, flank and others), are prioritized: every frame each skill reports whether it wants to run, and the highest-priority one wins. Each skill runs its own state machine and pushes simpler behaviors, such as MoveToLocation, StandAndShoot and TakeCover, onto a behavior stack. Coordination goes through a global Combat Coordinator that hands out roles such as Flanker and OpportunisticShooter, so one NPC keeps shooting while the others take cover or flank. The per-frame skill check does the job a root selector does in a BT, with no tree.

Horizon Zero Dawn (Guerrilla Games)

The machines are not driven by behavior trees either. Guerrilla's own summary of their AI is "a combination of HTN planning and utility based decision making" plus "systems we developed to coordinate groups of agents"[16], and the HTN planner grew out of the one Guerrilla had used since Killzone 2. Julian Berteling's GDC 2018 talk covers the move from Killzone's human enemies in corridors to more than 25 distinct character types in an open world, including navigation for agents of very different sizes. Thompson's write-up of the talks adds two details. Which machine gets to attack is balanced by a utility function over its state, its distance to the player, whether the player knows it is there and the damage it has taken. And objects that can trip a machine's sensors (the player, rocks, fired arrows, other machines) carry information packets that tell the machine what it sensed, so different classes (Watchers for reconnaissance, Grazers for acquisition, Thunderjaws for combat) can react differently to the same event.

Hitman (IO Interactive)

Hitman is usually cited for scale, and the crowd numbers come from Kasper Fauerby's GDC Europe 2012 talk on Absolution: around 1,200 crowd agents with 500 on screen, at roughly 2 ms of PS3 PPU time for crowd AI and steering[12]. The crowd layer is not a behavior tree. Fauerby describes a state machine (idle, pending walk and walk for steering, plus gameplay states such as alert, prone and scared), and when a core mechanic such as close combat or a human shield needs a full character, a possession system upgrades the agent on demand to full NPC AI drawn from a small pool of invisible NPCs. The NPC layer is where the behavior trees are: Thompson's analysis of the 2016 game reports that its core AI "is reliant on behaviour trees", first devised for Absolution, with "over 300 active behaviour-tree-driven AI in a given level" and distant ones updated less often[13]. The split is the lesson: BTs for the characters gameplay depends on, a far cheaper model for the crowd around them.

15Pitfalls

16What's next

Next in the series, Navmesh & Steering covers what happens after the tree picks a MoveTo: a path across a navigation mesh, then steering and local avoidance to follow it without walking into other agents. Beyond that, behavior trees themselves are still moving:

17Sources

  1. Damian Isla. "Handling Complexity in the Halo 2 AI." GDC 2005 proceedings, Gamasutra, 11 March 2005. gamedeveloper.com. Describes the Halo 2 AI as "a hierarchical finite state machine (HFSM) or a behavior tree, or even more specifically, a behavior DAG", with prioritized-list and sequential decision schemes, impulses, stimulus behaviors and behavior tagging.
  2. Alex J. Champandard. "Understanding Behavior Trees." AiGameDev.com, 6 September 2007. web.archive.org (the site is offline). Frames behaviors as HFSM states with their outgoing transitions removed, sequenced by the parent scope as in a programming language, with sequences and selectors as the standard composites and decorators on top.
  3. Michele Colledanchise, Petter Ögren. Behavior Trees in Robotics and AI: An Introduction. Chapman & Hall/CRC, 2018; arXiv:1709.00084. arxiv.org. The strongest available reference for BT semantics, and the source for §11: reactive composites "keep sending ticks to the children to the left of a running child", composites with memory "always remember whether a child has returned Success or Failure", and memory nodes "can be considered to be syntactic sugar" since a memoryless tree plus auxiliary conditions reproduces them. Also formalizes why Condition nodes never return Running.
  4. Alejandro Marzinotto, Michele Colledanchise, Christian Smith, Petter Ögren. "Towards a Unified Behavior Trees Framework for Robot Control." IEEE ICRA 2014. ieeexplore.ieee.org. Reviews the BT literature for its lack of "consistency and mathematical rigor", proposes a unified framework, and gives equivalence notions between BTs and controlled hybrid dynamical systems.
  5. Matteo Iovino, Edvards Scukins, Jonathan Styrud, Petter Ögren, Christian Smith. "A survey of Behavior Trees in robotics and AI." Robotics and Autonomous Systems, 2022. sciencedirect.com. Survey of BT extensions, formal properties and applications across game AI and robotics. Source for the §1 quote on where the transition logic lives, and for the history crediting Mateas and Stern and Isla.
  6. Epic Games. "Behavior Tree in Unreal Engine: Overview." Unreal Engine 5.8 documentation. dev.epicgames.com. States that Unreal BTs "are event-driven to avoid doing unnecessary work every frame", recommends conditional decorators over condition leaves, and replaces the general parallel composite with Simple Parallel, Services and Observer Aborts.
  7. Epic Games. "Overview of State Tree in Unreal Engine." UE 5.8 Documentation. dev.epicgames.com. Epic's definition, quoted in §16: StateTree "is a general-purpose hierarchical state machine that combines the Selectors from behavior trees with States and Transitions from state machines", selecting states on demand through transitions. The page does not deprecate Behavior Trees or recommend StateTree over them.
  8. Unity Technologies. Unity Behavior (com.unity.behavior) manual and changelog. docs.unity3d.com. "A visual tool for authoring behaviors that control non-player characters (NPCs), gameplay logic, objects, and scene interactions". The changelog dates 1.0.0 to 17 September 2024 and 1.0.16 to 26 May 2026. Unity's own term is behavior graph, because "branches can merge back", so it is BT-like rather than strictly a tree.
  9. Davide Faconti et al. BehaviorTree.CPP (v4.x, MIT) and its documentation. behaviortree.dev. A widely used open-source BT runtime, and the clearest statement that re-tick policy is per node type: its Sequences page tabulates what Sequence, ReactiveSequence and SequenceWithMemory do when a child returns FAILURE or RUNNING (restart or tick again), with AsyncSequence behaving like Sequence but yielding after each successful child.
  10. Alex J. Champandard and Philip Dunstan. "The Behavior Tree Starter Kit." Game AI Pro, Chapter 6. gameaipro.com. Builds a minimal BT runtime step by step; its open-source Behavior Tree Starter Kit is "intended to serve as an example of a working BT for you to learn from". Also defines first- and second-generation BTs and describes event-driven traversal with a behavior scheduler, citing Bulletstorm's event-driven version.
  11. Mika Vehkala (Remedy), Bobby Anguelov (WB Games Montreal), Ben Weber (Twitch). "AI Arborist: Proper Cultivation and Care for Your Behavior Trees." GDC 2017. gdcvault.com (PDF, free). Studio-agnostic guidance on keeping large behavior trees maintainable. Source for the slide in Anguelov's section, quoted in §11, that BTs "are inherently bad at two things: Transitions/Interruptions, Behavior Prioritization".
  12. Kasper Fauerby. "Crowds in Hitman: Absolution." GDC Europe 2012, IO Interactive. gdcvault.com (PDF, free). Source for the Hitman crowd figures in §14: "Around 1200 agents per crowd, 500 on-screen", and a PS3 budget of PPU 5 ms with crowd AI and steering at about 2 ms. Also documents the crowd's state machine and the possession system that upgrades an agent on demand to full NPC AI from a small pool of invisible NPCs.
  13. Tommy Thompson. "The AI of Hitman (2016)." Game Developer, 7 August 2019. gamedeveloper.com. Secondary analysis drawing on IO Interactive conference talks; cited in §13 and §14 for the claims that the core NPC AI "is reliant on behaviour trees", first devised for Absolution, with "over 300 active behaviour-tree-driven AI in a given level" and distant AI updated less often.
  14. Bobby Anguelov. "Behavior Trees: Breaking the Cycle of Misuse." 2020. takinginitiative.wordpress.com (PDF). The written treatment of the reactivity and prioritization problems in §11, by one of the GDC 2017 speakers. Event-driven BTs fixed the cost of re-evaluating from the root, but "behavioral switches will not occur until the currently selected branch completes or fails"; monitor nodes that "continuously poll the environment" restore it at a cost that grows with depth; and global override monitors for prioritization turn the tree into "an expensive, unreadable and error-prone finite state machine".
  15. Travis McIntosh. "The Last of Us: Human Enemy AI." GDC 2014. gdcvault.com. Written up as "Human Enemy AI in The Last of Us," Game AI Pro 2, Chapter 34, 2015: gameaipro.com (PDF). Section 34.5 describes the prioritized skills in an FSM, each with its own state machine, and the behavior stack; the Combat Coordinator and its roles follow.
  16. Julian Berteling. "Beyond 'Killzone': Creating New AI Systems for 'Horizon Zero Dawn'." GDC 2018. gdcvault.com. With Guerrilla Games, "The AI of Horizon Zero Dawn," Game AI North, 2017 (slides and abstract): guerrilla-games.com, the source of the "combination of HTN planning and utility based decision making" quote; and Tommy Thompson, "Behind the AI of Horizon Zero Dawn," Parts 1 and 2, Game Developer, 2019, which summarizes both talks and is the source for the attack-selection utility and the information packets.
  17. Luke Dicken, Dino Dini, Dave Mark. "Architecture Tricks: Managing Behaviors in Time, Space, and Depth." GDC AI Summit, 2013. gdcvault.com. Mark's segment introduces the Infinite Axis Utility System, which he and Mike Lewis presented in full at GDC 2015 in "Building a Better Centaur: AI at Massive Scale," on the AI rebuild for Guild Wars 2.
  18. Jeff Orkin. "Three States and a Plan: The A.I. of F.E.A.R." GDC 2006. gamedevs.org. Goal-Oriented Action Planning. The FSM has three states; A* plans over the action space. Emergent flanking, suppression, and grenade-flushing behavior.
  19. Troy Humphreys. "Exploring HTN Planners Through Example." Game AI Pro, Chapter 12. gameaipro.com. Total-order forward decomposition planner used in Transformers: Fall of Cybertron, which the chapter reports as "considerably faster" than the GOAP system in War for Cybertron.
  20. Davide Faconti et al. "Parallel Nodes." BehaviorTree.CPP 4.8 documentation, and src/controls/parallel_node.cpp. behaviortree.dev. Supports §8: the success_count and failure_count ports with their -1 and 1 defaults, "It is completed when either the SUCCESS or FAILURE threshold is reached. Any remaining running children are halted", and ParallelAll, which "always executes ALL children to completion. It never halts children early." The source adds the second failure route the docs omit, (children_count - failure_count_) < required_success_count, and the completed_list_ that keeps a resolved child from being re-ticked.
  21. Daniel Stonier et al. "Composites." py_trees documentation (devel branch). py-trees.readthedocs.io. The counterexample cited in §8: "A parallel ticks every child every time the parallel is itself ticked", with the SuccessOnAll and SuccessOnSelected policies optionally "synchronised in which case children that tick with SUCCESS will be skipped on subsequent ticks."
  22. Epic Games. "Behavior Tree Node Reference: Decorators" and "...: Composites." Unreal Engine 5 documentation. Decorators, Composites. The verbatim Observer Aborts definitions quoted in §11 (None, Self, Lower Priority and Both); decorators "are attached to either a Composite or a Task node"; and Simple Parallel's Finish Mode (Immediate or Delayed).
  23. Davide Faconti et al. "Asynchronous Actions." BehaviorTree.CPP 4.8 documentation. behaviortree.dev. The halt contract used in §11: an asynchronous node "May return RUNNING instead of SUCCESS or FAILURE, when ticked" and "Can be stopped as fast as possible when the method halt() is invoked"; "Frequently, the method halt() must be implemented by the developer", and when another node triggers a halt "the onHalted() method is invoked."
  24. Damián Isla. "Building a Better Battle: The Halo 3 AI Objectives System." GDC 2008. gdcvault.com. Builds on the Halo 2 behavior tree's prioritized-list scheme for encounter design: "A Tree of Prioritized Tasks" into which squads are poured and filter down, pulled up when a higher-priority task activates.

See also