Opens an external site in a new window
Mental Health Awareness Month
“Community”
RODNEY LAB
  • Home
  • Plus +
  • Newsletter
  • Links
  • Profile
RODNEY LAB
  • Home
  • Plus +
  • Newsletter
  • Links

Rapier Physics with Macroquad: Rust Game Physics 🎱️ # Rapier Physics with Macroquad: Rust Game Physics 🎱️ #

blurry low resolution placeholder image Rapier Physics with Macroquad
  1. Home Rodney Lab Home
  2. Blog Posts Rodney Lab Blog Posts
  3. Gaming Gaming Blog Posts
<PREVIOUS POST
NEXT POST >
LATEST POST >>

Rapier Physics with Macroquad: Rust Game Physics 🎱️ #

Updated 7 hours ago
12 minute read
Gunning Fog Index: 6.3
Content by Rodney
blurry low resolution placeholder image Author Image: Rodney from Rodney Lab
SHARE:

Rust Game Dev: Rapier and Macroquad #

In this post on using Rapier Physics with Macroquad, we take a look at getting started with Rapier and how you might set it up in a Macroquad project. In recent posts, we have looked at Rust game dev engines and Rust game physics engines. Here, we take one from each of those posts and create a couple of apps. The first is just a bouncing ball, based on the Rapier Getting Started Guide. Next, we level things up, and use Rapier height fields, sensors, collision events for a slightly more sophisticated simulation.

Macroquad is a fast and lightweight Rust game development library, intended to be easy to use and well-suited to game prototyping. Rapier, is probably the most established game physics engine in the Rust game dev ecosystem. It is a collection of crates for 2D and 3D game physics with regular (f32) and high precision (f64) offerings. Rapier is fully-featured, and also compiles to WASM, for use on the web.

With the introductions out of the way, let’s take a closer look at what we are working on. I am new to game development in Rust, so please reach out if you have alternative ways of proceeding here, which I could try to incorporate.

🧱 What we’re Building #

Please enable JavaScript to watch the video 📼

Rapier Physics with Macroquad: Rapier hello world example

We look at a couple of examples. The first, has just two physical rigid bodies: a ball and a ground surface. Porting the Rapier Getting Started guide to Macroquad, we see the ball bounce on the ground before coming to rest.

blurry low resolution placeholder image Rapier Physics with Macroquad: 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.
Rapier Physics with Macroquad: Floating Bubble Example

Stepping up a notch, the second example uses more Rapier features. Reversing gravity, bubbles fired in random directions float to the top of the screen, where they are caught by a Rapier height field.

⚙️ Rapier Physics with Macroquad Project Config #

To start, here is the Cargo.toml:

Cargo.toml
toml
    
[package]
name = "rapier-example"
version = "0.1.0"
edition = "2021"
license = "BSD-3-Clause"
repository = "https://github.com/rodneylab/rapier-example"
# macroquad 0.3.26 requires MSRV 1.73
rust-version = "1.73"
description = "Rapier Physics with Macroquad 🗡️ building a basic game physics simulation in Rust using rapier physics and Macroquad for rendering 🖥️"
[dependencies]
crossbeam = "0.8.4"
macroquad = { version = "0.4.12", default-features = false }
rand = "0.9.1"
rand_distr = "0.5.1"
rapier2d = { version = "0.26.1", features = ["simd-stable"] }
  • The world is not particularly large, or fast-paced for either demo, so the f32 version of rapier should suffice, and we will just use 2D simulation.
  • The bubble demo uses rand and rand_distr to generate a standard normal distribution for randomly deciding the initial ball velocity.
  • We use crossbeam for Rapier collision event handling.

👋🏽 Rapier Physics with Macroquad Hello World #

I set the project up as a series of examples, so you can place the source code for the hello world example in a new examples folder as examples/hello_world.rs. Here is the full Macroquad code for this first example (based on Rapier Rust Getting Started ):

examples/hello_world.rs — click to expand code.
examples/hello_world.rs
rust
    
1 use macroquad::{
2 color::Color,
3 input::{is_key_released, KeyCode},
4 shapes::draw_circle,
5 window::{clear_background, next_frame, Conf},
6 };
7 use rapier2d::{
8 dynamics::{
9 CCDSolver, ImpulseJointSet, IntegrationParameters, IslandManager, MultibodyJointSet,
10 RigidBodyBuilder, RigidBodySet,
11 },
12 geometry::{BroadPhaseMultiSap, ColliderBuilder, ColliderSet, NarrowPhase},
13 na::{vector, Vector2},
14 pipeline::{PhysicsPipeline, QueryPipeline},
15 prelude::nalgebra,
16 };
17
18 pub const CARROT_ORANGE: Color = Color {
19 r: 247.0 / 255.0,
20 g: 152.0 / 255.0,
21 b: 36.0 / 255.0,
22 a: 1.0,
23 };
24 pub const GUNMETAL: Color = Color {
25 r: 49.0 / 255.0,
26 g: 57.0 / 255.0,
27 b: 60.0 / 255.0,
28 a: 1.0,
29 };
30
31 const WINDOW_WIDTH: f32 = 1366.0;
32 const WINDOW_HEIGHT: f32 = 768.0;
33 // 1 metre is 50 pixels
34 const PHYSICS_SCALE: f32 = 50.0;
35
36 #[derive(Debug)]
37 struct Ball {
38 radius: f32,
39 position: Vector2<f32>,
40 }
41
42 impl Default for Ball {
43 fn default() -> Ball {
44 Ball {
45 radius: 0.6,
46 position: vector![
47 WINDOW_WIDTH / (2.0 * PHYSICS_SCALE),
48 WINDOW_HEIGHT / (2.0 * PHYSICS_SCALE),
49 ],
50 }
51 }
52 }
53
54 fn conf() -> Conf {
55 #[allow(clippy::cast_possible_truncation)]
56 Conf {
57 window_title: String::from("Rapier Macroquad Hello World"),
58 window_width: WINDOW_WIDTH as i32,
59 window_height: WINDOW_HEIGHT as i32,
60 high_dpi: true,
61 ..Default::default()
62 }
63 }
64
65 #[macroquad::main(conf)]
66 async fn main() {
67 let mut ball = Ball::default();
68
69 let mut rigid_body_set = RigidBodySet::new();
70 let mut collider_set = ColliderSet::new();
71
72 // Create the ground
73 let collider_half_thickness = 0.05;
74 let collider = ColliderBuilder::cuboid(100.0, collider_half_thickness)
75 .translation(vector![
76 0.0,
77 (WINDOW_HEIGHT / -PHYSICS_SCALE) - collider_half_thickness
78 ])
79 .build();
80 collider_set.insert(collider);
81
82 // Create the bouncing ball
83 let rigid_body = RigidBodyBuilder::dynamic()
84 .translation(ball.position)
85 .build();
86 let collider = ColliderBuilder::ball(0.6).restitution(0.7).build();
87 let ball_body_handle = rigid_body_set.insert(rigid_body);
88 collider_set.insert_with_parent(collider, ball_body_handle, &mut rigid_body_set);
89
90 // Create other structures necessary for the simulation
91 let gravity = vector![0.0, -9.81];
92 let integration_parameters = IntegrationParameters::default();
93 let mut physics_pipeline = PhysicsPipeline::new();
94 let mut island_manager = IslandManager::new();
95 let mut broad_phase = BroadPhaseMultiSap::new();
96 let mut narrow_phase = NarrowPhase::new();
97 let mut impulse_joint_set = ImpulseJointSet::new();
98 let mut multibody_joint_set = MultibodyJointSet::new();
99 let mut ccd_solver = CCDSolver::new();
100 let mut query_pipeline = QueryPipeline::new();
101 let physics_hooks = ();
102 let event_handler = ();
103
104 // run the game loop, stepping the simulation once per frame.
105 loop {
106 clear_background(GUNMETAL);
107
108 if is_key_released(KeyCode::Escape) {
109 break;
110 }
111
112 draw_circle(
113 PHYSICS_SCALE * ball.position.x,
114 -PHYSICS_SCALE * ball.position.y,
115 PHYSICS_SCALE * ball.radius,
116 CARROT_ORANGE,
117 );
118 physics_pipeline.step(
119 &gravity,
120 &integration_parameters,
121 &mut island_manager,
122 &mut broad_phase,
123 &mut narrow_phase,
124 &mut rigid_body_set,
125 &mut collider_set,
126 &mut impulse_joint_set,
127 &mut multibody_joint_set,
128 &mut ccd_solver,
129 Some(&mut query_pipeline),
130 &physics_hooks,
131 &event_handler,
132 );
133
134 let ball_body = &rigid_body_set[ball_body_handle];
135 println!("Ball altitude: {}", ball_body.translation().y);
136
137 // update ball position (used for drawing)
138 ball.position = *ball_body.translation();
139
140 next_frame().await;
141 }
142 }

The Rapier docs are fantastic, both the User Guide and the library API docs. The Common Mistakes section of the User Guide  is a good port of call for initial issues.

  • we use SI units with the Rapier model, so scale pixels (used by Macroquad for rendering) with a factor of 50 pixels per metre (line 34). The Common Mistakes doc (just mentioned) recommends scaling.
  • for colliders, we are generally working in half extents, so we give the radius of the ball and the half-thickness of the collider.
  • we set gravity to -9.81 in line 91 (we are working in SI units). Notice the Rapier Physics and Macroquad rendering vertical axes increase in opposite directions, with the rendering scale zero at the top and increasing downwards.
  • we don’t need physics hooks or an event handler in this example, so set them each to the unit type (lines 101 & 102).

Collider Properties #

Restitution is a measure of how much kinetic energy the ball retains on collision. With zero restitution, it will not bounce, while with 1.0, it will rebound with equal and opposite force. You can set other physical properties on colliders in the builder. Density, for example, is the easiest way to make a body heavier (hence adjusting movement and collision characteristics).

Running the Hello World Example #

To run the example, use the following command in the Terminal:

    
cargo run --example hello_world
blurry low resolution placeholder image Rapier Physics with Macroquad: A carrot orange ball at the centre of a gun metal window hovers, presumably falling.
Rapier Physics with Macroquad: Hello World Example

🫧 Levelling Up: Floating Ball Example #

Next, let’s turn to the floating ball example. In this example, we spawn floating balls that get caught at the top of the screen, by a collider. I used a standard normal distributed random variable to set the ball’s initial velocity; we do not just fire them vertically.

Example Features #

The ceiling is composed of a saw-tooth-like height field, instead of a flat cuboid. The idea here is to stop the balls sliding straight to the corners when they hit the ceiling. We look closer at the height field in a moment.

A new ball is only spawned once the existing ones have come to rest. This island manager helps here to check if they are still any active dynamic balls, before spawning a new one.

Finally, there is a sensor collider at the bottom of the window. Sensors just check a collision has occurred, and are inert. Typically, you can use them to find out when an object has entered an area. As the windows fills, eventually, there will be no more space for a freshly spawned ball, and it will bounce off other balls and hit the bottom of the window. I use an event handler so when a ball hits the sensor at the bottom of the window, I can set the simulation to end.

blurry low resolution placeholder image Rapier Physics with Macroquad: 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.
Rapier Physics with Macroquad: Floating Bubble Example

Bubble Example Code #

Here is the code for the example, which I saved to examples/bubbles.rs:

examples/bubbles.rs — click to expand code.
examples/bubbles.rs
rust
    
1 use macroquad::{
2 color::Color,
3 input::{is_key_released, KeyCode},
4 miniquad::date,
5 rand::{self as macroquad_rand, srand},
6 shapes::draw_circle,
7 window::{clear_background, next_frame, Conf},
8 };
9 use rand::{rngs::StdRng, Rng, SeedableRng};
10 use rand_distr::StandardUniform;
11 use rapier2d::{
12 dynamics::{
13 CCDSolver, ImpulseJointSet, IntegrationParameters, IslandManager, MultibodyJointSet,
14 RigidBodyBuilder, RigidBodyHandle, RigidBodySet,
15 },
16 geometry::{
17 BroadPhaseMultiSap, ColliderBuilder, ColliderSet, CollisionEvent, CollisionEventFlags, NarrowPhase,
18 },
19 math::Isometry,
20 na::{vector, DVector, Vector2},
21 pipeline::{ActiveEvents, ChannelEventCollector, PhysicsPipeline, QueryPipeline},
22 prelude::nalgebra,
23 };
24
25 pub const BLUE_CRAYOLA: Color = Color {
26 r: 33.0 / 255.0,
27 g: 118.0 / 255.0,
28 b: 1.0,
29 a: 1.0,
30 };
31
32 pub const CARROT_ORANGE: Color = Color {
33 r: 247.0 / 255.0,
34 g: 152.0 / 255.0,
35 b: 36.0 / 255.0,
36 a: 1.0,
37 };
38 pub const CELESTIAL_BLUE: Color = Color {
39 r: 51.0 / 255.0,
40 g: 161.0 / 255.0,
41 b: 253.0,
42 a: 1.0,
43 };
44 pub const GUNMETAL: Color = Color {
45 r: 49.0 / 255.0,
46 g: 57.0 / 255.0,
47 b: 60.0 / 255.0,
48 a: 1.0,
49 };
50 pub const SUNGLOW: Color = Color {
51 r: 4253.0 / 255.0,
52 g: 202.0 / 255.0,
53 b: 64.0 / 255.0,
54 a: 1.0,
55 };
56
57 const BALL_COLOURS: [Color; 4] = [BLUE_CRAYOLA, CARROT_ORANGE, CELESTIAL_BLUE, SUNGLOW];
58
59 const WINDOW_WIDTH: f32 = 1366.0;
60 const WINDOW_HEIGHT: f32 = 768.0;
61 // 1 metre is 50 pixels
62 const PHYSICS_SCALE: f32 = 50.0;
63 const BALL_RADIUS: f32 = 0.6;
64
65 #[derive(Debug)]
66 struct Ball {
67 radius: f32,
68 position: Vector2<f32>,
69 physics_handle: Option<RigidBodyHandle>,
70 colour: Color,
71 }
72
73 impl Default for Ball {
74 fn default() -> Ball {
75 Ball {
76 radius: BALL_RADIUS,
77 position: vector![
78 WINDOW_WIDTH / (2.0 * PHYSICS_SCALE),
79 (2.0 * BALL_RADIUS) - (1.0 * WINDOW_HEIGHT / PHYSICS_SCALE)
80 ],
81 physics_handle: None,
82 colour: BALL_COLOURS[macroquad_rand::gen_range(0, BALL_COLOURS.len())],
83 }
84 }
85 }
86
87 impl Ball {
88 fn physics_handle(&mut self, physics_handle: RigidBodyHandle) -> &mut Ball {
89 self.physics_handle = Some(physics_handle);
90 self
91 }
92 }
93
94 fn conf() -> Conf {
95 #[allow(clippy::cast_possible_truncation)]
96 Conf {
97 window_title: String::from("Macroquad Rapier Bubbles"),
98 window_width: WINDOW_WIDTH as i32,
99 window_height: WINDOW_HEIGHT as i32,
100 high_dpi: true,
101 ..Default::default()
102 }
103 }
104
105 fn create_physics_for_ball(
106 ball: &Ball,
107 rigid_body_set: &mut RigidBodySet,
108 collider_set: &mut ColliderSet,
109 normal_distribution: &mut StdRng,
110 ) -> RigidBodyHandle {
111 let pseudo_random_value: f32 = normal_distribution.sample(StandardUniform);
112 let x_velocity: f32 = (2.0 * pseudo_random_value) - 1.0;
113 let linear_velocity = vector![x_velocity, 1.0];
114 let rigid_body = RigidBodyBuilder::dynamic()
115 .translation(ball.position)
116 .linvel(linear_velocity)
117 .build();
118 let collider = ColliderBuilder::ball(BALL_RADIUS)
119 .restitution(0.0)
120 .density(0.001)
121 .active_events(ActiveEvents::COLLISION_EVENTS)
122 .build();
123 let ball_body_handle = rigid_body_set.insert(rigid_body);
124 collider_set.insert_with_parent(collider, ball_body_handle, rigid_body_set);
125 ball_body_handle
126 }
127
128 fn create_ceiling(ceiling_width: f32, max_balls: u32, collider_set: &mut ColliderSet) {
129 let collider_half_thickness = 0.05;
130 let nsubdivs: usize = (max_balls * 2)
131 .try_into()
132 .expect("Expected fewer subdivisions");
133 let heights = DVector::from_fn(nsubdivs + 1, |i, _| if i % 2 == 0 { -1.2 } else { 0.0 });
134 let collider =
135 ColliderBuilder::heightfield(heights, vector![ceiling_width, collider_half_thickness])
136 .translation(vector![
137 0.5 * WINDOW_WIDTH / PHYSICS_SCALE,
138 -1.0 * collider_half_thickness
139 ])
140 .friction(1.0)
141 .restitution(0.0)
142 .build();
143 collider_set.insert(collider);
144 }
145
146 fn create_ground(collider_set: &mut ColliderSet) {
147 let collider_thickness = 0.1;
148 let collider = ColliderBuilder::cuboid(100.0, collider_thickness)
149 .translation(vector![
150 0.0,
151 (WINDOW_HEIGHT / -PHYSICS_SCALE) - 0.5 * collider_thickness
152 ])
153 .sensor(true)
154 .build();
155 collider_set.insert(collider);
156 }
157
158 fn create_side_walls(gap: f32, collider_set: &mut ColliderSet) {
159 // left wall
160 let collider_half_thickness = 0.05;
161 let collider =
162 ColliderBuilder::cuboid(0.5 * WINDOW_HEIGHT / PHYSICS_SCALE, collider_half_thickness)
163 .position(Isometry::new(
164 vector![
165 gap - collider_half_thickness,
166 (WINDOW_HEIGHT / (-2.0 * PHYSICS_SCALE))
167 ],
168 std::f32::consts::FRAC_PI_2,
169 ))
170 .build();
171 collider_set.insert(collider);
172
173 // right wall
174 let collider_half_thickness = 0.05;
175 let collider =
176 ColliderBuilder::cuboid(0.5 * WINDOW_HEIGHT / PHYSICS_SCALE, collider_half_thickness)
177 .position(Isometry::new(
178 vector![
179 (WINDOW_WIDTH / PHYSICS_SCALE) + collider_half_thickness - gap,
180 (WINDOW_HEIGHT / (-2.0 * PHYSICS_SCALE))
181 ],
182 3.0 * std::f32::consts::FRAC_PI_2,
183 ))
184 .build();
185 collider_set.insert(collider);
186 }
187
188 fn draw_balls(balls: &[Ball]) {
189 for ball in balls {
190 let Ball {
191 colour,
192 position,
193 radius,
194 ..
195 } = ball;
196 draw_circle(
197 PHYSICS_SCALE * position.x,
198 -PHYSICS_SCALE * position.y,
199 PHYSICS_SCALE * radius,
200 *colour,
201 );
202 }
203 }
204
205 fn update_balls(balls: &mut [Ball], rigid_body_set: &RigidBodySet) {
206 for ball in balls {
207 if let Some(handle) = ball.physics_handle {
208 let ball_body = &rigid_body_set[handle];
209 ball.position = *ball_body.translation();
210 }
211 }
212 }
213
214 #[macroquad::main(conf)]
215 async fn main() {
216 // seed macroquad random number generator
217 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
218 {
219 srand(date::now().floor() as u64);
220 }
221
222 let mut rigid_body_set = RigidBodySet::new();
223 let mut collider_set = ColliderSet::new();
224
225 // Create the ground
226 create_ground(&mut collider_set);
227
228 // Create the ceiling
229 // maximum number of balls that can fit across the window
230 let max_balls: u32;
231 #[allow(
232 clippy::cast_precision_loss,
233 clippy::cast_sign_loss,
234 clippy::cast_possible_truncation
235 )]
236 {
237 max_balls = ((WINDOW_WIDTH / PHYSICS_SCALE) / (2.0 * BALL_RADIUS)).floor() as u32;
238 }
239
240 let ceiling_width: f32;
241 #[allow(clippy::cast_precision_loss)]
242 {
243 ceiling_width = max_balls as f32 * BALL_RADIUS * 2.0;
244 }
245 create_ceiling(ceiling_width, max_balls, &mut collider_set);
246
247 // gap at left and right edge of window
248 let gap = 0.5 * ((WINDOW_WIDTH / PHYSICS_SCALE) - ceiling_width);
249
250 // Create the left and right wall
251 create_side_walls(gap, &mut collider_set);
252
253 // Create ball
254 let mut normal_distribution = StdRng::from_os_rng();
255 let mut new_ball = Ball::default();
256 let ball_body_handle = create_physics_for_ball(
257 &new_ball,
258 &mut rigid_body_set,
259 &mut collider_set,
260 &mut normal_distribution,
261 );
262 new_ball.physics_handle(ball_body_handle);
263 let mut balls: Vec<Ball> = Vec::with_capacity(256);
264 balls.push(new_ball);
265
266 // Create other structures necessary for the simulation
267 // positive gravity indicates it is applied upwards (reversed)
268 let gravity = vector![0.0, 1.0];
269 let integration_parameters = IntegrationParameters::default();
270 let mut physics_pipeline = PhysicsPipeline::new();
271 let mut island_manager = IslandManager::new();
272 let mut broad_phase = BroadPhaseMultiSap::new();
273 let mut narrow_phase = NarrowPhase::new();
274 let mut impulse_joint_set = ImpulseJointSet::new();
275 let mut multibody_joint_set = MultibodyJointSet::new();
276 let mut ccd_solver = CCDSolver::new();
277 let mut query_pipeline = QueryPipeline::new();
278 let physics_hooks = ();
279 let (collision_send, collision_recv) = crossbeam::channel::unbounded();
280 let (contact_force_send, _contact_force_recv) = crossbeam::channel::unbounded();
281 let event_handler = ChannelEventCollector::new(collision_send, contact_force_send);
282
283 let mut paused = false;
284
285 // run the game loop, stepping the simulation once per frame.
286 loop {
287 if is_key_released(KeyCode::Escape) {
288 break;
289 }
290
291 clear_background(GUNMETAL);
292 draw_balls(&balls);
293
294 if !paused {
295 physics_pipeline.step(
296 &gravity,
297 &integration_parameters,
298 &mut island_manager,
299 &mut broad_phase,
300 &mut narrow_phase,
301 &mut rigid_body_set,
302 &mut collider_set,
303 &mut impulse_joint_set,
304 &mut multibody_joint_set,
305 &mut ccd_solver,
306 Some(&mut query_pipeline),
307 &physics_hooks,
308 &event_handler,
309 );
310
311 // update ball positions (used for drawing)
312 update_balls(&mut balls, &rigid_body_set);
313
314 // wait for existing balls to settle before spawning a new one
315 if island_manager.active_dynamic_bodies().is_empty() {
316 let mut new_ball = Ball::default();
317 let ball_body_handle = create_physics_for_ball(
318 &new_ball,
319 &mut rigid_body_set,
320 &mut collider_set,
321 &mut normal_distribution,
322 );
323 new_ball.physics_handle(ball_body_handle);
324 balls.push(new_ball);
325 }
326
327 // end simulation if a ball touches the ground
328 while let Ok(collision_event) = collision_recv.try_recv() {
329 if let CollisionEvent::Started(
330 _collider_handle_1,
331 _collider_handle_2,
332 CollisionEventFlags::SENSOR,
333 ) = collision_event
334 {
335 paused = true;
336 }
337 }
338 }
339 next_frame().await;
340 }
341 }

🥊 Placing Colliders #

You can use a translation to place a collider, like we did in the hello world example. You might also use the rotation method on the collider builder, to set the collider’s rotation in radians. If you want to set both, like we do for the left and right walls, then you can pass an isometry to the position method:

    
ColliderBuilder::cuboid(0.5 * WINDOW_HEIGHT / PHYSICS_SCALE, collider_half_thickness)
.position(Isometry::new(
vector![ translation_x, translation_y ],
std::f32::consts::FRAC_PI_2,
))
.build();

The position method sets translation and rotation and will override those values if you wet them individually.

🪚 Height Field #

A height field is a convenient and efficient way of creating a piecewise linear collider, like the saw-tooth-shaped collider at the top of the window. I use it there, as a kind of egg carton, to give the balls a natural resting position.

The height field has vertices at regular intervals along the x-axis, and you pass in the heights of the vertices (from the x-axis) in a vector:

    
let heights = DVector::from_fn(nsubdivs + 1, |i, _| if i % 2 == 0 { -1.2 } else { 0.0 });
let collider =
ColliderBuilder::heightfield(heights, vector![ceiling_width, collider_half_thickness])
.build();

Here we have a zigzag height field, which has -1.2 height for even-indexed vertices and 0.0 for odd ones.

🏝️ Island Manager #

The island manager is a register of dynamic bodies, which we can query to get a collection of all active dynamic bodies at each step:

    
if island_manager.active_dynamic_bodies().is_empty() {
// spawn new ball
// ... TRUNCATED
balls.push(new_ball);
}

In this case, we want all existing balls to settle before spawning a fresh on, so check the collection of active dynamic bodies is empty.

🛫 Ground Collision Sensor & Event Handler #

We set up the event handler in lines 280 – 282:

    
let (collision_send, collision_recv) = crossbeam::channel::unbounded();
let (contact_force_send, _contact_force_recv) = crossbeam::channel::unbounded();
let event_handler = ChannelEventCollector::new(collision_send, contact_force_send);

Then we can loop though collision events to check if any collisions involved a sensor:

    
while let Ok(collision_event) = collision_recv.try_recv() {
if let CollisionEvent::Started(
_collider_handle_1,
_collider_handle_2,
CollisionEventFlags::SENSOR,
) = collision_event
{
paused = true;
};
}

Since the ground is the only sensor, there is no need to check collision handles, we can just go ahead and pause the simulation. As well as CollisionEvent::Started, there is CollisionEvent::Stopped, which might be useful in other scenarios.

Running the Bubbles Example #

To run the example, use the following command in the Terminal:

    
cargo run --example bubbles

Please enable JavaScript to watch the video 📼

Rapier Physics with Macroquad: Rapier bubbles example

🏁 Rapier Physics with Macroquad Simulation: Next Steps #

I was impressed how quickly I could get going using Rapier physics with Macroquad, that said, it is worth spending a little more time to remove some rough corners. I might develop this into a game, like the Bubble Trouble arcade game. For that it would be worth:

  • nailing down the physics, so the balls do not roll when they hit the ceiling,
  • making the balls “stickier”, so other balls cannot knock settled balls out of position; and
  • letting the ceiling drop over time, perhaps by applying an impulse, to make the game a little harder.

I might also preserve a version, as is. I find it so relaxing! It is more satisfying than doom-scrolling social feeds 😄.

Interested to know if you might play around with the demo a little and what improvements you decide on.

🗳 Poll #

Which engine or library do you use for prototyping in Rust?
Voting reveals latest results.

🙌🏽 Rapier Physics with Macroquad: Wrapping Up #

In this Rapier Physics with Macroquad post, we got an introduction to working with Macroquad and Rapier. In particular, we saw:

  • how to configure Rapier with Macroquad;
  • porting the Rapier Getting Started code to Macroquad; and
  • more advanced Rapier features such as height fields, event handlers and the Island Manager .

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?

🙏🏽 Rapier Physics with Macroquad: 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.

blurry low resolution placeholder image ask Rodney X (formerly Twitter) avatar

Rodney

@askRodney

Just dropped a new blog post on how you can use Rapier with Macroquad for fast game prototyping.

We start with a hello world ball drop, then level up to a floating ball example with a Rapier height field.

Hope you find it useful!

https://t.co/kJ3fHi7Gjo #askRodney

— Rodney (@askRodney) May 1, 2024

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.

Thanks for reading this post. I hope you found it valuable. Please get in touch with your feedback and suggestions for posts you would like to see. Read more about me …

blurry low resolution placeholder image Rodney from Rodney Lab
TAGS:
RUSTGAMING

Likes:

Likes

  • Dylan </closingtags.com> profile avatar
Likes provided by Mastodon & X via Webmentions.

Related Posts

blurry low resolution placeholder image Jolt Physics raylib: trying 3D C++ Game Physics Engine 🎱

Jolt Physics raylib: trying 3D C++ Game Physics Engine 🎱

c++
gaming
<PREVIOUS POST
NEXT POST >
LATEST POST >>

Leave a comment …

Your information will be handled in line with our Privacy Policy .

Ask for more

1 Nov 2022 — Astro Server-Side Rendering: Edge Search Site
3 Oct 2022 — Svelte eCommerce Site: SvelteKit Snipcart Storefront
1 Sept 2022 — Get Started with SvelteKit Headless WordPress

Copyright © 2020 – 2025 Rodney Johnson. All Rights Reserved. Please read important copyright and intellectual property information.

  • Home
  • Profile
  • Plus +
  • Newsletter
  • Contact
  • Links
  • Terms of Use
  • Privacy Policy
We use cookies  to enhance visitors’ experience. Please click the “Options” button to make your choice.  Learn more here.