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

Sensors, memory and events

This chapter gives the guard a lookout camera, a memory of where it last saw the intruder, and a line to shout when the intruder slips away.

Sensors

A sense can sit on its own entity, a sensor, instead of on the agent. The sensor is SensorOf the agent. What it senses goes to the agent’s Perceived<S>, as if the agent sensed it itself.

The guard’s post has a camera high on the wall, looking along the corridor the guard patrols:

use std::f32::consts::PI;
use bevy::prelude::*;
use bevy_arbor::prelude::*;
fn spawn(mut commands: Commands, guard: Entity) {
commands.spawn((
    Sight::new(30.0).with_half_angle(PI / 6.0),
    SensorOf(guard),
    Transform::from_xyz(-15.0, 4.0, 0.0),
));
}

When the camera sees the intruder, so does the guard. Its sees_intruder condition doesn’t change.

  • A sensor looks from its own transform. The camera stays on the wall while the guard walks.
  • For a sensor that moves with the agent, eyes on a head say, parent it with ChildOf(agent) as well. SensorOf is only about perception.
  • The sensor keeps nothing itself. Everything goes to the agent.
  • Despawning the agent despawns its sensors. The agent’s Sensors lists them.
  • A sensor despawned, detached, or moved to another agent stops counting at once. What only it saw is lost, as if it had looked away. Detached, it senses for itself again.

An agent can have several sensors of one sense, and its own as well. For sight, what any of them sees is active. It’s lost only once none of them sees it. The camera looking away doesn’t lose what the guard still sees.

Memory

When the intruder goes out of view, the guard doesn’t forget it at once. The percept stays in Perceived<Sight>, no longer active. Its trace, in Traces<Sight>, says when and where the guard lost it. remembers_intruder reads the first:

use bevy::prelude::*;
use bevy_arbor::prelude::*;
#[derive(Component, Clone)] struct Patrol { from: f32, to: f32 }
#[derive(Component, Clone)] struct Chase;
#[derive(Component, Clone)] struct Search;
fn sees_intruder(seen: &Perceived<Sight>) -> bool {
    seen.active().any(Percept::is_hostile)
}

fn remembers_intruder(seen: &Perceived<Sight>) -> bool {
    seen.iter().any(|p| p.is_hostile() && !p.is_active())
}

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

The Search task reads the trace and walks to where the intruder was lost:

use bevy::prelude::*;
use bevy_arbor::prelude::*;
#[derive(Component, Clone)] struct Search;
fn search(
    time: Res<Time>,
    mut tasks: Tasks<&Search, (&Perceived<Sight>, &Traces<Sight>, &mut Transform)>,
) {
    tasks.for_each(|_, mut task, (seen, traces, mut at)| {
        let last = seen
            .iter()
            .filter(|p| p.is_hostile() && !p.is_active())
            .find_map(|p| traces.get(p.entity()));
        let Some(last) = last else {
            task.fail();
            return;
        };
        let gap = last.position.x - at.translation.x;
        if gap.abs() < 0.5 {
            task.succeed();
        } else {
            at.translation.x += gap.signum() * 3.0 * time.delta_secs();
        }
    });
}

When the memory fades, the intruder is forgotten. remembers_intruder turns false, and the guard goes back to its patrol.

How long it remembers

Each sense remembers for its Sense::MEMORY:

SenseRemembers for
Sight10 s
Shared10 s
Hearing5 s
Touch5 s

Memory counts from when the thing was last sensed. What’s active never fades: the guard doesn’t forget the intruder while it’s looking at it. An agent with nothing due to fade costs nothing to check that tick.

To change it for one agent, spawn it with its own Perceived:

use std::time::Duration;
use bevy::prelude::*;
use bevy_arbor::prelude::*;
#[derive(Component)] struct Guard;
fn spawn(mut commands: Commands) {
commands.spawn((
    Guard,
    Sight::new(12.0).with_lose_range(15.0),
    Perceived::<Sight>::remembering(Duration::from_secs(30)),
    Transform::default(),
));
}

Duration::MAX remembers for good.

Memory belongs to the agent, not to a sensor. The guard and its camera share one memory of sight, and one set of traces.

Events

Perception triggers events on the agent, one set per sense:

EventWhen
Noticed<S>Something comes to its attention, or is active again
Lost<S>Something stops being active: out of view, for sight
Forgotten<S>It forgets something

entity is the agent. what is the thing. Observe them for barks, alerts and UI:

use bevy::prelude::*;
use bevy_arbor::prelude::*;
fn where_did_they_go(lost: On<Lost<Sight>>, names: Query<&Name>) {
    if let Ok(name) = names.get(lost.what) {
        info!("{}: where did {name} go?", lost.entity);
    }
}

fn add(app: &mut App) {
app.add_observer(where_did_they_go);
}

Only sustained senses, like sight, lose things. A noise is noticed, then forgotten.

Pitfalls

  • Don’t drive trees with events. A tree already wakes when Perceived<S> changes, which carries the same news. Use conditions such as remembers_intruder, and keep events for side effects.
  • A sensor that doesn’t follow. SensorOf doesn’t move the sensor. Add ChildOf for that.
  • A trace is the past. For the intruder’s position while in view, read its transform.