Gravity Well Arena

Top-down combat built on the Schwarzschild metric, Verlet integration, and null geodesics

Screenshot of Gravity Well Arena
Gravity Well Arena's callsign entry screen.

I wanted to build a game where the physics weren't decorative. A lot of space games put a black hole in the background and give it a slight pull on the player's ship, but it doesn't actually change how you think about the game. The idea behind Gravity Well Arena was to take Schwarzschild time dilation, the real thing from general relativity, and make it the mechanic that every other system depends on.

Simulated gravitational lensing by a black hole
Simulated gravitational lensing by a black hole in front of the Large Magellanic Cloud. Wikimedia Commons

You pilot a ship around black holes. When you dive closer to a gravity well, the simulation dilates: the external universe speeds up relative to you. Enemies move faster, projectiles close in quicker, everything outside your local frame accelerates. But your own controls stay at normal speed, because the game runs on the player's proper time. This creates a situation where going deeper into a well gives you a tactical edge (from your perspective, the world is in slow motion and you have more time to aim) but simultaneously makes the environment more dangerous (from the world's perspective, you're reacting in slow motion). Balancing these two effects is the entire game.

The Schwarzschild metric and time dilation

Gravitational time dilation diagram
Gravitational time dilation near a massive object. Clocks closer to the mass tick slower relative to distant observers. Wikimedia Commons

The Schwarzschild metric describes the geometry of spacetime around a non-rotating, uncharged mass \(M\). The time dilation factor at radial coordinate \(r\) from the center is:

\[\frac{d\tau}{dt} = \sqrt{1 - \frac{r_s}{r}}, \qquad r_s = \frac{2GM}{c^2}\]

where \(\tau\) is proper time (the clock of the local observer), \(t\) is coordinate time (the distant observer's clock), and \(r_s\) is the Schwarzschild radius. As \(r \to r_s\), the ratio \(d\tau/dt \to 0\): local time freezes relative to the outside universe. At \(r = \infty\) the ratio is 1 and there is no dilation.

The game maps this directly onto gameplay. Each frame, the simulation computes the dilation factor at the player's position and scales the time step for all external entities accordingly. If the player is deep in the gravity well with \(d\tau/dt = 0.3\), every enemy, projectile, and environmental hazard ticks at \(1/0.3 \approx 3.3\times\) the player's rate. The player's own controls tick at their normal rate, since the game runs in the player's proper time frame. The result: diving deep gives you more time to react (the world looks slow-motion from your frame), but the world perceives you as nearly frozen.

Orbital mechanics

Ship and projectile trajectories are integrated using Stormer-Verlet integration, a symplectic method that conserves energy over long trajectories better than Euler integration. The update for position \(\mathbf{x}\) with acceleration \(\mathbf{a}\) is:

\[\mathbf{x}(t + \Delta t) = 2\mathbf{x}(t) - \mathbf{x}(t - \Delta t) + \mathbf{a}(t)\,\Delta t^2\]

The gravitational acceleration from a mass at position \(\mathbf{p}\) follows the Newtonian inverse-square law: \(\mathbf{a} = -GM\,(\mathbf{x} - \mathbf{p}) / |\mathbf{x} - \mathbf{p}|^3\). For binary black hole levels, accelerations from both masses are summed, producing a restricted three-body problem where the two black holes orbit each other on fixed paths while the player's ship (negligible mass) moves in their combined field. The overlapping dilation fields create regions where time dilation changes rapidly with position, making trajectory prediction difficult.

Some levels have two black holes orbiting each other. Binary systems create chaotic dynamics where the dilation fields overlap and interfere. You can slingshot between the two wells, picking up speed from one and using the other's dilation to create a timing advantage, but the trajectories become very difficult to predict.

Weapons

There are six weapons, each designed to interact with gravity wells differently. The railgun fires a fast kinetic projectile that curves in gravitational fields (Verlet-integrated like the ship). The mass driver is slower but heavier. The photon lance fires along null geodesics (\(ds^2 = 0\)), tracing the path light would follow through curved spacetime. In practice, you can shoot it around a black hole and hit a target on the other side, because the beam bends toward the mass just as starlight bends around the sun. The gravity bomb drops a temporary point mass that distorts nearby trajectories, effectively adding a term to the gravitational potential. The impulse rocket delivers an orbital kick on impact, shoving targets deeper into the gravity well (into stronger dilation). The tidal mine settles into a stable orbit and detonates when something passes at a different altitude, dealing damage proportional to the tidal gradient \(\partial^2 \Phi / \partial r^2\) between the two altitudes.

The weapon balance ended up being shaped by the dilation mechanic in ways I didn't fully anticipate. Weapons that are slow in flat space become threatening in dilated space because the target is experiencing compressed time. This meant that the "weak" weapons (mines, gravity bombs) become the most dangerous in the deep zones where dilation is strongest.

Bot archetypes

I built six AI personalities that each try to exploit the time dilation mechanic from a different angle. The Skirmisher stays mobile and avoids committing to deep orbits. The Diver rushes into the Furnace or Abyss for burst damage. The Vulture stays at the Rim and picks off weakened targets at range. The Anchor locks down a zone with area-denial weapons. The Swarm uses multiple small ships in coordinated formations. The Commander is an elite solo operator that actively counters the player's depth strategy, climbing when you dive and diving when you climb.

The Diver was the hardest to tune. Its strategy requires going deep, which means high dilation, which means everything outside is fast. If the Diver's aggression threshold is too high, it suicides into the Abyss and can't react to incoming fire. If it's too low, it never commits and plays like a worse Skirmisher. Getting the balance took many iterations of adjusting how the bot evaluates the risk of its current depth against the potential damage output.

Procedural audio

There are zero audio files in the repository. Every sound, engine hum, weapon discharge, explosion, ambient tone, is synthesized at runtime. I initially planned to add audio files later and used procedural synthesis as a placeholder, but the procedural sounds ended up sounding better than anything I found in free asset libraries. The engine hum pitch-shifts with dilation, which gives you an audio cue for how deep you are without looking at the HUD. That was accidental but it works.

Everything else

Rendering uses wgpu with custom WGSL shaders for gravitational lensing (background stars distort near wells), bloom, and accretion disk effects. Levels are procedurally generated from deterministic seeds. There's a four-act narrative delivered through mission briefings and radio chatter, which was an interesting constraint to work within given the procedural structure.

Global leaderboards run on a Cloudflare Worker backed by D1 SQLite. Server-side validation checks score plausibility (is this score achievable on this seed?) rather than replaying the entire game. The leaderboard is offline-first: scores save locally and sync to the cloud when connectivity is available. Player identity is UUID-based, no login required.

The game compiles to native binaries for Windows, macOS, and Linux, and also to WebAssembly for the browser version.