Sunday, September 13, 2015

It's a Lovely Day for a Postmortem

After Liberated Pixel Cup, I promised myself I would never again enter a month-long game jam competition. It was just too exhausting. And then I discovered js13k. I guess I can't refuse a challenge. I also noticed the numerology around the number 13. I dig it! That's my number. To make matters even better, the competition deadline fell on my birthday (today).



What Is It?

The theme word announced for js13k this year was "reversed". That provoked many immediate ideas, the most enticing of which was a driving game where ... you guessed it! You drive in reverse. I envisioned a game like Outrun, Rad Racer, or Road Rash. A road vanishing in the distance, and all you can do is drive away from it. Absurd.

Soon enough, the Scope Creep Monster came knocking. I suddenly had a fully 3D rendered world, with trees, and rocks, and billboards, and mountains. Other cars on the road to avoid, and cops that chase you with sirens blaring (because hey, you're causing moral outrage through reckless driving).

The cutback finally came just a few days into the project. I decided to take inspiration from Desert Bus, known for 8 hours of driving on a perfectly straight road in an empty desert with literally nothing exciting happening ever. That's the game I wanted! The worst game ever made could only be made worse by driving backwards. This is its story.

First Steps

My first task was to find a way to get a full 3D game crammed into 13KB. This is no easy challenge, mind you. The most popular 3D framework is probably Three.js, and it clocks in at over 400KB minified! Another popular framework, Babylon.js, is over 800KB... You know what that means. Roll-your-own 3D engine? Pretty much! I had to use straight-up WebGL without all the fancy doodads provided by a framework. (Note: There are probably minimal 3D frameworks out there, I just don't know of any.)

With my framework chosen (lol), I set out to research some of the tricks used by js1k alumni. This is the same idea, except they get a meager 1,024 bytes to work with. Obviously they have some pretty good tech to do much in a little over a thousand bytes! (The first three paragraphs in this blog post are about 1KB.) I was able to find some good tips on The Googlie. Many of the tips were obvious, but then there were a lot of truly innovative ones, too! I definitely recommend checking out some of the previous js1k entries, even if just for inspiration!

My first commit had a blue triangle rotating and scaling on a white background. Super exciting! Important note: this is my first attempt at making a 3D game. I know the theory, let's put it to the test.

Art, Music, and Maps

When I said many of the tips were "obvious", I was really referring to procedural content generation. You're not going to squeeze a large map into such a tiny space, but you can randomly generate a map that goes on for infinity with just a small piece of code! And you can go deeper; generate all of your art, music, and sound effects procedurally, too!


So, a few lines of code can generate textures and music, huh? An excellent paper from 2006 explains in detail: Procedural Content Generation by Bjarki Gu├░laugsson. This is the original source of my own personal connection with PCG; a paper which I read back then eagerly seeking information after the release of the 96KB FPS, .kkreiger.

The true first step, then, is to choose a PCG algorithm. For simplicity, I decided I would only use one, with a generator pattern if I needed it to create different kinds of content. There are numerous algorithms to pick from. The first that came to mind was Perlin Noise, of which I am familiar. The thing is, everyone's using Perlin Noise, since Minecraft exploded its popularity. (Nothing wrong with that, and the noise function is fully capable of doing a lot more than create Minecraft clones.)

I wanted to try something new. What I found was the diamond-square algorithm. What convinced me about this was the happy "starry sky and mountain" image about 1/3 down the page. Followed up by a cloud. Perfect, it's exactly what I needed!

After a few false starts, I had the diamond-square working in JavaScript. Right away, I had a solid-colored mountain rendered from a single row of my 2D height map. Then I added all of the rows as individual layers, each with a slightly darker color. For added "coolness", I used the same height map to render clouds.


From there, a few adjustments were made to the layer positioning and interpolation within the cloud. Crucially, the 2D height map also grew from 128x128 to 512x512 (which was deemed capable of reasonable quality, but incapable of reasonable speed). This is what I ended up with, still just rendering directly to a single texture (e.g. not usable for animations).


Important note: I've never used procedural content generation in a game. I know the theory, let's put it to the test.

Sundown

Porting that texture renderer to WebGL was a pain. Ideally, I would have generated 3D geometry and used the fragment shader to add haze at runtime. But I wanted an old-school look. Remember, Rad Racer, not Need For Speed! So I settled on 8 layers, each drawn with 16 rows of the height map.

It also didn't help that I had a bug in my vertex shader. It took two days to determine the cause of the unusual rendering of my 3D geometry. It was simple: place a textured quad in 3D space at "some appropriate scale", then place another quad in front of it, scaled less, etc. Until all layers where in place. The issue I had was finding out what "the proper scale" was for each layer. From testing, it appeared to be on a logarithmic scale; for each unit of distance back, the layer size had to double to get decent results. It was truly mind boggling.

After fighting with the geometry and triple-checking my perspective matrix, I broke down and compared my code against a perspective demo provided by Gregg Tavares. Perspective matrix computation was spot-on. Then it hit me; vertex shader! Comparing the two, I noticed the only real difference is that I was multiplying the position vector by the matrix. You're doing it wrong.

With the geometry fixed, I went on to add colors to my vertex attributes (for linear gradients). The gradients evolved into a sunset, which demanded a new color scheme.


This was my first attempt at picking the right color scheme. It's pretty close to the final choice. After that, I added four layers of clouds, each with their own random color level with distinct spectrums; orange, blue, purple, peach. This is what gives it that "Arizona sunset" look. Seeing it in motion is strikingly realistic.


w00t! Background is done. All that's left is something to drive on...

The Road Ahead

As a dumb test, I created three triangles and laid them out in my 3D coordinate space. Looks like a perspective road to me!


The trouble here is the vanishing point goes into infinity. To simulate this with a rectangular road would have been infeasible; the road is always going to hit the back of my perspective frustum before it gets to 1px wide. Not to mention the road needs to curve, and will make it too wonky to fake or force the perspective. The solution then is to fade the road into the mountain background using those linear gradients that I love so much.


Shown in this screenshot is bad premultiplied alpha blending, which was fixed afterward. :) It still demonstrates the technique well enough. The perspective looks quite different as well, because the road is now made with a rectangle laying down on the surface, instead of a triangle.

Also notice the difference in the clouds. No code changes. Just regenerated with different random values. Pretty striking!

Now about that dull road ...

The Road Behind

I had to mull over the challenge of constructing curved roads for a few days. After all, at this point I only had a single, gray rectangle. I would have to chop it into smaller rectangles, and then somehow squeeze and stretch them in the right way to get a realistic winding road.

Back to the fractals! I already had a pluggable noise generator, since my diamond-square implementation was built with it in mind. I did some research on pink noise and experimented with the algorithm presented on that site. I found that it looked too much like white noise for my tastes. By limiting the range of motion for a random walk of white noise, I could generate much more suitable pink-ish noise with the diamond-square.

I plugged in a visualization of the fractal data (red), and drew a "road" (black) by rotating line segments by a fraction of the fractal data on every step.


And another one for comparison.


Not bad! However, I did find out later that the algorithm which drew these "roads" was not pivoting along the line segment as it was supposed to, but pivoting at its origin in the upper center of the image. That's why the roads seem very smooth at first, and then get more chaotic toward the end, where the small rotations have a greater effect on the more distant segments.

Now I have a road paving algorithm, this is going to be awesome!

Extending the 1D Road to 2D

My road is so far still single-dimensional; it's just a line. I have to extend it to 2D by giving it arbitrary width. Doing this required a plan. For every segment, I would extend perpendicular (to the paving direction) by half of the width in each direction. These two points would be recorded. Then the segment would be rotated, and walk one unit to pave the segment. This process would be repeated, each time creating a rectangle (quad) from the two new points and the two last points, building a road around my simple 1D curvy line.


This is what I saw in my head as I designed the road 2D paving algorithm. In step 1, the red dot is the current location, and the two blue dots are the "next points" to compute. This is a straight segment.

In step 2, the current location is rotated, walked one unit, and then it's just a straight shot out to the sides to hit the blue points, which are what I want to capture.

The 3rd step shows the current location (red dot) advanced, and the extraneous geometry removed, leaving two triangles (a single quad).

The algorithm repeats like this forever, building an infinite road that is anti-dull. With this idea in place, I updated my visualization to draw 2D roads.


Score.

Extending the 2D Road to 3D

This was so easy, it almost doesn't deserve an entire section of this postmortem. But I'm gonna, because I have another screenshot to show!

The whole process of the 3D extension was to use the X-Z plane. Ta da! While hills were definitely a part of my original Scope Creep Monster plan, there was no way that would have been a realistic goal. So the road never diverges from -1 on the Y axis (which is below the camera at Y:0).

Here is the first 3D road that I was able to render. Notice the black-and-white pattern, clearly showing the individual quad segments, and a small piece of the 2D visualization of which it represents in 3D.


I still didn't have a texture for it, but even dull gray would be better than a piano road.


Very nice! I can now generate a road infinitely. My fractal is 128x128, so if I compute segment angles for each point in the fractal, that gives me 16,384 segments of road before it cycles. Each segment is 1 unit deep, or about 1 meter in human length. 16 km of random road for free!

To keep memory usage sane for the road, only 28 segments are kept at a time. (28 happens to be evenly divisible by 4; the number of segments of depth for my asphalt texture, and also happens to be the depth at which the first layer of mountains appears.) When a new segment needs to be created, all segments are shifted up (into the distance) and the farthest segment gets pushed off into the void. Because the road is cyclical, the texture coordinates from the dropped segment are reused by the new segment. (This will become important later on.)

Of course, I'm still very far from being done, by this point. I'm already 3 weeks into the competition, and I don't even have any interactive elements. So far it's all been hand-wavy "realistic graphics are so important!" stuff. That was my biggest mistake of this project. (Don't get me wrong, it's a great tech demo.) Time to work on game play elements, right?

Nope

Gouraud shaded roads do not belong with that sunset! We need an asphalt texture. Yes, Leo, yes.


I looked at a lot of photos of asphalt on The Googlie Images. (Try telling that to your girlfriend...) Seems like it should be pretty easy to generate a texture... Just a bunch of white noise made really dark! But white noise doesn't actually look like asphalt, it looks like TV static. And darkening TV static just looks like dark TV static. No, we need a method. Something more natural. Something that looks bumpy, instead of noisy.

In my search, I came across this Gimp tutorial: http://gimpchat.com/viewtopic.php?f=23&t=9760 It isn't super great, but it would do. I have the directions, now I need to codify it. I already have clouds, and white noise is a good replacement for HSV noise. The steps I'm missing are "sparkle", "emboss", and "gaussian blur". (I skipped making any cracks.) Blur is an easy one; just a matter of duplicating neighboring pixels with some falloff. I was able to combine sparkle and emboss into a single approximation by applying another "neighboring pixel" algorithm after the blur step; if the neighboring pixels are too dark, make this pixel bright. Throw in some desaturation and...


That'll do! Bonus: this texture wraps appropriately, which is important for realism (there I go again!)

Now it looks [a little bit] like freshly paved asphalt. It's missing something, though. Oh yeah, lines for the driving lanes. This was a fun bit! I created something like a paint roller simulation to make sure the deep crevasses in the asphalt left air bubbles that would pop before the paint dries.


I kid, I kid. The intension, of course, was for the texture to look like the paint had imperfections. That's what makes the game's unique style so "realistic"; I relished in the imperfections! The algorithm is simple; if the color is too dark, leave it dark. If its brightness is above a certain threshold, replace it with a yellow or white pixel. Plus some white noise for added roughness.

Two interesting points: First, the vertical lines do not have a perfectly flat edge (again, by design). There is an extra condition along with the brightness test; on the edges of the lines, the brightness threshold is raised, meaning fewer pixels will be colored. Second, the texture shown here is what appears in the final build, even though two other line patterns are generated (double solid, and broken left). This one creates the broken right line.

My intention was to randomly choose one of the three textures for a random run of highway. It's complicated a bit by my road paving algorithm, which is inherently iterative. The idea was to take the value in the first column of the fractal on the current row, and derive two numbers: which line pattern to use [0..2] and how long to let it run. That would have been deterministic.

The harder part, IMHO, would be putting the proper texture coordinates into the geometry buffer as it shifts. Remember when I said that the texture coordinate reuse would be important? This is where it bites us. With additional textures on the road, my buffer is no longer cyclical, making the shift operation hard. Not impossible, just "I don't have time to do this right now."




A selection of screenshots showing the different line patterns. Now I have a road that is drawn correctly. 2 days left in the competition, and I still need to make the camera move along the road, and add some kind of interactivity. Jeez!

LERP FTW

Linear interpolation (LERP, aka tween) is a glorious thing. You've seen it before. You've probably even used it. It creates smooth (linear) transitions between two states over time. To create the driving animation between plays, I run a LERP between each road segment, along the midline. It interpolates the camera position and rotation in one step. Finally, the camera is translated to the right side of the road, about where a driver would be sitting (if you're in the US).

The LERP was a bit of a problem to get right. Mostly because camera matrices are awful. I ended up with a decent matrix inversion function based on maths from an awesome website I found. It's slightly cheaper (and easier to understand) than the popular method.

Anyway, I finally got something to work "good enough", and that's what you see in the final build. It's a little bit bouncy when the camera jumps to the next segment. I haven't looked into any way to make that better.

1.5 days remain, and I still don't have any interactive elements to the game! My pacing on this was atrocious.

Input Output

Scope Creep Monster was getting a little unruly, to be quite honest. I still wanted it all; desktop support, mobile support, keyboard input, gamepad input, motion controls, ... I must be insane.

Sure! Why not? It's not like it's a whole lot of code! In an evening, I had a full featured input module that covered keyboard, gamepad, and motion controls. Each input sets normalized state in an input object, which is used later by the game.

I plugged my new input object into the camera code, which required a lot of refactoring, actually. But it worked! With a little bit of physics code, I was able to drive in reverse! Of course, the first iteration was horribly broken. The auto-driving camera was responsible for shifting the road segments, but with a human in control, I cannot use the same LERP! Humans don't drive linearly.

What I needed was collision detection; determine when the camera crossed the "current location" of the road generator, and generate the next segment! The road would be generated as it was driven on. (You can see the effect of this in the final build by making a really sharp turn... There's nothing but the void behind you!) I needed collision detection anyway to keep the player on the road. Any off-roading could not be allowed.

I could have used any number of collision algorithms, like SAT. But I felt that was unnecessary overkill, there had to be a cheaper way! In a quick flash of inspiration, I decided to try a triangulation between the camera and the two points on either side of the road. (Remember the image earlier with the red dot and two blue dots? Yes those blue dots.) This turned out to be really easy! Not even a triangulation, really. Just two dot-products.

I have no idea what a dot-product is (it's a 1D projection of one vector onto another), but if you take the dot product of a vector against itself, you get the square of the vector's length! It's basically the pythagorean theorem, so yeah, I guess it is triangulation, of sorts! Anyway, the camera's position minus the point's position is the vector whose length I want. The variables are as follows:

  • The road is 6 units wide
  • The camera is somewhere within the area covered by the last [generated] segment of the road.
  • The camera can only move backwards

Given these constraints, it's easy to compute when the player passes between the two points: take the square root of each vector, and add them. If the sum is near 6 (the width of the road) then the line between the two points is being crossed. Try it out on a sheet of graph paper! It's simple geometry. For simplicity, I consider "near 6" to be < 6.1. That gives a 10% margin of error, more than enough to account for weird floating point precision and rounding errors.

That takes care of generating more road. Determining whether the player left the road was equally easy! The road is 6 units across, so logically, if either point is ever more than 6 units away from the camera, the player has left the road. This can be simplified a bit because you don't need a square root of the dot-product; just compare the dot-product directly to 6².

Magic! I have a complete game, with a full day to spare. What could possibly go wrong?

Sound Makes It FUN

This is a tip I learned long, long ago; music and sound effects are the real key to creating an enjoyable experience. This is part of the "juicing" philosophy to game development. You can have the craziest screen shake, most intense particle effects, and super colorful warpy shader things... But without sound effects to go with it, it's kind of dead. While my game has none of those juicy things, I certainly didn't go crazy on sounds, either!

I woke up on "launch day", with a full 16-and-something hours until deadline. The first thing on my TODO list was music. Earlier I had played with the idea of fully procedural music generation, so I did some studying, and now have a cursory understanding of the math behind music theory. There's not much to it, but I don't have enough practical knowledge to make use of any of that. My attempt with the WebAudio API was the ultimate disaster. Thankfully, that never made it into a single commit.

I was then informed on The Twitters by a fellow js13k developer, "Sonant-x to the rescue!" Holy crap, Ryan, you saved the day! Within an hour or two, I had my sound track. It turned out well. So well in fact, that I've had the music playing on repeat almost constantly since I wrapped it up. How in the hell? I'm no musician. Did that really come from my brain? Props to Nicolas Vanhoren, Marcus Geelnard, and Jake Taylor for their work which culminated into a tool that allows a tool like me to create a melody so peaceful and serene.

You're gonna need it, because this game ... Oh, this game.

The music really brought the project to fruition, IMO. But with over half a day remaining, there was plenty of time to juice it up and make the experience a little more coherent. I added a title screen by reusing the LERP animation (auto-driving) and putting "cursive" font-family text (a div element) over the canvas. I re-reused the animation for the loss screen, and made the loss text as happy as I could. Because let's face it, the game is truly brutal and unforgiving. Every little bit of happiness that can be impressed upon my unfortunate audience will be well-deserved.

Oh yeah, and iOS is a constant source of grief! I had to add a "Tap to continue" message after the loader for iOS, because SCREW YOU APPLE. That's why. iOS silences all audio until audio is played in response to a user interaction. I don't know why. I pinched a small function from howler.js to enable audio on iOS in response to a tap. The tap only enables audio about 10% of the time on the iOS simulator. It seems a bit better on real hardware. Oh well, I give up. Chrome for Android works flawlessly without this terrible hack.

Sound effects! I was still missing sound effects. There's only one sound effect that a driving game truly needs. A motor! I knew from my previous music research that synthesis was the way to go. But I stopped short of understanding Fourier Transforms, so I couldn't really get into it that much. Instead, The Googlie showed me the light! A tiny javascript library that generates motor sounds! With a bit of tuning, it would be perfect.

First, the library as-is only generated audio for the left channel in a stereo audio buffer. Easy enough to duplicate the wave into both channels. And second, it only generated purely random (white noise) sounds. I wanted more control over it, so I added a simple deterministic generator that produced a fine motor sound. It just had to be hooked up to my physics (completed earlier in the day, following input). Velocity affects both motor volume and frequency, which is pretty darned convincing.

Strangely, the motor sound is kind of awful on Chrome for Android. I haven't looked into why that is.

Final Touches

There isn't much more to say about the development process. I added a score counter which tracks the distance driven, and saves the high score to localStorage. Nothing special there! Another fix added late was support for 3:1 screen ratios (crazy), because the cloud layers are capable of scrolling all the way until a gap appears at the right side on wider display ratios. This was fixed by stretching the clouds horizontally a little bit.

And crucially, a game breaking bug was fixed just before submission; it only occurred in the minified/compressed build, due to overaggressive compression techniques! At just under 9KB (with 4KB to spare, that's about 30%) I could really afford to loosen up on the compression. I removed the breaking code, which cost 70 bytes (lol) but increased compatibility. All good!

What Went Wrong

iOS was painful almost the entire way. First, I was surprised an early build worked at all on my ancient iPhone 4S. Little did I know that adding an awesome hack would completely break it on iOS. I demand iOS compatibility! So I removed the hack very late to get it working acceptably again. And the final punch to my gut from iOS is the terrible motion controls. I didn't bother adding a manual calibration step (which was honestly a mistake). The gyro is not the most accurate thing in the world. It kind of sucks, actually.

The other thing I'm disappointed with is spending 90% of my time on graphics. A fatal rookie mistake if ever there was one. I don't know if I'll ever learn to mitigate this issue properly.

What Went Right

Uh, well, yeah. It's safe to say that pretty much everything (except iOS) went very well! There were normal development hiccups and bugs along the way (par for the course), but looking back, I don't think I've ever had a project that I've been more proud of.

There were a lot of personal firsts, here! First 3D game, first game using Procedural Content Generation, first to use WebAudio, WebGL, Gamepad, and DeviceOrientation APIs... To say that I did poorly with any of these things is missing the point! You gotta start somewhere, and for a game like Lovely Drive to set the bar high for myself right off the bat, in so many areas... Well, that's really quite humbling. (To myself, anyway.)

I feel like I also hit my Desert Bus homage goal (after cutting out the Scope Creep Monster). You can drive forever in this game, backwards of course. Importantly, you can take your time, or blaze a trail recklessly. There's no time limit, there are no obstacles to overcomplicate the already-cramped highway. The presentation is one of serenity and tranquility. I've had the music on loop almost non-stop, not out of hubris or ego. But because it truly instills an emotional reaction when I listen to it. Especially with a beautiful, inviting (ceremonial, even) font overlaid on a never ending desert sunset. It's probably the most gorgeous thing I will ever make.

I didn't have any trouble with the file size. The only reason I almost hit 9KB before release was because I had to add a bunch of one-off code (the devil, to any size-restricted project). I can thank math for that.

Closing Thoughts

The game is not the most fun to play. It is challenging, and perhaps could have an audience in the party game crowd (see who can get the farthest distance in the shortest time?) But really it is not a casual game by any stretch. It is Desert Bus in reverse. That sums it up nicely.



The font is different between Desktop and Mobile. I didn't do that; browser vendors did.



1 comment:

Unknown said...

Very in depth article! Your curvy road blew my mind :)