/ xkcd-gravity-escape-speed

Development notes from xkcd's "Gravity" and "Escape Speed"

A screenshot of the "Escape Speed" game, featuring a small spaceship moving past a sign saying "Welcome to Origin!", with red debug lines overlayed

On April 20th, xkcd published Escape Speed, the 14th annual April Fools comic we’ve made together. Escape Seed is a large space exploration game created and drawn by Randall Munroe. I coded the engine and editor, with game logic and asset processing by davean. The game map was edited by Patrick Clapp, Amber, Kevin, Benjamin Staffin, and Janelle Shane.

This was one of the most ambitious — and most delayed — April Fools comics we’ve ever shipped. The art, physics, story, game logic, and render performance all needed to work together to pull this off. We decided to take the time to get it right.

The game is a spiritual successor to last year’s Gravity space exploration comic. Our goal was to deepen the game with a bigger map and more orbital mechanics challenges to play with.

Here’s a few stories from the development of these two games.

Spherical cows

There’s this old physics joke about simplifying the model to ease calculation.

For this comic, we took it literally.

When we made Hoverboard in 2015, Randall made a clever decision to avoid drawing a walk cycle: give the character a hoverboard. This papered over a ton of collision glitches too.

For Gravity, I faced a similar predicament. Here’s an excerpt from our chat:

A screenshot of a Slack chat log transcript:
gentlemen, I've been thinking a lot about collisions
I've so far been imagining how the ship will bounce off of things in the environment, and what that should feel like
a scenario I really haven't liked is when the ship is traveling at a tangent to a planet and lands with a lot of lateral speed
if we naively auto rotate the ship upright when altitude is close to landing, it's gonna look really cheesy as the ship has to decelerate fast or weirdly slides across the surface
I also think it's kinda weird to have this indestructible ship which bounces elastically off everything
so, I had an idea, a wonderfully stupid hoverboard level idea
we give the ship a shield.
what if we drew a circle around the ship whenever there's collision contact around a fixed radius
suddenly all sorts of awkward collision or navigation problems disappear, AND it explains why this ship don't blow up

It’s almost always a win to simplify the requirements. I love when a simplification is both clearer to understand and easier to implement. The shield is cheesy, but it works!

Designing for discovery in an empty space

Space is vast and mostly empty. Unlike hoverboard, where basic movement leads you to new discoveries, moving arbitrarily in a space game quickly gets you lost:

reddit comments on Escape Speed:
Varandru: "I've heard somewhere that space travel somehow hits on both agoraphobia and claustrophobia simultaneously. Which is a neat fact if you're not travelling in space..."
bryancrain88: "This game gave me a greater fear of space than anything I've read or seen. The visceral sense of how easy it is to get lost in it."

We knew that guiding players towards interesting places would be a core design problem. One of Randall’s ideas was to have a “compass” pointing towards the nearest point of interest. This evolved into a cloud of dots around the ship with subtle cues for object distance and size:

Another useful mechanic was to make the camera rotate so that the direction gravity’s pointing is down. This was originally so that planets would rotate under you as you orbited them (turning text upright), but we also discovered it was a useful cue for when you were approaching something. The starfield and dot cloud “wayfinder” helped to make this camera rotation legible.

Finally, we added some signs around the map. This gave players ambient awareness of notable story areas before visiting them.

Collision map steganography

One challenge that always comes up when we make an explorable comic is collision mapping. We want to control which areas are passable independently from display, so we can create secret passages! 🤫

Typically we’ve hidden collision data in the least significant bit (LSB) of one of the color channels. When Randall draws the map, he uses solid colors (usually red) to indicate special passable areas, and we update the colors in our image processing scripts.

This was the approach we used for Hoverboard and Gravity. For each frame, we render the vicinity around the player to an offscreen canvas and check the LSB values for passability (e.g. even = solid, odd = passable). You can run ze.goggles() in the JS console to see this hidden collision canvas (with some debug overlays added).

Screenshot of Hoverboard with the collision canvas debug view visible

This works reasonably well, but you have to be careful around antialiasing. Since resizing adds interpolated grayscale values to the image, this can add unintended LSB values to the images.

Hoverboard used a hack which only considered dark pixels (channel value < 100) for the LSB checks. For Gravity, we wanted both light and dark passable areas, so davean wrote a custom image resizer to preserve the LSB values.

Crop of the Gravity sun source file from Gravity with passable pixels colored red

We also started using transparency in the images so we could have multiple overlapped layers with a moving starfield background. While sound in theory, adding alpha caused a lot of pain. We kept finding areas with phantom collidability which didn’t appear in the source images. After hours of tedious debugging and pixel peeping, I discovered that canvas uses premultiplied alpha in the backing store. This can cause values in the canvas to mismatch what was written.

We resorted to a backup plan proposed by davean: render the collision data into the LSB of the alpha channel (the alpha channel itself isn’t premultiplied). This wasn’t without its own issues (e.g. when flattening multiple layers into the collision canvas, their alpha values sum) — but it worked well enough to ship.

An awesome SVG hack, dashed by Safari

For Escape Speed, we wanted to find a cleaner approach for collision maps. A lingering issue with using the alpha channel was some encoded values would be very slightly transparent. I had hoped to process the image data client-side to fix the alpha values (e.g. rounding 254 up to 255), but in practice this was an unacceptable performance trade-off.

What if we could remap the color channels using CSS? It turns out this is possible using SVG filters! We can use a matrix transform to map the red channel to luminosity, blue to alpha, and leave green for collision:

<filter id="bw" x="0" y="0" width="100%" height="100%">
    values="1 0 0 0 0
            1 0 0 0 0
            1 0 0 0 0
            0 0 1 0 0"

Here’s what the image data looks like with these color mappings:

Crop of the sun source file from Escape Speed colored blue and pink, and impassible elements colored white

This was a fantastic solution. It worked nicely during early development. Unfortunately once we tested more, we discovered SVG filters are unusably slow in Safari. :(

Back to the drawing board…

A screenshot of a Slack chat log transcript:
davean: Goddard pushed with the change, will get geoverse rebuilding, but check goddard?
We can go back to that hack ... at least on safari
funny if safari gets the bad color
I can add a 4th image format to the processing
which direction did you want the LSB?
chromakode: lol plz no
davean: Already done and pushed
chromakode: are we even using black
davean: I don't even know what that means anymore

It was March 29th, and at this point we wanted whatever was most likely to work. Elegance be damned. We switched to what we’d been avoiding the whole time: separate collision map images. This doubled the images downloaded, but it wasn’t too bad: the monochrome images compress well and HTTP/2 or QUIC make it less of a perf hit than in our early 2010s tiled games.

An unlikely niche for TypeScript

As our games have grown in complexity, so have the accompanying datasets:

  • Hoverboard had a an array of coin positions
  • Gravity had a JSON blob describing planet locations and sizes
  • Escape Speed has a TypeScript map and IDE:

A screenshot of the editor for Escape Speed, with a game preview on the left with menus for customizing parameters, and a code editor on the right

To make editing and expanding the game map ergonomic, we knew early on we’d want a live editor. Unexpectedly, I found that a VSCode-like TypeScript editor was the sweet spot. This followed from a couple decisions motivated by efficiency:

  • We knew we’d want some kind of linting or validation on the map to catch mistakes before they hit production.

  • Our asset pipeline outputs a JSON data with all of the layer names and dimensions. We could use this in the types to give editors realtime feedback on possible options and catch typos!

    export type LocationName = keyof typeof imageData.locations
    export type LayerName<Name extends LocationName> =
      keyof (typeof imageData)['locations'][Name]['layers']
  • The easiest way to get a good live TypeScript editing experience was to pull in Monaco.

Compared to building a validator in CI (slow iteration feedback) or a visual editor (huge scope), TypeScript was quick to adopt and easy enough to manipulate. Putting everything in a web page made it super easy to onboard collaborators: rather than cloning a repo and installing a toolchain, they got a state of the art code editor in one click.

The editor embeds rollup to compile the map files in the browser, and prettier to keep code style consistent. It’s kind of amazing to me that the logical path led to building a custom IDE, and actually reduced the amount of stuff I had to build!

Game engine growing pains: constant timestep

This bug arrived the way most tricky ones do, with an impossible-seeming observation. Randall noticed his ship somehow had less thrust than earlier in the day. Retracing his steps, he’d been testing the game at his office, lugged into his drawing tablet display. Now, running on his laptop, the gravity seemed wrong somehow. Several other people investigated and saw no difference.

Randall had recently upgraded to an M2 Macbook Pro, which has a 120hz variable refresh rate display. Oh no. I tested on my gaming monitor, and sure enough, I was now seeing it too.

Some of this physics code dates back to Hoverboard, which ran ticks in sync with drawing frames. More frames = faster physics ticks. However, with 8 more years of wisdom working on Gravity, I’d updated this code to factor in frame delta time. In theory, this would compensate for variation in frame times. I combed through the physics tick function and fixed a couple minor cases I’d missed, but the movement wasn’t any better.

xkcd #1700, "New Bug" (transcript from explainxkcd.com):
[Cueball sits at his desk in front of his computer leaning back and turning away from it to speak to a person off-panel.]
Cueball: Can you take a look at the bug I just opened?
Off-panel voice: Uh oh.
[Zoom out and pan to show only Cueball sitting on his chair facing away from the computer, which is now off-panel. The person speaking to him is still of panel even though this panel is much broader.]
Off-panel voice: Is this a normal bug, or one of those horrifying ones that prove your whole project is broken beyond repair and should be burned to the ground?
[Zoom in on Cueball's head and upper torso.]
Cueball: It's a normal one this time, I promise.
Off-panel voice: OK, what's the bug?
[Back to a view similar to the first panel where Cueball has turned towards the computer and points at the screen with one hand.]
Cueball (edited from the original comic with a screenshot of a Slack message): ok so why did gravity change?
Off-panel voice: I'll get the lighter fluid.

I consulted davean’s deep knowledge of game programming techniques. We walked through the code together over Discord. Neither of us could spot an obvious problem with the formulae until we started stepping through the physics state tick by tick. At last, we spotted a deeper problem!

The ship has a special behavior when launching from a landed state: it gets an extra boost of thrust. While not realistic, this made launching feel tight and responsive. This “launch speed” was set to provide 2500x the normal acceleration, lasting only a single frame. Even with frame delta time factored in, the physics integrator was not accurate enough for a 1 tick acceleration of this magnitude to be consistent between 60 and 120hz.

The solution was to switch to a constant timestep. Instead of ticking with the frame updates, we use a constant physics tick rate of 120hz. When a frame is drawn, we run the physics to “catch up” to the current time in fixed steps. However! Now that physics isn’t ticking in sync with frames, we have to lerp your position to account for the frame happening between two physics ticks.

This is probably table stakes for anyone who’s written a game physics engine, but the naive approach had been good enough up to this point. No one had noticed the variation in behavior until we fine-tuned the ship to barely be able to escape the starting planet.

One of the beautiful things about revisiting old code is it shows you how your way of thinking has changed. Looking through old Hoverboard source, I could anticipate bugs I’d missed. Places jumped out where I’d take a different approach. I understand these problems better now because I’ve seen more. I’ll probably look back on Escape Speed someday in a similar way.

I hope you enjoyed these stories from our development of Escape Speed and Gravity. We had a ton of fun making these comics, and hope have fun exploring them too. If you’d like to read more xkcd stories, check out Notes from the development of xkcd’s “Pixels” and Checkbox.

/ shape


Shape #120, "forward-sightedness", a black and white drawing of a childlike robot with bright beams of light streaming from its eyes

Shape began as a shower thought one summer evening in 2006. I was 16, and back then my home on the web was deviantART. I admired artists who could draw beautiful forms from a blank slate; in contrast, I felt like I was in a rut composing geometric presets. So, I challenged myself to draw something new from imagination each night. To keep the art simple and focused on form, I imposed a minimalist black and white format. The focus was on drawing shapes.

At first I was skeptical I would keep the habit, and probably wouldn’t have succeeded except for a trick: I promised myself I’d make 30 shapes before I’d share them with anyone. This turned out to be unreasonably effective at motivating me to continue until it became a habit. By the time I’d finished #30, the project had clicked: I’d gotten through the iffy beggining part and was now producing work that I loved and loving staying up late doing it. I settled into a groove of mostly silouhetted forms and conceptual art, with a few comics and typography experiments thrown in.

Overall, I drew 166 shapes, trailing off after my 17th birthday. Shape was often autobiographical, a process of digesting the most notable feeling or memory of the day. At the time, I was in my first year of an accelerated bachelor’s CS track at Portland State University. 2006 was filled with schoolwork, reflections on growth, and my first teenage romance. Shape was a perfect outlet to explore really specific ideas and feelings — without being too specific.

Shape taught me what a successful creative process looks like (for me, at least!). It worked because getting started was easy and I could make incremental progress each day. In another time or medium I don’t think it’d have come together so well. I’ve tried to rekindle that serial art journal magic again, but it’s easy to get overly ambitious. I find when takes more than a couple hours to make a piece, it’s harder to keep that raw connection to the work. Shape worked well because it was so efficient.

To this day, Shape remains one of the projects I’m most proud of. It’s powerful to believe in your work, and so fun and satisfying to learn by doing. Many nights set new high watermarks in the creative reach I felt working in vectors. At the time I felt I was making some of my favorite art I’d ever made, and I still feel this way about Shape today.

Restoring old artwork via time travel

Over time, the custom Wordpress site behind Shape bitrotted and stopped working, and eventually I took it offline around 2014. Oof. Bringing it back has been a rainy day project for almost a decade. A priority for the project would be to leave Shape in a format more likely to continue working over time. Recently, static site generators emerged as an attractive choice.

Last weekend, I reworked the original PHP templates and CSS in Astro, bringing the original pages back online. Then, I set to work modernizing them. My goal was to re-render the original Inkscape SVGs in higher resolution, so they’d look sharp on modern 2x density screens.

This process turned out… a lot more interesting I thought.

Initially, my batch re-export of the original SVGs looked good enough. To make it easier to spot any differences, I hacked the page template to alternate between new and old renders:

Clearly, fonts I’d referenced in the art were missing, and there were some text kerning differences. I whipped up a script to index every missing font used in the source and got to work finding them on the web.

I also spotted some variations in line dash positioning, and subtle differences in pattern rendering. But wait — what’s that going on with the base of the monitor in the top left image? That was a regular circle element. Why was it distorted?

It became clear that to accurately upscale these drawings, I’d need to replicate all the bugs and quirky behaviors from Inkscape 16 years ago. I started looking at inkscape.org’s version archive, but then I realized: hey, I have a disk image of the laptop I used to draw these images. Could I render in the same copy of Inkscape I drew them in? (see also: Backing up 18 years in 8 hours)

I fished the image out of my backups, spun up a VM using virt-manager, and it booted!

A screenshot of a virtual machine running my old laptop's Ubuntu 7.04 desktop environment

Wow. It’s difficult to describe what an intense feeling it was to crack open a time capsule like this. There was my login screen from 2007 staring back at me. I got the password on the first try. Waiting was my old desktop, lost emails, browser history, all frozen in time. I loved revisiting screenshots of old projects and desktop themes. Absolute nostalgia bomb. Inkscape worked too!

With the upscales now rendered in my original copy of Inkscape 0.45.1, I was still seeing some issues with dash positioning, so I tested out 0.44 as well. No difference. In fact, if I rendered an image at 1x, dash positions matched, but they’d appear differently at 2x and 4x. This appears to be a bug in Inkscape of that era, and didn’t impact the images much, so not worth fixing.

There were a couple other small differences (mainly cases where I probably forgot to save the final SVG after rendering, oops), and in one case, I’d downloaded the wrong font with the same name. I manually fixed the vector originals as best I could, and accepted that it wouldn’t be totally pixel perfect.

The results turned out beautifully. It was exciting to experience these familiar works in 2x density for the first time. One cool surprise was how the finer details affected perceived contrast in some of the images. Where previously tiny details antialiased into gray, the 2x renders became bright white on black.

I was already a digital packrat before this exercise, but this has only further convinced me of the unpredictable value of preserving old data in situ. If you have the space, disk images are by far the easiest and most flexible way to archive old computers. Hopefully these new 4x renders will be large enough to stand the test of time, but if I ever wanted to return to this project someday, it’s nice to know I can pick up where I left off.

Sea Change

If I were to choose a favorite piece from Shape, it’d be “Sea Change”. Coming back to Shape half a lifetime later, it in many ways expressed my feelings transitioning into young-adulthood. Striving for context, to be understood, and to be loved. One of the paradoxes of growing up is that both your desires and how you understand what you want are developing at the same time.

When I drew “Sea Change”, I couldn’t yet put it in words, but I was getting close to figuring out the solution to this paradox: that what I wanted and who I was would create each other.

To get where I wanted to go, I needed to change my perspective.

Shape #124, "Sea Change", a black and white drawing of a figure swimming at the bottom of a vast tubular ocean, reaching up towards an island hanging down from above

There’s a funny little coda to this story. In my early 20s I printed out a poster-sized version of this image and hung it above my desk.

At the time, I was living my dream job at reddit. I’d grown up, moved out, gotten where I wanted to go, and beyond. I was happy, but I truly did not know what came next.

I didn’t realize at the time, but a couple years later I noticed I’d hung the poster upside down.

"Sea Change" flipped upside down