Gearbox is a statechart library for the Bevy game engine.
Why gearbox
State machines are useful everywhere in games - AI behavior, ability lifecycles, UI flows, animation controllers. But state machines in an ECS are a hard problem. Gearbox solves this by representing state machines as regular entity hierarchies. States are entities. Transitions are entities. Everything lives in the ECS.
- Pure ECS. States and transitions are entities with components. Query for active states with
Added<Active>, or expose a state on the machine root with aStateComponent. - Statechart semantics. Hierarchy, parallel regions, history, guards, delays and terminal states follow the same rules as XState and SCXML.
- Parallel by construction. Resolution runs in a dedicated schedule, so your guards and side effects are ordinary Bevy systems and run in parallel like any other.
- Message-driven. Trigger transitions by writing Bevy messages. Read the matched payload in a side-effect phase to apply damage, spend a resource, or fire the next message.
- Data-driven. Author a whole chart as one
bsn!scene. Save and load it as a Bevy scene, or edit it while the game runs. - Visual editor. Build, edit, and monitor state machines in a running game over the Bevy Remote Protocol. A development tool: the
serverfeature opens an HTTP server on loopback and can read and write files under the game'sassets/, so enable it in dev builds only.
Read the guide for a tutorial on how to build a statechart start to finish.
Getting started
use *;
use GearboxPlugin;
Building a state machine
use *;
use ;
use *;
States nest under Substates [ .. ], edges under Transitions [ .. ], and
#Name references resolve to sibling states in the scene. A parent with an
InitialState is sequential; a parent without one is a parallel region.
Triggering transitions
Define a message with #[derive(GearboxMessage)], marking the entity it's
addressed to with #[gearbox(target)] (the message listener walks SubstateOf
from there to find the machine root):
use *;
use *;
// Write it from any system.
The derive registers the message type through inventory, and GearboxPlugin
installs every derived message's listener on build, so a derived message needs
no extra wiring. Generic message types can't be auto-registered; call
app.register_transition::<M>() for those.
Guarded transitions
A Transitions with multiple of the same edge acts as a branching transition. The statechart will attempt each edge in order. This means that you should carefully order your transitions. In the following example, note that the more strict transition (alive -> dead) is before the less strict transition (alive -> hurt).
;
#Alive Transitions
// A guard is a system in BlockerPhase that vetoes candidates carrying the marker.
State components
State components are inserted into the statechart root when the state that has the state component is entered. The component is removed from the root when the state is exited. You can use this to selectively expose states to the ECS.
use *;
;
// On the state, in the scene:
#Walking
// While that state is active the root carries `Walking`, so
// `Query<&mut Velocity, With<Walking>>` finds walking characters.
Reacting to state changes
Attach entry actions in the scene, or query Active from systems:
// In the scene: an EnterState observer on the state entity.
#Invoking on
// From a system, ordered after GearboxSet:
Features
- Hierarchical states (nested state machines / statecharts)
- Parallel regions
- Shallow and deep history
- Message-driven transitions with per-edge validators
- Guarded transitions: ordered candidates, first passing guard wins, guardless fallback
- Built-in
InState/NotInStateguards for coordinating parallel regions - Always-edges (automatic transitions on entry) and delayed edges (timer-based), both guardable
- Terminal states that emit
Doneto their parent; a parallel state is done when every region is - Side effects with payloads via
Matched<M>, skipped for vetoed transitions - State components (auto insert/remove on the machine root)
- Entry/exit observers (
EnterState/ExitState, fired in statechart order inside the schedule) andAdded<Active>queries - Reset edges (clear history under a subtree on transition)
- Internal vs external transitions
- Bridge to Bevy
States(#[state_bridge]) - Optional bevy_gauge integration:
Delaydriven by an attribute (gaugefeature) - Optional editor server (
serverfeature, off by default) for the visual editor
Examples
cargo run --example <name>; add --features server and the editor can connect to it.
| How do I... | Example | Shows |
|---|---|---|
| build and run a chart at all? | hello_statechart |
a bsn! chart, a derived message, Added<Active> after GearboxSet |
| run independent regions, and make one depend on another? | parallel_regions |
a parallel root, one message per region, InState |
| branch on a condition, or use the message's payload? | guarded_transitions |
ordered candidates, guard systems, Matched<M>, BlockedEdges |
| filter a message by its payload without a guard system? | validators |
MessageValidator, #[gearbox(validator = ..)], a catch-all fallback edge |
| share one transition across several states? | hierarchy |
an ancestor's edge, nested InitialState, the active path from StateMachine |
| react to a message without leaving the state? | internal_transitions |
EdgeKind::Internal vs an external self-loop, SideEffectPhase |
| resume where I left off? | history |
History::Deep / Shallow, ResetEdge |
| nest a sub-chart and continue when it finishes? | sub_charts |
TerminalState, Done, parallel completion |
| query "which characters are walking" from a normal system? | state_components |
StateComponent marker and payload forms, StateInactiveComponent |
| run logic inside the resolution loop, or on exit? | schedule_phases |
ExitPhase / EntryPhase / SideEffectPhase systems, on(ExitState), FixedUpdate |
drive Bevy States from a chart? |
state_bridge |
#[state_bridge], OnEnter, DespawnOnExit |
Scenes and serialization
Author with bsn!. Bevy 0.19 has no .bsn asset format yet; a chart the editor
saves is written as a Bevy DynamicScene (.scn.ron) and reloaded through
bevy_world_serialization. Substates and Transitions are saved with the
chart, like Bevy's Children, so edge order (the priority order for guarded
edges) survives a round trip. A saved scene holds structure, not runtime state:
Active, timers, history and on(..) observers are left out. Your own
components on states and edges are included when they derive Reflect with
#[reflect(Component)] and are registered with register_type; anything else
is skipped and the editor logs what it left out.
Version Table
| Bevy | Gearbox |
|---|---|
| 0.19 | 0.9 |
| 0.19 | 0.8 |
| 0.19 | 0.7 |
| 0.18 | 0.6 |
| 0.18 | 0.5 |
| 0.17 | 0.4 |
Contributing
Feel free to open issues or create pull requests if you encounter any problems.
Ask us on the Bevy Discord server's Gearbox topic in #ecosystem-crates for larger changes or other things if you feel like so!
License
Dual-licensed under MIT (LICENSE-MIT) or Apache 2.0 (LICENSE-APACHE).