Composable Alternatives to Bevy's RunCriteria, States, FixedTimestep

This crate offers alternatives to the Run Criteria, States, and FixedTimestep scheduling features currently offered by the Bevy game engine.

The ones provided by this crate do not use "looping stages", and can therefore be combined/composed together elegantly, solving some of the most annoying usability limitations of the respective APIs in Bevy.

Version Compatibility Table:

|Bevy Version|Crate Version| |------------|-------------| |0.6 |0.1, 0.2 |

How does this relate to the Bevy Stageless RFC?

This crate draws very heavy inspiration from the "Stageless RFC" proposal for Bevy.

Big thanks to all the authors that have worked on that RFC and the designs described there.

I am making this crate, because I believe the APIs currently in Bevy are sorely in need of a usability improvement.

I figured out a way to implement the ideas from the Stageless RFC in a way that works within the existing framework of current Bevy, without requiring the complete scheduling API overhaul that the RFC proposes.

This way we can have something usable now, while the remaining Stageless work is still in progress.

Dependencies

The "run conditions" functionality is always enabled, and depends only on bevy_ecs.

The "fixed timestep" functionality is optional ("fixedtimestep" cargo feature, enabled by default) and adds a dependency on bevy_core (needed for Res<Time>).

The "states" functionality is optional ("states" cargo feature, enabled by default) and adds a dependency on bevy_utils (to use Bevy's preferred HashMap implementation).

Run Conditions

This crate provides an alternative to Bevy Run Criteria, called "Run Conditions".

The different name was chosen to avoid naming conflicts and confusion with the APIs in Bevy. Bevy Run Criteria are pretty deeply integrated into Bevy's scheduling model, and this crate does not touch/replace them. They are technically still there and usable.

How Run Conditions Work?

You can convert any Bevy system into a "conditional system". This allows you to add any number of "conditions" on it, by repeatedly calling the .run_if builder method.

Each condition is just a Bevy system that outputs (returns) a bool.

The conditional system will present itself to Bevy as a single big system (similar to Bevy's system chaining), combining the system it was created from with all the condition systems attached.

When it runs, it will run each condition, and abort if any of them returns false. The main system will run only if all the conditions return true.

(see examples/conditions.rs for a more complete example)

```rust use bevy::prelude::; use iyes_loopless::prelude::;

fn main() { App::new() .addplugins(DefaultPlugins) .addsystem( notifyserver .runif(inmultiplayer) .runif(on_mytimer) ) .run(); }

/// Condition checking our timer fn onmytimer(mytimer: Res) -> bool { mytimer.timer.justfinished() }

/// Condition checking if we are connected to multiplayer server fn inmultiplayer(gamemode: Res, connected: Res) -> bool { *gamemode == GameMode::Multiplayer && connected.isactive() }

/// Some system that should only run on a timer in multiplayer fn notify_server(/* ... */) { // ... } ```

It is highly recommended that all your condition systems only access data immutably. Avoid mutable access or locals in condition systems, unless are really sure about what you are doing. If you add the same condition to many systems, it will run with each one.

There are also some helper methods for easily adding common kinds of Run Conditions: - .run_if_not: invert the output of the condition - .run_on_event::<T>(): run if there are events of a given type - .run_if_resource_exists::<T>(): run if a resource of a given type exists - .run_unless_resource_exists::<T>(): run if a resource of a given type does not exist - .run_if_resource_equals(value): run if the value of a resource equals the one provided - .run_unless_resource_equals(value): run if the value of a resource does not equal the one provided

And if you are using States: - .run_in_state(state) - .run_not_in_state(state)

If you need to use classic Bevy States, you can use these adapters to check them with run conditions: - .run_in_bevy_state(state) - .run_not_in_bevy_state(state)

Fixed Timestep

This crate offers a fixed timestep implementation that uses the Bevy Stage API. You can add a FixedTimestepStage to your App, wherever you would like it to run. Typically, a good place would be before CoreStage::Update.

It is a container for multiple child stages. You might want to add multiple child SystemStages, if you'd like to use Commands in your systems and have them applied between each child stage. Or you can just use one if you don't care. :)

Every frame, the FixedTimestepStage will accumulate the time delta. When it goes over the set timestep value, it will run all the child stages. It will repeat the sequence of child stages multiple times if needed, if more than one timestep has accumulated.

(see examples/fixedtimestep.rs for a complete working example)

```rust use bevy::prelude::; use iyes_loopless::prelude::;

fn main() { // prepare our stages for fixed timestep // (creating variables to prevent code indentation // from drifting too far to the right)

// can create multiple, to use Commands
let mut fixed_first = SystemStage::parallel();
// ... add systems to it ...

let mut fixed_second = SystemStage::parallel();
// ... add systems to it ...

App::new()
    .add_plugins(DefaultPlugins)
    // add the fixed timestep stage:
    .add_stage_before(
        CoreStage::Update,
        "my_fixed_update",
        FixedTimestepStage::new(Duration::from_millis(250))
            .with_stage(fixed_first)
            .with_stage(fixed_second)
    )
    // add normal bevy systems:
    .add_startup_system(setup)
    .add_system(do_thing)
    .run();

} ```

Since this implementation does not use Run Criteria, you are free to use Run Criteria for other purposes. Or better yet: don't, and use the Run Conditions from this crate instead! ;)

Fixed Timestep Info

From within your fixed timestep systems, you can use Res<FixedTimestepInfo> to get info about the current fixed timestep parameters, like the timestep duration and amount of over-step.

This resource is managed by the FixedTimestepStage. It will be inserted before your systems get run, and removed afterwards.

rust fn my_fixed_update(info: Res<FixedTimestepInfo>) { println!("Fixed timestep duration: {:?} ({} Hz).", info.timestep(), info.rate()); println!("Overstepped by {:?} ({}%).", info.remaining(), info.overstep() * 100.0); }

States

(see examples/menu.rs for a complete example)

This crate offers a states abstraction that works as follows:

You create one (or more) state types, usually enums, just like when using Bevy States.

However, here we track states using two resource types: - CurrentState(T): the current state you are in - NextState(T): use this whenever you want to change state

Registering the state type, how to add enter/exit systems

To "drive" the states, you add a StateTransitionStage to your App. This is a Stage that will take care of performing state transitions and managing CurrentState<T>. It being a separate stage allows you to control at what point in your App the transitions happen.

You can add child stages to it, to run when entering or exiting different states. This is how you specify your "on enter/exit" systems. They are optional, you don't need them for every state value.

When the transition stage runs, it will use change detection to check if the NextState resource has been changed. If yes, a transition will be performed: - run the "exit stage" (if any) for the current state - change the value of CurrentState - run the "enter stage" (if any) for the next state

The StateTransitionStage will ensure that both the CurrentState and NextState resources exist. It will insert them on first run, and re-insert them later if they are missing for whatever reason.

Triggering a Transition

If you want to perform a state transition, simply change NextState<T>. You can either mutate the value directly, or re-insert a new resource (using Commands or direct world access). Either will work.

If you mutate CurrentState<T> directly, you will effectively bypass the transition mechanism. You will change the "active state" without running the exit/enter systems (you probably don't want to do this). Normally, you shouldn't do this. Let the transition stage manage its value.

Multiple state transitions can be performed in a single frame, if you change NextState<T> from within an exit/enter system.

Update systems

For the systems that you want to run every frame, we provide a .run_in_state(state) and .run_not_in_state(state) run conditions.

You can add systems anywhere, to any stage (incl. behind fixed timestep), and make them conditional on one or more states, using those helper methods.

```rust use bevy::prelude::; use iyes_loopless::prelude::;

[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]

enum GameState { MainMenu, InGame, }

fn main() { // prepare any stages for our transitions:

let mut enter_menu = SystemStage::parallel();
enter_menu.add_system(setup_menu);
// ...

let mut exit_menu = SystemStage::parallel();
exit_menu.add_system(despawn_menu);
// ...

let mut enter_game = SystemStage::parallel();
enter_game.add_system(setup_game);
// ...

// stage for anything we want to do on a fixed timestep
let mut fixedupdate = SystemStage::parallel();
fixedupdate.add_system(
    fixed_thing
        // only do it in-game
        .run_in_state(GameState::InGame)
);

App::new()
    .add_plugins(DefaultPlugins)
    // Add the "driver" stage that will perform state transitions
    // After `CoreStage::PreUpdate` is a good place to put it, so that
    // all the states are settled before we run any of our own systems.
    .add_stage_after(
        CoreStage::PreUpdate,
        "TransitionStage",
        StateTransitionStage::new(GameState::MainMenu)
            .with_enter_stage(GameState::MainMenu, enter_menu)
            .with_exit_stage(GameState::MainMenu, exit_menu)
            .with_enter_stage(GameState::InGame, enter_game)
    )
    // If we had more state types, we would add transition stages for them too...

    // Add a FixedTimestep, cuz we can!
    .add_stage_before(
        CoreStage::Update,
        "FixedUpdate",
        FixedTimestepStage::from_stage(Duration::from_millis(125), fixedupdate)
    )

    // Add our various systems
    .add_system(menu_stuff.run_in_state(GameState::MainMenu))
    .add_system(animate.run_in_state(GameState::InGame))

    .run();

}

```

State transitions under fixed timestep

If you have a state type that you are using for controlling fixed timestep stuff, you might want state transitions to happen only on fixed timestep (not just on any frame).

To accomplish that, you can add the StateTransitionStage as a child stage at the beginning of your FixedTimestepStage.

The stage types from this crate are composable like that! :) They accept any stage type.