Tainted\\Coders

Bevy Components

Bevy version: 0.16Last updated:

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:

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

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:

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:

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.