DEVELOPER DIARY
HOW GRAVITYDRIFT WAS BUILT — A SOLO DEV STORY
This is the full development diary for Gravitydrift. I started writing it partway through the project to have a record of the decisions, the mistakes, and the things that surprised me. I've since filled in the early entries from memory and notes. It's long. That's the point.
If you just want a quick summary: I'm a solo developer, I built the whole thing with vanilla JavaScript and a Canvas 2D renderer, no engine, no framework. It started as a weekend experiment in January and turned into something I'm genuinely proud of. The rest is details.
Gravitydrift is free to play. If you haven't tried it yet, start at the main menu. The diary will make more sense if you've actually touched the game.
January — Week 1
The Idea
Origin
It started with a YouTube video about the Voyager missions. There was an animation showing how NASA used Jupiter's gravity to fling Voyager 2 onto a trajectory toward Saturn — and then Saturn's gravity bent it toward Uranus, and so on. Four planets, one spacecraft, almost no fuel beyond what it took to get out of Earth's atmosphere. The whole thing felt elegant in a way that equations on a textbook page never quite communicate.
I kept thinking about that. The idea that you could aim a spacecraft not at where the planet is, but at where it will be, and that the gravity would do the rest. It's a kind of problem-solving that feels almost like a trick, like you're cheating the universe, except you're not — you're just reading it more carefully than most people bother to.
A week or so later I was procrastinating, the way you do, and I found myself sketching out on paper what a game based on that idea would feel like. You draw back a ship like a slingshot. You let go. The ship arcs through space, bends around planets, and either makes it to the destination or vanishes into the dark. Simple. Satisfying.
I made a note at the time: "needs to feel like you're reading the universe, not fighting it."
I've built games before — nothing released, just personal projects that got halfway done and then life happened. This felt different because the core mechanic was so clear. It wasn't a platformer where I'd have to design twenty levels of increasing complexity. It was one mechanic: drag, release, watch. Gravitydrift. Even that name came quickly, which is usually a sign I'm onto something.
I gave myself the weekend to build a prototype. I told myself if it wasn't fun by Sunday evening I'd drop it. By Sunday afternoon I had a ship orbiting a planet, which wasn't what I planned, but it was so satisfying to watch that I knew the game was real.
January — Week 1–2
First Physics Loop
Physics
I deliberately chose not to use a game engine. I've used Unity, I've used Phaser, and while both are fine tools, they come with a cost: you spend a significant portion of your time learning the framework rather than building the game. For something this physics-heavy, I wanted to understand every line of the simulation.
The physics are straightforward Newtonian gravity. For each planet, every frame, you calculate the vector from the ship to the planet, find the distance, apply Newton's law:
Where G is a tunable gravitational constant, M is the planet's mass (derived from its radius and a density factor), and r is the distance between the ship and the planet's center. The force vector is then added to the ship's velocity, and the velocity is added to the ship's position. That's Euler integration — basic, but perfectly good for a game.
The first thing I discovered is that the gravitational constant G needs to be wildly different from the real physical constant for a game to be playable. Real gravity works over astronomical distances. In a game running on a 1800×900 pixel canvas, distances are in the hundreds of units at most. I set G = 6000 and tuned from there. The number is meaningless in real terms; it just produces the right feel.
The second thing I discovered is that Euler integration is unstable if your timestep is too large. When the ship gets very close to a planet, the acceleration is enormous (r is small, so r² is tiny, so the force is huge), and if you're only updating physics once per frame at 60Hz, the ship can tunnel through the planet entirely. The fix is to step physics at a fixed small timestep — I settled on 1/60th of a second — and accumulate real time to decide how many steps to take per frame. This means the physics behave identically regardless of whether the player is on a 60Hz phone or a 144Hz monitor. It also means the simulation is deterministic: the same initial conditions will always produce the same trajectory.
Getting that determinism right turned out to be important in ways I didn't anticipate. More on that later.
By the end of week two, I had a ship that responded to gravity correctly. It would fall toward planets, orbit if the initial velocity was right, and fly away on hyperbolic trajectories if it had enough speed. I had no renderer beyond fillRect squares for the ship and circles for planets, but the physics were solid. I spent probably three evenings just watching the ship move and adjusting the constants.
January — Week 2–3
Getting Something On Screen
Renderer
Visuals
The renderer started as a single function that clears the canvas, draws circles for planets, a small square for the ship, and a dot trail behind it. About twenty lines of code. It worked fine for prototyping but it looked awful, and I knew from past experience that looking awful kills motivation. I needed the game to look like a game.
The aesthetic I was going for was minimalist space: dark navy background, subtle star field, clean geometry. No pixel art, no cartoon style — something that felt like looking at a real mission control display. The color palette locked in early: deep near-black background (#080818), gold highlights (#ffd84d), cool blue for UI elements. Those three colors do almost everything in the game.
Stars were one of the first visual improvements. I generate a fixed set of random star positions when the canvas is created, then draw them once per frame as tiny circles with slightly randomized opacity. It's cheap and it works. The important thing is that the stars are tied to the coordinate system, not the screen, so they scroll correctly as an anchoring element even though the game doesn't actually scroll.
The planet images came later. For the prototype I used procedural circles — a filled circle with a radial gradient glow effect. That actually looked okay for the smaller planet types, and I kept a version of it for the glow/aura layer that still exists under the sprites today. Eventually I brought in actual PNG assets: nine planet types ranging from a soft white dwarf to a black hole. Getting those images to render at the right size and to look sharp on high-DPI screens required a DPR (device pixel ratio) fix that I'll talk about in the mobile section.
The trail was one of those decisions that seems trivial but turned out to define the game's feel. The ship leaves a fading dotted line behind it as it flies. The trail stores the last 900 positions — fifteen seconds of flight at 60Hz — and draws each dot with decreasing opacity toward the tail. It serves two purposes: aesthetically, it makes the trajectory visible and beautiful. Functionally, when you crash and retry, the previous trail lingers on screen in a dimmer color, giving you a visual reference for where you went wrong. That "ghost trail" feature came from a moment of laziness — I forgot to clear it on reset — and it turned out to be one of the best design decisions in the game.
The ghost trail happened by accident. I just forgot to clear it. Kept it immediately.
The destination ring is a glowing circle with a pulsing animation driven by a sine wave on the glow opacity. It took maybe forty minutes to get looking right and it's been unchanged since January. Some things just work on the first try.
January — Week 3
Drag to Launch
Input
Feel
The launch mechanic took longer to get right than the physics did. The idea is simple: you press and hold on the ship, drag outward, and release to launch. The ship fires in the opposite direction of your drag, like a real slingshot. The further you drag, the faster it launches.
The first version just mapped drag distance directly to velocity. It worked, but the maximum speed was too variable — on a large screen you could drag further and get more speed, which introduced a screen-size dependence into the gameplay. The fix was to cap the drag radius at a fraction of the smaller canvas dimension and then scale velocity proportionally. On any screen, the maximum drag gives the maximum launch speed.
I added a visual arrow that draws from the ship in the launch direction, with its length representing the launch speed. The arrow uses a Canvas2D path with an arrowhead. This gives the player a clear preview of the launch before releasing. Without it, aiming felt like guesswork.
Touch input was wired up alongside mouse input from the start — same handlers, just different event types. touchstart → mousedown equivalent, and so on. Getting the coordinates right required subtracting the canvas's bounding rect from the touch position, and also accounting for the letterbox offset (the black bars on non-16:9 screens). That letterbox calculation came later; in the early January version the game just used the full screen and the coordinate math was simpler.
State machine: the game has five states — IDLE, DRAGGING, FLYING, WIN, CRASH. The transition logic is clean: you can only start dragging from IDLE, the ship only moves in FLYING, crashes and wins branch into their respective states, and both reset back to IDLE (after a brief pause for crash). Keeping it as an explicit state machine rather than a pile of boolean flags made the code much easier to reason about, especially once I added the multiplayer variant.
February — Week 1–2
Planet Types and Gravity Tuning
Game Design
Physics
The most important game design decision in Gravitydrift is probably the planet type system. The game would be much less interesting if every planet had the same gravitational strength. By giving each planet a different density, and therefore a different mass per unit of radius, you create wildly different tools for the player to work with.
I settled on nine types, loosely inspired by real astrophysical objects:
- White — the lightest, barely more than decorative. Good for gentle curves.
- Grey — a bit more pull, roughly Earth-like in density.
- Blue — ice giant density, reliable mid-weight option.
- Green — feels like a dense rocky world. The workhorse of generated levels.
- Yellow — gas giant range, significantly heavier than it looks.
- Red — dense, serious gravity well. Requires respect.
- Red Star — a stellar object. The gravity is intense and the visual is dramatic.
- Neutron Star — absurdly dense, tiny radius, enormous gravity. Flying near one is almost always a mistake unless you plan it carefully.
- Black Hole — maximum density, visually distinct with no atmosphere, just darkness. The event horizon is the planet radius. If you touch it, you're gone.
The density factors are: white=0.4, grey=0.7, blue=1.0, green=1.6, yellow=2.5, red=5, red_star=12, neutron_star=30, black_hole=50. These numbers were tuned over about two weeks of playtesting. The neutron star and black hole needed special treatment — their radii are fixed at very small values (neutron star is always tiny regardless of the "size" parameter) so that their enormous mass is concentrated into a realistic pinpoint.
One early design mistake: I initially let the level generator place black holes and neutron stars freely in generated levels. The result was almost always unsolvable — the gravity was so dominant that nearly every trajectory ended in a collision. I had to add constraints to the generator: exotic objects are placed sparingly and only in harder difficulty tiers, and there are minimum distance rules between them and the ship start and destination positions.
Tuning the feel of each planet type is one of those tasks that can swallow days. I'd adjust a density constant, run the generator a few times, play ten levels, adjust again. The current numbers feel right. I've been using them for weeks without wanting to change them.
February — Week 2
The Level Generator
Procedural Generation
Hard Problem
Hand-crafting every level would have taken forever and killed the game's replayability. I needed a procedural level generator that could produce a variety of layouts at three difficulty settings — easy, medium, hard — and guarantee that each generated level was actually solvable.
That last requirement is the hard part. Generating a random arrangement of planets is easy. Verifying that it's solvable is not.
My solution was pragmatic rather than mathematically rigorous. The generator doesn't prove solvability — it runs a simulation of a range of possible launch angles and speeds, and if at least one of those simulated shots reaches the destination, the level passes. If no shot from the tested range works, the generator discards the level and tries again. This is brute-force verification, not proof, but it works well in practice because the search space is reasonably small at easy and medium difficulty.
The generation process:
- Fix the ship start position (upper-left area) and destination position (lower-right area), with some randomization within those zones.
- Place 2–6 planets depending on difficulty, avoiding minimum overlap with each other, the ship, and the destination.
- Assign planet types based on difficulty (easy levels get lighter planets, hard levels can include red stars and black holes).
- Run 2,000+ test shots across a grid of launch angles and speeds.
- If at least one succeeds, keep the level. Otherwise, regenerate.
The generator uses a seeded random number function so that any level can be reproduced from its seed. This is essential for multiplayer — all players get the same level from the same seed — and for the "share this level" functionality I have planned but haven't implemented yet.
One thing I learned: the test shots need to use exactly the same physics code as the actual game. Early on I had a slightly simplified version in the generator that didn't accumulate the trail or handle boundary checks the same way. Levels were being flagged as solvable but weren't actually solvable in the real game. Sharing the same stepPhysics() function between the generator and the game loop fixed this, and it also meant that fixes to the physics automatically improved the generator's accuracy.
The generator finds solvable levels by brute-forcing 2000 test shots. It's not elegant but it ships.
Hard mode levels have an additional constraint: the "easy" solution path needs to be obscured. I achieve this loosely by requiring that the obvious straight-line path to the destination is blocked or uninteresting. It's heuristic — I'm not actually optimizing the aesthetic difficulty — but combined with harder planet types and tighter margins, the hard levels feel substantially different from the easy ones.
February — Week 3
Building the Level Editor
Feature
UI/UX
I built the level editor because I wanted to create a few hand-crafted "showcase" levels that demonstrated the full potential of the mechanics. I could have just written the level data as JSON manually, but I wanted something I could also hand to players and let them build with.
The editor shares the game's renderer almost entirely — it uses the same renderGame() function in a "preview" mode. This kept the visual appearance consistent and meant I didn't have to maintain two separate drawing codepaths. The editor just adds an overlay of selection handles, drag targets, and tool UI on top of the standard game view.
Features I built, roughly in order:
- Click to place planets, drag to move them
- Planet type selector (the sidebar with all nine types)
- Size slider for each planet
- Destination ring placement and radius adjustment
- Checkpoint ring placement (optional; checkpoints must be passed before the destination counts)
- Force fields — invisible barriers that destroy the ship on contact
- Ship start position placement
- Undo / redo (Ctrl+Z / Ctrl+Y) — this was annoying to implement but would have been more annoying without
- Test mode — play the level directly inside the editor without saving
- Save and publish to the shared level library
The hardest part of the editor was the coordinate system. The game internally uses a fixed 1800×900 logical coordinate space, and levels are stored in a normalised 0–1 system (so x: 0.5, y: 0.5 is always the center regardless of screen resolution). The editor has to translate between screen pixels, logical game coordinates, and normalised storage coordinates at every interaction. I got the math wrong several times before it was solid.
The sidebar — the tool panel on the left — was the source of a long-running UI bug. When the sidebar slid out, the canvas area would resize and recalculate the letterbox, which would trigger a canvas clear, which would cause a one-frame flash. Getting the layout right required fixing the canvas area as an absolutely-positioned element that never moves, and having the sidebar slide in/out over a static black background. When the sidebar is hidden, it reveals black nothing — the same black as the space outside the game boundary. It looks intentional. It is, now.
The editor is publicly accessible from the main menu. Levels published from it appear immediately in the Saved Maps browser, sortable by newest, highest rated, and most played. The rating system is a five-star vote with Bayesian smoothing to prevent one-vote wonders from topping the charts.
February — Week 3–4
Multiplayer
Feature
WebSockets
Complexity
Multiplayer was an ambitious addition and I'm still not completely sure it was the right call at this stage of the project. But I wanted it, so I built it.
The design is competitive: 2–8 players are put in a room, given the same level, and race to solve it in the fewest attempts. A live leaderboard shows everyone's attempt count in real time, and the player who finishes first with the fewest attempts wins. Players can keep trying to improve their attempt count even after someone finishes.
The networking uses WebSockets. The server is Node.js with the ws package — about 400 lines that manage room creation, room joining, relaying messages between players, and tracking game state. I deliberately kept the server as a relay rather than an authority: clients handle their own physics, and the server just broadcasts events. This keeps server load minimal and means the game doesn't desync in normal conditions.
Room codes are four random uppercase letters. Short enough to type into a phone while talking to someone. You create a room, read out the code, your friends join. The host picks whether to play a generated level or a saved one from the library, then starts the game.
The messages are simple: attempt (player tried a shot), finish (player solved the level), play_again (someone wants another round). The server broadcasts the attempt count of every player to all players in the room whenever someone makes a new attempt. The leaderboard updates live.
Multiplayer has a separate game file (mp-game.js) because it's different enough from single-player to warrant it: there's no difficulty selector, no new map button, the flow is lobby → game → results → lobby. The physics and renderer are shared — same physics.js, same renderer.js, same level format. Just a different state machine wrapped around them.
The biggest technical headache in multiplayer was making sure all players got the same level. Generated levels are seeded, so in theory you just share the seed and everyone regenerates locally. But the seed-to-level function needs to be deterministic across browsers and devices, which means no Math.random() — I use a seeded PRNG. And all the planet placement, size, and type logic needs to produce identical output from identical seeds. Testing this across Chrome, Firefox, and Safari took a while.
An alternative would be for the host to generate the level and send the full level data to all players as JSON. I actually implemented this for generated levels (the host sends the fully-resolved normalized level object) to avoid any seed-to-level determinism issues. Saved levels are fetched by ID from the server, so all players end up with the same JSON from the same API call.
March — Week 1
Visual Polish
Visuals
Polish
I spent the first week of March on visual polish. The game played well but didn't look as good as it could. A few specific things were bothering me.
Planet atmospheres. The planet sprites are good, but they sat flatly on the black background with no sense of scale or presence. Real planets have atmospheres — glowing halos of gas visible from space. I added a per-planet atmosphere layer drawn as a radial gradient between the planet surface and some distance beyond it, with colors tuned to each planet type. The blue planet gets a thin blue-white halo like Earth's atmosphere from orbit. Red stars and neutron stars get enormous bright halos — these objects radiate so intensely in real life that the effect is almost overwhelming. The black hole gets nothing: it absorbs everything, even light, and a glowing halo would be visually wrong.
These atmospheres are drawn in a single pass before the planet sprite, so they don't interfere with the crisp edge of the PNG image. They fade from an inner glow to full transparency, so they never look painted-on. This change had a bigger visual impact than I expected — the game went from looking like a game to looking like an actual scene from space.
The explosion. When the ship crashes, it explodes. The original explosion was a burst of particles that disappeared in about half a second — functional, but way too fast. I slowed it down significantly, removed the debris chunks (they felt wrong — a spacecraft in vacuum doesn't scatter pieces that fall to the ground), and tuned the particle decay curves so the fireball blooms quickly and then the smoke lingers much longer. A shockwave ring expands outward and fades. The whole thing now lasts about two to three seconds, which gives you time to wince properly before the round resets.
The destination ring. I tightened the pulse animation on the destination and made the color slightly warmer. Small change, bigger feel.
UI labels. The attempt counter, seed display, difficulty buttons, and the new-map button are all drawn directly onto the canvas in a fixed position rather than using HTML overlay elements. This keeps everything in one rendering context and avoids z-index and positioning headaches. The font is monospace throughout — it fits the space-mission-control aesthetic and doesn't require loading a web font.
March — Week 1–2
Mobile Problems
Mobile
DPR / Canvas
Testing on my phone in early March revealed two separate problems I should have caught much earlier. Both were embarrassing in hindsight.
Problem 1: Grainy planets on high-DPI screens.
On a desktop monitor, the planet images looked crisp. On my phone, they looked like they'd been upscaled from a thumbnail. The issue: I was setting the canvas to CSS pixel dimensions and drawing into it at CSS pixel resolution. On a phone with a device pixel ratio of 3, the canvas had one physical pixel for every three it should have had. Everything was being upscaled by the browser at display time, and upscaling produces the blurry/grainy look I was seeing.
The fix: set the canvas buffer size to windowWidth × DPR by windowHeight × DPR, then use ctx.setTransform() to scale all drawing by the DPR factor. This means your drawing coordinates are still in logical CSS pixels (easy to reason about), but the actual canvas buffer is at native physical resolution (looks sharp on screen). The setTransform call also bakes in the letterbox offset, so the game content is centered automatically regardless of aspect ratio.
This was a bigger refactor than it sounds because I was using canvas.width and canvas.height throughout the codebase for game logic — collision detection, boundary checks, coordinate transforms. After the DPR fix, those values were physical pixel sizes, not logical game coordinates. I had to separate the concepts: physical canvas size (for buffer allocation and CSS) vs. logical game size (for all game logic). I added canvas.logW and canvas.logH properties, set them to the fixed game dimensions (1800×900), and replaced all game-logic uses of canvas dimensions with these properties.
Problem 2: Image loading latency on mobile.
On a fast Wi-Fi desktop connection, images loaded before you could notice them being absent. On mobile, there was a visible delay after the level started before the planet sprites appeared — you'd see the glow halos and collision geometry for a second before the actual images popped in. Not terrible, but unprofessional.
Two changes: I added <link rel="preload"> tags to all game HTML files for all ten image assets, which tells the browser to start fetching them immediately when parsing the HTML rather than waiting until the JavaScript requests them. I also set fetchPriority = "high" on all Image objects created in JavaScript. Together these ensure images are in memory before the game loop needs them.
Bonus problem: aspect ratio cheating.
While investigating the mobile layout, I noticed something uncomfortable: if you opened the game in a narrow browser window on desktop, the game world shifted — planets that were in certain positions on a 16:9 screen were in different positions on a 4:3 screen. This meant the physics of a level were different depending on your window size. That's a fairness problem.
The fix was to lock the game to a fixed 1800×900 logical coordinate space and letterbox it into whatever screen you're on — exactly like how movies letterbox on your TV. Planets are always at the same logical coordinates. A narrower screen just sees the game content smaller (but still fully visible), with black bars on the sides. No more cheating by squishing your browser window.
March — Week 2
The Determinism Problem
Critical Bug
Physics
This one bothered me for a while before I understood what was happening.
A friend who was testing the game mentioned that the same shot on their phone sometimes hit the checkpoint and sometimes didn't. I'd set up a specific trajectory on my desktop and described it to them precisely — same drag direction, same distance — and the results were inconsistent on mobile. Not dramatically different, but enough to matter. A shot that reliably passed through a tight checkpoint on my screen would miss by a few pixels on their phone.
My first guess was floating-point inconsistency across different JavaScript engines. This is a real thing — different JS engines can produce slightly different results for the same floating-point operations. But after investigating, that wasn't the primary issue.
The actual problem was my game loop. I was calling updatePhysics() once per frame, using the real dt (delta time) between frames. On a 60Hz desktop screen, dt is approximately 16.67 milliseconds every frame. On a 120Hz phone screen, dt is approximately 8.33 milliseconds every frame. Since the physics are integrated step-by-step, more steps with smaller dt produces a different trajectory than fewer steps with larger dt — not because either is wrong, but because they're approximating a continuous curve differently. Same input, different physics, different outcome.
The solution is the same one used by every serious game physics engine: a fixed timestep accumulator. Instead of stepping physics once per frame with the variable real dt, you accumulate real time, then consume it in fixed 1/60s chunks. A 120Hz screen runs the physics loop twice per frame (consuming two 1/60s steps). A 60Hz screen runs it once. Both produce identical trajectories for identical inputs.
After the fixed timestep fix: same shot, same result, every device, every frame rate. Immediately one of the most satisfying bugs I've ever fixed.
The side effect of fixed timestep is that rendering can be out of phase with physics — on a 120Hz screen, you render at 120Hz but physics only advance at 60Hz, so every other render is showing a "stale" ship position. The fix is render interpolation: instead of drawing the ship at its current physics position, you draw it at a position interpolated between its previous and current physics position, weighted by how far through the current physics timestep you are. The ship motion becomes perfectly smooth at any frame rate, while the physics remain fixed and deterministic.
This was probably the most technically satisfying thing I built for Gravitydrift. It's invisible when it works — it just feels right — but it's the kind of thing that separates a game that feels solid from one that feels slightly off.
March — Week 2–3
Going Live
Deployment
Infrastructure
Getting a game live is a different skill set from building one, and I find it less satisfying. But it has to be done.
The server is a Node.js application serving static files from a public/ directory and providing a small REST API and a WebSocket endpoint. I deployed it to Railway, which is a platform-as-a-service that handles infrastructure in a way I don't have to think about much. The whole deployment is: push to Git, Railway picks it up, restarts the server. That's it. For a solo side project this is the right amount of complexity.
Persistence uses Supabase — hosted PostgreSQL with a clean REST client. Levels are stored in a levels table with normalised JSON, and ratings in a ratings table. For anonymous players (no login required to play or rate), I use a fingerprint stored in localStorage to prevent the same device from rating a level more than once. It's not bulletproof but it's sufficient for a community of reasonable humans.
The Service Worker (sw.js) caches static assets for offline capability and makes the game installable as a PWA on Android and iOS. This was about two hours of work and makes the mobile experience significantly better — on a subsequent visit the game loads from cache almost instantly.
I also spent a day creating the legal and informational pages that are necessary for AdSense approval: a privacy policy explaining what data is collected (fingerprint ID and star ratings), a contact page, an about page, and an educational content page covering the actual science behind the game. These pages are genuinely useful — I've had several messages from teachers asking about using the game in class — but I won't pretend they weren't also required for monetization compliance.
The game went live publicly in mid-March. Within the first few days, a handful of levels had been published by players using the editor. That was the moment it stopped feeling like a private project and started feeling like something that existed in the world. A strange feeling. Not entirely comfortable yet.
There's a long list of things I still want to fix and build. The game is playable and the core loop is solid, but "playable" and "done" are very different things. Right now I'm focused on stability and making sure the experience is smooth across as many devices as possible before I add more features. The bugs you find after real users get hold of something are always different from the bugs you find yourself.
March — Week 3
Naming It Properly and Going Legit
Identity
Infrastructure
The game was called "Space Slingshot" for the entire development period up to this point. That's what I'd been typing into browser tabs, commit messages, and HTML title tags since January. It wasn't a real name — it was a description. I knew it wasn't the final name. I just hadn't decided on the real one.
The problem with a placeholder name is that it starts to feel permanent if you're not careful. Every day you use it, it gets a little harder to change. By mid-March I was looking at "SPACE SLINGSHOT" in the header of the main menu and it genuinely bothered me. It sounded like something you'd find on a flash game portal in 2008.
I spent an evening brainstorming. The criteria: short, says what the game is, works in a monospace font, has a free .io domain, sounds like a real browser game and not a corporate product. I went through a lot of candidates — Orbitalsling, Planethop, Gravdash, and others. Gravitydrift was the one that landed. Gravity is the mechanic. Drift is what your ship does. It flows well, reads clean at any size, and the domain gravitydrift.io was available. Done.
Doing the rename across the entire codebase was tedious but not difficult: over twenty files, several case variants (Gravitydrift, GRAVITYDRIFT, gravitydrift), manifest.json, service worker. Took an hour. Should have done it in January.
Lesson: name your project early. A placeholder name isn't neutral — it shapes how you think about the thing you're building.
Around the same time I was working through what it takes to be approved for Google AdSense. I'd assumed this was mostly a formality — you add a script tag, Google reviews your site, you start showing ads. The reality is more involved. AdSense reviewers look for a specific set of signals that the site is legitimate: a real privacy policy that explicitly names Google's advertising cookies, a terms of service (especially important for any site with user-generated content), a contact page, and enough original content to demonstrate that the site has value beyond just being an ad container.
I had some of this already. I didn't have a Terms of Service. My privacy policy was decent but didn't explicitly mention AdSense by name. I didn't have a cookie consent banner, which is legally required under GDPR for any European users — and is required by AdSense policy regardless of where your users are.
So I spent a few evenings building it properly: Terms of Service, Content Guidelines for the Level Editor, a cookie consent banner that fires on first visit and stores the decision in localStorage, an FAQ page, a changelog, and updates to the privacy policy with explicit AdSense language and a GDPR section. None of this is fun to write. All of it is necessary.
The cookie banner was technically straightforward — a fixed div injected into the DOM by a separate script, with accept/decline buttons, dispatching a CustomEvent on decision so that any future AdSense integration can gate on it properly. Styling it to match the game's aesthetic without looking like every other dark-mode cookie banner took longer than the logic did.
Getting all of this right is the unsexy work of turning a game into a product. I don't love doing it but I understand why it exists. A site without these signals looks like a hobby project that might disappear tomorrow. A site with them looks like something someone is taking seriously.
March — Ongoing
Where Things Stand
Mid-Development
I'm writing this in the middle of it, not at the end. The game is live, people are playing it, and I'm still pushing commits every few days. This section is a snapshot, not a conclusion.
Building Gravitydrift has taken roughly two and a half months of spare-time work so far — typically an evening or two per week, sometimes more when I get on a streak. I have a day job. This is a night project. That rhythm shapes everything about how you build: you don't start an ambitious feature on a Sunday evening because you won't finish it before you need to sleep, and you don't leave things broken because you might not touch the code again for three days. Everything has to be shippable at the end of every session. That constraint is actually healthy — it keeps scope tight.
The decision to use no framework was right for this project. Vanilla JavaScript and Canvas 2D are genuinely sufficient for a 2D physics game, and the constraint of not having abstractions to lean on forced me to understand every part of the system. The physics code is readable. The renderer is readable. The WebSocket protocol is a handful of message types. If something breaks, I know exactly where to look. I've never once wished I'd used Unity or Phaser for this.
Things I'd do differently if I started over:
- Mobile-first from day one. The DPR fix and the letterbox fix were retrofits. Treating them as the default from the beginning would have saved meaningful time and prevented the embarrassing period where mobile looked bad.
- Fixed timestep from day one. Three evenings to implement properly. Should have been the first thing I wrote. The determinism problem it causes is exactly the kind of subtle bug that's hard to diagnose weeks later.
- Name it earlier. "Space Slingshot" was a placeholder that stuck around for months. Naming it Gravitydrift earlier would have given the project more of its own identity sooner and made it easier to talk about.
- Start making hand-crafted levels sooner. The generator is good at producing solvable levels, but the best levels in the library are the ones someone designed with intention. I've been building infrastructure instead of content, and that balance should have flipped earlier.
Things I wouldn't change:
- The core mechanic. Drag, release, watch. It's exactly what it should be. I haven't wanted to change it once.
- The ghost trail. Still the best accidental feature in the game.
- Supabase + Railway. Zero friction, zero regrets.
- The monospace aesthetic. It defines the whole personality of the project and I arrived at it immediately without deliberation, which usually means it's correct.
- The nine planet types with dramatically different densities. This single decision gives the level design an enormous range of dynamics from a very small surface area of game rules.
What's coming next — roughly, not as promises:
- Per-level leaderboards. Right now there's no way to know how few attempts a level can be solved in, or who got there first. That should be visible.
- A curated campaign mode. A hand-designed sequence of levels that teaches the mechanics progressively, starting from "this is gravity" and building to the tricky stuff.
- More planet interaction types. I have ideas around planets that move, or gravitational fields that change over time. I don't know if they'll make it in but the physics engine can handle them.
- Better multiplayer polish. Rooms work, the race and siege formats work, but the lobby experience is rough and the end-of-round flow needs more thought.
- Sound. Yes, there is no sound. I know. It's on the list.
A game is never finished, it's only abandoned or released. Gravitydrift is released. It is very much not finished.
I'll keep adding entries here as things develop. The next one will probably be about the campaign mode, or about whatever unexpected technical problem surfaces when more people start playing. One always does.
If you find a bug, a badly-designed level, or a trajectory that should work and doesn't — the contact page is right there. Solo developers run on feedback and I read everything.