Bevy Apps
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
:
World
which holds your data (entities, components)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:
Main
holds all of our game logicExtract
move data from the main world to the render worldRender
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:
Update
runs once every loopFixedUpdate
runs once every fixed timestep loopStartup
runs once at startup
However, there are many other ScheduleLabel
in the Main
schedule we can tap into:
PreStartup
Startup
PostStartup
First
PreUpdate
StateTransition
RunFixedUpdateLoop
which runsFixedUpdate
conditionallyUpdate
PostUpdate
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>
:
- Bevy will add a resource for both
State<S>
andNextState<S>
to your app. - It will also add systems for handling transitioning between states.
Your NextState<S>
is an enum that can be in one of two states:
NextState::Pending(s)
: The next state has been triggered and will transitionNextState::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();
}