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! ;
let tree = behave! ;
// 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.set_parent;
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 SucceedingTimer 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!
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:
// add a global observer to answer conditional queries for HeightCheck:
app.add_observer;
// you respond by triggering a success or failure event created by the ctx:
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 for conditionals
I considered doing control flow by taking an IntoSystem with a defined In and Out type,
something like this:
pub type BoxedConditionSystem = ;
Then you could defined a cond system like, which is quite convenient:
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.