Bevy Components
Components are what 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. It is basically a primary key for a row we are building up dynamically.
To add health, damage, or anything representing the state of our entities, we add a Component
struct that holds the data.
Components are essentially stored in giant arrays of the same type. An entity's ID is the index in each array that we grab to get all the components that belong to a specific entity.
Defining a component
We define components as structs that implement Component
, usually through its derive
macro.
#[derive(Component)]
struct Position {
x: i32,
y: i32
}
This derive macro will add behavior to this struct at compile time and make them available to be queried inside our systems.
When we query for a component, we can ask for mutable or read-only access. This helps Bevy manage data access between systems and is what enables it to run our systems in parallel when they both do not need mutable access to the same data.
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
}
These kind of components without data act as indexes on our rows. They let us mark components as a type and then query for these in our systems.
Immutable components
Components that need to spawn and then never be changed can be marked as immutable.
#[derive(Component)]
#[component(immutable)]
struct MaxPlayerSpeed(u64)
This is going to guarantee that there is never an exclusive reference &mut
created while this component is on an entity.
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.
For example, imagine we have an array of Health
and Position
components:
Health: [Health(100.0), Health(72.0)]
Position: [Position(3, 2), Position(3, 1)]
If the entity's index is 0
then we can go into each array and access the components at that index, giving us Health(100.0)
and Position(3, 2)
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 enabling your CPU to fetch more data from its memory 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 arrays 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;
When to use one over the other? It depends on whether you will be querying or changing the components more.
Something like an Explode
component that is both 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
.
When we want some components of an entity, Bevy looks up that entity's index in each relevant Table
or SparseSet
. 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 which try and store groups of components together how they are most likely to be accessed.
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 of components
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.
Bundles are only used for adding groups of components and then they cease to exist. You cannot query for them.
To define a Bundle
, 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 }
});
Tuples of bundles also implement Bundle
(for tuples up to 15 bundles, sadly Rust is still missing variadic generics). 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
(yo, dawg).
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
.
Using required components
In Bevy 0.15
a newer feature was added called required components which Bevy moved to using internally. Bundles are here to stay but required components are usually a more ergonomic way to enforce dependencies between components.
The bundle from above could be rewritten with required components:
#[derive(Component)]
#[require(Position, Ship)]
struct Player;
fn spawn_player_with_required_components(
mut commands: Commands
) {
commands.spawn(Player);
}
When a component is spawned, if it has any required components, it will automatically add them unless we override them. The only requirement is that each required component implements the Default
trait.
All these required calls are recursive. If a component you require has required components, they will also be added.
If we wanted to we could change the players ship by providing one ourselves, which will override the one added by require
:
fn spawn_player_with_required_components(
mut commands: Commands
) {
commands.spawn((Player, Ship::Destroyer));
}
Now our player ship is a Ship::Destroyer
rather than whatever the default Ship
was.
Required components are essentially a zero cost abstraction. It will not add additional components if we provide our own.
All of Bevy's provided components have moved to this newer style, however you are still free to use bundles where you see fit.
We can provide inline component values for the component that will be spawned:
#[derive(Component)]
#[require(
B(1), // tuple structs
C { // named-field structs
x: 1,
..Default::default()
},
D::One, // enum variants
E::ONE, // associated consts
F::new(1) // constructors
)]
struct A;
These can also take arbitrary expressions with =
#[derive(Component, PartialEq, Eq, Debug)]
#[require(Health = init_health())]
struct Player;
fn init_health() -> C {
Health(100)
}
If you needed to, you could add these required components dynamically during runtime by modifying the World
and calling register_required_components
:
# let mut world = World::default();
// Register Health as required by Player.
world.register_required_components::<Player, Health>();
Component hooks
Hooks are functions that will run at a particular point in the component's lifecycle.
You have 4 kinds of hooks you can tag your derive
macro with:
#[component(on_add = on_add_function)]
#[component(on_insert = on_insert_function)]
#[component(on_replace = on_replace_function)]
#[component(on_remove = on_remove_function)]
The intended purpose is for handling side effects that need to happen. Creating a cached index to clean up resources or hierarchical data in your app would be a good example.
Component storage
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.