Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Bloop ECS API

Now that we understand conceptually how ECS works, let's take a look at Bloop's Rust ECS API.

Components

To declare components in Rust, create a struct tagged with the component attribute:

#[component]
struct Health {
    hitpoints: i32,
}

Components must implement Copy. The #[component] attribute automatically derives this for the struct, but it may need to be manually derived for nested types.

Further, due to the current implementation of scene loading, components must also implement serde::Deserialize. Like Copy, this is automatically derived by the #[component] attribute, but may need to be manually derived for nested types.

Resources

Bloop (and many other ECS engines) provides a second type of data building block in addition to components: resources. Resources are declared as a struct just like components, but there only ever exists one global instance of the resource. Also unlike components, resources may not be assigned to entities.

Resources can be thought of as safe global variables.

#[resource]
struct GameState {
    score: i32,
}

Resources must implement Default, so that the engine can initialize them when the game starts. However, they are not required to implement Copy, and may contain complex types.

Entities

Spawning entities in Bloop is done via the Engine struct.

Engine::spawn() takes a slice of ComponentRef structs. Creating this ComponentRef slice manually is a bit difficult, so the bundle!() macro is provided which can take component references directly:

// Spawn an entity with a Transform and Velocity component.

let transform = Transform::default();
let velocity = Velocity::default();

let entity = Engine::spawn(bundle!(&transform, &velocity));

Accessing the EntityId in a Query

It is often necessary to know the EntityId of a given entity, most commonly to despawn the entity. While Engine::spawn() returns an EntityId, in most cases is not practical nor efficient to track spawned entities manually.

To solve this, it was decided to make EntityId a component, just like any other. Any entity is guaranteed to have an EntityId component. Thus, EntityId can be included in a query as a regular component. See Queries.

#[system]
fn my_system(query: Query<(&Transform, &EntityId)>) {
    my_query.for_each(|(transform, entity_id)| {
        // ..
    });
}

Systems

Bloop provides two types of systems:

  • #[system], a function which runs once per frame.
  • #[system_once], a function which runs only once, on the first frame of the game.

Declaring systems

In Rust, free functions are interpreted as systems when tagged with the #[system] and #[system_once] attributes.

#[system]
fn my_system() {}

#[system_once]
fn my_startup_system() {}

System inputs

Bloop's tooling is set up to work like dependency injection: simply declare any desired system inputs as parameters, and the engine will provide the correct inputs at runtime.

Systems may only take resources, queries, and event readers/writers as input.

Resources

To access a resource in a system, add a parameter taking the resource wrapped in a Ref<T> struct or a Mut<T> struct. Ref<T> forbids mutable access to the resource, while Mut<T> allows mutable access.

#[system]
fn my_system(game_state: Mut<GameState>, input: Ref<InputState>) {
    // We now have access to the `GameState` and `InputState` resources!

    // Because we asked for Mut<GameSate>, we may modify it.
}

Queries

Queries are how systems access entities and their components. Queries specify a set of components, which determines the entities they will access. Any entity which has at least the specified set of components will be included in the query.

Just like resource inputs, each query parameter must specify whether it will access the component immutably (Ref<T>) or mutably (Mut<T>).

#[system]
fn system_a(query: Query<Ref<Transform>>) {
    query.for_each(|transform| {
        // Runs for each entity with a transform component.
    });
}

// For queries with more than one parameter, use a tuple of components.

#[system]
fn system_b(query: Query<(Mut<Transform>, Ref<Velocity>)>) {
    query.for_each(|transform, velocity| {
        // Runs for each entity with a transform and a velocity component.

        // We may modify the transform.
    });
}

The mutability/const-ness of resource inputs and query components is important. This helps Bloop to build the system execution graph (including parallelizing system execution [not yet implemented]).