Bevy Apps
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
:
World
which holds your data (entities, components)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 ScheduleLabel
s 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:
PreStartup
Startup
PostStartup
First
PreUpdate
StateTransition
RunFixedUpdateLoop
which runsFixedUpdate
conditionallyUpdate
PostUpdate
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>
:
- Bevy will add a resource for both
State<S>
andNextState<S>
to your app. - 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:
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 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();
}