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 📼
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.

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
:
[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.73rust-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.0"rand_distr = "0.5.1"rapier2d = { version = "0.23.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
andrand_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.
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 };1718 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 };3031 const WINDOW_WIDTH: f32 = 1366.0;32 const WINDOW_HEIGHT: f32 = 768.0;33 // 1 metre is 50 pixels34 const PHYSICS_SCALE: f32 = 50.0;3536 #[derive(Debug)]37 struct Ball {38 radius: f32,39 position: Vector2<f32>,40 }4142 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 }5354 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 }6465 #[macroquad::main(conf)]66 async fn main() {67 let mut ball = Ball::default();6869 let mut rigid_body_set = RigidBodySet::new();70 let mut collider_set = ColliderSet::new();7172 // Create the ground73 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_thickness78 ])79 .build();80 collider_set.insert(collider);8182 // Create the bouncing ball83 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);8990 // Create other structures necessary for the simulation91 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 = ();103104 // run the game loop, stepping the simulation once per frame.105 loop {106 clear_background(GUNMETAL);107108 if is_key_released(KeyCode::Escape) {109 break;110 }111112 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 );133134 let ball_body = &rigid_body_set[ball_body_handle];135 println!("Ball altitude: {}", ball_body.translation().y);136137 // update ball position (used for drawing)138 ball.position = *ball_body.translation();139140 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 line91
(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

🫧 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.

Bubble Example Code #
Here is the code for the example, which I saved to examples/bubbles.rs
:
examples/bubbles.rs
— click to expand code.
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 };2425 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 };3132 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 };5657 const BALL_COLOURS: [Color; 4] = [BLUE_CRAYOLA, CARROT_ORANGE, CELESTIAL_BLUE, SUNGLOW];5859 const WINDOW_WIDTH: f32 = 1366.0;60 const WINDOW_HEIGHT: f32 = 768.0;61 // 1 metre is 50 pixels62 const PHYSICS_SCALE: f32 = 50.0;63 const BALL_RADIUS: f32 = 0.6;6465 #[derive(Debug)]66 struct Ball {67 radius: f32,68 position: Vector2<f32>,69 physics_handle: Option<RigidBodyHandle>,70 colour: Color,71 }7273 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 }8687 impl Ball {88 fn physics_handle(&mut self, physics_handle: RigidBodyHandle) -> &mut Ball {89 self.physics_handle = Some(physics_handle);90 self91 }92 }9394 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 }104105 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_handle126 }127128 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_thickness139 ])140 .friction(1.0)141 .restitution(0.0)142 .build();143 collider_set.insert(collider);144 }145146 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_thickness152 ])153 .sensor(true)154 .build();155 collider_set.insert(collider);156 }157158 fn create_side_walls(gap: f32, collider_set: &mut ColliderSet) {159 // left wall160 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);172173 // right wall174 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 }187188 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 }204205 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 }213214 #[macroquad::main(conf)]215 async fn main() {216 // seed macroquad random number generator217 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]218 {219 srand(date::now().floor() as u64);220 }221222 let mut rigid_body_set = RigidBodySet::new();223 let mut collider_set = ColliderSet::new();224225 // Create the ground226 create_ground(&mut collider_set);227228 // Create the ceiling229 // maximum number of balls that can fit across the window230 let max_balls: u32;231 #[allow(232 clippy::cast_precision_loss,233 clippy::cast_sign_loss,234 clippy::cast_possible_truncation235 )]236 {237 max_balls = ((WINDOW_WIDTH / PHYSICS_SCALE) / (2.0 * BALL_RADIUS)).floor() as u32;238 }239240 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);246247 // gap at left and right edge of window248 let gap = 0.5 * ((WINDOW_WIDTH / PHYSICS_SCALE) - ceiling_width);249250 // Create the left and right wall251 create_side_walls(gap, &mut collider_set);252253 // Create ball254 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);265266 // Create other structures necessary for the simulation267 // 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);282283 let mut paused = false;284285 // run the game loop, stepping the simulation once per frame.286 loop {287 if is_key_released(KeyCode::Escape) {288 break;289 }290291 clear_background(GUNMETAL);292 draw_balls(&balls);293294 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 );310311 // update ball positions (used for drawing)312 update_balls(&mut balls, &rigid_body_set);313314 // wait for existing balls to settle before spawning a new one315 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 }326327 // end simulation if a ball touches the ground328 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_event334 {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// ... TRUNCATEDballs.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 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 #
🙌🏽 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.
Just dropped a new blog post on how you can use Rapier with Macroquad for fast game prototyping.
— Rodney (@askRodney) May 1, 2024
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
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.