Components allow us to attach data to our entities.
An Entity
on its own is not very useful. It just represents a globally unique
ID for some thing in our game.
So when we need to add health, or damage, or anything representing a state our entities can be in we do so by adding a component that holds the data.
Bevy uses it’s own ECS to represent lots of its internals as components:
- Resources are components without an entity
- Your game window is a
Window
component. - Your cameras are some entity with a
Camera
component. - Handles are components we attach to entities
Most of the things you will be interested in will involve querying for one component or another through your systems.
Defining a component
We define components as normal structs, but we tell Bevy about them by using the derive macro:
#[derive(Component)]
struct Position {
x: i32,
y: i32
}
A Component
may be a struct but it can also be other data types like an enum
or zero sized type:
// A simple marker type
#[derive(Component)]
struct Player;
// Or an enum
#[derive(Component)]
enum Ship {
Destroyer,
Frigate,
Scout
}
Structs that are defined this way will be able to attach to the entities in our game world.
Components are columns
A good mental model to have is that each of your components are the columns in your database, while the entities are your rows.
Thinking this way makes some important things more obvious:
- Each entity can only have one component of each type
- When you insert a component on an entity that already has one of that type, you will replace the original
So we can translate something like this:
fn spawn_entities(
mut commands: Commands
) {
commands.spawn((
Health(100.0),
Player,
Position { x: 3, y: 2 }
));
commands.spawn((
Health(72.0),
Enemy,
Position { x: 3, y: 1 }
));
}
As creating an imaginary database that looks like this:
Entity ID | Health | Player | Position | Enemy |
---|---|---|---|---|
1 | 100.0 | X | (3, 2) | |
2 | 72.0 | (3, 1) | X |
In reality the components are stored as many separate contiguous arrays. Each
array has components of a single type. The Entity
is actually the index of all
its components in each different array.
This is why we can only have one component of each type. You can only have one component in the slot that matches our entity’s index.
This is all done to boost performance by helping your CPU to more fetching of data from its cache.
Adding components to entities
We add components to entities through our commands:
fn spawn_player(
mut commands: Commands
) {
commands
.spawn_empty()
.insert(Player)
.insert(Ship::Destroyer)
.insert(Position { x: 1, y: 2 });
}
This schedules a series of commands to run that will add a new Entity
and then
insert each component by placing it in the array’s we mentioned in the previous
section.
These arrays can be one of two specialized types:
Table
-> Faster for iterating (the default)SparseSet
-> Faster for adding and removing
To use a SparseSet
we use an additional attribute when we derive our
component:
#[derive(Component)]
#[component(storage = "SparseSet")]
struct Explode;
Something like an Explode
component that is used to trigger behavior and will
be added and removed more than its read would be a great choice for
a SparseSet
over a Table
.
The Entity
is used as an index for components that belong to it.
Bevy would place the component for an entity at the specific index
matching the Entity
.
Later, when we want to get components for our entity we look up that index in
each Table
or SparseSet
storing each different component. This is how our
individual components end up being a part of one thing in our game world.
In reality there is some hidden complexity to make this an even faster process using archetypes.
Bevy uses these archetypes to determine which systems can run in parallel and other performance gains.
The ability to add or remove components from our entity actually comes from the
Bundle
trait. All components implement this trait. When we derive our
components, we have also implemented Bundle
.
Using bundles
A Bundle
can hold a set of other components.
This is useful for when inserting components individually can be repetitive. Or we want to ensure all players are spawned with a certain set of components.
Defining a bundle is just like our components. We use a derive macro:
#[derive(Bundle)]
struct PlayerBundle {
player: Player,
ship: Ship,
position: Position
}
Once derived, instead of inserting our individual components we can insert our bundle instead:
commands
.spawn_empty()
.insert(PlayerBundle{
player: Player,
ship: Ship::Destroyer,
position: Position { x: 1, y: 2 }
});
But its more common to implement our own interfaces for creating our bundles to reduce the boilerplate:
impl PlayerBundle {
pub fn new(x: i32, y: i32) -> Self {
Self {
player: Player,
ship: Ship::Destroyer,
position: Position { x, y }
}
}
}
fn spawn_player_bundle(
mut commands: Commands
) {
commands
.spawn_empty()
.insert(PlayerBundle::new(1, 2));
}
Tuples of bundles also implement Bundle
(for tuples up to 15 bundles). This
means we could have made a bundle of our components without defining a type:
fn spawn_player_tuple(
mut commands: Commands
) {
commands
.spawn_empty()
.insert((
Player,
Ship::Destroyer,
Position { x: 1, y: 2 }
));
}
Each Component
implements Bundle
and a tuple of bundles is also
itself a Bundle
.
And taking it further, our tuples can contain nested tuples of bundles which allows us to nest our bundles to get around the 15 tuple limit.
A tuple of nothing ()
also counts as a bundle, which can be useful for
spawning entities using World::spawn_batch
.
The component derive macro
Using #[derive(...)]
tells our compiler to generate code that will implement
any traits we put within the parenthesis using their default implementation.
Default here means the implementation defined on the trait itself.
Not all Rust traits will define derivable default implementations. Something like
Display
will be specific to each struct we put it onto. But for our components
the trait itself is quite simple:
// https://github.com/bevyengine/bevy/blob/2a036b658f31e830a903c8ab9d0f437b0bab069b/crates/bevy_ecs/src/component.rs#L149
pub trait Component: Send + Sync + 'static {
// A marker type indicating the storage type used for this component.
// This must be either [`TableStorage`] or [`SparseStorage`].
type Storage: ComponentStorage;
}
Components have two types of storage with different trade offs:
TableStorage
(default): Fast for iteration, slow for adding/removingSparseSetStorage
: Fast for adding/removing, slow for iterating
Bevy is marking the trait as being Send + Sync + 'static
. These are trait
bounds and they specify that any implementation of the Component
trait must
also have these traits satisfied.
What Send + Sync
means is that our Component
is safe to pass between threads,
which Bevy uses to run our systems in parallel. The 'static
is a lifetime
which is saying that our Component
will live for the entire duration of our
program.
This doesn’t mean that components never get cleaned up, however. It just means that any reference to our component can live forever with the borrowing rules of Rust.