As discussed in this post there are some potential advantages in representing physical quantities like position, velocity, and similar in a type-safe way.
Instead of using simple floating point types, we can write our own wrapper types that have semantic meanings like ‘length’, ‘speed’, or ‘time’ and only allow physically correct operations between them.
That is exactly what we will look into here. We will implement the basic types for such a system, and show what operations between them are relevant and well defined.
Physical units of interest
Of course there is a large number of possible physical units that we could try and represent. If we were to make a type for every single such unit, we would have a lot of work to do.
More importantly, there are an infinite number of unit combinations. For instance, we could consider not only units like length, but also length squared, or cubed, or to any other power.
It is of course entirely possible to write a system supporting such arbitrary units.
However, since this blog is about game development, we will limit ourself to the kind of units and their combinations that are at the core of almost any game.
This means will limit ourselves entirely to the types that are related to the movement of objects. Of course our approach can be used for other areas as well, but speaking of locations and velocities will be a great and familiar example – and the most useful for actual games.
In the previous post we already mentioned the example types of
Time, corresponding to the units of distance, distance/time, distance/(time*time), and time itself respectively.
The relationship between these types is fairy easy, so we will use them as our starting point.
Note that we are not actually talking about ‘meters’ or ‘seconds’ for the units of our types. I want to refrain from such qualifications, since many games may not use them as their basic unit of measurements. One unit of distance might as well refer to one kilometer, mile, or an arbitrary length like the width of a tile. Similar, games might measure time in months, years, or milliseconds. By talking about ‘distance units’ and ‘time units’ instead of these concrete examples, we remain much more flexible and make our code more reusable without causing confusion.
To begin as easy as possible, let us start with only a single dimension of space.
In this one dimension, an objects location can be represented by a single signed number. The same goes for it’s velocity and acceleration. Unsurprisingly, time is exactly one single dimension as well, and can also be represented as such.
Since the names of
Velocity imply directions, I am going to instead use the names of
Speed to represent distance/length and velocity in our single dimension.
The choice of these names is mostly esoteric, but I think they result in neat and easily understood code as well.
Note: Physically speaking, speed would be undirected in the sense that it cannot be negative. I still think that it is a better choice of name however, which I hope will become clear below.
Timespans vs. timestamps
To be able to use
Acceleration in the same equation, we need types to represent the concept of time.
What I noticed when implementing this was that there is a distinct semantic difference between the concepts of ‘timespan’ (the length of an interval of time) and ‘timestamp’ (a fixed point in time).
Both of these concepts have the unit ‘time’ (for example measured in seconds), but we still would not want to use them interchangeably.
For example, it makes no semantic sense to add two timestamps: 9:00 + 10:00 = ??. While this is mathematically allowed, it makes no sense in context. Adding timespans however makes perfect sense: 1h + 2h = 3h.
Since the whole reason we are writing any of these types is to be able to detect their misuse and bugs caused by it at compile time, I decided to add two distinct types for the two semantic meanings:
Position vs. distance/length
You may notice that our
Unit type could be split in a similar fashion as
Instant. There is a clear semantic difference between the concept of position, and the difference between two positions, even though both are measured in the same unit of length (for example meters).
However, I think that this would be an unnecessary over complication.
In fact, I tried splitting the type like that, but quickly discovered that values representing one dimensional absolute positions are hardly ever used, except as components of higher dimensional vectors, and a few special cases.
Therefore I made the decision not to make this distinction here. Of course, if you think that the distinction is important, you can make the appropriate changes. Also feel free to let me know if you think you have a good argument I may have overlooked.
Operations within and between types
Most of the types are very similar when it comes to their operations that do not interact with the other ones.
For example, except for
Instant (because of its absolute nature), they all are proper one dimensional vector spaces. That means that for example two
Units can be added, subtracted, and that any
Unit value can be multiplied by a floating point scalar (or divided by an inverse scalar).
In addition, we could divide one
Unit value by another, resulting in a scalar representing the ratio of the two values.
The same operations work just as well for
Instant is more limited, since its definition is a stricter one. In fact, we can hardly do anything with it that does not involve any of the other types.
What we can do however is subtract two
Instants to receive a
Timespan representing the interval or difference between the two. And of course we can add a
Timespan to an
Instant to get a new
Instant to progress time.
Further, we can integrate
Speed by multiplying it with a
Timespan, returning a
Unit value. Similarly,
Acceleration integrates to
On the other side we can also differentiate
Speed values by dividing by a
Timespan to get
Acceleration values respectively.
In total that makes for the following list of possible operations:
// vector fields (with unit as example) // the same also for speed, acceleration and timespan unit + unit = unit unit - unit = unit unit * scalar = unit unit / scalar = unit unit / unit = scalar // instant instant - instant = timespan instant + timespan = instant // integrations speed * time = unit acceleration * time = speed // differentiation unit / time = speed speed / time = acceleration
These are all the operations we need to be able to do arithmetic with these types in a completely type-safe fashion.
Note that I have not given any actual code. The implementation of any of these operators are simple one-liners, and the types itself are nothing more than a wrapper around a floating point value.
If you are interested in my implementation however, feel free to check the full code on GitHub.
I hope this small introduction into the implementation of type-safe physical unit types has been interesting to you.
If so, make sure to share the post on your favourite social media.
Make sure to check back next week when I will talk about implementing similar types for more than one-dimensional spaces.
And of course feel free to leave a comment if you have any questions or other feedback.
Enjoy the pixels!