Tainted\\Coders

Bevy Apps

Bevy version: 0.15Last updated:

An App is what runs the main loop of our game.

Every loop, Bevy will execute our systems which will update our world, which is what actually holds the state of everything.

There are two core parts of every App:

  1. World which holds your data (entities, components)
  2. Schedule which holds your logic (systems)

The Schedule is what lets Bevy order our systems into groups to run them at a certain point in our game.

We add systems with a specific ScheduleLabel and that determines where in the schedule they run. The two most common you will see are the Update and Startup schedules.

The Schedule is advanced by a run function that you can override to fine tune the behavior of your game loop.

Different games need different kind of loops. Civilization will advance its schedule differently than Counter Strike. Our run function can be tuned to meet the demands of our specific game.

The App is the public interface to configure all this. You will define one for every game you make.

Defining an app

We define our app in main.rs which will be executed when we run our binary after compiling.

Calling App::run will start your loop and begin advancing your schedule using the default run function.

fn main() {
  App::new()
    .add_systems(Update, hello_world_system)
    .run();
}

fn hello_world_system() {
  println!("hello world");
}

The default run function simply runs once and exits. However, most games will want to add the DefaultPlugins plugin, which will change the run function to an infinite loop.

fn main() {
  App::new()
    .add_plugins(DefaultPlugins)
    .add_systems(Update, hello_world_system)
    .run();
}

The DefaultPlugins is a collection of core plugins that allow your game to render on a window provided by your operating system and other necessities of your game.

Unless you are trying to run in a headless mode (without any graphics), or have a good reason to, we always include this in our App definitions.

Plugins

Bevy uses an architecture that lets you organize your game into encapsulated features called plugins.

Plugins can do a lot, but conceptually they are quite simple. They can be boiled down to a function that modifies an App it gets passed.

fn plugin(app: &mut App) {
  app.add_system(some_plugin_system)
}

fn main() {
  App::new().add_plugins(plugin)
}

These plugins are useful for bundling up all the setup and runtime behavior of our feature into something we can toggle on and off. It encapsulates the complexity of the feature into something we can "plug in" to our app.

Most libraries such as bevy_rapier will be plugins.

For more advanced plugins we can construct our own by implementing the Plugin trait on a struct:

pub struct CameraPlugin;

impl Plugin for CameraPlugin {
  fn build(&self, app: &mut App) {
    app.add_systems(Startup, initialize_camera);
  }
}

fn initialize_camera(mut commands: Commands) {
  commands.spawn(Camera2d);
}

By performing the necessary setup such as adding systems, resources and events to your game, each plugin is responsible for injecting its behavior into your game loop.

An ideal plugin would fully encapsulate its dependencies, but that's not always possible. It can be challenging to draw the line between which logic should go into which plugin.

Usually I find it convenient to keep the main.rs file quite clean and move core logic out to a plugin like GamePlugin which can further call other plugins needed to run core parts of your game.

Plugin configuration

If your plugin becomes complicated enough to demand configuration you can add fields to the struct that implements Plugin.

pub struct CameraPlugin {
  debug: bool,
}

impl Plugin for CameraPlugin {
  fn build(&self, app: &mut App) {
    app.add_systems(Startup, initialize_camera);

    if self.debug {
      // Do something
    }
  }
}

fn main() {
  App::new()
    .add_plugins(CameraPlugin { debug: true })
}

There is also the PluginGroup trait which allow us to group related plugins together and then configure them later. This can be great for writing a plugin that others can add to their game.

pub struct GamePlugins;

impl PluginGroup for GamePlugins {
  fn build(self) -> PluginGroupBuilder {
    PluginGroupBuilder::start::<Self>()
      .add(CameraPlugin::default())
      .add(PhysicsPlugin::default())
      .add(LogicPlugin)
  }
}

This will let us (or anyone consuming your plugins) configure exactly how the set of plugins runs in the context of our app:

fn main() {
  App::new()
    .add_plugins(DefaultPlugins)
    .add_plugins(
      game::GamePlugins
        .build()
        .disable::<physics::PhysicsPlugin>()
    )
    .run();
}

Running apps

When we run an App we are calling its app runner function.

Depending on the type of runner function, the call usually never returns, running our game in an infinite loop.

We can customize the default runner function by configuring it when we build our App:

// This app wil run once
fn main() {
  App::new()
    .add_plugins(DefaultPlugins.set(ScheduleRunnerPlugin::run_once()))
    .add_plugins(game::GamePlugins.build())
    .run();
}
// This app wil run 60 times per second
fn main() {
  App::new()
    .add_plugins(
      DefaultPlugins.set(
        ScheduleRunnerPlugin::run_loop(
          Duration::from_secs_f64(1.0 / 60.0)
        )
      )
    )
    .add_plugins(game::GamePlugins.build())
    .run();
}

We can even provide our own custom runner function if the default doesn't suit our game loop:

#[derive(Resource)]
struct Input(String);

fn my_runner(mut app: App) -> AppExit {
  println!("Type stuff into the console");
  for line in std::io::stdin().lines() {
    {
      let mut input = app.world_mut().resource_mut::<Input>();
      input.0 = line.unwrap();
    }
    app.update();
  }

  AppExit::Success
}

fn main() {
  App::new().set_runner(my_runner).run();
}

As of 0.14 a runner must return AppExit::Success.

Schedules

A schedule is a collection metadata, systems, and the executor responsible for running them. Your App will call Schedule::run on each schedule you have added. This will pass your world in to be mutated by your systems.

Bevy has 3 main schedules it runs:

  1. Main holds all of our game logic
  2. Extract move data from the main world to the render world
  3. Render renders everything

We really only care about Main unless you are keen on doing some kind of advanced graphics processing.

So when you define an app and add your systems like:

app.add_systems(Update, hello_world)

The Update is a ScheduleLabel that will map to the Schedule which actually executes the hello_world system.

You will mostly be adding your logic to the three main schedule labels:

  1. Update runs once every loop
  2. FixedUpdate runs once every fixed timestep loop
  3. Startup runs once at startup

However, there are many other ScheduleLabel in the Main schedule we can tap into:

  1. PreStartup
  2. Startup
  3. PostStartup
  4. First
  5. PreUpdate
  6. StateTransition
  7. RunFixedUpdateLoop which runs FixedUpdate conditionally
  8. Update
  9. PostUpdate
  10. Last

For example, if you are authoring a plugin you might want to hook into the PreUpdate or PostUpdate schedules to fit into users games without worrying about ordering within a schedule.

On your first run your startup schedules will fire:

`PreStartup` ->
`Startup` ->
`PostStartup` ->

Then your normal game loop begins:

v-----------------------<
`First` ->              |
`PreUpdate` ->          |
`StateTransition` ->    |
`RunFixedUpdateLoop` -> |
`Update` ->             |
`PostUpdate` ->         |
`Last` -----------------^

Interestingly, RunFixedUpdateLoop is implemented to be independent of the number of times your loop runs. Instead, it will run the schedule FixedUpdate only when a certain amount of time has passed.

Here is a sketch of what this looks like internally in Bevy:

#[derive(Resource)]
struct FixedTimestepState {
  accumulator: f64,
  step: f64,
}

// An exclusive system that runs our FixedUpdate schedule manually
fn fixed_timestep_system(world: &mut World) {
  world.resource_scope(|world, mut state: Mut<FixedTimestepState>| {
    let time = world.resource::<Time>();
    state.accumulator += time.delta_secs_f64();

    while state.accumulator >= state.step {
      world.run_schedule(FixedUpdate);
      state.accumulator -= state.step;
    }
  });
}

fn main() {
  App::new()
    .add_systems(Update, fixed_timestep_system)
    .run();
}

This means that if we are running game tests, the behavior for your systems added to the schedule FixedUpdate won't run until the correct amount of time has passed.

#[cfg(test)]
mod tests {
  fn hello_world() {
    println!("Hello World!");
  }

  fn test_fixed_game_loop() {
    let app = App::new();
    app.add_systems(FixedUpdate, hello_world);

    app.update(); // This won't actually run the system
  }
}

There are no easy solutions here so I've been changing my fixed system unit tests to add to the Update schedule instead to be more controllable.

App states

Your App is always in a certain AppState. This state determines which Schedule your app runs.

So your App is acting like a finite state machine and your logic triggers moving from one state to another.

Creating app states

States in Bevy are any enum or struct that implements the States trait.

#[derive(Debug, Clone, Eq, PartialEq, Hash, Default, States)]
enum AppState {
  #[default]
  MainMenu,
  InGame,
  Paused,
}

fn spawn_menu() {
  // Spawn a menu
}

fn play_game() {
  // Play the game
}

fn main() {
  App::new()
    // Add our state to our app definition
    .init_state::<AppState>()
    // We can add systems to trigger during transitions
    .add_systems(OnEnter(AppState::MainMenu), spawn_menu)
    // Or we can use run conditions
    .add_systems(Update, play_game.run_if(in_state(AppState::InGame)))
    .run();
}

When you call App::init_state<S>:

  1. Bevy will add a resource for both State<S> and NextState<S> to your app.
  2. It will also add systems for handling transitioning between states.

Your NextState<S> is an enum that can be in one of two states:

  1. NextState::Pending(s): The next state has been triggered and will transition
  2. NextState::Unchanged: The next state has not been triggered

We transition from one state to another by calling NextState::set(S) in any of our systems.

One of those systems that got added when we called App::init_state was apply_state_transition<S> which is scheduled to run in the PreUpdate stage of your app.

This system triggers the OnExit(PreviousState) and OnEnter(YourState) schedules once before finally transitioning to our next state. This is assuming it has been set and is currently NextState::Pending(S).

We cannot transition back to the same state we are on. So if you accidentally set the NextState to the current state nothing will happen.

If we wanted to create explicit transitions we could implement the logic on our state:

impl AppState {
  fn next(&self) -> Self {
    match *self {
      AppState::MainMenu => AppState::InGame,
      AppState::InGame => AppState::Paused,
      AppState::Paused => AppState::InGame,
    }
  }
}

Changing app states

Changing the state of your app will change the Schedule that runs each tick.

When you transition to a new app state OnExit(State) and OnEnter(State) schedules are run before transitioning to the states Schedule.

We can trigger these changes by using the NextState resource from within our systems:

fn pause_game(
  mut next_state: ResMut<NextState<AppState>>,
  current_state: Res<State<AppState>>,
  input: Res<ButtonInput<KeyCode>>,
) {
  if input.just_pressed(KeyCode::Escape) {
    next_state.set(AppState::MainMenu);
  }
}

Sub-apps

Apps can have a SubApp added to them:

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

let mut app = App::new();
app.insert_sub_app(MySubApp, SubApp::new());

Each SubApp contains its own Schedule and World which are separate from your main App.

Sub apps were created to enable processes to keep state separate from the main application like during pipelined rendering. They will run consecutively after our main app, instead of in parallel.

To understand them a bit more, we can create a somewhat impractical but educational example.

Lets say we were making a game where we had separate chunks of our game we wanted to process completely separately and then sync with the main game world.

First we could define some kind of "chunks" that have a certain state we want to control, like changing their color:

#[derive(Default, Clone, Debug)]
enum ChunkState {
  Red,
  Green,
  #[default]
  Blue,
}

#[derive(Resource, Default, Clone)]
struct Chunk {
  id: u32,
  state: ChunkState,
}

#[derive(Resource)]
struct Chunks(HashMap<u32, Chunk>);

Then we can create a plugin that creates and inserts a sub app on our main app. Remember that this sub app will run consecutively after our main app, not in parallel.

#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, AppLabel)]
pub struct ChunkApp;

fn update_chunks(mut chunks: ResMut<Chunks>) {
  for chunk in chunks.0.values_mut() {
    match chunk.state {
      ChunkState::Red => chunk.state = ChunkState::Green,
      ChunkState::Green => chunk.state = ChunkState::Blue,
      ChunkState::Blue => chunk.state = ChunkState::Red,
    }
  }
}

struct ChunksPlugin;

impl Plugin for ChunksPlugin {
  fn build(&self, app: &mut App) {
    let mut sub_app = SubApp::new();

    // Set up our sub app
    sub_app
      .insert_resource(Chunk::default())
      .add_systems(Update, update_chunks);

    // Set up how we will extract data from our main world -> sub world
    sub_app.set_extract(|main_world, sub_world| {
      let mut chunks = main_world.resource_mut::<Chunks>();
      let chunk = sub_world.resource::<Chunk>();
      chunks.0.insert(chunk.id, chunk.clone());
    });

    // Add our sub app to our main app
    app.insert_sub_app(ChunkApp, sub_app);
  }
}

For a more complete example with more performance concerns you can check out pipelined_rendering.rs in bevy/crates/bevy_render which uses async.

Multithreading

Apps by default will run on multiple threads. The Scheduler is working hard to try and run your systems in parallel when they have disjoint sets of queries.

We can configure this behavior by changing the ThreadPoolOptions of the TaskPoolPlugin:

fn main() {
  App::new()
    .add_plugins(DefaultPlugins.set(TaskPoolPlugin {
      task_pool_options: TaskPoolOptions::with_num_threads(4),
    }))
    .run();
}

Running headless apps

If you want to run your app without spawning a window or using any rendering systems, and with the minimum amount of resources, we can use MinimalPlugins instead of the DefaultPlugins we normally add.

  App::new()
    .add_plugins(MinimalPlugins)
    .add_systems(Update, hello_world_system)
    .run();

This can be useful for writing and running tests that include various plugins from your game but don't need to be displayed on the screen.

If instead you wanted most other systems to run like Bevy's assets, scenes, etc but not render to your screen you could configure the DefaultPlugins to do so:

use bevy::prelude::*;
use bevy::render::{
  settings::{RenderCreation, WgpuSettings},
  RenderPlugin,
};

fn main() {
  App::new()
    .add_plugins(DefaultPlugins.set(RenderPlugin {
      synchronous_pipeline_compilation: true,
      render_creation: RenderCreation::Automatic(WgpuSettings {
        backends: None,
        ..default()
      }),
    }))
    .run();
}