#tree #node-tree #bevy #game #ai #entity-component

bevy_behave

A behaviour tree plugin for bevy with dynamic spawning

3 releases

new 0.1.2 Feb 10, 2025
0.1.1 Feb 5, 2025
0.1.0 Feb 4, 2025

#119 in Game dev

Download history 265/week @ 2025-02-03

265 downloads per month

MIT/Apache

740KB
1K SLoC

bevy_behave

crates.io

A behaviour tree plugin for bevy with dynamic spawning.

When an action (leaf node / task node) in the behaviour tree runs, it will spawn an entity with the components you specified in the tree definition. The tree then waits for this entity to trigger a status report, at which point the entity will be despawned.

Conditionals are implemented with observers, see below.

This was an experiment to see if I could make an ergonomic bevyish way to do behaviour trees, I think it's turning out fairly well. Please do offer feedback (good or bad) if you check it out!

let npc_entity = get_enemy_entity();
let player_entity = get_player_entity();

// the tree definition (which is cloneable).
// and in theory, able to be loaded from an asset file (unimplemented).
// when added to the BehaveTree component, this gets transformed internally to hold state etc.
//
// These trees are `ego_tree::Tree<Behave>` if you want to construct them manually.
// Conventient macro usage shown below.
//
// Breaking it into two trees and composing, just to show how it's done.
let chase_subtree = behave! {
    Behave::Sequence => {
        Behave::dynamic_spawn((
            Name::new("Move towards player while in range"),
            MoveTowardsPlayer{player_entity, speed: 100.0}
        )),
        // MoveTowardsPlayer suceeds if we catch them, in which randomize our colour.
        // This uses a trigger to take an action without spawning an entity.
        Behave::trigger_req(RandomizeColour),
        // then have a nap (pause execution of the tree)
        // NB: this only runs if the trigger_req was successful, since it's in a Sequence.
        Behave::Wait(5.0),
    }
};

let tree = behave! {
    Behave::Forever => {
        // Run children in sequence until one fails
        Behave::Sequence => {
            // WAIT FOR THE PLAYER TO GET CLOSE
            // Spawn with any normal components that will control the target entity:
            Behave::dynamic_spawn((
                Name::new("Wait until player is near"),
                WaitUntilPlayerIsNear{player_entity}
            )),
            // CHASE THE PLAYER
            @ chase_subtree
        }
    }
};

// Spawn an entity to run the behaviour tree.
// Make it a child of the npc entity for convenience.
// The default is to assume the Parent of the tree entity is the Target Entity you're controlling.
commands.spawn((
    Name::new("Behave tree for NPC"),
    BehaveTree::new(tree)
)).set_parent(npc_entity);

When a dynamic spawn happens, the entity is given the components you provided along with a BehaveCtx component, which will tell you the target entity the tree is controlling, and a mechanism to trigger a status report for success or failure.

Have a look at the chase example.

Control Flow Nodes

Currently supported control flow nodes:

Node Description
Sequence Runs children in sequence, failing if any fails, succeeding if all succeed
Fallback Runs children in sequence until one succeeds. If all fail, this fails
Invert Inverts success/failure of child. Must only have one child
AlwaysSucceed Always succeeds
AlwaysFail Always fails
TriggerReq Triggers an event, which the user observes and responds to with a success or failure report

Task Nodes

Node Description
Wait Waits this many seconds before Succeeding
Timer is ticked inside the tree, no entities are spawned.
DynamicSpawn Spawns an entity when this node in the tree is reached, and waits for it to trigger a status report.
Once the entity triggers a status report, it is immediately despawned.

Unimplemented but possibly useful Task Nodes:

Node Description
ExistingEntity When this node on the tree is reached, a BehaveCtx is inserted.
The tree then waits for this entity to trigger a status report.
On completion, BehaveCtx is removed, but nothing is despawned.

Useful components

Triggering completion after a timeout

To trigger a status report on a dynamic spawn task after a timeout, use the BehaveTimeout helper component:

let tree = behave! {
    Behave::dynamic_spawn((LongRunningTaskComp::new(), BehaveTimeout::from_secs(5.0, true)))
}

This will get the BehaveCtx from the entity, and trigger a success or failure report for you after the timeout.

How conditionals/non-spawning tasks work

I'm using observer events to implement no-entity-required tasks. You specify an arbitrary struct which is delivered in a generic trigger which also carries a BehaveCtx value. The observer can then respond with success or failure.

// Conditionals are types that are delivered by a trigger:
#[derive(Clone)]
struct HeightCheck {
    min_height: f32,
}

// add a global observer to answer conditional queries for HeightCheck:
app.add_observer(on_height_check);

// you respond by triggering a success or failure event created by the ctx:
fn on_height_check(trigger: Trigger<BehaveTrigger<HeightCheck>>, q: Query<&Position>, mut commands: Commands) {
    let ctx: BehaveTriggerCtx = trigger.event().ctx();
    let height_check: HeightCheck = trigger.event().value();
    // lookup the position of the target entity (ie the entity this behaviour tree is controlling)
    let character_pos = q.get(ctx.target_entity()).expect("Character entity missing?");
    if character_pos.y >= height_check.min_height {
        commands.trigger(ctx.success());
    } else {
        commands.trigger(ctx.failure());
    }
}

Performance

  • There's just one global observer for receiving task status reports from entities or triggers.
  • Most of the time, the work is being done in a spawned entity using one of your action components, and in this state, there is a marker on the tree entity so it doesn't tick or do anything until a result is ready.
  • Avoided mut World systems – the tree ticking should be able to run in parallel with other things (i think).
  • So a fairly minimal wrapper around basic bevy systems.

In release mode, i can happily toss 100k enemies in the chase demo and zoom around at max framerate. It gets slow rendering a zillion gizmo circles before any bevy_behave stuff gets in the way.

Chase example

This is the chase example from this repo, running in release mode on an M1 mac with 100k enemies. Each enemy has a behaviour tree child and an active task component entity. So 1 enemy is 3 entities.

https://github.com/user-attachments/assets/e12bc4dd-d7fb-4eca-8810-90d65300776d

Video from my space game

Here I have more complex behaviour trees managing orbits, landing, etc. Lots of PID controllers at work. No attempts at optimising the logic yet, but I can add 5k ships running behaviours. Each is a dynamic avian physics object exerting forces via a thruster.

https://github.com/user-attachments/assets/ef4f0539-0b4d-4d57-9516-a39783de140f

License

Same as bevy: MIT or Apache-2.0.

Notes

The behave! macro is an extension of the ego_tree::tree! macro, i need to upstream the subtree merging feature.

todo

  • validate tree shape. some nodes need a specific number of children.

desired tests

  • dynamic spawn that gives result in an onadd trigger
  • dynamic spawn that gives result during running
  • conditional that gives result
  • tree shape stuff

Paths not taken

Alternative approach taking `IntoSystem` (not taken)

Alternative approach for conditionals

I considered doing control flow by taking an IntoSystem with a defined In and Out type, something like this:


pub type BoxedConditionSystem = Box<dyn System<In = In<BehaveCtx>, Out = bool>>;

#[derive(Debug)]
pub enum Behave {
    // ...
    /// If, then
    Conditional(BoxedConditionSystem),
}

impl Behave {
    pub fn conditional<Marker>(system: impl IntoSystem<In<BehaveCtx>, bool, Marker>) -> Behave {
        Behave::Conditional(Box::new(IntoSystem::into_system(system)))
    }
}

Then you could defined a cond system like, which is quite convenient:

fn check_distance(In(ctx): In<BehaveCtx>, q: Query<&Position, With<Player>>) -> bool {
    let Ok(player_pos) = q.get(ctx.target_entity).unwrap();
    player_pos.x < 100.0
}

However I don't think the resulting data struct would be cloneable, nor could you really read it from an asset file for manipulation (or can you?)

I would also need mutable World in the "tick trees" system, which would stop it running in parallel maybe. Anyway observers seem to work pretty well.

Dependencies

~22–33MB
~538K SLoC