Bevy Entities
At a high level, an Entity exclusively owns zero or more Component instances. Entities are the objects of our game world.
An entity on its own holds no data or behavior. The actual Entity is just an identifier to find associated components where the real data is stored.
Each entity can only have a single component of each type. These components can be added and removed dynamically over the course of the entity's lifetime.
A good mental model to use is that entities represent a row in an in-memory database, while components are our columns.
Entities are identifiers
The Entity type is a lightweight identifier that's valid only for the world it is sourced from.
// https://github.com/bevyengine/bevy/blob/main/crates/bevy_ecs/src/entity/mod.rs
#[derive(Clone, Copy)]
pub struct Entity {
index: EntityIndex,
generation: EntityGeneration
}
This is a slightly simplified view: the real struct is #[repr(C, align(8))] and orders its fields based on the target endianness so it can be treated as a single u64 for fast hashing and comparison.
The type itself is a simple holder of both an index and a generation. The two form a generational index. This allows fast insertion after data removal in an array while maintaining the contiguous memory layout that makes ECS more performant.
The maximum number of entities allowed at one time is u32::MAX.
Entity allocation and lifecycle
Allocation of an Entity ID is done through an allocator. Bevy's allocator is designed in a way that it does not have to have world access to generate an ID.
So the allocation of an ID is separated from the actual spawning of the entity.
Therefore an entity has a 5 step lifecycle:
- Unallocated - the entity does not have a global ID yet
- Allocated - the allocator handed out an ID but it's not spawned yet
- Spawned - The entity now actually exists in the world
- Despawned - The entity no longer exists in the world
- Freed - the ID is returned to the allocator. Its generation has already been incremented, so old references are now invalid
Entities are spawned through methods like World::spawn (or Commands::spawn from inside a system). Once spawned they can be changed via Commands::entity.
Entities have components
Components live in contiguous arrays grouped by type. To find an entity's components, Bevy looks up the entity's EntityLocation, which records the Table the entity's components are stored in and the TableRow the entity occupies within that table.
That TableRow is then used to index into the array for each component type the entity has. By occupying that row, the component can be said to belong to the entity.
Entities and components together only represent data. Behavior is introduced through your systems and observers.
Spawning entities
First, we can spawn an empty entity:
fn spawn_player(mut commands: Commands) {
commands.spawn_empty();
}
This gives you a new identifier but no components attached. To attach them we can chain an insert onto our previous call:
fn spawn_full_player(mut commands: Commands) {
commands
.spawn_empty()
.insert(Player)
.insert(Ship::Destroyer)
.insert(Position { x: 1, y: 2 });
}
Because this is so common, there is a more convenient spawn method that takes the tuple of components we want to add on our newly spawned entity:
fn spawn_player_with_bundle(mut commands: Commands) {
commands.spawn((Player, Ship::Destroyer, Position { x: 1, y: 2 }));
}
This will both spawn the entity and associate the given components.
We can also use Bevy Scene Notation (BSN) to spawn entities with components:
fn player() -> impl Scene {
bsn! {
Player
Ship::Destroyer
Position { x: 1, y: 2 }
}
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, player.spawn())
.run();
}
Entities are local to the world
Each World keeps a list of Entities which stores the metadata for every entity index:
// https://github.com/bevyengine/bevy/blob/main/crates/bevy_ecs/src/entity/mod.rs
pub struct Entities {
meta: Vec<EntityMeta>,
}
Each EntityMeta tracks the current generation of an EntityIndex, whether it is currently spawned (its location), and when it was last spawned or despawned.
The actual allocation of ids is handled by a separate EntityAllocator. The allocator hands out indices from a free list, which ensures that two entities that are alive at the same time never share the same index. The generation distinguishes a reused index from its previous lives.
Generations are incremented each time an entity with a given index is despawned, just before its ID is freed back to the allocator. This serves as a "count" of the number of times a given index has been reused.
Because EntityGeneration is a u32, this counter will eventually wrap around after enough reuses of the same index. This lets two different entities share the same index and generation, a situation Bevy calls aliasing and logs a warning for. It is best to stop holding onto an Entity once you know it has been despawned.
These unique identifiers enable Bevy to allocate them in a lazy way. It first hands out a fresh ID and then the entity can be spawned at a later time.
This test illustrates the concept:
#[derive(Component, Debug, Clone, Copy)]
struct Health(i32);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn allocate_and_spawn() {
let mut world = World::default();
// The allocator hands out an entity id without touching the world
let id = world.entity_allocator().alloc();
// Later we can spawn the entity using that id
let mut entity = world.spawn_empty_at(id).unwrap();
entity.insert(Health(0));
assert_eq!(entity.get::<Health>().unwrap(), &Health(0));
}
}
In an actual application we don't actually manage any of this ourselves. We use the commands system parameter instead:
fn spawn_health(mut commands: Commands) {
commands.spawn(Health(0));
}
Entities are stored in tables
Entities is a type held by your World and holds the metadata of all the entities in the World. Each piece of metadata contains:
- The generation of every entity.
- The alive/dead status of a particular entity. (i.e. "has entity 3 been despawned?")
- The location of the entity's components in memory (via [
EntityLocation])
The EntityLocation contains the Archetype and Table the entity belongs to, plus its row in each, which is how Bevy finds the entity's components.
Each Table has a Column for each component type it stores:
// https://github.com/bevyengine/bevy/blob/main/crates/bevy_ecs/src/storage/table/mod.rs
pub struct Table {
columns: ImmutableSparseSet<ComponentId, Column>,
entities: Vec<Entity>,
}
The ImmutableSparseSet can be understood as a simple HashMap, and a Column is a type-erased contiguous buffer holding each component value along with its change detection ticks.
To get a row out of our table we look at the entity's EntityLocation. It tells us both which Table the entity lives in and the TableRow it occupies. That row is then used to index into each Column.
So if we had a table with 3 columns:
Health column: [_, 50, _]
Player column: [_, X, _]
Enemy column: [_, _, _]
We could get the components for the entity at row 1 which would be:
Health(50)
Player
This is a simplified illustration. In practice you rarely look up a TableRow yourself; Bevy stores it inside the entity's EntityLocation and queries use it internally to fetch components.
For sorting, Entity is ordered first by its index and then by its generation, so lower indexes come first.
Entities enable structure of arrays
This kind of storage concept is called structure of arrays (SoA) instead of arrays of structures (AoS).
In an AoS program we could imagine a more traditional object oriented game engine like Godot. Our structures hold all of our components. So one object with many properties, each one being a component:
struct Player {
health: u32,
speed: u32,
name: String,
team: u32
}
We could think about our game loop iterating over each player and performing its required logic:
fn movement_system(mut players: Query<&mut Player>) {}
fn attacking_system(mut players: Query<&mut Player>) {}
One problem is that we cannot split these mutable references up anymore. Each system that does anything to player will have to wait its turn to perform. The more we centralize god objects like this the harder the problem gets.
Each query would also require more memory, one system might use only the name, but loads all the rest of its components all the same.
Instead in Bevy we use structure of arrays to do the same thing:
struct Player;
struct Health(u32);
struct Name(String);
struct TeamId(u32);
When we want to create a system that decrements our health under some condition, we do not also need to mutably borrow the other components.
Bevy will work hard to try and schedule your systems to run in parallel if they don't need mutable access to the same data.
Archetypes group components by entities
So which Table does an entity's components go into? That's where the archetype comes in.
Every entity has an ArchetypeId based on the combination of components that entity has.
A world has only one Archetype for each unique combination of components on your entities. Their ArchetypeId is locally unique to a world, not globally unique between worlds.
Archetypes point to a particular table, but multiple archetypes may store their table components in the same table.
Both Archetypes and Tables are created but never cleaned up. They are not removed and persist until the world is dropped.
Archetypes are useful when used by the scheduler:
fn system_a(query: Query<&mut Health, With<Player>>) {}
fn system_b(query: Query<&mut Health, Without<Player>>) {}
system_b will run in parallel with system_a, even though the two use a mutable reference to the same component type.
Even though both queries reference the same component type (and therefore the same ComponentId), each archetype assigns that component its own ArchetypeComponentId, which lets the scheduler identify these disjoint queries.