Tainted\\Coders

Bevy Apps

Bevy version: 0.14Last updated:

An App is what controls the main loop of your game so that our systems can update our world.

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 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.

Advancing the schedule with your run function will call your systems which will manipulate the data inside our World.

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 run function.

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

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

The DefaultPlugins adds the core plugins that allow your game to render on a window provided by your operating system. 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 individual features called plugins.

Lets say one feature might be Physics. Bevy allows us to write a PhysicsPlugin that adds physics to our game.

These plugins are useful for bundling up all the setup and runtime behavior of our feature into something we can toggle on and off.

When you want to use a library like bevy_rapier you do so by adding it as a plugin.

To add plugins we simply call App::add_plugins and pass in something that implements the Plugin trait:

fn main() {
  App::new()
    .add_plugins(GamePlugin)
    .add_plugins(PhysicsPlugin)
    .add_plugins(CameraPlugin)
    .run()
}

We create a plugin by implementing Plugin on a struct and defining a method build which should mutate the App passed to it.

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(Camera2dBundle::default());
}

But we can also simplify our plugins and just define a function and use that directly:

pub fn camera_plugin(app: &mut App) {
  app.add_systems(Startup, initialize_camera);
}

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

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

The ideal for a plugin would be that it enables a feature and can be easily toggled on and off without affecting the rest of your game in unexpected ways.

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.

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

We can configure our plugins by providing options on 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
    }
  }
}

There are also PluginGroup types which allow us to group related plugins together and then configure them later, which 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.

All your schedules are stored in a Schedules resource that maps ScheduleLabels to their corresponding Schedule.

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.

There are many other ScheduleLabel 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

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_seconds_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.

Before 0.14 the next state was a struct containing an Option<S> but has since been changed so that 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 if 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.

But they can also be used to separate the logic of your game into isolated units.

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. 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 behaviour 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();
}