5 unstable releases

new 0.3.0 Dec 1, 2024
0.2.1 Jul 17, 2024
0.2.0 Jul 4, 2024
0.2.0-rc.0 Jun 18, 2024
0.1.0 May 23, 2024

#291 in Game dev

Download history 16/week @ 2024-09-14 5/week @ 2024-09-21 9/week @ 2024-09-28 5/week @ 2024-10-05 119/week @ 2024-11-30

120 downloads per month

MIT/Apache

120KB
2K SLoC

Flexible game states

Crates.io Docs License

pyri_state is a bevy_state alternative offering flexible change detection & scheduling.

#[derive(State, Clone, PartialEq, Eq)]
struct Level(usize);

app.add_systems(StateFlush, state!(Level(4 | 7 | 10)).on_enter(save_progress));

Read the documentation or check out the examples folder for more information.

Comparison to bevy_state

State pattern-matching

In pyri_state, state pattern-matching is directly supported:

// Save progress when entering level 4, 7, or 10.
app.add_systems(StateFlush, state!(Level(4 | 7 | 10)).on_enter(save_progress));

There are a few ways to do this using bevy_state:

  1. Add a system for every possible matching state.
for x in [4, 7, 10] {
    app.add_systems(OnEnter(Level(x)), save_progress);
}
  1. Use a custom substate.
app.add_systems(OnEnter(SaveProgressLevel), save_progress);

#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
struct SaveProgressLevel;

impl SubStates for SaveProgressLevel {
    type SourceStates = Level;

    fn should_exist(sources: Level) -> Option<Self> {
        matches!(sources, Level(4 | 7 | 10)).then(Self)
    }
}
  1. Use a custom schedule.
app.add_systems(OnSaveProgress, save_progress);

app.add_systems(
    StateTransition,
    last_transition::<Level>
        .pipe(run_save_progress)
        .in_set(EnterSchedules::<Level>::default()),
);

#[derive(ScheduleLabel, Clone, Eq, PartialEq, Hash, Debug)]
struct OnSaveProgress;

fn run_save_progress(transition: In<Option<StateTransitionEvent<S>>>, world: &mut World) {
    if matches!(transition.0, Some(StateTransitionEvent {
        entered: Some(Level(4 | 7 | 10)),
        ..
    })) {
        let _ = world.try_run_schedule(OnSaveProgress);
    }
}

Note that option 1 is prohibitively expensive when the pattern has too many matches, like Level(x) if x % 2 == 0. Options 2 and 3 add a confusing layer of indirection and boilerplate, hiding the actual pattern-matching in the SubStates implementation or the run_my_schedule exclusive system.

Even worse, option 2 is subtly broken: if you transition from state A to B where both states match the pattern, bevy_state will silently discard the substate's transition because it's a same-state transition.

State refreshing

In pyri_state, state refreshing is supported out-of-the-box:

// Restart game on R press.
app.add_systems(Update, Level::refresh.run_if(input_just_pressed(KeyCode::R)));
// Schedule a system for when any level restarts.
app.add_systems(StateFlush, Level::ANY.on_refresh(|| info!("Restarted level")));
// Refreshing a state will also reuse its exit, trans, and enter hooks.
app.add_systems(StateFlush, Level::ANY.on_exit(tear_down_level));
// You can explicitly check whether the state has changed, if you want.
app.add_systems(StateFlush, Level::ANY.on_enter(load_new_level.run_if(Level::will_change)));

The equivalent in bevy_state requires building your own custom schedules (e.g. OnReExit, OnReTransition, OnReEnter, OnChangeExit, OnChangeTransition, OnChangeEnter, etc.) and hooking them into the state transition internals, as in this example. This is a seriously discouraging amount of boilerplate for something that should be a basic feature.

And more

  • Custom storage: In pyri_state, the next state can be stored in any custom data structure. For example, you can store the next state in a stack to implement a "back button" feature for a menu state as easily as MyMenuState::pop. This is currently impossible in bevy_state, which only supports enum NextState.
  • Direct mutation: In pyri_state, systems can mutate the next state value directly (e.g. level.0 += 1). In bevy_state, you have to clone the current state, mutate it, and set that as the next state. As a consequence, if multiple systems mutate the same state on the same frame, they'll completely overwrite each other, leading to rare, confusing bugs that direct mutation would often circumvent entirely.
  • Local states: In pyri_state, states can be components. This is currently impossible in bevy_state, which only supports global states.

Bevy version compatibility

bevy version pyri_state version
0.15 0.3
0.14 0.2
0.13 0.1

License

This crate is available under either of MIT or Apache-2.0 at your choice.

Dependencies

~10–45MB
~720K SLoC