Tiny Platformer

Mon, May 27, 2013

I’ve been thinking about making a platform game recently, but I’ve never done one before, so I wanted to start small, very, very small!

How about starting with just a tiny rectangle player character jumping around on some larger rectangle platforms.

No, I’m not going to remake Thomas was Alone (a great game by the way).

Instead I just want to make a quick-and-dirty simple platform mechanic just to get started. No complexity, no baddies, no treasure to collect, just a single level.

You can view the source code or “play” the game below, just use the LEFT and RIGHT arrow keys to move the little yellow fellow around and SPACE to jump.

Sorry, this example cannot be run because your browser does not support the <canvas> element
ARROW keys to move, SPACE to jump

I told you it would be small

Finally, a weekend project that actually took less than one weekend!

I hope to build on top of this with a more robust platform game over the summer, but for now, let’s look at what it takes to get this minimal mechanic implemented:


A note on code structure: Remembering that this is just an example of a simple mechanic, and not production ready code, we don’t need to over-engineer any kind of complicated OO class design. In fact, to keep this simple I use global variables and methods (oh the horror!)

Constants and Variables

Start with a number of (tunable) constants to define our world:

var MAP      = { tw: 64, th: 48 }, // the size of the map (in tiles)
    TILE     = 32,                 // the size of each tile (in game pixels)
    METER    = TILE,               // abitrary choice for 1m
    GRAVITY  = METER * 9.8 * 6,    // very exagerated gravity (6x)
    MAXDX    = METER * 20,         // max horizontal speed (20 tiles per second)
    MAXDY    = METER * 60,         // max vertical speed   (60 tiles per second)
    ACCEL    = MAXDX * 2,          // horizontal acceleration -  take 1/2 second to reach maxdx
    FRICTION = MAXDX * 6,          // horizontal friction     -  take 1/6 second to stop from maxdx
    JUMP     = METER * 1500;       // (a large) instantaneous jump impulse

In addition, we have our game canvas, its context, and our player object:

var canvas   = document.getElementById('canvas'),
    ctx      = canvas.getContext('2d'),
    width    = canvas.width  = MAP.tw * TILE,
    height   = canvas.height = MAP.th * TILE,
    player   = { x: 320, y: 320, dx: 0, dy: 0 };

And a couple of utility methods for converting between tile and pixel coordinates:

var t2p = function(t) { return t*TILE;             },
    p2t = function(p) { return Math.floor(p/TILE); },

Level Data

We start with a simple COLOR palette, and an array of COLORS to represent each tile type:

var COLOR  = { BLACK: '#000000', YELLOW: '#ECD078', BRICK: '#D95B43', PINK: '#C02942', PURPLE: '#542437', GREY: '#333', SLATE: '#53777A' },
    COLORS = [ COLOR.BLACK, COLOR.YELLOW, COLOR.BRICK, COLOR.PINK, COLOR.PURPLE, COLOR.GREY ];

We can then use the open source Tiled map editor to create the level layout:

The editor creates an xml .TMX output file that defines the level, but I’m not going to be taking advantage of any advanced features, so instead I can simply export it as JSON, manually pull out the data for the layer, and paste it directly into my source code as a simple array of cells, along with a couple of simple accessor methods:

var cells = [5, 5, 5, 5, 5, 5, ... ],
    cell  = function(x,y)   { return tcell(p2t(x),p2t(y));    },
    tcell = function(tx,ty) { return cells[tx + (ty*MAP.tw)]; };

If I add multiple levels at a later date then I could easily load the exported .json files using an ajax call at the start of each level and a quick call to JSON.parse() in order to extract the cells, but I don’t need that for this demo.

Game Loop

As usual, we will be using requestAnimationFrame to ensure our rendering loop is as smooth as possible with a controlled, fixed timestep update loop that is independent of our rendering loop. I explored this previously when implementing BoulderDash.

We will need a way to measure the current time. In modern browsers we can use the high resolution javascript timer, with a fall-back for older browsers:

function timestamp() {
  if (window.performance && window.performance.now)
    return window.performance.now();
  else
    return new Date().getTime();
}

The render loop runs as often as the browser allows, while the update loop is fixed by accumulating time until we reach the step required to trigger an update():

var fps  = 60,
    step = 1/fps,
    dt   = 0,
    now, last = timestamp();

function frame() {
  now = timestamp();
  dt = dt + Math.min(1, (now - last) / 1000);
  while(dt > step) {
    dt = dt - step;
    update(step);
  }
  render(ctx, dt);
  last = now;
  requestAnimationFrame(frame, canvas);
}

frame(); // start the first frame

NOTE: Since requestAnimationFrame will go idle when the browser is not visible, it is possible for dt to be very large, therefore we limit the actual dt to automatically ‘pause’ the loop when this happens

Interesting to note is that when we do have a large dt we make sure to run update() in a while loop to ensure the game ‘catches up’ without missing any updates, but we don’t bother doing this for render() where simply rendering the most recent state once is enough to catch up.

Keyboard Input

In addition to the running game loop, we need to gather input from the user. We simply record if the player is trying to move left, right, or jump. The update() loop takes care of actually moving the player.

document.addEventListener('keydown', function(ev) { return onkey(ev, ev.keyCode, true);  }, false);
document.addEventListener('keyup',   function(ev) { return onkey(ev, ev.keyCode, false); }, false);

function onkey(ev, key, down) {
  switch(key) {
    case KEY.LEFT:  player.left  = down; return false;
    case KEY.RIGHT: player.right = down; return false;
    case KEY.SPACE: player.jump  = down; return false;
  }
}

Updating

The update() loop is where the magic happens!

function update(dt) {

  // magic happens

}

Told you!


The only entity that needs updating is the player:


Starting with some local variables:

var wasleft  = player.dx < 0,
    wasright = player.dx > 0,
    falling  = player.falling;

We can accumulate the horizontal and vertical forces that currently apply:

player.ddx = 0;
player.ddy = GRAVITY;

if (player.left)
  player.ddx = player.ddx - ACCEL;     // player wants to go left
else if (wasleft)
  player.ddx = player.ddx + FRICTION;  // player was going left, but not any more

if (player.right)
  player.ddx = player.ddx + ACCEL;     // player wants to go right
else if (wasright)
  player.ddx = player.ddx - FRICTION;  // player was going right, but not any more

if (player.jump && !player.jumping && !falling) {
  player.ddy = player.ddy - JUMP;     // apply an instantaneous (large) vertical impulse
  player.jumping = true;
}

And integrate the forces to calculate the new position (x,y) and velocity (dx,dy):

player.y  = Math.floor(player.y  + (dt * player.dy));
player.x  = Math.floor(player.x  + (dt * player.dx));
player.dx = bound(player.dx + (dt * player.ddx), -MAXDX, MAXDX);
player.dy = bound(player.dy + (dt * player.ddy), -MAXDY, MAXDY);

One tricky aspect of using a frictional force to slow the player down (as opposed to just allowing a dead-stop) is that the force is highly unlikely to be exactly the force needed to come to a halt. In fact, its likely to overshoot in the opposite direction and lead to a tiny jiggling effect instead of actually stopping the player.

In order to avoid this, we must clamp the horizontal velocity to zero if we detect that the players direction has just changed:

if ((wasleft  && (player.dx > 0)) ||
    (wasright && (player.dx < 0))) {
  player.dx = 0; // clamp at zero to prevent friction from making us jiggle side to side
}

Collision Detection

Our collision detection logic is greatly simplified by the fact that the player is a rectangle and is exactly the same size as a single tile. So we know that the player can only ever occupy 1, 2 or 4 cells:

This means we can short-circuit and avoid building a general purpose collision detection engine (e.g. a quad tree) by simply looking at the 1 to 4 cells that the player occupies:

var tx        = p2t(player.x),
    ty        = p2t(player.y),
    nx        = player.x%TILE,         // true if player overlaps right
    ny        = player.y%TILE,         // true if player overlaps below
    cell      = tcell(tx,     ty),
    cellright = tcell(tx + 1, ty),
    celldown  = tcell(tx,     ty + 1),
    celldiag  = tcell(tx + 1, ty + 1);

If the player has vertical velocity, then check to see if they have hit a platform below or above, in which case, stop their vertical velocity, and clamp their y position:

if (player.dy > 0) {
  if ((celldown && !cell) ||
      (celldiag && !cellright && nx)) {
    player.y = t2p(ty);       // clamp the y position to avoid falling into platform below
    player.dy = 0;            // stop downward velocity
    player.falling = false;   // no longer falling
    player.jumping = false;   // (or jumping)
    ny = 0;                   // - no longer overlaps the cells below
  }
}
else if (player.dy < 0) {
  if ((cell      && !celldown) ||
      (cellright && !celldiag && nx)) {
    player.y = t2p(ty + 1);   // clamp the y position to avoid jumping into platform above
    player.dy = 0;            // stop upward velocity
    cell      = celldown;     // player is no longer really in that cell, we clamped them to the cell below 
    cellright = celldiag;     // (ditto)
    ny        = 0;            // player no longer overlaps the cells below
  }
}

Once the vertical velocity is taken care of, we can apply similar logic to the horizontal velocity:

if (player.dx > 0) {
  if ((cellright && !cell) ||
      (celldiag  && !celldown && ny)) {
    player.x = t2p(tx);       // clamp the x position to avoid moving into the platform we just hit
    player.dx = 0;            // stop horizontal velocity
  }
}
else if (player.dx < 0) {
  if ((cell     && !cellright) ||
      (celldown && !celldiag && ny)) {
    player.x = t2p(tx + 1);  // clamp the x position to avoid moving into the platform we just hit
    player.dx = 0;           // stop horizontal velocity
  }
}

The last calculation for our update() method is to detect if the player is now falling or not. We can do that by looking to see if there is a platform below them:

  player.falling = ! (celldown || (nx && celldiag));

And thats the update() loop complete, running this at 60fps gives us our simple platform game mechanic.

Rendering

Finally, rendering our map is trivial. We use the standard <canvas> fillRect() method to draw rectangles for each tile and one more for the player:

function render(ctx) {

  // render map
  var x, y;
  for(y = 0 ; y < MAP.th ; y++) {
    for(x = 0 ; x < MAP.tw ; x++) {
      ctx.fillStyle = COLORS[tcell(x,y)];
      ctx.fillRect(x * TILE, y * TILE, TILE, TILE);
    }
  }

  // render player
  ctx.fillStyle = COLOR.YELLOW;
  ctx.fillRect(player.x, player.y, TILE, TILE);
}

Since the map is static, we could have easily cached a rendered version in an off-screen canvas, but for something this simple performance is not an issue, so we can just re-render it every frame and keep the code clear and simple.

Conclusion

That is a very, very, minimal platform game, but it gets me started! and leaves so much more to add later:

Maybe, one day I’ll add some of those things, but until then…

Enjoy!