Rapier Physics with Units of Measurement: Utilize Rust Types
Tags: rust, gamedev
Published: 2024-05-15 17:14:45

---
title: "Rapier Physics with Units of Measurement: Utilize Rust Types"
published: "true"
description: "Rapier Physics with Units of Measurement 📏 leveraging the Rust type system for SI units for Rust game dev physical properties ⏲️."
tags: "rust, gamedev"
canonical_url: "https://rodneylab.com/rapier-physics-with-units-of-measurement/"
cover_image: "https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qeqrsir6s3284q7048bm.png"
---
## 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&rsquo;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&rsquo;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`:

```toml
[dependencies]
# ...TRUNCATED
rapier2d = { version = "0.19.0", features = ["simd-stable"] }
uom = "0.36.0"
```

For my use case, this worked just fine (with default features), though you might need to <a href="https://docs.rs/crate/uom/latest/features">tweak the `uom`  features</a>, 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):

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

#[macro_use]
extern crate uom;
```

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

In this post, we use the example from the earlier <a  href="https://rodneylab.com/rapier-physics-with-macroquad/">Rapier Physics with Macroquad floating bubble post</a>.  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.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vgwvnntxs7gsll5d01zw.png)

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:

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

`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 `f32`s whenever I needed to pass the value to Rapier.  You could go a step further and use <a href="https://www.lpalmieri.com/posts/2020-12-11-zero-to-production-6-domain-modelling/">type-driven development</a>, where (by design) only validated quantities can get passed to the Rapier physics engine.

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

```rust
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();
```

`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:

```rust
unit! {
    system: uom::si;
    quantity: uom::si::length;
 
    // 1 metre is 50 px
    @pixel: 0.02; "px", "pixel", "pixels";
}
```

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

Here:

- `system` adds the new unit to the <a href="https://docs.rs/uom/latest/uom/si/index.html">`uom` in-built SI units</a>;
- `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:

```rust
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
}
```
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:

```rust
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,
        );
    }
}
```

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&rsquo;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 <strong>use the <code>uom</code> Rust crate to add SI units of measurement to game physics</strong>;
- 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 <a href="https://github.com/rodneylab/rapier-units-of-measurement">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?

## 🙏🏽 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 <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.