Behaviour Trees in Games and Robotics
Exploring how game AI and NPC behaviour can transfer to robotics, with a small C# tutorial alongside a larger simulation project.
- Built
- Stack
- C#, Unity, Behaviour trees
Tutorial · C#
From game AI to robot behaviour
I'm a game developer, and watching the progress in robotics has made me curious about how experience building games can transfer to programming robots. In games, we give NPCs goals, decide which actions take priority, and make them react when the world changes. I'm exploring how those same ideas can help organise robot behaviour.
I'm building a more complex robot simulation to showcase on video, and publishing this small article to help others understand the principles behind it. We'll start with a few C# classes and build up to a behaviour tree. The example focuses on deciding what to do; sensing, navigation, and physical control are separate parts of a robot system.
To keep the code easy to follow, imagine a home robot on its way to the kitchen. A child walks close to it, so the robot stops. When the child moves away, the robot continues towards the kitchen. We have one task, one reason to interrupt it, and a clear priority: stop while the child is nearby.
Start with the behaviour
The rule fits in an ordinary conditional:
if (robot.ChildNearby)
robot.Stop();
else if (!robot.AtKitchen)
robot.MoveToKitchen();
else
robot.Stop();
For these two behaviours, this is a reasonable solution. A behaviour tree becomes useful when we want to compose actions and conditions into reusable branches, then see their priorities in one place.
We'll build this tree:
Selector
├── Sequence
│ ├── Child nearby?
│ └── Stop and wait
└── Go to kitchen
Read it from the top: try the child branch first. If it doesn't apply, try going to the kitchen.
1. Give every node the same contract
A tree is evaluated through repeated calls to Tick(). Each node reports one of three results:
| Result | Meaning in this example |
|---|---|
Success | The robot reached the kitchen, or a condition is true. |
Failure | A condition is false; that branch doesn't apply. |
Running | The robot is still walking, or must keep waiting. |
Failure doesn't necessarily mean an error. For “Child nearby?”, it simply means “no.”
public enum Status { Success, Failure, Running }
public abstract class Node
{
public abstract Status Tick();
}
Running is what lets a short function represent an action that takes time. It doesn't create a background thread or block until the action finishes. The function returns promptly; a later tick checks progress again.
2. Make walking an action
The tree needs a small view of the robot:
public interface IRobot
{
bool ChildNearby { get; }
bool AtKitchen { get; }
void MoveToKitchen();
void Stop();
}
IRobot separates decisions from movement. In a Unity scene, an implementation would read the child's distance and the robot's position, and forward movement requests to a mover. Repeating MoveToKitchen() should maintain the destination without restarting the journey.
The action itself only asks whether the robot has arrived:
public sealed class GoToKitchen : Node
{
private readonly IRobot robot;
public GoToKitchen(IRobot robot) => this.robot = robot;
public override Status Tick()
{
if (robot.AtKitchen)
{
robot.Stop();
return Status.Success;
}
robot.MoveToKitchen();
return Status.Running;
}
}
On the first tick, the robot requests movement and returns Running. On later ticks, it checks again. Once it reaches the kitchen, it stops and returns Success.
At this point, our entire tree could be just new GoToKitchen(robot).
3. Add the child condition and waiting action
A condition turns a yes-or-no question into a node result. The waiting action explicitly stops the robot and keeps returning Running:
public sealed class Condition : Node
{
private readonly Func<bool> check;
public Condition(Func<bool> check) => this.check = check;
public override Status Tick() => check() ? Status.Success : Status.Failure;
}
public sealed class StopAndWait : Node
{
private readonly IRobot robot;
public StopAndWait(IRobot robot) => this.robot = robot;
public override Status Tick()
{
robot.Stop();
return Status.Running;
}
}
There is no timer here. Waiting ends when the child condition becomes false and the tree chooses the other branch.
That explicit Stop() matters. A movement system can keep following its last destination even when its action node is no longer being ticked. Choosing another branch doesn't automatically cancel movement.
4. Connect them with a Sequence
A Sequence visits its children in order. It continues only when a child returns Success. A Failure or Running result is returned immediately.
public sealed class Sequence : Node
{
private readonly Node[] children;
public Sequence(params Node[] children) => this.children = children;
public override Status Tick()
{
foreach (Node child in children)
{
Status result = child.Tick();
if (result != Status.Success) return result;
}
return Status.Success;
}
}
For our child branch, this means:
- If the child isn't nearby, return
Failurewithout callingStopAndWait. - If the child is nearby, tick
StopAndWait, which stops movement and returnsRunning.
This implementation starts from the first child on every tick. That keeps the condition live even while the waiting action is running.
5. Choose priorities with a Selector
A Selector also visits children in order, but it continues only when a child returns Failure:
public sealed class Selector : Node
{
private readonly Node[] children;
public Selector(params Node[] children) => this.children = children;
public override Status Tick()
{
foreach (Node child in children)
{
Status result = child.Tick();
if (result != Status.Failure) return result;
}
return Status.Failure;
}
}
Now assemble the complete behaviour:
Node root = new Selector(
new Sequence(
new Condition(() => robot.ChildNearby),
new StopAndWait(robot)
),
new GoToKitchen(robot)
);
The first branch has higher priority because it appears first. While that branch returns Running, the selector never reaches GoToKitchen.
As with our sequence, this selector starts from its first child every tick. Behaviour-tree libraries offer different restart and memory policies; this example deliberately uses reactive evaluation. See the BehaviorTree.CPP explanation of reactive evaluation for a comparison.
Follow one interruption
Imagine evaluating the tree repeatedly while a separate movement update advances the robot:
| Moment | Child branch | Kitchen action | Visible behaviour |
|---|---|---|---|
| Child is far away | Failure | Running | Robot walks towards the kitchen. |
| Child approaches | Running | Not ticked | Robot stops at the next decision tick. |
| Child stays nearby | Running | Not ticked | Robot remains still. |
| Child leaves | Failure | Running | Robot continues from its current position. |
| Robot arrives | Failure | Success | Robot stops in the kitchen. |
The kitchen action doesn't save a paused instruction pointer. When it is selected again, it checks the current world state and requests the same destination.
For a Unity demo, we can evaluate decisions approximately every 0.1 seconds and update movement every frame. Create the tree once, then call root.Tick() on that schedule. Continue ticking after Success so the child condition remains responsive. A node tick should return quickly; it must never contain a loop that waits for arrival.
Two small details when moving into the scene
A single distance threshold can make the robot alternate between moving and stopping if the child hovers near the boundary. My home-robot demo uses two thresholds: enter the waiting state below 2 metres, and leave it at 2.5 metres or more. This is called hysteresis. The tree still reads one ChildNearby value; the distance logic maintains it.
Also, restarting a sequence means earlier successful actions may run again. This tiny tree has no completed walking step before another movement action. If we extend it to “go to the kitchen, then go elsewhere,” we must revisit the unconditional Stop() on arrival: re-ticking that completed step could interfere with the later action. We would need completion-aware actions or a sequence that remembers its progress, with explicit interruption handling.
Download the complete C# source: HomeRobotExample.cs.