Tainted\\Coders

Bevy Components

Bevy version: 0.14Last updated:

In an entity component system (ECS) you can imagine that your rows are your entities and your components are your columns.

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.

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:

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
}

This derive macro will add behavior to this struct at compile time and make them available through our queries.

When we query for a component, we can ask for mutable or read-only access and then do whatever we want with its fields to change the state of our entities.

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.

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 IDHealthPlayerPositionEnemy
1100.0X(3, 2)
272.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 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 arrays we mentioned in the previous section.

These arrays can be one of two specialized types:

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 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 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.

In Bevy 0.15 there will be a newer feature that will be replacing bundles called required components which I will update here when they are released.

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.

Its important to understand that a bundle does not actually exist. You cannot query for them. They are only used for adding groups of components and then they cease to exist.

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 recommended to implement your 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, 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.

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://docs.rs/bevy/latest/bevy/ecs/component/trait.Component.html
pub trait Component:
  Send
  + Sync
  + 'static {
  const STORAGE_TYPE: StorageType;

  // Provided method
  fn register_component_hooks(_hooks: &mut ComponentHooks) { ... }
}

Components have two types of storage with different trade offs:

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.