🐣 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 previous post, I used the Shipyard ECS with Macroquad. 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://rodneylab-nebula-a-1zqt.shuttle.app/v1/lab/post/macroquad-rapier-ecs/macroquad-rapier-ecs-bubble-demo.920a95a58187.png?w=592&h=333&fit=min&auto=format)
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 C/C++ flecs ECS library, 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:
91 let _ball_entity = world.spawn((92 Position(new_ball_position),93 CircleMesh {94 colour: BALL_COLOURS[macroquad_rand::gen_range(0, BALL_COLOURS.len())],95 radius: Length::new::<length::meter>(BALL_RADIUS),96 },97 CircleCollider {98 radius: Length::new::<length::meter>(BALL_RADIUS),99 physics_handle: None,100 },101 Velocity(new_ball_velocity),102 ));
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 post on adding units of measurement to a Rapier game, 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://rodneylab-nebula-a-1zqt.shuttle.app/v1/lab/post/macroquad-rapier-ecs/macroquad-rapier-ecs-bubbles-example-end.ac0d15b7d5cc.png?w=592&h=333&fit=min&auto=format)
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.
34 #[derive(Component, Debug)]35 pub struct CuboidCollider {36 pub half_extents: Vector2<f32>,37 pub position: Isometry<f32, Unit<Complex<f32>>, 2>,38 pub translation: Vector2<f32>,39 }4041 #[derive(Component, Debug)]42 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:
196 pub fn spawn_ground_sensor_system(mut commands: Commands) {197 let collider_half_thickness = Length::new::<length::meter>(0.05);198 let window_width = Length::new::<pixel>(WINDOW_WIDTH);199 let window_height = Length::new::<pixel>(WINDOW_HEIGHT);200 commands.spawn((201 CuboidCollider {202 half_extents: vector![203 0.5 * window_width.get::<length::meter>(),204 collider_half_thickness.value205 ],206 translation: vector![207 0.0,208 (-window_height - collider_half_thickness).get::<length::meter>()209 ],210 position: Isometry2::identity(),211 },212 Sensor,213 ));214 }
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:
122 pub fn update_balls_system(123 mut query: Query<(&mut Position, &mut Velocity, &CircleCollider)>,124 physics_engine: Res<PhysicsEngine>,125 ) {126 for (mut position, mut velocity, circle_collider) in &mut query {127 if let Some(handle) = circle_collider.physics_handle {128 let ball_body = &physics_engine.rigid_body_set[handle];129 position.0 = vector![130 Length::new::<length::meter>(ball_body.translation().x),131 Length::new::<length::meter>(ball_body.translation().y)132 ];133 velocity.0 = vector![134 VelocityUnit::new::<velocity::meter_per_second>(ball_body.linvel().x),135 VelocityUnit::new::<velocity::meter_per_second>(ball_body.linvel().y)136 ];137 }138 }139 }
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:
94 pub fn create_cuboid_sensors_system(95 query: Query<&CuboidCollider, With<Sensor>>,96 mut physics_engine: ResMut<PhysicsEngine>,97 ) {98 for collider in query.iter() {99 let new_collider =100 ColliderBuilder::cuboid(collider.half_extents.x, collider.half_extents.y)101 .position(collider.position)102 .translation(collider.translation)103 .sensor(true)104 .build();105 physics_engine.collider_set.insert(new_collider);106 }107 }
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:
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:
148 loop {149 // TRUNCATED...150 clear_background(GUNMETAL);151152 let simulation_state = world153 .get_resource::<SimulationState>()154 .expect("Expected simulation state to have been initialised");155156 match &simulation_state.mode {157 SimulationMode::Running => {158 playing_schedule.run(&mut world);159 }160 SimulationMode::Paused => paused_schedule.run(&mut world),161 }162163 next_frame().await;164 }
That’s it! We covered all the major constituents of an ECS!
Please enable JavaScript to watch the video 📼
🗳 Poll #
🙌🏽 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
With
andWithout
; and - ECS schedules for different game or simulation states.
I hope you found this useful. As promised, you can get the full project code on the Rodney Lab GitHub repo . 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 consider supporting me through Buy me a Coffee.
Just dropped a new post on using Macroquad with Rapier physics and Bevy ECS.
— Rodney (@askRodney) May 15, 2024
We look at:
— main ECS parts,
— integrating Bevy ECS with Macroquad; and
— Bevy ECS features like queries and schedules.
Hope you find it useful!
#askRodney #rustlang #gamedevhttps://t.co/y72RoSb8Yf
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 @askRodney on X (previously Twitter) and also, join the #rodney Element Matrix room. Also, see further ways to get in touch with Rodney Lab. I post regularly on Game Dev as well as Rust and C++ (among other topics). Also, subscribe to the newsletter to keep up-to-date with our latest projects.