Tainted\\Coders

Bevy Worlds

Bevy version: 0.19Last updated:

A World is what holds the state of our App. It contains all the entities, components, and resources, along with the schedules, messages, observers, and registered systems that make up our game.

A good mental model to have is that a World is an in-memory database for all our data. It stores components in archetypes (one per unique combination of components) and keeps an index so that we can go from an Entity to the archetype holding its components.

Most of Bevy's APIs like queries and commands eventually mutate this storage somewhere underneath.

In fact, when you are modifying the App in your main function, you are really modifying the underlying game World:

fn main() {
  App::new()
    .add_plugins(DefaultPlugins)
    // This schedules `hello_world` to run every frame.
    .add_systems(Update, hello_world)
    .run();
}

Doing so directly on the underlying World would look like:

fn build_world_manually() {
  let mut world = World::new();

  // Register our system with the world and then run it.
  let system_id = world.register_system(hello_world);
  world.run_system(system_id).unwrap();
}

App adds a few things on top of a World: a runner that drives the game loop, plugin registration, and one or more sub-apps (each with its own World). We cover those in the apps guide. The data our systems operate on still lives in the World.

The reason why the World is usually hidden from you is because Bevy is trying to run all our systems in parallel. If we had each system manipulating the world directly then none of them could run at the same time.

This is why when you execute commands, they are not applied immediately. Instead they are queued and applied at a sync point by Bevy's ApplyDeferred system, which takes exclusive mutable access to the World so each command can mutate our game's state independently all at once.

Creating a world

Normally we have a World created for us when we App::new(), but there are many circumstances where we might want to create a new one.

For example, during testing its common to create a fresh world that you set up to test a specific plugin or function:

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn test_world() {
    let mut world = World::new();

    // Set up and test the world
  }
}

Every World is assigned a unique WorldId when it is created. Bevy uses this id to tie a system's cached state like its queries, SystemState, and other SystemParams to the exact World it was initialized for. If you try to run that system against a different World it will panic, which prevents unsound access instead of silently reading the wrong data.

Another example is when we save or load scenes. Saving a scene means extracting data out of an existing World into a DynamicWorld, which can then be serialized to a scene file:

/// Extract data out of the world so it can be serialized to a scene file.
fn save_scene_system(world: &mut World) {
  // A `DynamicWorld` holds a snapshot of our entities and resources.
  let dynamic_world = DynamicWorld::from_world(world);

  // The type registry tells Bevy how to represent each reflected component
  // and resource, so we need it to serialize the scene.
  let type_registry = world.resource::<AppTypeRegistry>().read();
  let serialized_world = dynamic_world.serialize(&type_registry).unwrap();

  info!("{serialized_world}");
}

Loading does the reverse: the scene file is deserialized back into a DynamicWorld, which is then written into a World.