Bevy Entities
At a high level, an Entity
exclusively owns zero or more Component
instances.
Each entity can only have a single component of each type. These types can be added or removed dynamically over the course of the entity's lifetime.
An entity is an identifier that represents the index of its related components.
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, Eq, Ord, PartialEq, PartialOrd)]
pub struct Entity {
generation: u32,
index: u32,
}
The type itself is a simple holder of both a index
and a generation
property.
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.
Entities are local to the world
Each World
keeps a list of Entities
which contains 3 sets of entity IDs:
freelist
: IDs, previously freedreserved
: list of IDs that were once in the freelist, but got reservedpending
: count of new IDs which do not exist yet
The generational index ensures that two entities that are existing at the same time will never share the same index.
Generations are incremented each time an entity with a given index is despawned. This serves as a "count" of the number of times a given index has been reused.
These unique identifiers enable Bevy to allocate them in a lazy way. It first reserves an ID and then can allocate it later.
This test illustrates the concept:
#[cfg(test)]
mod tests {
#[derive(Component)]
struct Health(i32);
#[test]
fn reserve_and_spawn() {
let mut world = World::default();
// We reserve an entity id
let id = world.entities().reserve_entity();
// Then we lazily spawn the entity using
// the reserved id
world.flush();
let mut entity = world.entity_mut(id);
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 for 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 information about the Table
the entity's components are stored in.
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, ThinColumn>,
entities: Vec<Entity>,
}
The ImmutableSparseSet
can be understood as a simple HashMap
, and a Column
is a type-erased Vec<T: Component>
.
To get a row out of our table we use the Entity
as an index on 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 an entity with an ID of 2
which would be:
Health(50)
Player
This is a simplified illustration. Bevy will actually create a type TableRow
representing this ID, and we can use the Entity
identifier to get the TableRow
.
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 component has an ArchetypeId
based on the combination of components their 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 Archetype
s and Table
s 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 in parallel with system_a
, even though the two use a mutable reference to the same component type.
Even though both components share the same ComponentId
, they actually have different ArchetypeComponentId
s which lets the scheduler identify these disjoint queries.