GravWell
Back in Februrary of 2019, I decided to embark on a little side-project. I wanted an excuse to learn how to use the fabulous BabylonJS game engine along with gaining proficiency in TypeScript, I had been avidly reading through Winchell Chung's amazing Atomic Rockets website (seriously, if you have any enthusiasm about space travel, science fiction, rockets, etc. you need to check this out!) when I came across the gravity maps section. In a flash of inspiration, I came up with GravWell.
Original post image by Josh Elster based on artwork by H.K. Wimmer
Picture a patch of space somewhere in our Solar system. According to Einstein, mass tells that piece of space how to move, and space tells mass where to move. Mass - i.e. the distribution of matter in space - gives objects their gravity, which in turn dictates the shape of space-time around that mass. The analogy most often used to describe this is as a rubber sheet, with marbles or balls of different weights causing the rubber to dimple in various ways. A star makes a deep dimple in the sheet that the far smaller and less massive planets endlessly circle, but it's important to remember that though they are less massive, those planets do have enough mass to affect the shape of space around them. The dimples are a 2-D representation of our 3-D space, but it's a good enough analogy for our purposes.
In GravWell, players must navigate their ship around a stellar system while avoiding unplanned "lithobraking" - crashing - contact with any of the celestial bodies. At all times, the celestial spheres tug at the players' ship with the force of their gravity. The ship has powerful engines, and armed with a map of the gravitational potential, players are given the tools to escape from their gravitational prison. This map (a form of height map) is computed by dividing up a 2-dimensional world map up into "chunks" of what I call "grav units", measured in meters. Due to some quirks of the underlying framework, this unit must be some power of 2 - 2, 4, 8, 16, 32, 64, 128 etc. The value this parameter takes dictates the resoution capability of the physics simulation - if the grav unit is set to 128m that means the world map is divided into chunks 128 meters in size. Each chunk is treated as a single point for purposes of computation, which when combined with the world size parameters figures the absolute minimum memory requirements for a given world size because the height map array storing this data must contain an entry for every single point on the map. This is represented as a 3-Vector (x, y, z) giving the point's location in world space. Because we are simulating a 2-D environment however, we can use the unneeded y- axis scalar to store the value for the magnitude of gravity at that point. The player's ship follows the contours of gravity as it is pulled around the system by the planetary bodies comprising that system - a central star along with 1 or more planets. When the simulation updates, planets orbit their star in pre-determined orbital paths controlled by the parameters of the primary and the planet. In other words, the planets are "on rails", and unlike the player's ship they are not (at this time) affected by masses other than the star.
As a game, GravWell isn't dissimilar from a half-pipe snowboarding type of game. Players will have points added or subtracted from their scores based on various factors such as:
- Total time to completion
- Delta-V expended (i.e., how much fuel was used by propulsion by the player)
- Number of times player corrected their course
- Orbits completed around planetary bodies
I want to be able to see the different Lagrange Points - points where the gravitational forces between two bodies cancel out - as their ships are taken along by the flow of gravity. I want players to be able to enter orbits around planets or even those L- points by intuitively using the fabulous computational power of the human mind to read a real-time gravity map. Players complete scenarios by achieving and end goal, like achieving system-escape velocity. Each scenario can have a different set of starting conditions for its' stellar sytem, with different numbers of planets, theirs and the star's mass, radius orbital distances, etc..
This is a good time to mention the performance characteristics of GravWell. Computing the gravitational field of any point in the world is a relatively light-weight operation in terms of CPU cycles. This is a good thing because as discussed above, this computation needs to be performed for every point in the height map. Every frame. There are some optimizations in place, such as skipping points inside of a planetary body or short-circuiting calculations for points extremely far from the camera, but that isn't near enough to get to a minimally desired frame rate on most hardware. JavaScript's inherently synchronous nature is naturally the reason why this is such a bottleneck to performance, and of course the easy answer to this is to perform the height map updates someplace else.
Eventually, I'd like to allow height map data to be streamed from sources external to the game - whether it's a browser worker process, a Blazor webasm, or a server-based API endpoint. This would allow for some potentially exciting simluations and scenarios that would go far beyond what GravWell is currently capable of today. For example, it's currently very difficult to balance performance above ~10fps with world sizes larger than about 9600m x 9600m. The inverse square behavior of gravity along with its pathetically weak Gravitational Constant place some constraints on the minimum mass, orbital radii, which produces some indirect constraints on the overall world and thus the height map size which then leads to poor framerates and OOM problems, etc. I've got a whole swathe of features planned for GravWell, and eventually I'm thinking about open-sourcing the whole thing and making the repos public. What do you think? Is this something you're interested in contributing towards? I'm interested in feedback - please feel free to @ or DM me on Twitter with your thoughts!