bevy_fsm
Observer-driven finite state machine framework for Bevy ECS.
Bevy Compatibility
| Bevy | bevy_fsm |
|---|---|
| 0.17 | 0.2 |
| 0.16 | 0.1 |
Features
- Enum-based states: Keep your states as simple enum variants
- Observer-driven: React to state changes via Bevy observers
- Variant-specific events: No runtime state checks needed in observers
- Flexible validation: Per-entity and per-type transition rules
- Clean API: FSMPlugin for automatic setup
- Initial state support: Automatic enter events when FSM components are added
- Organized hierarchy: Observers automatically organized in entity hierarchy
Quick Start
use *;
use ;
use EnumEvent;
;
Core Concepts
FSMTransition Trait
Implement this trait to define which state transitions are valid:
EnumEvent and FSMState Derives
bevy_fsm uses two derive macros from bevy_enum_event:
#[derive(EnumEvent)]- Generates variant-specific event types in amodulename::Varianthierarchy#[derive(FSMState)]- Implements FSM-specific trigger methods for Enter/Exit/Transition events
Together they enable:
- Type-safe variant-specific events
- Automatic Enter/Exit event triggering
- Full N×N transition event support
use ;
// Use with Enter/Exit wrappers:
FSMPlugin - Automatic Setup
The easiest way to register an FSM is with FSMPlugin:
use FSMPlugin;
fsm_observer! Macro
Use the fsm_observer! macro to register variant-specific observers with automatic hierarchy organization:
use ;
Manual Observer Registration
If you prefer manual control, you can register observers directly:
use ;
// Handles state transition requests
app.world_mut.add_observer;
// Triggers enter events when FSM is first added to entity
app.world_mut.add_observer;
// Variant-specific observers
app.world_mut.add_observer;
Generic Event Observers
You can also observe generic events if you need runtime state checking:
Advanced Features
Per-Entity Configuration with Priority Model
FSMOverride allows per-entity transition control with a priority-based system: config takes precedence over FSMTransition rules.
Priority Principle: Config Wins, Rules Fill Gaps
- Whitelist: Transitions ON the list are immediately accepted (config wins)
- Whitelist: Transitions NOT on the list check FSMTransition if
with_rules()is used, else denied - Blacklist: Transitions ON the list are immediately denied (config wins)
- Blacklist: Transitions NOT on the list check FSMTransition if
with_rules()is used, else accepted
use FSMOverride;
// Example 1: Force allow specific transition (override FSMTransition)
commands.entity.insert;
// Idling->Flying: ACCEPT (whitelisted, config wins)
// Idling->Walking: DENY (not whitelisted)
// Example 2: Whitelist + fallback to FSMTransition for others
commands.entity.insert;
// Idling->Flying: ACCEPT (whitelisted, config wins)
// Idling->Walking: Check FSMTransition (not whitelisted, rules fill gap)
// Example 3: Force deny specific transition
commands.entity.insert;
// Idling->Running: DENY (blacklisted, config wins)
// Idling->Walking: ACCEPT (not blacklisted)
// Example 4: Blacklist + fallback to FSMTransition for others
commands.entity.insert;
// Idling->Running: DENY (blacklisted, config wins)
// Idling->Walking: Check FSMTransition (not blacklisted, rules fill gap)
FSMOverride Modes
whitelist([...]): Only listed transitions pass immediately. Others denied unlesswith_rules()is used.blacklist([...]): Listed transitions denied immediately. Others allowed unlesswith_rules()is used.allow_all(): All transitions pass (bypass FSMTransition unlesswith_rules()is used).deny_all(): All transitions denied (immutable state).
Using with_rules()
The with_rules() method enables FSMTransition validation for transitions NOT decided by the config:
// Without with_rules: whitelist is sole authority
whitelist
// A->C: ACCEPT (whitelisted)
// A->B: DENY (not whitelisted)
// With with_rules: whitelist wins, FSMTransition fills gaps
whitelist.with_rules
// A->C: ACCEPT (whitelisted, FSMTransition NOT checked)
// A->B: Check FSMTransition (not whitelisted, rules enabled)
Context-Aware Validation
Use world state in transition validation:
Event Types
Each FSM generates several event types. All transition events implement EntityEvent and contain an entity field to identify the target entity:
StateChangeRequest<S>: Request to change an entity's state (containsentityandnextfields)Enter<S>: Generic enter event (containsentityandstatefields)Exit<S>: Generic exit event (containsentityandstatefields)Transition<S, S>: Generic transition event (containsentity,from, andtofields)
The states themselves generate standard events. They are usually unit events without data.
modulename::Variant: Type-safe variant event types (used withEnter<T>andExit<T>wrappers)
In observer functions, access the entity via trigger.event().entity.
How It Works
When a state change is requested:
apply_state_requestobserver validates the transition- Exit events are triggered:
Exit<S>(generic) andExit<modulename::Variant>(type-safe) - Transition event is triggered:
Transition<S, S>withfromandtofields - State component is updated on the entity
- Enter events are triggered:
Enter<S>(generic) andEnter<modulename::Variant>(type-safe)
When an FSM component is first added:
on_fsm_addedobserver detects the new component- Enter events are triggered for the initial state
Best Practices
- Use FSMPlugin for automatic FSM setup (recommended)
- Use fsm_observer! macro for registering observers with automatic hierarchy organization
- Use variant-specific observers for cleaner code without state checks
- Keep transition logic simple in
can_transition - Use context validation (
can_transition_ctx) for world-dependent rules - Derive FSMState and Reflect together for full functionality
- Use snake_case when accessing generated modules (e.g.,
Enter<lifefsm::Dying>) - Import Enter and Exit from
bevy_fsmwhen using variant-specific observers
Migration from Bevy 0.16 to 0.17
-
Observer parameter type: Change
Trigger<Event>toOn<Event>// Old (Bevy 0.16): // New (Bevy 0.17): -
Accessing the target entity: Change
trigger.target()totrigger.event().entity// Old (Bevy 0.16): let entity = trigger.target; // New (Bevy 0.17): let entity = trigger.event.entity; -
Triggering events: Use
trigger()instead oftrigger_targets(), and include the entity in the event struct// Old (Bevy 0.16): commands.trigger_targets; // New (Bevy 0.17): commands.trigger;
Important: Timing of Initial Enter Events
WARNING: When an FSM component is added during entity spawn, the initial Enter event fires in the same frame, before the entity is fully initialized.
What This Means
let entity = commands.spawn.id;
When this spawn occurs:
- FSM component is added
on_fsm_addedobserver fires immediatelyEnter<life_fsm::Alive>event is triggered- Other components may not exist yet!
- Asset handles may not be loaded
Consider using ignore_fsm_addition() if you don't need initial Enter events:
app.add_plugins;
Testing
use ;
Module Structure
bevy_fsm/
├── src/lib.rs # Core traits and observer functions
├── Cargo.toml
└── README.md
bevy_enum_event/ # Separate crate (dependency)
├── src/lib.rs # EnumEvent and FSMState derive macros
├── Cargo.toml
└── README.md
Note: bevy_fsm depends on bevy_enum_event with the fsm feature enabled.
AI Disclaimer
- Refactoring and documentation supported by Claude Code
- Minor editing supported by ChatGPT Codex
- The process and final releases are thoroughly supervised and checked by the author
License
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT License (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.