Zovirl Industries

Mark Ivey’s weblog

Making of an HTML5 game: Forest

I wrote earlier about Forest, a meditation game I made for the Super Friendship Club's Mysticism Pageant (you can play it here). That article covered my motivations and inspirations, but not the details of how it was put together. This article covers the details.

Overview

The game uses HTML5, specifically the <canvas> tag for graphics and the <audio> tag for music. The world is procedural, which means that instead of using photoshop to paint the levels by hand I made some algorithms to generate the world. This has two advantages:

  1. It is a very fast way to get a lot of variety, which helps the world look natural and convincing.
  2. It was more fun. I didn't have to sit down and paint 20 or 30 different trees by hand.

I worked on this in the evenings and on weekends for about three weeks during the pageant, and then spent a couple more weeks cleaning things up and polishing it after. It is about 1500 lines of JavaScript.

Procedural Trees

I had the good fortune to be able to work on the code for generating the trees while sitting on the porch of a cabin near Lake Tahoe. It was quite relaxing, reclining in the shade of towering pine trees while making a virtual forest.

The normal way of doing procedural trees appears to be L-systems, but that seemed too complicated. I did something simpler:

  1. Trees are made up of blocks of varying sizes and colors.
  2. Blocks are laid out in straight lines to make branches, with blocks getting smaller along the branch.
  3. Starting from the main branch (the trunk), side branches split off recursively.
  4. Lengths and angles are randomly jittered, so some branches are longer, some are shorter, they branch off at slightly different angles, etc.
  5. At the end of some branches a foliage block is added in a somewhat-random shade of green.

Procedural Grass and Dirt

Grass has somewhat random heights and colors:

// Returns a css color string like "rgb(30,40,50)"
function rgb(r,g,b) { 
  var f = Math.floor; 
  return "rgb(" + f(r) + "," + f(g) + "," + f(b) + ")";
}

var width = 3;
for (var x = 0; x < SCREEN_WIDTH; x += width) {
  // Draw one blade of grass
  var shade = Math.random() * 5;
  context.fillStyle = rgb(40 + shade, 110 + shade, 50 + shade);
  var depth = Math.random() * 2;
  var height = depth + 10 + Math.random() * 10;
  context.fillRect(x, SCREEN_HEIGHT - GROUND_HEIGHT + depth, width, -height);
}

(I didn't bother including it in the sample code, but there's also a little logic to adjust the color if the grass is in a sunbeam)

The dirt has randomly-sized dark and light rectangles layered on it:

// Ground
context.fillStyle = "rgb(35, 50, 50)";
context.fillRect(0, SCREEN_HEIGHT, SCREEN_WIDTH, -GROUND_HEIGHT);

// Dark spots
context.fillStyle = "rgb(32, 47, 47)";
for (var i = 0; i < 20; i++) {
  context.fillRect(Math.random() * SCREEN_WIDTH, 
    SCREEN_HEIGHT - Math.random() * GROUND_HEIGHT,
    Math.random() * 50 + 10, 
    Math.random() * 50 + 10);
}

// Light spots
context.fillStyle = "rgb(38, 53, 53)";
for (var i = 0; i < 20; i++) {
  context.fillRect(Math.random() * SCREEN_WIDTH, 
    SCREEN_HEIGHT - Math.random() * GROUND_HEIGHT, 
    Math.random() * 50 + 10, 
    Math.random() * 50 + 10);
}

Canvas optimizations

I started out just drawing the entire canvas from scratch in every frame, using fillRect() calls for almost everything (remember, the grass, dirt, trees, and player are all made out of blocks). Performance was terrible.

I did some testing and found that at 30 FPS I could only expect to get 5000 100x100 pixel fillRect() calls. That's not nearly enough. Each tree is built from about 1000 blocks so that would only get me five trees. Worse, <canvas> appears to be fillRate() limited. If I want to fill the whole 800x600 canvas, I only get 500 fillRect() calls.

Since the trees don't change, I looked into pre-rendering them to off-screen canvas elements, then using drawImage() to copy them into the main canvas. Performance testing suggested that at 30 FPS I could get about 35 800x600 drawImage() calls. This is more than enough, since I only need 3 layers of trees to give enough depth.

For scrolling, there are two canvases for each layer. When one of the canvases scrolls off the left side of the main canvas, it is redrawn and starts scrolling in from the right side of the main canvas. This means in total, there are only 8 drawImage() calls per frame (three tree layers plus one ground layer, two canvases per layer).

Colors

I tried different color palettes:

  1. Warm tones with plenty of yellow in the shades of green. This helped create the warm, lazy summer afternoon feel I wanted.
  2. Cooler blue tones to help make the yellow fireflies stand out. The fireflies looked great but the forest felt too cold.
  3. A day/night cycle slowly changing between bright, warm greens in the daytime and dark, cool blues at night (there were even stars which came out at night). This turned out to not be as awesome as I had imagined.

In the end, I compromised and ended with with a somewhat cooler version of #1:

Text

I browsed through Google's repository of web fonts until I found Tangerine Bold, which I think goes very nicely with the theme.

I wanted to use JavaScript to render the text into the canvas directly, but I couldn't find a way to make it blocky. The obvious solution is to draw the text at a small size so the pixels are large in relation to the text, then enlarge the image. However I couldn't stop the browser from doing interpolated scaling, so the text ended up blurry instead of blocky.

I gave up on JavaScript for this and used Python instead to pre-render the text into images. I also added a slight drop-shadow to help the text stand out from the background. Using images instead of rendering from JavaScript adds about 300 KB of data to download, but at least it works.

from PIL import Image, ImageDraw, ImageFont, ImageFilter

def colorize(image, textColor, backgroundColor):
  colored = image.convert("RGBA")
  colored.putdata([textColor if value == 0 else backgroundColor 
                   for value in image.getdata()])
  return colored

text = "Breathe"
font = ImageFont.truetype('Tangerine_Bold.ttf', 36)

image = Image.new("1", font.getsize(text), '#FFF')
draw = ImageDraw.Draw(image)
draw.text((0, 0), text, font=font)
# Use nearest-neighbor when enlarging to make it blocky
image = image.resize([i*2 for i in image.size], Image.NEAREST)

coloredText = colorize(image, (187, 221, 153), (0, 0, 0, 0))
shadow = colorize(image, (0, 0, 0), (0, 0, 0, 0))
for i in range(3):  # Takes several blurs to get blurry enough
  shadow = shadow.filter(ImageFilter.BLUR)

shadow.paste(coloredText, (0, 0), coloredText)
shadow.save("breathe.png")

Movement

I wanted the movement to have a specific feel:

  1. Syncing input to breathing gives a slow pace
  2. Possible to run
  3. Possible to quickly slow down from a run

When space is held, there's an acceleration which decreases as the player's speed approaches the target speed. Rapidly clicking space builds up an acceleration boost which allows running. Finally, drag slows the player down quickly from high speeds but has little effect at slow speeds.

The acceleration from tapping space once is so small it can barely be noticed. This gives a bad experience if the player just taps space once at the start of the game because it looks like nothing happened. To fix this, if the player isn't moving and space is pressed, the speed jumps straight to the target speed. This provides nice feedback and makes it obvious that pressing space did something.

function Player() {
  this.x = 0; // position
  this.dx = 0; // speed
  this.ax = 0; // acceleration
  this.clicks = 0;  // Number of times player pressed button within clickWindow 
  this.clickWindow = 5; // Number of seconds to measure button presses over
  this.running = false; // True if player is holding down button
  this.targetSpeed = 50; // Desired speed if button is held continuously
}

// dt is how much time has elapsed since the last call to update()
Player.prototype.update = function(dt) { 
  // calculate bonus acceleration for clicking button rapidly
  this.clicks = Math.max(0, this.clicks);
  var clickRate = this.clicks/this.clickWindow;
  var boost = 49 * clickRate * clickRate;

  var drag = .003 * this.dx * this.dx;
  this.ax = boost - drag;

  if (this.running) {
    // When running, provide an alternate, possibly higher, acceleration
    this.ax = Math.max(this.ax, boost + this.targetSpeed - this.dx); 
  }
  
  this.dx += this.ax * dt;
  this.x += this.dx * dt;
}

Player.prototype.startRunning = function() {
  if (this.running) {
    return;
  }
  this.running = true;
  if (this.dx == 0) {
    // not moving yet, provide a "kick" so it is obvious something happened.
    this.dx = this.targetSpeed;
  }
  this.clicks++;
  var that = this;
  setTimeout(function() {that.clicks--;}, this.clickWindow*1000);
}

Player.prototype.stopRunning = function() {
  this.running = false;
}

var player = new Player();
var SPACE = 32;
$(document).keydown(function(e) {e.which == SPACE && player.startRunning();});
$(document).keyup(function(e) {e.which == SPACE && player.stopRunning();});
// Prevent space from scrolling the page
$(document).keypress(function(e) {return e.which != SPACE});

Metrics

Google Analytics can do event tracking, which is perfect for gathering metrics on how many players start the game and how many players make it all the way to the end.

function trackEvent(action) {
  if (_gaq) {
    _gaq.push(["_trackEvent", "forest", action]);
  } else {
    console.log("Analytics not loaded. Not logging event: " + action);
  }
}

Even though the game only lasts five minutes, the metrics made it obvious that players were leaving before finishing. Based on this, I added the "Breathe" and "Relax" reminders in the middle to hint that the game doesn't just run on forever. Metrics showed an improvement in the number of players reaching the end after this change.