bevy_gearbox 0.9.0

State machine system for the bevy game engine
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
## Your first statechart

Gearbox state machines are entity hierarchies: **states are entities**,
**transitions are entities**, and everything lives in the ECS. You author them
as `bsn!` scenes, which spawn the whole tree in one go.

This guide builds a small character chart step by step:

```text
Character (StateMachine, initial = Alive)
├── Alive (initial = Standing)
│   ├── Standing  ──Jump──> Jumping
│   └── Jumping   ──Land──> Standing
└── Dead
```

`Standing` and `Jumping` are substates of `Alive`: the character can only jump
or stand while alive. `Alive` and `Dead` are substates of the root.

Every section below ends with the example that runs it. The full list, one
question each, is in the README; the smallest is
[`examples/hello_statechart.rs`](examples/hello_statechart.rs).

### Building the chart

Author the machine as one `bsn!` scene. States nest under `Substates [ ... ]`,
edges under `Transitions [ ... ]`, and `#Name` references resolve to sibling
states within the scene. The relationship blocks set the `SubstateOf` and
`Source` back-references for you.

The root entity is also a state entity, so it can carry your gameplay
components right alongside `StateMachine`:

```rust
use bevy::prelude::*;
use bevy::scene::prelude::{bsn, CommandsSceneExt};
use bevy_gearbox::prelude::*;

fn spawn_character(mut commands: Commands) {
    commands.spawn_scene(bsn! {
        #Character
            Player
            Collider::capsule(1.0, 0.5)
            Hitpoints { max: 100.0, current: 100.0 }
            StateMachine InitialState(#Alive)
        Substates [
            #Alive InitialState(#Standing) Substates [
                #Standing Transitions [
                    (Target(#Jumping) MessageEdge::<Jump>)
                ],
                #Jumping Transitions [
                    (Target(#Standing) MessageEdge::<Land>)
                ],
            ],
            #Dead,
        ]
    });
}
```

To attach a machine to an entity you already spawned (one that has no machine
yet), use `apply_scene` instead of `spawn_scene` - the scene's root patches onto
the existing entity. A scene's `Substates [ .. ]` / `Transitions [ .. ]` block
replaces any list already on the entity, so add edges to a live machine with
`insert_related`, not a second scene:

```rust
commands.entity(player).apply_scene(bsn! {
    StateMachine InitialState(#Alive)
    Substates [ /* ... */ ]
});
```

[`examples/hierarchy.rs`](examples/hierarchy.rs) shows what nesting buys you: one
`Damage` edge on `Alive` covers every state under it, and `Respawn` targets
`Alive` itself, leaving the nested `InitialState`s to land on `Idle`.

### Triggering transitions

Transitions fire in response to **messages**. Define one with
`#[derive(GearboxMessage)]`, marking the entity it's addressed to with
`#[gearbox(target)]`. The message listener walks `SubstateOf` from that entity
to find the machine root, so you can address either the root or any substate:

```rust
#[derive(Message, Clone, Reflect, GearboxMessage)]
pub struct Jump {
    #[gearbox(target)]
    pub target: Entity,
}

#[derive(Message, Clone, Reflect, GearboxMessage)]
pub struct Land {
    #[gearbox(target)]
    pub target: Entity,
}
```

Deriving `GearboxMessage` also auto-registers the type through `inventory`.

```rust
app.add_plugins(GearboxPlugin::default());
```

Generic message types can't be auto-registered through `inventory`. Register
those explicitly with `app.register_transition::<Jump>()`.

Then write messages from any system - an input system fires `Jump`, a physics
system fires `Land`:

```rust
fn jump_input(
    input: Res<ButtonInput<KeyCode>>,
    q_players: Query<Entity, With<Player>>,
    mut writer: MessageWriter<Jump>,
) {
    if input.just_pressed(KeyCode::Space) {
        for player in &q_players {
            writer.write(Jump { target: player });
        }
    }
}
```

If two edges on the active branch match the same message, the deeper (leaf)
state wins. In parallel regions, each region consumes the message independently.

#### Filtering with a validator

By default every message of the right type matches. To filter per edge, supply
a custom validator:

```rust
#[derive(Message, Clone, Reflect, GearboxMessage)]
#[gearbox(validator = HighDamageOnly)]
pub struct Attacked {
    #[gearbox(target)]
    pub target: Entity,
    pub amount: f32,
}

#[derive(Default, Clone)]
pub struct HighDamageOnly;

impl MessageValidator<Attacked> for HighDamageOnly {
    fn matches(&self, msg: &Attacked) -> bool {
        msg.amount >= 50.0
    }
}
```

[`examples/validators.rs`](examples/validators.rs) opens a three-digit vault
with one `Press` message type and a validator per edge.

### Querying active states with `StateComponent`

The machine changes state internally, but from the outside you need a way to
tell what state it's in - for instance, to make your physics act on jumping
characters. A `StateComponent` clones its payload onto the machine **root**
while its state is active, and removes it when the state exits.

Register the marker with `#[state_component]` and put `StateComponent::<T>` on
the state:

```rust
#[state_component]
#[derive(Component, Clone, Default)]
pub struct Jumping;

// In the scene, on the #Jumping state:
#Jumping
    StateComponent::<Jumping>
    Transitions [ (Target(#Standing) MessageEdge::<Land>) ]
```

A payload that isn't `Default` is passed explicitly:
`StateComponent::<Speed>(Speed(7.5))`.

Now, while `Jumping` is active, the root carries a `Jumping` component, so a
plain query finds jumping characters:

```rust
fn falling_system(mut q_jumping: Query<&mut Velocity, With<Jumping>>) {
    for mut velocity in &mut q_jumping {
        // apply gravity to airborne characters
    }
}
```

> `StateInactiveComponent` is the inverse: it attaches its payload to the root
> while the state is **inactive**, removing it once the state becomes active.

[`examples/state_components.rs`](examples/state_components.rs) uses all three
forms: a `Moving` marker, a `Speed` payload, and a `Controllable` that goes
away while stunned.

### Reacting to enter/exit

There are two ways to run logic on state changes.

**Query change detection** (preferred for systems). Order your system after
`GearboxSet` so it sees this frame's changes:

```rust
fn on_enter(q_entered: Query<(Entity, &Active), Added<Active>>) {
    for (state, active) in &q_entered {
        // `state` was just entered; `active.machine` is the machine root.
    }
}

fn on_exit(mut removed: RemovedComponents<Active>) {
    for state in removed.read() {
        // `state` was just exited.
    }
}

app.add_systems(Update, (on_enter, on_exit).after(GearboxSet));
```

**Observers** (`EnterState` / `ExitState` entity events). These are triggered
on the state entity inside the gearbox schedule, in statechart order: exits
deepest-first in `ExitPhase`, then entries shallowest-first in `EntryPhase`. A
state passed through within one frame gets both. Each carries the state and
its machine root:

```rust
fn on_enter_jumping(enter: On<EnterState>, mut q_velocity: Query<&mut Velocity>) {
    // `enter.state` is the entered state; `enter.machine` is the character root.
    if let Ok(mut velocity) = q_velocity.get_mut(enter.machine) {
        velocity.0.y += 5.0; // apply a jump impulse
    }
}

// Attach the observer to the #Jumping state entity from inside the scene:
#Jumping on(on_enter_jumping)
```

Systems can also live *inside* the schedule, in `GearboxPhase::ExitPhase` or
`EntryPhase`, where they see every step of a same-frame cascade.
[`examples/schedule_phases.rs`](examples/schedule_phases.rs) spawns and removes a
turret beam that way, and runs the whole chart in `FixedUpdate`.

### Automatic and timed transitions

Not every edge needs a message.

- **`AlwaysEdge`** fires as soon as its source state becomes active. Use it to
  auto-advance a chart with no external trigger.
- Add a **`Delay`** to any edge to fire it after a duration while the source
  stays active - `Delay::from_secs_f32(0.8)` for a 0.8s cooldown.
- A **`TerminalState`** emits a `Done` message addressed to its parent when
  entered, so a `MessageEdge::<Done>` on the parent can transition out once a
  sub-chart finishes. A parallel state is done when every region has reached
  a terminal state; its `Done` is addressed to the parallel state itself, and
  nested parallel states cascade. `Done` only matches edges on the state it is
  addressed to (or below it), never an ancestor's.

```rust
#Ready Transitions [
    (Target(#Invoking) AlwaysEdge)                       // fire immediately
],
#Invoking Transitions [
    (Target(#Cooldown) AlwaysEdge Delay::from_secs_f32(0.3))
],
#Cooldown Transitions [
    (Target(#Ready) AlwaysEdge Delay::from_secs_f32(0.8))  // cooldown, then loop
],
```

A message with no matching edge on any active state is simply dropped, so a
`Fire` edge on `Ready` alone gates firing during the other two states with no
"is on cooldown" check anywhere. With the `gauge` feature, a `Delay` can alias
a gauge attribute so cooldowns respond to live stat changes.
[`examples/sub_charts.rs`](examples/sub_charts.rs) chains sub-charts with
`TerminalState` and `Done`, including a parallel gather step that finishes only
when both regions do.

### Side effects with payloads

`EnterState` / `ExitState` tell you a state changed, but not *why*. When a
transition should carry data - apply damage, spend a resource - read the
`Matched<M>` message. Gearbox writes one whenever a `MessageEdge<M>` matches,
carrying the original message plus the transition context (`source`, `target`,
`edge`, `machine`).

Run the reader in `GearboxPhase::SideEffectPhase`, and skip transitions that a
blocker vetoed by checking `BlockedEdges`:

```rust
fn apply_damage(
    mut reader: MessageReader<Matched<Attacked>>,
    blocked: Res<BlockedEdges>,
    mut q_hp: Query<&mut Hitpoints>,
) {
    for m in reader.read() {
        if blocked.is_blocked(m.edge) {
            continue; // a blocker rejected this transition
        }
        if let Ok(mut hp) = q_hp.get_mut(m.machine) {
            hp.current -= m.message.amount;
        }
    }
}

app.add_systems(
    GearboxSchedule,
    apply_damage.in_set(GearboxPhase::SideEffectPhase),
);
```

To make a state *accept* `Attacked` without leaving its current substate, give
it an **internal self-loop**. An internal edge (`EdgeKind::Internal`) keeps the
source and its active children intact rather than exiting and re-entering:

```rust
#Alive InitialState(#Standing) Transitions [
    (Target(#Alive) MessageEdge::<Attacked> EdgeKind::Internal)
] Substates [ /* Standing, Jumping ... */ ]
```

Sending `Attacked { target: character, amount }` is safe when the character is
`Dead`: `Dead` has no `Attacked` edge, so no `Matched<Attacked>` is produced and
no damage is applied. Edges are **external** by default; mark them
`EdgeKind::Internal` only when you want to stay within the source state.
[`examples/internal_transitions.rs`](examples/internal_transitions.rs) puts the
two side by side: an internal `Coin` self-loop that keeps the level, and an
external `Restart` that re-enters and resets it.

### Guards: ordered candidates, first passing guard wins

A conditional transition is several edges for the same trigger, listed in
priority order, each carrying whatever guard it needs, the same shape as an
XState transition array. Every matching edge along the active leaf's ancestor
chain is proposed as a candidate (deeper state first, then `Transitions`
order); guards veto candidates; the first survivor is applied. A guardless
edge last in the list is the fallback.

A guard is a marker component on the edge plus a system in
`GearboxPhase::BlockerPhase` that sets `blocked = true` on the candidates it
rejects. The order of the `Transitions [ .. ]` list is the priority order:

```rust
#[derive(Component, Default, Clone)]
struct HpIsZero;

#Alive Transitions [
    (Target(#Dead)  MessageEdge::<Attacked> HpIsZero),  // taken only if the guard passes
    (Target(#Hurt)  MessageEdge::<Attacked>),           // otherwise
]

fn hp_is_zero_guard(
    mut candidates: MessageMutator<TransitionMessage>,
    q_guard: Query<(), With<HpIsZero>>,
    q_hp: Query<&Hitpoints>,
) {
    for c in candidates.read() {
        let Some(edge) = c.edge else { continue };
        if q_guard.contains(edge) && q_hp.get(c.machine).is_ok_and(|hp| hp.current > 0.0) {
            c.blocked = true;
        }
    }
}

app.add_systems(GearboxSchedule, hp_is_zero_guard.in_set(GearboxPhase::BlockerPhase));
```

Two guards are built in. `InState(#Other)` vetoes the edge unless that state
is active, and `NotInState(#Other)` unless it is inactive (XState's `stateIn`).
They are how one parallel region reads another, with no Rust:

```rust
#Holstered Transitions [
    (Target(#Drawn) MessageEdge::<Draw> InState(#Standing)),  // only while standing
],
```

The same rule applies to `AlwaysEdge` lists and to delayed edges: two
always-edges on one state form an XState `always: [ .. ]` list, and two edges
with the same `Delay` form an `after: { ms: [ .. ] }` list. If every candidate
on the leaf is vetoed, the parent's edges are tried, as in SCXML. Side-effect
systems see a `Matched<M>` for every candidate and skip the ones in
`BlockedEdges`, so only the winner's payload is applied.
[`examples/guarded_transitions.rs`](examples/guarded_transitions.rs) is a
playable version: light and heavy hits pick between `Hurt`, `Staggered` and
`Dead` through two guards and a fallback;
[`examples/parallel_regions.rs`](examples/parallel_regions.rs) uses `InState` to
let the weapon region depend on the posture region.

### History and Bevy `States`

Two more things a chart can do, each with its own example:

- **History.** A state with `History::Deep` (or `Shallow`) remembers its active
  substates when exited and restores them when re-entered; a `ResetEdge` on
  the way back in forgets them. [`examples/history.rs`]examples/history.rs
  pauses and resumes a level select.
- **Bevy `States`.** Mark a `States` enum `#[state_bridge]`, derive `Component`
  on it, and put its values on the chart's state entities: entering one sets
  `NextState`, so `OnEnter`, `in_state` and `DespawnOnExit` work unchanged.
  [`examples/state_bridge.rs`]examples/state_bridge.rs drives a menu, loading
  and playing screen that way.

---

Every example is a real windowed app, one question each; the README lists
them. Run one with `--features server` and the gearbox editor can connect to it.