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