Crate bevy_fsm

Crate bevy_fsm 

Source
Expand description

Observer-driven finite state machine framework for Bevy ECS.

This crate provides a lightweight framework for modeling enum-based state machines using Bevy’s observer system. State machines are defined as enum components with automatic variant-specific event generation.

§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
  • Minimal API: FSMPlugin for automatic setup
  • Organized hierarchy: Observers automatically organized in entity hierarchy

§Quick Start

use bevy::prelude::*;
use bevy_fsm::{FSMState, FSMTransition, FSMPlugin, StateChangeRequest, Enter, Exit, Transition, fsm_observer};
use bevy_enum_event::EnumEvent;

fn plugin(app: &mut App) {
    // FSMPlugin automatically sets up the observer hierarchy on first use
    app.add_plugins(FSMPlugin::<LifeFSM>::default());

    // Use fsm_observer! macro for variant-specific observers
    // This is functionally identical to a typed global observer but gets automatically parented
    // into a custom FSMObservers/LifeFSM/on_enter_dying hierarchy that keeps observers nicely
    // sorted by their respective FSM.
    fsm_observer!(app, LifeFSM, on_enter_dying);
    fsm_observer!(app, LifeFSM, on_exit_alive);
    fsm_observer!(app, LifeFSM, on_transition_dying_dead);
}

// Zero boilerplate - just derive FSMTransition for "allow all" behavior!
#[derive(Component, EnumEvent, FSMTransition, FSMState, Reflect, Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[reflect(Component)]
enum LifeFSM {
    Alive,
    Dying,
    Dead,
}

// For custom transition rules, skip FSMTransition derive and manually implement:
// impl FSMTransition for LifeFSM {
//     fn can_transition(from: Self, to: Self) -> bool {
//         matches!((from, to),
//             (LifeFSM::Alive, LifeFSM::Dying) |
//             (LifeFSM::Dying, LifeFSM::Dead)) || from == to
//     }
// }

#[derive(Component)]
struct DyingAnimation;

fn on_enter_dying(trigger: Trigger<Enter<life_fsm::Dying>>, mut commands: Commands) {
    let entity = trigger.target();
    commands.entity(entity).insert(DyingAnimation);
}

fn on_exit_alive(trigger: Trigger<Enter<life_fsm::Alive>>) {
    let entity = trigger.target();
    println!("Entity {} was unalived.", entity);
}

fn on_transition_dying_dead(
    trigger: Trigger<Transition<life_fsm::Dying, life_fsm::Dead>>,
    mut commands: Commands
) {
    let entity = trigger.target();
    commands.entity(entity).despawn()
}

§Context-Aware Transitions

For more complex validation logic that requires access to the World (e.g., checking other components), override can_transition_ctx:

use bevy::prelude::*;
use bevy_fsm::{FSMState, FSMTransition};

#[derive(Component)]
struct Energy(f32);

#[derive(Component, Clone, Copy, Debug, Hash, PartialEq, Eq)]
enum ActionFSM {
    Idle,
    Casting,
    Exhausted,
}

impl FSMState for ActionFSM {}

impl FSMTransition for ActionFSM {
    fn can_transition(from: Self, to: Self) -> bool {
        // Basic state-only rules
        matches!((from, to),
            (ActionFSM::Idle, ActionFSM::Casting) |
            (ActionFSM::Casting, ActionFSM::Idle) |
            (ActionFSM::Casting, ActionFSM::Exhausted) |
            (ActionFSM::Exhausted, ActionFSM::Idle)) || from == to
    }

    fn can_transition_ctx(world: &World, entity: Entity, from: Self, to: Self) -> bool {
        // First check basic rules
        if !<Self as FSMTransition>::can_transition(from, to) {
            return false;
        }

        // Additional context-aware validation
        if matches!(to, ActionFSM::Casting) {
            // Only allow casting if entity has enough energy
            if let Some(energy) = world.get::<Energy>(entity) {
                return energy.0 >= 10.0;
            }
            return false;
        }

        true
    }
}

§Observer Hierarchy

The first FSMPlugin added to your app automatically creates a hierarchical organization for all FSM observers:

FSMObservers (root)
├─ LifeFSM
│  ├─ apply_state_request
│  ├─ on_fsm_added
│  ├─ on_dying
│  └─ on_dead
├─ BlockFSM
│  ├─ apply_state_request
│  └─ ...
└─ ...

This hierarchy is created automatically when you add your first FSM plugin, with no additional setup required.

Macros§

fsm_observer
Macro for registering FSM observers sorting them into the per-FSM hierarchy.

Structs§

Enter
Event fired when an entity enters a state.
Exit
Event fired when an entity exits a state.
FSMOverride
Component for optional per-entity state machine configuration.
FSMPlugin
Generic plugin for FSM types that automatically sets up core observers.
StateChangeRequest
Event requesting a state change for an entity.
Transition
Event fired for state transitions.

Enums§

RuleType
Configuration mode for FSM transition validation set in the FSMOverride component.

Traits§

FSMState
Core FSM trait implemented automatically by #[derive(FSMState)].
FSMTransition
Trait for defining transition logic.

Functions§

apply_state_request
Observer that applies state change requests.
attach_observer_to_group
Attaches an observer entity to the hierarchy for the FSM type S.
on_fsm_added
Observer that triggers enter events when an FSM component is first added.

Derive Macros§

EnumEvent
Derive macro that generates Bevy event types from enum variants.
FSMState
Derive macro for generating FSM state infrastructure.
FSMTransition
Derive macro for generating a default FSMTransition implementation.