Exactly a year ago, I chose to once again try my hand at making a game, and over the course of the year, I’ve tweeted biweekly progress updates in gif format. Here are some of the highlights.
From the start, I knew I had to think optimization-first. The universe is kind of big, so I kicked things off by investigating how to only show small parts of it at a time. The first step towards this led to implementing an octree, which was great for very quick access to only visible parts of the universe, enabling selective instantiation of objects in the scene graph. I don’t think Unity would’ve been too happy about instantiating trillions of game objects.
This ended up lending itself really well to how the layer backing the scene graph was modelled as well. For example, if only 1 galaxy is going to be visible at a given moment, we shouldn’t also need to create the trillions of other galaxies that would also potentially exist, or the millions of stars in each of those galaxies.
Where this fell apart was the assumption that everything was physically static. Because the universe is constantly expanding and it’s bodies in perpetual motion, assuming a static box should contain each body was not going to be an option.
The next iteration removed binary partitioning altogether, in favour of the gravitational relationships between parent and child bodies. For example, as Earth is a parent of its one moon, it’s also a child of the sun, which is (ok, was) a child of a larger cluster of stars. Each body defines a “reach”, which encapsulates the farthest orbit of all its children, and given a “visibility distance” (ie. max distance in which objects can be viewed from the game’s camera), visible bodies are determined by the intersection of two spheres, one located at the body’s center with a radius of its reach, and one at the camera’s center with a radius of its visibility distance.
This design enabled parent and child bodies and their siblings to be created at any point, and released from memory when no longer visible.
7 Digits of Precision Will Only Go so Far
While exploring the on-demand loading architecture, I quickly ran into an issue: objects in my scene would jump around or just stop showing up altogether. When I opened up the attributes inspector panel in the editor, I started seeing this everywhere:
Vector3 uses the
float type (which is only precise to 7 digits), any object in the scene with a
Vector3 position containing a dimension approaching
1e+6 started to behave irradically as it lost precision.
Since I was using
Vector3 for both data modelling and positions of objects in the scene (no way around that), my kneejerk reaction was to start by defining my own
double-backed vector type which would at least double the numbers I could use for modelling. This got me a bit further but still wasn’t addressing how those values would be represented in the scene. At this point I was rendering the entire universe in an absolute scale where 1 unity unit equaled a certain number of kilometers, resulting in values way beyond 16 digits of precision. Fiddling with the kilometer ratio wasn’t going to solve this problem, as objects were always way too small or way too far away.
// In model land...
body.absolutePosition = body.parent.absolutePosition + body.relativePosition
// In scene land...
bodyObject.transform.position = body.absolutePosition * kmRatio;
One solution was to instead use a “contextual” coordinate system. Because the gameplay oriented around one body at a time, I could simply derive all other body positions relative to the contextual body. In other terms, the contextual body would always sit at
(0,0,0), and all other visible bodies would be relatively nearby. And because the camera would always be located near the contextual body (focusing on one body at a time), as long as my visible distance was well within the 7 digit limit of
float, I could safely convert all
double vectors into
Vector3s, or even scrap use of
double in this context entirely.
var ctxPos = contextualBody.absolutePosition;
var bodPos = body.absolutePosition;
bodyObject.transform.position = ctxPos - bodPos;
// Huge loss of precision
This ended up working until it didn’t, which was very soon. Here’s a fun little phenomenon:
(1e+16 + 2) == (1e+16 + 3)
(1e+17 + 2) == (1e+17 + 3)
Despite using this “contextual position” for scene objects, the actual values being calculated were still being truncated pretty badly even before being turned into
Vector3s, whenever the contextual position was a large enough value (ie. containing a dimension approaching or exceeding 1e+17). My kneejerk was to once again supersize my custom vectors and turn all those
decimals to get even more precision (~29 digits). This felt extremely inelegant and lazy.
With the goal of making all position values as small as possible, I decided to just scrap the universe-space
absolutePosition design altogether, in favour of something a little more clever and cost-effective.
One way to avoid
absolutePosition in contextual position calculation was to rely instead on relational information, such as the position of a body relative to it’s parent. Since
absolutePosition could be derived by crawling up the relation graph and summing relative positions, the same value could be calculated by instead finding the lowest common ancestor of both contextual and given body, and calculate their distance relative to it. Effectively, shortening the resulting value significantly.
var lca = lowestCommonAncestor(contextualBody, body);
var ctxPos = contextualBody.relativePosition(relativeTo: lca);
var bodPos = body.relativePosition(relativeTo: lca);
// if body == contextualBody, this is (0,0,0)
bodyObject.transform.position = contextualBodyPosition - bodyPosition;
The result? Values well below that 7 digit ceiling! 🎉
Visuals and Interaction
One of the biggest challenges of this project has been visuals. Specifically choosing the right kind of UI element for interacting with non-conventional types of information, such as small points in 3D space. Over time, this project has seen so many iterations for how to best solve these problems:
Another challenge has been figuring out how to interact with the surface of a planet. Although this gameplay mechanic will likely be cut in favour of a coarser grain level of interaction, a big part of it was figuring out how to evenly distribute points on a sphere, and whether or not that should be done in 3D or in 2D:
Navigating between a contextual body and it’s parent or children is an ongoing challenge, as it’s a constant battle between utility and simplicity, and needless complexity creeps up all the time. For example, the first iteration of navigation had a neat yet unnecessary mosaic of boxes around the top of the screen representing individual bodies that could be navigated to. I decided to dramatically simplify for a few reasons:
- The gameplay doesn’t necessitate travelling between multiple layers at a time (eg. moon -> galaxy)
- The hierarchical structure wasn’t being communicated very well with boxes stacking horizontally and vertically
- Individual boxes didn’t clearly describe the body they represented, and icons weren’t going to be enough.
The current gameplay now shows one body at a time in the navigation and visible bodies can be interacted with more contextually, via 2D UI following objects in 3D space.
With the universe sandbox in a stable place, I’m hoping to shift all energy now towards gameplay and game mechanics. Because I’m hoping to implement a real-time strategy element, I need think about how resources are mined, how territory is captured, how interaction with other players and AI works, and a lot more.
If you’re interested in learning more about this project, email me at firstname.lastname@example.org or contact me on twitter at @_gabrieloc.