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
265 downloads per month
740KB
1K
SLoC
bevy_behave
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