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

Custom Components

So far we've only been using builtin components like Transform, CircleRender, and Color. But to make a game, we need so much more power!

Luckily, defining components is essentially just another attribute macro, #[component]. A component is a Struct or enum and must be serializable and implement the Copy trait. More importantly, they require a dependency we haven't added to our cargo.toml file yet. We must add serde to our dependencies.

cargo.toml
[package]
name = "<your_project_name>"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib"]

[dependencies]
engine = { git = "https://github.com/bloopgames/bloop-rs.git", tag = "0.0.22" }
serde = "1.0.219"

[build-dependencies]
module-codegen = { git = "https://github.com/bloopgames/bloop-rs.git", tag = "0.0.22" }

Doing that should do the trick. You should then run cargo clean and cargo build in your project's root directory to make sure serde is properly integrated and that the macro gets compiled correctly.

Defining a Component

It's very easy to define a component.

#[component]
struct SomeComponent;

There, we did it! A component with no data is also known as a tag component, which can be very convenient as we'll see in a minute.

Of course, you may want data in your component, which is simple as well:

#[component]
struct Health{
    current_health: f32,
    max_health: f32,
}

Solving our Made Up Problem

In our previous step we pretended to "accidentally" move both the player and the enemy with the same system, and now want to differentiate between the two. The simplest way is to create a Player tag component.

srclib.rs
#[component]
struct Player;

Add it to our player_spawn function:

srclib.rs
Engine::spawn(bundle!(transform, color, circle_render, &Player));

And make our query also query for it:

srclib.rs
#[system]
fn move_player(mut player: Query<(Mut<Transform>, Ref<Player>)>) {
    player.for_each(|(mut transform, _)| { // <- Note we use an underscore instead of "player" because we don't actually care about reading or manipulating that component.
        //....
    });
}

When you put it all together, it looks like so:

srclib.rs
use engine::prelude::*;
mod ffi;

#[component]
struct Player;

#[system_once]
fn spawn_player() {
    let transform = &Transform {
        position: Vec2::new(110., 0.),
        scale: Vec2::splat(3.0),
        ..Default::default()
    };
    let color = &Color::new(0.96, 0.65, 0.14, 0.9);
    let circle_render = &CircleRender {
        num_sides: 20,
        ..Default::default()
    };
    Engine::spawn(bundle!(transform, color, circle_render, &Player));
}

#[system_once]
fn spawn_enemy() {
    let transform = &Transform {
        position: Vec2::new(-110., 0.),
        scale: Vec2::splat(3.0),
        ..Default::default()
    };
    let color = &Color::new(0.2, 0.2, 0.9, 0.9);
    let circle_render = &CircleRender {
        num_sides: 20,
        ..Default::default()
    };
    Engine::spawn(bundle!(transform, color, circle_render));
}

#[system]
fn move_player(mut player: Query<(Mut<Transform>, Ref<Player>)>) {
    player.for_each(|(mut transform, _)| {
        if (transform.position.x >= 100.) {
            transform.position.y += 5.0;
        } else if (transform.position.x <= -100.) {
            transform.position.y -= 5.0;
        }

        if (transform.position.y >= 100.) {
            transform.position.x -= 5.0;
        } else if (transform.position.y <= -100.) {
            transform.position.x += 5.0;
        }
    });
}

You'll have to run the code yourself to verify that only the player's orange circle moves! But we have no time to waste, we gotta learn about Resources, the final ECS concept, if we want to actually control our character!