Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Your first agent

In this chapter the guard learns to patrol. Then it learns to chase the intruder when it sees one.

The app

Start with the plugins from Installation. Perception is set to 2D, and the task systems run after ArborSystems:

use bevy::prelude::*;
use bevy_arbor::prelude::*;
fn spawn(_: Commands) {}
fn patrol() {}
fn main() {
    App::new()
        .add_plugins((
            DefaultPlugins,
            ArborPlugins.with_space(Space::Planar),
        ))
        .add_systems(Startup, spawn)
        .add_systems(FixedUpdate, patrol.after(ArborSystems))
        .run();
}

Patrolling

A tree decides what to do. A task does it. Patrol is a task: a component of yours, with what the guard needs to know.

use bevy::prelude::*;
use bevy_arbor::prelude::*;
#[derive(Component)]
struct Guard;

#[derive(Component, Clone)]
struct Patrol {
    from: f32,
    to: f32,
}

fn guard() -> Behavior {
    behavior_tree! {
        Forever => {
            task(Patrol { from: -10.0, to: 10.0 })
        }
    }
}

fn spawn(mut commands: Commands) {
    commands.spawn((
        Guard,
        Facing(Dir3::X),
        Transform::from_xyz(0.0, 0.0, 0.0),
        BehaviorTree::new(guard()),
    ));
}

When the tree reaches task(Patrol { .. }), it spawns a task entity with a Patrol component. The task is linked to the guard. The tree keeps it alive until the branch ends.

Forever restarts its child whenever it finishes. Here, the patrol never finishes, so it just keeps going.

Doing the work

Your systems do the work of a task. Tasks pairs each task with components of its agent. Here it pairs each Patrol with the guard’s transform and facing:

use bevy::prelude::*;
use bevy_arbor::prelude::*;
#[derive(Component, Clone)] struct Patrol { from: f32, to: f32 }
fn patrol(
    mut tasks: Tasks<&Patrol, (&mut Transform, &mut Facing)>,
    time: Res<Time>,
) {
    tasks.for_each(|patrol, _task, (mut transform, mut facing)| {
        let x = transform.translation.x;
        // Only a turn changes `Facing`, so the guard's tree isn't woken
        // every tick.
        if x >= patrol.to {
            facing.set_if_neq(Facing(Dir3::NEG_X));
        } else if x <= patrol.from {
            facing.set_if_neq(Facing(Dir3::X));
        }
        transform.translation += facing.0 * 3.0 * time.delta_secs();
    });
}

The guard walks to one end, turns, and walks back. It keeps Facing up to date, so it looks where it walks. In a side-scroller the sprite flips but the transform doesn’t turn, so sight reads Facing instead.

The second argument is the task’s Task component. A task that should end reports through it, with task.succeed() or task.fail(). The patrol never ends, so it ignores it.

Seeing the intruder

Now give the guard eyes, and something to see:

use bevy::prelude::*;
use bevy_arbor::prelude::*;
#[derive(Component)] struct Guard;
#[derive(Component, Clone)] struct Patrol { from: f32, to: f32 }
#[derive(Component, Clone)]
struct Chase;

#[derive(Component)]
struct Intruder;

fn sees_intruder(seen: &Perceived<Sight>) -> bool {
    seen.active().any(Percept::is_hostile)
}

fn guard() -> Behavior {
    behavior_tree! {
        Forever => {
            Select => {
                when(sees_intruder) => task(Chase),
                task(Patrol { from: -10.0, to: 10.0 }),
            }
        }
    }
}

fn spawn(mut commands: Commands) {
    commands.spawn((
        Guard,
        Sight::new(12.0).with_lose_range(15.0),
        Team(1),
        Facing(Dir3::X),
        Transform::from_xyz(0.0, 0.0, 0.0),
        BehaviorTree::new(guard()),
    ));
    commands.spawn((
        Intruder,
        Perceivable,
        Team(2),
        Transform::from_xyz(30.0, 0.0, 0.0),
    ));
}

The guard sees 12 units ahead. Once it sees something, it keeps seeing it up to 15 units away. The intruder is Perceivable and on another team, so to the guard it is hostile.

Select runs the first branch whose condition passes. While the guard sees the intruder, that’s Chase. Otherwise it’s Patrol.

sees_intruder is a typed condition: a plain function of components on the agent. The tree asks it again only when Perceived<Sight> changes. That happens only when something comes into view or leaves it. In between, the tree sleeps.

Chasing

The chase system finds the intruder among what the guard sees, and walks toward it:

use bevy::prelude::*;
use bevy_arbor::prelude::*;
#[derive(Component, Clone)] struct Chase;
fn chase(
    mut tasks: Tasks<&Chase, (&Perceived<Sight>, &mut Transform)>,
    targets: Query<&GlobalTransform>,
    time: Res<Time>,
) {
    tasks.for_each(|_, _, (seen, mut transform)| {
        let target = seen
            .active()
            .find(|percept| percept.is_hostile())
            .and_then(|percept| targets.get(percept.entity()).ok());
        if let Some(target) = target {
            let toward = (target.translation() - transform.translation).normalize_or_zero();
            transform.translation += toward * 4.0 * time.delta_secs();
        }
    });
}

Add chase next to patrol, after ArborSystems.

When the intruder comes into view, the tree wakes. sees_intruder passes. The Patrol task is despawned and a Chase task is spawned, in the same tick. When the intruder gets away, the opposite happens.

Pitfalls

  • Live positions. Perceived doesn’t hold where things are now. That would change it every frame and wake the tree for nothing. Read the target’s transform, as chase does.
  • System order. Run task systems after ArborSystems. Otherwise a task spawned this tick may wait a tick before it moves.
  • Task components must be Clone. The tree spawns a fresh copy each time the branch starts.

Next, How it fits together shows how these parts share a tick. Building trees teaches the guard to fight.