Schedules

See also: ECS Intro: Your Code, for a general overview of Bevy's scheduling framework.


All systems to be run by Bevy are contained and organized using schedules. A schedule is a collection of systems, with metadata for how they should run, and an associated executor algorithm to run the systems.

A Bevy app has many different schedules for different purposes, to run them in different situations.

Scheduling Systems

If you want a system to be run by Bevy, you need to add it to a schedule via the app builder. Writing a new Rust function and forgetting to add it / register it with Bevy is a common mistake.

Whenever you add a system, you specify what schedule to put it in:

// add something to the Update schedule (runs every frame)
app.add_systems(Update, camera_movement);

// add something to the Startup schedule (runs once at app startup)
app.add_systems(Startup, setup_camera);

Per-System Configuration

You can add metadata to your systems, to affect how they will be run.

This can include:

  • Run Conditions to control if a system should run
  • Ordering Dependencies, if a system should run before/after specific other systems in the same schedule
  • System Sets to group systems together, so common configuration can be applied to all of them

When the schedule runs, the executor algorithm will honor all of this configuration when determining if a system is ready to run. A system is ready when all of the following is true:

  • No other currently-running system is accessing any of the same data mutably (as per the system parameters)
  • All of the systems ordered "before" have finished or have been skipped due to run conditions
  • The system's run conditions all return true

When a system becomes ready, it will be run on an available CPU thread. Systems run in a non-deterministic order by default! A system might run at different times every frame. If you care about its relationship to other systems, add ordering dependencies.

Dynamically Adding/Removing Systems

Bevy's schedules do not (yet?) support adding and removing systems at runtime. You need to configure everything ahead of time.

You should add all systems you might want to run, and then control them using run conditions. That is the mechanism for disabling them if they shouldn't run.

Bevy's App Structure

Bevy has three primary/foundational schedules: Main, Extract, Render. There are also other schedules, which are managed and run within Main.

In a normal Bevy app, the Main+Extract+Render schedules are run repeatedly in a loop. Together, they produce one frame of your game. Every time Main runs, it runs a sequence of other schedules. On its first run, it also first runs a sequence of "startup" schedules.

Most Bevy users only have to deal with the sub-schedules of Main. Extract and Render are only relevant to graphics developers who want to develop new/custom rendering features for the engine. This page is only focused on Main. If you want to learn more about Extract and Render, see this page about Bevy's rendering architecture.

The Main Schedule

Main is where all the application logic runs. It is a sort of meta-schedule, whose job is to run other schedules in a specific order. You should not add any custom systems directly to Main. You should add your systems to the various schedules managed by Main.

Bevy provides the following schedules, to organize all the systems:

The intended places for most user systems (your game logic) are Update, FixedUpdate, Startup, and the state transition schedules.

Update is for your usual game logic that should run every frame. Startup is useful to perform initialization tasks, before the first normal frame update loop. FixedUpdate is if you want to use a fixed timestep.

The other schedules are intended for engine-internal functionality. Splitting them like that ensures that Bevy's internal engine systems will run correctly with respect to your systems, without any configuration on your part. Remember: Bevy's internals are implemented using ordinary systems and ECS, just like your own stuff!

If you are developing plugins to be used by other people, you might be interested in adding functionality to PreUpdate/PostUpdate (or the Fixed equivalents), so it can run alongside other "engine systems". Also consider PreStartup and PostStartup if you have startup systems that should be separated from your users' startup systems.

First and Last exist only for special edge cases, if you really need to ensure something runs before/after everything else, including all the normal "engine-internal" code.

Configuring Schedules

Bevy also offers some features that can be configured at the schedule level.

Single-Threaded Schedules

If you consider multi-threading to not be working well for you, for whatever reason, you can disable it per-schedule.

In a single-threaded schedule, systems will run one at a time, on the main thread. However, the same "readiness" algorithm is still applied and so systems can run in an undefined order. You should still specify ordering dependencies where you need determinism.

// Make FixedUpdate run single-threaded
app.edit_schedule(FixedUpdate, |schedule| {
    schedule.set_executor_kind(ExecutorKind::SingleThreaded);

    // or alternatively: Simple will apply Commands after every system
    schedule.set_executor_kind(ExecutorKind::Simple);
});

Ambiguity Detection

The Ambiguity Detector is an optional Bevy feature that can help you debug issues related to non-determinism.

// Enable ambiguity warnings for the Update schedule
app.edit_schedule(Update, |schedule| {
    schedule.set_build_settings(ScheduleBuildSettings {
        ambiguity_detection: LogLevel::Warn,
        ..default()
    });
});

It will print warnings for any combination of systems where at least one of them accesses some piece of data (resource or component) mutably, but the others don't have explicit ordering dependencies on that system.

Such situations might indicate a bug, because you don't know if the systems that read the data would run before or after the system that mutates the data.

It is up to you to decide if you care about this, on a case-by-case basis.

Deferred Application

Normally, Bevy will automatically manage where Commands and other deferred operations get applied. If systems have ordering dependencies on one another, Bevy will make sure to apply any pending deferred operations from the first system before the second system runs.

If you would like to disable this automatic behavior and manually manage the sync points, you can do that.

app.edit_schedule(Update, |schedule| {
    schedule.set_build_settings(ScheduleBuildSettings {
        auto_insert_apply_deferred: false,
        ..default()
    });
});

Now, to manually create sync points, add special [apply_deferred] systems where you like them:

app.add_systems(
    Update,
    apply_deferred
        .after(MyGameplaySet)
        .before(MyUiSet)
);
app.add_systems(Update, (
    (
        system_a,
        apply_deferred,
        system_b,
    ).chain(),
));

Main Schedule Configuration

The order of schedules to be run by Main every frame is configured in the MainScheduleOrder resource. For advanced use cases, if Bevy's predefined schedules don't work for your needs, you can change it.

Creating a New Custom Schedule

As an example, let's say we want to add an additional schedule, that runs every frame (like Update), but runs before fixed timestep.

First, we need to create a name/label for our new schedule, by creating a Rust type (a struct or enum) and deriving ScheduleLabel + an assortment of required standard Rust traits.

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

Now, we can init the schedule in the app, add it to MainScheduleOrder to make it run every frame where we like it, and add some systems to it!

// Ensure the schedule has been created
// (this is technically optional; Bevy will auto-init
// the schedule the first time it is used)
app.init_schedule(PrepareUpdate);

// Add it to the MainScheduleOrder so it runs every frame
// as part of the Main schedule. We want our PrepareUpdate
// schedule to run after StateTransition.
app.world.resource_mut::<MainScheduleOrder>()
    .insert_after(StateTransition, PrepareUpdate);

// Now we can add some systems to our new schedule!
app.add_systems(PrepareUpdate, (
    my_weird_custom_stuff,
));