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
#
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 #
uom
to your Project #
You can just add uom
to your Cargo.toml
:
1 [dependencies]2 # ...TRUNCATED3 rapier2d = { version = "0.23.1", features = ["simd-stable"] }4 uom = "0.36.0"
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):
29 use uom::{30 si::{31 f32::{Length, Velocity},32 length, velocity,33 },34 unit,35 };3637 #[macro_use]38 extern crate uom;
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 #

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:
52 #[derive(Debug)]53 struct Ball {54 radius: Length,55 position: Vector2<Length>,56 physics_handle: Option<RigidBodyHandle>,57 colour: Color,58 }
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 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:
102 let x_velocity: Velocity =103 Velocity::new::<velocity::meter_per_second>(pseudo_random_value);104 let y_velocity: Velocity = Velocity::new::<velocity::meter_per_second>(1.0);105106 let linear_velocity = vector![x_velocity.value, y_velocity.value];107108 let rigid_body = RigidBodyBuilder::dynamic()109 .translation(vector![ball.position.x.value, ball.position.y.value])110 .linvel(linear_velocity)111 .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 theuom
types. -
For
rapier2d
, I need to pass the velocity components asf32
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 annalgebra
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";}
Remember to include the snippet mentioned above if you use this macro.
Here:
-
system
adds the new unit to theuom
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}
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:
190 fn draw_balls(balls: &[Ball]) {191 for ball in balls {192 let Ball {193 colour,194 position,195 radius,196 ..197 } = ball;198 draw_circle(199 position.x.get::<pixel>(),200 -position.y.get::<pixel>(),201 radius.get::<pixel>(),202 *colour,203 );204 }205 }
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.
Please enable JavaScript to watch the video 📼
🗳 Poll #
🙌🏽 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.
Just dropped a new blog post on adding physical units of measurement to Macroquad game with Rapier physics.
— Rodney (@askRodney) May 8, 2024
uom crate helps:
— convert between rendering and physics units;
— sense check calcs; and
— define custom quantities.
Hope you find it useful!
https://t.co/RbBvvr3t5g
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.