Trees
A behavior tree says what the guard does, and in what order of priority. You
write its shape with behavior_tree! and put it on the guard as a
BehaviorTree.
A guard that fights
The guard chases and shoots an intruder it sees, searches for one it remembers, and patrols otherwise:
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;
#[derive(Component, Clone)] struct Shoot;
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 guard() -> Behavior {
behavior_tree! {
Forever => {
Select => {
when(sees_intruder) => Sequence => {
task(Chase),
task(Shoot),
Wait(0.8),
},
when(remembers_intruder) => task(Search),
task(Patrol { from: -10.0, to: 10.0 }),
}
}
}
}
Read it from the top:
Foreverruns its child again each time it ends, so the guard never stops.Selectruns the first branch whose decorator passes. A branch withoutwhenalways passes, so patrolling is the fallback.Sequenceruns its children in turn: chase, shoot, then wait 0.8 seconds.task(..)is work your systems do. See Tasks.
Put the tree on the guard as you spawn it:
use bevy::prelude::*;
use bevy_arbor::prelude::*;
#[derive(Component)] struct Guard;
fn guard() -> Behavior { behavior_tree! { Wait(1.0) } }
fn spawn_guard(mut commands: Commands) {
commands.spawn((Guard, Transform::default(), BehaviorTree::new(guard())));
}
Nodes
Composites have several children:
| Node | What it does |
|---|---|
Select => { .. } | Runs the first branch whose decorator passes, and switches when that changes |
Fallback => { .. } | Like Select, but never interrupts what it started |
Scored => { .. } | Tries branches from the highest score(system) down, until one succeeds |
Random(roll) => { .. } | Tries children in a random order, until one succeeds |
Sequence => { .. } | Runs children in turn, until one fails |
All => { .. } | Runs children together. Fails at the first failure, succeeds once all succeed |
Any => { .. } | Runs children together. Succeeds at the first success, fails once all fail |
Race => { .. } | Runs children together. The first to end decides |
Modifiers have one child:
| Node | What it does |
|---|---|
Forever => { .. } | Runs its child again each time it ends |
Repeat(times) => { .. } | Runs its child times times. Fails at its first failure |
RepeatWhile(decorator) => { .. } | Runs its child over and over while the decorator passes |
Invert => { .. } | Reverses its child’s result |
Succeed => { .. } | Succeeds whatever its child did |
Commit => { .. } | Can’t be interrupted once started. See Interruption |
Timeout(length) => { .. } | Cancels and fails its child after length |
State(bundle) => { .. } | Puts bundle on the agent while its child runs. See Tasks |
Socket(key) | Runs the subtree plugged into the agent. See Large trees |
Leaves do the work:
| Node | What it does |
|---|---|
task(bundle) | Spawns a task entity and waits for it to report. See Tasks |
act(system) | Runs a system once. Its bool is the result |
Wait(length) | Succeeds after length |
@ expr | Puts in a subtree built elsewhere. See Large trees |
A length is seconds, like Wait(0.8), or any Duration. A number is
checked as the tree compiles, so a negative wait is a compile error. If you
give a one-child node several children, they run as a Sequence.
Select and Fallback
Select keeps asking its decorators. When the guard spots an intruder, the
Select cancels the patrol and starts the chase. When the intruder is gone,
it cancels the chase. This makes the guard react at once.
A branch that fails falls through to the next one. It stays out until its
decorator stops passing, so it can’t restart itself in a loop. A branch that
succeeds ends the Select.
Fallback asks each decorator only when it gets to it. What it started runs
to the end. Use it where reacting isn’t wanted. Interruption
compares the two.
Running things together
All, Any and Race run their children at the same time. The guard shouts
for help while it chases:
use bevy::prelude::*;
use bevy_arbor::prelude::*;
#[derive(Component, Clone)] struct Chase;
#[derive(Component, Clone)] struct Shout;
fn tree() -> Behavior {
behavior_tree! {
All => {
task(Chase),
task(Shout),
}
}
}
When a parallel node settles, it cancels the children still running.
Scored and Random
Scored asks a score for each branch as it starts, and tries them from the
highest down. A score is a system that takes the agent and returns an f32.
Random shuffles its children with rolls from a system of yours, so you
choose the randomness:
use bevy::prelude::*;
use bevy_arbor::prelude::*;
#[derive(Component, Clone)] struct Patrol { from: f32, to: f32 }
#[derive(Component, Clone)] struct Search;
fn suspicion(In(_guard): In<Entity>) -> f32 {
0.3
}
fn boredom(In(_guard): In<Entity>) -> f32 {
0.6
}
fn roll(In(_guard): In<Entity>) -> f32 {
// A number from 0 up to 1, from your random source.
0.5
}
fn tree() -> Behavior {
behavior_tree! {
Scored => {
score(suspicion) => task(Search),
score(boredom) => Random(roll) => {
task(Patrol { from: -10.0, to: 0.0 }),
task(Patrol { from: 0.0, to: 10.0 }),
},
}
}
}
- A score or roll can return
Result<f32>, so you can use?. An error goes to Bevy’s error handler. - A branch whose score is an error or NaN goes last.
Without the macro
The macro calls Behavior’s builders. You can call them yourself, for
trees built from data:
use bevy::prelude::*;
use bevy_arbor::prelude::*;
#[derive(Component, Clone)] struct Patrol { from: f32, to: f32 }
#[derive(Component, Clone)] struct Chase;
fn sees_intruder(seen: &Perceived<Sight>) -> bool { seen.active().any(Percept::is_hostile) }
use bevy_arbor::bt::behavior::Branch;
fn guard() -> Behavior {
Behavior::forever(Behavior::select(vec![
Branch::when(sees_intruder, Behavior::task(Chase)),
Branch::always(Behavior::task(Patrol { from: -10.0, to: 10.0 })),
]))
}
The tree on the guard
TreeFinishedis triggered on the guard when its tree ends.BehaviorTree::outcomesays how it ended. A tree underForevernever ends.BehaviorTree::restartstarts a finished tree again.TreePausedon the guard holds its tree where it is: no decorators asked, nothing started, and its task skipped byTasks. Time still passes, so aWaitmay be over when you remove it.- Removing the tree, replacing it, or despawning the guard cancels what the tree was running.
Pitfalls
- A
Selectends when a branch succeeds. WithoutForeveraround it, the guard stops after its first chase. - Order matters in a
Select. A branch withoutwhencatches everything, so put it last.