Tainted \\ Coders

Bevy Components

Last updated:

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:

  1. Each entity can only have one component of each type
  2. 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/removing
  • SparseSetStorage: 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.

Read more