Summary
A behaviour tree (BT) is a hierarchical decision structure for reactive AI. Instead of storing one current state and hard-coding transitions between states, a BT repeatedly ticks from the root downward, asking which branch should run now. This makes BTs especially good for NPCs that must constantly reprioritise between competing behaviours such as fleeing, attacking, investigating, or patrolling. (Yannakakis and Togelius, Artificial Intelligence and Games, see source-ai-and-games; Millington, Artificial Intelligence for Games, see source-artificial-intelligence-for-games)
Key ideas
- Selector: returns success on the first child that succeeds; use for priorities.
- Sequence: returns failure on the first child that fails; use for multi-step requirements.
- Decorator: wraps a child and modifies how it runs or returns.
- Leaf node: an action or condition.
- Status: every node returns Success, Failure, or Running. Running means the work is still under way and the tree should tick it again next update (Champandard and Dunstan, “The Behavior Tree Starter Kit”, Game AI Pro, see source-game-ai-pro-360-architecture).
- Ticking: the tree is re-evaluated repeatedly, which keeps the AI reactive.
- Blackboard/context: shared memory the nodes can read and write.
Implementation
public enum NodeState { Success, Failure, Running }
public abstract class BTNode
{
public string Name { get; }
protected BTNode(string name) => Name = name;
public abstract NodeState Tick();
}
public sealed class SelectorNode : BTNode
{
private readonly List<BTNode> children;
public SelectorNode(string name, params BTNode[] children) : base(name)
=> this.children = new List<BTNode>(children);
public override NodeState Tick()
{
foreach (BTNode child in children)
{
NodeState result = child.Tick();
if (result != NodeState.Failure)
return result;
}
return NodeState.Failure;
}
}In Unity, keep BT nodes as plain C# classes rather than MonoBehaviours. The agent owns the tree and calls root.Tick() from Update(). The constructor signature (string name, params BTNode[] children) matches the runnable example below, so this snippet and BehaviourTree.cs can be used interchangeably.
Runnable example: a guard NPC
A complete, package-free version of this framework and a guard that uses it are in code/unity-csharp/game-ai/behaviour-tree/:
- BehaviourTree.cs —
NodeState,BTNode,SequenceNode,SelectorNode,InverterNode, andConditionNode/ActionNodeleaves that wrap a lambda or method - GuardAI.cs — builds the guard’s tree, ticks it every frame, and holds the conditions and actions
- README.md — scene setup in about ten minutes, what to try, and extension ideas
The guard’s tree, highest priority first:
Selector "Root"
├── Sequence "Flee when hurt" : Health low? → Flee
├── Sequence "Attack when close" : Player in reach? → Inverter(Cooling down?) → Strike
├── Sequence "Chase when seen" : Can see player? → Chase
└── Action "Patrol"Code walkthrough
Leaves wrap ordinary methods. ConditionNode takes a Func<bool> and returns Success or Failure; ActionNode takes a Func<NodeState>. The guard’s behaviour therefore lives in normal private methods (CanSeePlayer(), Chase(), Patrol()), and the tree only decides which of them runs. Building the tree is a single nested constructor call in Awake():
root = new SelectorNode("Root",
new SequenceNode("Flee when hurt",
new ConditionNode("Health low?", ShouldFlee),
new ActionNode("Flee", Flee)),
// ... "Attack when close" branch omitted here ...
new SequenceNode("Chase when seen",
new ConditionNode("Can see player?", CanSeePlayer),
new ActionNode("Chase", Chase)),
new ActionNode("Patrol", Patrol));Sequence and selector are AND and OR. A sequence stops at the first child that does not succeed, so a failed condition at its start blocks the action after it. A selector stops at the first child that does not fail, so the first branch whose condition holds wins. Champandard and Dunstan describe these two composites as the AND and OR at the core of a behaviour tree (Champandard and Dunstan, “The Behavior Tree Starter Kit”, Game AI Pro, see source-game-ai-pro-360-architecture).
Running carries an action across frames. Patrol() and Chase() move the guard a little and return Running, because walking to a waypoint takes many frames. Strike() returns Success at once. A selector treats Running like Success: it stops there and does not try lower branches. When the movement is done by a NavMeshAgent, the same rule applies: the move leaf sets a destination once and keeps returning Running until the agent arrives, rather than re-issuing the order every tick (see unity-navmesh, “From decision to motion”).
The Inverter turns a condition into its opposite. Rather than writing a separate “not cooling down” method, the attack branch wraps AttackCoolingDown in an InverterNode. While the attack is cooling down, the attack branch fails and the selector falls through to Chase, which holds the guard at striking distance.
Re-evaluating from the root gives interruption for free. These composites keep no memory of which child was running, so every tick starts again at the top. As soon as “Health low?” becomes true, the flee branch wins on the very next frame, whatever the guard was doing. Champandard and Dunstan call a selector that re-checks higher-priority branches in this way an active selector, as opposed to a passive one that resumes the child it chose earlier; they use it so that attacking a visible enemy can interrupt searching or patrolling (Champandard and Dunstan, “The Behavior Tree Starter Kit”, Game AI Pro, see source-game-ai-pro-360-architecture). The cost is that every condition above the running branch is re-checked each tick, and an action cannot rely on being resumed. Production frameworks therefore usually store the running child, or schedule nodes through events, and choose per selector whether higher branches may interrupt.
Two thresholds stop flicker. ShouldFlee() starts fleeing below one health value and stops above a higher one. With a single cut-off, a guard regenerating health would switch between fleeing and fighting every frame around that value. This is the same oscillation problem that utility-ai solves with a commitment bonus.
Unity’s Behavior package
For production work in Unity 6, Unity provides the Behavior package (com.unity.behavior). The Unity Manual describes it as “a graph-based tool to create and run behavior trees with a modular architecture”, with a library of prebuilt nodes, C# integration for custom nodes, reusable subgraphs, and real-time debugging in Play mode (Unity Technologies, Unity Manual: Behavior). Writing the small framework above first makes it much easier to understand what the package’s nodes are doing; the package is the better choice once trees grow large or designers need to edit them.
Trade-offs
- Better than ai-state-machine-pattern when an NPC has many overlapping priorities.
- Heavier to debug than a tiny FSM if the tree grows without tooling or naming discipline.
- Still reactive: by itself, a BT does not plan long action sequences the way goal-oriented-action-planning does.
- Ticking every frame is rarely necessary. Champandard and Dunstan note that many games update each tree every other frame or at about 5 Hz, spreading the cost of many NPCs across frames (Champandard and Dunstan, “The Behavior Tree Starter Kit”, Game AI Pro, see source-game-ai-pro-360-architecture).
Examples
- Dragon Age: Inquisition combines tree-like execution structures with utility-style evaluation (Hanlon and Watts, Game AI Pro 360, see source-game-ai-pro-360-character-behavior).
- Modern production AI often uses BTs as the readable designer-facing layer even when lower systems handle steering, navigation, or utility scoring.
Practice
Build the guard from code/unity-csharp/game-ai/behaviour-tree/ in a new 3D scene, then add one new branch: Search. When the guard loses sight of the player, it should walk to the player’s last seen position before returning to its patrol.
- Store
lastSeenPositionand ahasLastSeenflag inCanSeePlayer()whenever it returns true. - Add a sequence “Search last known position” between “Chase when seen” and “Patrol”, with a condition (
hasLastSeen) and an action that walks there and clears the flag on arrival.
Success test:
- the guard still patrols, chases, strikes and flees exactly as before
- after you drag the player out of its view cone, the guard walks to where it last saw you, then resumes patrolling
- if it sees you again on the way, it chases immediately without finishing the search
- the Current Action field shows “Search” while searching
Extension:
- add a
WaitNodethat returns Running for two seconds, and make the guard look around at the search point before giving up
Self-test
- A sequence has three children. The first returns Success and the second returns Failure. What does the sequence return, and does the third child run?
- A selector’s first child fails and its second child returns Running. What does the selector return?
- Why do
Patrol()andChase()return Running instead of Success? - In the guard example, what does the
InverterNodedo to the “Cooling down?” condition, and what happens while the attack is cooling down? - Why does the guard stop patrolling as soon as it sees the player, even though nothing remembers that it was patrolling?
- Why does
ShouldFlee()use two health thresholds instead of one?
Answers
- Failure. A sequence stops at the first child that does not succeed, so the third child does not run.
- Running. A selector stops at the first child that does not fail and returns that child’s status.
- Walking to a waypoint or the player takes many frames. Running tells the parent the action is still under way, and it is ticked again next frame.
- It flips Success to Failure and Failure to Success. While cooling down, the attack branch fails, so the selector falls through to the chase branch, which holds the guard at striking distance.
- The tree is ticked from the root every frame, so the higher-priority “Chase when seen” branch is checked before “Patrol” each time. As soon as its condition is true it wins, which is how an active selector behaves.
- To avoid oscillation. With one cut-off, regenerating health would cross it repeatedly and the guard would switch between fleeing and fighting every frame; two thresholds make it commit to fleeing until it has properly recovered.
Related
game-ai-agent-design · overview-agent-ai-route · behaviour-trees-vs-goap · ai-state-machine-pattern · goal-oriented-action-planning · utility-ai · blackboard-architecture · npc-behaviour-readability · steering-behaviours · unity-navmesh · source-ai-and-games · source-artificial-intelligence-for-games · source-game-ai-pro-360-architecture