DEV Community

Cover image for Rapier Physics with Macroquad: Rust Game Physics
Rodney Lab
Rodney Lab

Posted on • Originally published at rodneylab.com

Rapier Physics with Macroquad: Rust Game Physics

Unit of Measurement in Game Dev

In this post, we will take a quick look at Rapier physics with units of measurement (UOM). In a recent post, trying out the Rust Rapier physics engine with Macroquad, we noted that Rapier recommends using full-scale measurements for more realistic simulations. We opted for SI units (metres for distance and metres per second for velocities). There was necessary conversion from these physical units to pixels, for rendering.

This got me thinking about leveraging Rust’s types for conversion between the physics and graphics units. I discovered the Rust uom crate, which has applications in Aerospace. In his Rust Nation UK talk, Lachezar Lechev mentioned how confusion over units might have been behind a costly financial loss in a real-world Aerospace project.

If professional teams working full-time on a project with such high financial stakes can make mistakes, then, probably, anyone is prone to making similar mistakes. So, I considered adding UOM to my Rust game stack and talk about how I set it up in this post.

📏 uom

The uom crate, has applications in Aerospace and Aeronautical Engineering. By defining quantities with a unit of measurement, it can catch basic errors at compile time. As an example, you might define the speed of a ball in metres-per-second, and its mass in kilograms. Now, if you try (erroneously) to add the mass to the velocity, in your Rust code — something that doesn’t make sense physically — you will get a compile-time error, potentially saving you debugging an error, which might be hard to catch.

uom also helps with conversions, so you can safely add a displacement in kilometres to a diameter in metres.

⚙️ Adding uom to your Project

You can just add uom to your Cargo.toml:

[dependencies]
# ...TRUNCATED
rapier2d = { version = "0.18.0", features = ["simd-stable"] }
uom = "0.36.0"
Enter fullscreen mode Exit fullscreen mode

For my use case, this worked just fine (with default features), though you might need to tweak the uom features, depending on your use case.

To define a custom pixel unit for conversion between the physics and rendering systems (using the uom unit macro), I also needed to add this snippet to my Rust source (last two lines):

use uom::{
    si::{
        f32::{Length, Velocity},
        length, velocity,
    },
    unit,
};

#[macro_use]
extern crate uom;
Enter fullscreen mode Exit fullscreen mode

We come to the full definition of the custom unit later.

In this post, we use the example from the earlier Rapier Physics with Macroquad floating bubble post. In the following section, we see some snippets where I added units to that code. Find a link to the full code repo further down.

Domain Units of Measurement

Rapier Physics with Units of Measurement: 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.

The demo features floating bubbles or balls. For the demo domain, I use uom to define the ball with typed values in SI units. For rendering, I will need to convert these to pixels, and for use with Rapier, I will need a raw float value.

Here is the ball struct Rust code:

#[derive(Debug)]
struct Ball {
    radius: Length,
    position: Vector2<Length>,
    physics_handle: Option<RigidBodyHandle>,
    colour: Color,
}
Enter fullscreen mode Exit fullscreen mode

uom has a predefined Length type alias using standard SI units for length quantities (metres). I use it here to set the type for the ball radius and current displacement within the simulation world.

🗡️ Rapier Physics Units

I kept things simple, and used SI units (with uom) within my own code, and converted the values to f32s whenever I needed to pass the value to Rapier. You could go a step further and use type-driven development, where (by design) only validated quantities can get passed to the Rapier physics engine.

Here is a code snippet, defining a new ball’s velocity and initializing it with Rapier:

let x_velocity: Velocity =
        Velocity::new::<velocity::meter_per_second>(pseudo_random_value);
let y_velocity: Velocity = Velocity::new::<velocity::meter_per_second>(1.0);

let linear_velocity = vector![x_velocity.value, y_velocity.value];

let rigid_body = RigidBodyBuilder::dynamic()
        .translation(vector![ball.position.x.value, ball.position.y.value])
        .linvel(linear_velocity)
        .build();
Enter fullscreen mode Exit fullscreen mode

Velocity is a predefined uom type aliases (like Length).

  • In the first line, above, I defined x_velocity to be some random float, and associated metres per second as units, using the uom types.
  • For rapier2d, I need to pass the velocity components as f32 values, so extract the raw value from the two, typed velocities via the .value field.
  • Finally, in the last line we pass the linear_velocity, as an nalgebra Vector2 of 32-bit floats (expected by Rapier).

The example might seem a little contrived, as I convert a 32-bit float to a uom velocity, and then immediately convert it back to a float for consumption by Rapier. We shall see in a later section, though, that you can tweak this slightly to define a value in one unit, and then extract a converted value in another unit for passing to Macroquad for rendering.

🖥️ Macroquad Render Units of Measurement

For rendering, I am using Macroquad, which works with pixels. In the previous post, I set a scale of 50 pixels per metre. I formalized that here using a uom custom unit.

Custom Pixel Unit

uom provides the unit macro for defining custom units, needed in your domain. I used that macro to define a new pixel unit as a length measurement:

unit! {
    system: uom::si;
    quantity: uom::si::length;

    // 1 metre is 50 px
    @pixel: 0.02; "px", "pixel", "pixels";
}
Enter fullscreen mode Exit fullscreen mode

Remember to include the snippet mentioned above if you use this macro.

Here:

  • system adds the new unit to the uom in-built SI units;
  • quantity defines the unit as a length; and
  • @pixel, in the final line, gives the abbreviation, singular and plural names for the unit.

Now, we can define variables using this new unit as a type, and convert between other units. As an example, the get_max_balls function uses quantities in both pixels and metres to determine the maximum number of balls that can fit across the app window, given the window has a pre-determined width:

fn get_max_balls() -> u32 {
    let window_width = Length::new::<pixel>(WINDOW_WIDTH);
    let ball_radius = Length::new::<length::meter>(BALL_RADIUS);

    (window_width / (2.0 * ball_radius)).value.floor() as u32
}
Enter fullscreen mode Exit fullscreen mode

Here, window_width is defined in pixels and ball_radius, in metres. Notice, we use .value (as in the previous example) to extract the raw float value. WINDOW_WIDTH and BALL_RADIUS are raw f32 constants.

To convert a length between different length quantities (for example metres to pixels), we can call the get method on the quantity. For example, here is a snippet for rendering the ball where we need to convert the internal metre lengths to pixels:

fn draw_balls(balls: &[Ball]) {
    for ball in balls {
        let Ball {
            colour,
            position,
            radius,
            ..
        } = ball;
        draw_circle(
            position.x.get::<pixel>(),
            -position.y.get::<pixel>(),
            radius.get::<pixel>(),
            *colour,
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

The position and radius are all stored in metre values internally, yet there is no need to make sure we have the right conversion factor to get pixels out; the custom uom does that for us. Although the calculations are relatively simple to perform manually, converting automatically can save you making a simple mistake when revisiting code you haven’t seen in a while.

🙌🏽 Rapier Physics with Units of Measurement: Wrapping Up

In this post on Rapier Physics with Units of Measurement, we got an introduction to working with units of measurement with Rapier. In particular, we saw:

  • how you can use the uom Rust crate to add SI units of measurement to game physics;
  • how you can define custom quantities and convert between units; and
  • interface with code that does not expect units.

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 Units of Measurement: 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.

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.

Top comments (0)