Macroquad Rapier ECS: Using Bevy ECS in Macroquad Game
Tags: rust, gamedev, bevy
Published: 2024-05-30 08:03:14

---
title: "Macroquad Rapier ECS: Using Bevy ECS in Macroquad Game"
published: "true"
description: "Macroquad Rapier ECS 🦀 Rust game dev — using bevy's 🧩 Entity Component System in a Macroquad game with Rapier physics."
tags: "rust, gamedev, bevy"
canonical_url: "https://rodneylab.com/macroquad-rapier-ecs/"
cover_image: "https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h9j5dm760dk1oz90ysuf.png"
---

## 🐣 Using Bevy Entity Component System outside of Bevy

Not every game needs an Entity Component System  (**ECS**), but for this Macroquad Rapier ECS post, I was keen to see how you might add an ECS to a Macroquad game with Rapier physics.

In a <a href="https://rodneylab.com/deploying-your-rust-wasm-game/">previous post, I used the Shipyard ECS with Macroquad</a>.  I have been working through a build-your-own physics engine tutorial, which used the Bevy game engine.  I loved the **ergonomics** of the embedded ECS, so thought, I would take it for a spin here.  **Rapier** the Rust physics engine used here has a Bevy plugin, but I wanted to try using both Rapier and the stand-alone **Bevy ECS outside of Bevy**, just for the challenge.

## 🧱 Macroquad Rapier ECS: What I Built

I continued with the bubble simulation, used for a few recent posts.  It had no ECS, dozens of entities and a few other features that made it interesting to try with an ECS data model.  So that was my starting point.

![Macroquad Rapier ECS: A collection of yellow, orange, and blue balls have floated to the top of the window in a screen-capture.  They are tightly packed, though not evenly distributed, with the collection being more balls deep at the centre of the window.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4to771ahbgvbewi3sdcq.png)

The simulation releases bubbles which float to the top of the screen, where they are trapped by a Rapier height field.  Once all existing bubbles are settled, the simulation spawns a fresh one.

The simulation uses a random number generator to decide on the initial velocity of spawned balls, and has running and paused states.  The simulation triggers the paused state when the bubbles almost completely fill the window.  Both the random number generator and the simulation state are **singletons**, and fit well into the ECS resource model.  I also put the physics engine itself into an ECS resource.  The <a href="https://rodneylab.com/c++-game-dev-libraries/">C/C++ flecs ECS library</a>, for example, calls resources singletons.

### The Simulation within the ECS Model

As you would expect, each bubble is an entity.  If you are new to ECSs you might imagine the ECS entities as rows of a database table.  In this database world, the table columns are components.  In my case, the balls all have the same components:
- a circle static mesh (which encodes colour and size data needed for rendering);
- a circle collider (for handling physical collisions with Rapier); and 
- position and velocity components (used for updating the simulation physics).

Systems mutate the component properties, for example, there is a ball update system, which uses Rapier, to work out what the current velocity of the bubble is, at each step, then update the velocity component.

In the following sections, we look at some code snippets to help illuminate the points here, and hopefully give you an idea of how I put the simulation together, adding an ECS to Macroquad. The **full code is on GitHub**, and there is a link to it further down.

## 🧩 Components

I just mentioned that bubble entities each have a few components.  Here is some example code for spawning a new ball using the simulation ECS:

```rust
let _ball_entity = world.spawn((
    Position(new_ball_position),
    CircleMesh {
        colour: BALL_COLOURS[macroquad_rand::gen_range(0, BALL_COLOURS.len())],
        radius: Length::new::<length::meter>(BALL_RADIUS),
    },
    CircleCollider {
        radius: Length::new::<length::meter>(BALL_RADIUS),
        physics_handle: None,
    },
    Velocity(new_ball_velocity),
));
```

The new entity gets a `Position`, `CircleMesh`, `CircleCollider` and `Velocity`.  The number of components here is arbitrary, and not fixed (as it might be when calling a constructor using an object-oriented approach).  This grants flexibility as you develop the simulation or game.

Also note, I follow a Rust convention in naming the `_ball_entity` identifier.  The initial underscore (`_`) indicates the identifier is not used anywhere else in the scope.  We do not need it later, since the systems used to query and mutate the component properties, operate on entities with specific sets of components (properties). We will see this more clearly later.

In an ECS model, the entity is not much more than an integer, which Bevy ECS can use as an ID.

### Units of Measurement

In a previous <a href="https://rodneylab.com/rapier-physics-with-units-of-measurement/">post on adding units of measurement to a Rapier game</a>, I introduced the length units used in the snippet above. This pattern of using units leveraging Rusts type system, sense-checking calculations and helping to avoid unit conversion errors.

## 🏷️ Macroquad Rapier ECS: Tags

The `Position` and `Velocity` components, mentioned before, are structs with associated data.  You can also have ECS tags, which are akin to components without data.  Rapier has a type of collider that just detects a collision, and beyond that does not interact with the physics system, these are **sensors**.  

![Macroquad Rapier ECS: A collection of yellow, orange, and blue balls have floated to the top of the window in a screen-capture.  They are tightly packed, though not evenly distributed, with the ball reaching almost down to the ground in the centre.  A lone ball sits bottom centre on the floor of the window.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f0vkr33vv6180kg7ovuu.png)

I used a `Sensor` tag in the ECS for colliders that are sensors, which helps to separate them out when running systems.  The only sensor in the simulation runs along the bottom of the window.  Towards the end of the simulation, when the window is almost full, a newly spawned bubble will inevitably bounce off an existing one and collide with the floor sensor.  I added a system to pause the simulation when this occurs, effectively ending the simulation.

```rust
#[derive(Component, Debug)]
pub struct CuboidCollider {
    pub half_extents: Vector2<f32>,
    pub position: Isometry<f32, Unit<Complex<f32>>, 2>,
    pub translation: Vector2<f32>,
}

#[derive(Component, Debug)]
pub struct Sensor;
```

This, above, code snippet shows a `CuboidCollider` (used for the floor sensor), which is a regular component and then the `Sensor` tag.  The code snippet, below, initializes the floor sensor:

```rust
pub fn spawn_ground_sensor_system(mut commands: Commands) {
    let collider_half_thickness = Length::new::<length::meter>(0.05);
    let window_width = Length::new::<pixel>(WINDOW_WIDTH);
    let window_height = Length::new::<pixel>(WINDOW_HEIGHT);
    commands.spawn((
        CuboidCollider {
            half_extents: vector![
                0.5 * window_width.get::<length::meter>(),
                collider_half_thickness.value
            ],
            translation: vector![
                0.0,
                (-window_height - collider_half_thickness).get::<length::meter>()
            ],
            position: Isometry2::identity(),
        },
        Sensor,
    ));
}
```

Note, the `Sensor` tag is included.  To spawn the wall colliders on either side of the window, a very similar block is used, only omitting the `Sensor` tag.  We will see how this can be used with systems next. 

## 🎺 Systems
Systems are code blocks, which are only run on components belonging to a specified component set.  As an example, here is the system for updating bubble position and velocity within the game loop:

```rust
pub fn update_balls_system(
    mut query: Query<(&mut Position, &mut Velocity, &CircleCollider)>,
    physics_engine: Res<PhysicsEngine>,
) {
    for (mut position, mut velocity, circle_collider) in &mut query {
        if let Some(handle) = circle_collider.physics_handle {
            let ball_body = &physics_engine.rigid_body_set[handle];
            position.0 = vector![
                Length::new::<length::meter>(ball_body.translation().x),
                Length::new::<length::meter>(ball_body.translation().y)
            ];
            velocity.0 = vector![
                VelocityUnit::new::<velocity::meter_per_second>(ball_body.linvel().x),
                VelocityUnit::new::<velocity::meter_per_second>(ball_body.linvel().y)
            ];
        }
    }
}
```

This system runs on any entity that satisfies the query of having `Position`, `Velocity` and `CircleCollider` components.  We can be more prescriptive, choosing entities that have a set of components, and also, either do, or do not have some other component or tag.  We use `With` when creating the floor sensor during the simulation initialization:

```rust
pub fn create_cuboid_sensors_system(
    query: Query<&CuboidCollider, With<Sensor>>,
    mut physics_engine: ResMut<PhysicsEngine>,
) {
    for collider in query.iter() {
        let new_collider =
            ColliderBuilder::cuboid(collider.half_extents.x, collider.half_extents.y)
                .position(collider.position)
                .translation(collider.translation)
                .sensor(true)
                .build();
        physics_engine.collider_set.insert(new_collider);
    }
}
```

As you might expect, the equivalent code for creating the side wall colliders uses `Without` in its query, and omits `.sensor(true)` in its Rapier setup code.

## 📆 Schedules

We use ECS schedules to determine when systems run.  The simulation has:

- a setup schedule, run once during simulation setup,
- a running schedule, executed on every run through the game loop in simulate/running mode; and
- a paused schedule, runs when we have paused the simulation.

Bevy ECS organizes the systems above into these schedules, which can include constraints to ensure systems run in the right order.

Here is the paused schedule code:

```rust
    let mut paused_schedule = Schedule::default();
    paused_schedule.add_systems(draw_balls_system);
```

This just needs to draw the balls (the screen is cleared in each game loop iteration, even while the simulation is paused). The running schedule features more systems.

That code above is triggered in the game loop, while the simulation state is set to paused:

```rust
    loop {
        // TRUNCATED...
        clear_background(GUNMETAL);

        let simulation_state = world
            .get_resource::<SimulationState>()
            .expect("Expected simulation state to have been initialised");

        match &simulation_state.mode {
            SimulationMode::Running => {
                playing_schedule.run(&mut world);
            }
            SimulationMode::Paused => paused_schedule.run(&mut world),
        }

        next_frame().await;
    }
```

That’s it! We covered  all the major constituents of an ECS!

## 🙌🏽 Macroquad Rapier ECS: Wrapping Up

In this post on Macroquad Rapier ECS, we got an introduction to working with an ECS in Macroquad.  In particular, we saw:

- the main constituents of an ECS including **resources, schedules and tags, as well as entities, components, and systems**;
- why you might want to add **tags, and how you can use them in Bevy ECS system queries along with <code>With</code> and <code>Without</code>**; and
- **ECS schedules for different game or simulation states**.

I hope you found this useful.  As promised, you can <a href="https://github.com/rodneylab/macroquad-rapier-bevy-ecs">get the full project code on the Rodney Lab GitHub repo</a>.  I would love to hear from you, if you are also new to Rust game development.  Do you have alternative resources you found useful? How will you use this code in your own projects?

## 🙏🏽 Macroquad Rapier ECS: Feedback

If you have found this post useful, see links below for further related content on this site.  Let me know if there are any ways I can improve on it. I hope you will use the code or starter in your own projects. Be sure to share your work on X, giving me a mention, so I can see what you did. Finally, be sure to let me know ideas for other short videos you would like to see. Read on to find ways to get in touch, further below. If you have found this post useful, even though you can only afford even a tiny contribution, please <a aria-label="Support Rodney Lab via Buy me a Coffee" href="https://rodneylab.com/giving/">consider supporting me through Buy me a Coffee</a>.

Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via <a href="https://twitter.com/messages/compose?recipient_id=1323579817258831875">@askRodney</a> on X (previously Twitter) and also, join the <a href="https://matrix.to/#/%23rodney:matrix.org">#rodney</a> Element Matrix room. Also, see <a aria-label="Get in touch with Rodney Lab" href="https://rodneylab.com/contact/">further ways to get in touch with Rodney Lab</a>. I post regularly on <a href="https://rodneylab.com/tags/gaming/">Game Dev</a> as well as <a href="https://rodneylab.com/tags/rust/">Rust</a> and <a href="https://rodneylab.com/tags/c++/">C++</a> (among other topics). Also, <a aria-label="Subscribe to the   Rodney Lab newsletter" href="https://newsletter.rodneylab.com/issue/latest-issue">subscribe to the newsletter to keep up-to-date</a> with our latest projects.