Skip to content
Viacheslav K.
← Lab

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:

ResultMeaning in this example
SuccessThe robot reached the kitchen, or a condition is true.
FailureA condition is false; that branch doesn't apply.
RunningThe 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 Failure without calling StopAndWait.
  • If the child is nearby, tick StopAndWait, which stops movement and returns Running.

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:

MomentChild branchKitchen actionVisible behaviour
Child is far awayFailureRunningRobot walks towards the kitchen.
Child approachesRunningNot tickedRobot stops at the next decision tick.
Child stays nearbyRunningNot tickedRobot remains still.
Child leavesFailureRunningRobot continues from its current position.
Robot arrivesFailureSuccessRobot 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.