Bevy stages or the frames lifecycle

Stages in Bevy are the lifecycle order of a frame. Every second your game runs, Bevy will run the stages in precise order as many times as your computer allows the resources to.

The CoreStage enum includes six stages which run in this order: First, Startup, PreUpdate, Update, PostUpdate, Last

Bevy Corestage Stages Order

The default Stage

When adding a system, Bevy will add it to the Update stage which is the default one. Understanding and knowing the order of the stages is mandatory because Bevy runs all the systems of a stage in parallel.

Bevy CoreStage stages run systems in parallel

Meaning that if you add two systems to the same one, the order of execution of each system is not guaranteed:

use bevy::{prelude::*};

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_system(print_hello)
        .add_system(print_world)
        .run();
}

fn print_hello () {
    info!("Hello");
}

fn print_world () {
    info!("world");
}

This code, as an example, will add both print_hello and print_world systems to the same default stage Update, they will be run in parallel so you may sometimes end up with world printed before Hello!

2022-01-19T21:22:00.581523Z  INFO bevy_stages: Hello
2022-01-19T21:22:00.581533Z  INFO bevy_stages: world
2022-01-19T21:22:00.586819Z  INFO bevy_stages: Hello
2022-01-19T21:22:00.586825Z  INFO bevy_stages: world
2022-01-19T21:22:00.592341Z  INFO bevy_stages: world
2022-01-19T21:22:00.592333Z  INFO bevy_stages: Hello
2022-01-19T21:22:00.597934Z  INFO bevy_stages: Hello

Add a system to a precise stage

When a stage runs, all of its systems are run in parallel, but stages themselves are run following the same order, always, with no parallelism. Each stage waits for the previous one to finish before being run, it's like a relay race but with the following runner staying motionless until he gets the relay 🏃.

Knowing this, we now can fix the previous problem by adding the print_world system to a stage that runs later than the default Update one using add_system_to_stage function:

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_system(print_hello)
        .add_system_to_stage(CoreStage::PostUpdate, print_world)
        .run();
}

The Startup stage

There is a stage that you won't be able to add a system using the add_system_to_stage function: the Startup stage. If you try to add a system to it, it will panic with this error message:

thread 'main' panicked at 'Stage 'Startup' does not exist or is not a SystemStage'

This is because the Startup stage is special: remember that I said that all the stages are run on each frame? I lied to keep it simple at the beginning!

The Startup stage is only run once when your app starts.

Bevy CoreStage lifecycle

To add a system to the startup stage, you need to use the add_startup_system function:

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_startup_system(print_hello)
        .add_system(print_world)
        .run();
}

This will render Hello only once, and loop over the other stages so it will spam your console with only world

2022-01-19T21:26:17.789876Z  INFO bevy_stages: Hello
2022-01-19T21:26:17.791784Z  INFO bevy_stages: world
2022-01-19T21:26:17.814870Z  INFO bevy_stages: world
2022-01-19T21:26:17.820475Z  INFO bevy_stages: world
2022-01-19T21:26:17.825652Z  INFO bevy_stages: world
2022-01-19T21:26:17.830933Z  INFO bevy_stages: world

Startup stage is actually composed by 3 stages

Diving into the Startup stage, you will discover that it is actually split into 3 different startup stages, named: PreStartup, Startup and PostStartup.

Bevy startup stage is composed by 3 stages

The StartupStage::Startup stage is the default one when adding startup systems with the add_startup_system function.

The same way you can add system at a precise position of a SystemStage, you can add startup systems at a precise position in the StartupStage lifecycle using add_startup_system_to_stage:

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_startup_system(print_hello)
        .add_startup_system_to_stage(StartupStage::PostStartup, print_world)
        .run();
}

Doing so, the print_hello system will now be called only once when the StartupStage::Startup stage ran because is the default one when using add_startup_system. And then print_hello will be called on StartupStage::PostStartup, which means that your console will always print the Hello and then world in the correct order, and only once!

2022-01-19T22:01:12.230452Z  INFO bevy_stages: Hello
2022-01-19T22:01:12.230819Z  INFO bevy_stages: world

Not enough stages? Create yours!

Sometimes, 5 stages + the Startup one isn't enough and you may need to create a more precise lifecycle for your game to run each frame in a precise order.

To do so, you can create as many stages as you want, using the add_stage function:

fn main() {
    static WORLD_STAGE_LABEL: &str = "WorldStage";

    App::new()
        .add_plugins(DefaultPlugins)
        .add_stage(WORLD_STAGE_LABEL, SystemStage::parallel())

        // Now you can add system to your new custom stage
        .add_system_to_stage(WORLD_STAGE_LABEL, print_hello)
        .run();
}

When adding a stage, it will be added after all the already added stages, which means in our case after the CoreStage::Last stage.

Note that you can define your stage to run systems in parallel as we have done in our example, but you can also make it single-threaded using the SystemStage::single_threaded() function.

⚠️ Having the possibility to add as many stage as you want doesn't mean that you should! Using Bevy's system ordering could solve this race concurrency problem without adding any stage and will run faster for complex systems.

Adding a stage at a precise position of the lifecycle

Because adding a stage at the end of the lifecycle may not be what you need to do, you can add a stage wherever you need in the lifecycle using add_stage_before and add_stage_after functions:

fn main() {
    static WORLD_STAGE_LABEL: &str = "WorldStage";
    static PRE_WORLD_STAGE_LABEL: &str = "PreWorldStage";

    App::new()
        .add_plugins(DefaultPlugins)

        // Adds a stage called WorldStage after the CoreStage::Update stage
        .add_stage_after(CoreStage::Update, WORLD_STAGE_LABEL, SystemStage::parallel())

        // Adds a stage called PreWorldStage **before** the WorldStage stage
        .add_stage_before(WORLD_STAGE_LABEL, PRE_WORLD_STAGE_LABEL, SystemStage::parallel())
        .run();
}

With the previous example, we end up with this frame lifecycle:

Expanding bevy lifecycle with two custom stages

Expanding the Startup lifecycle

In the same way that you can't add a system to the Startup stage using the add_system function, all the previous add_stage functions can only be called on a SystemStage. Meaning again that you won't be able to call them to expand your app's startup lifecycle.

That said, to add a stage to the startup lifecycle, there are the equivalents methods add_startup_stage, add_startup_stage_before and add_startup_stage_after:

fn main() {
    static STARTUP_LAST_STAGE_LABEL: &str = "StartupLast";

    App::new()
        .add_plugins(DefaultPlugins)

        // Adds a startup stage at the end of the Startup lifecycle
        .add_startup_stage(STARTUP_LAST_STAGE_LABEL, StartupStage::parallel())

        // Adds a startup stage called AfterStartupLast after the StartupLast stage
        .add_startup_stage_after(STARTUP_LAST_STAGE_LABEL, "AfterStartupLast" StartupStage::parallel())

        // Adds a startup stage called BeforeStartupLast before the StartupLast stage
        .add_startup_stage_before(STARTUP_LAST_STAGE_LABEL, "BeforeStartupLast" StartupStage::parallel())
        .run();
}

Comments

Be the first to post a comment!

Add a comment

Preview