Follow me

May 14, 2013

A simple platformer engine (part 1): basics

Writing a 2D platformer engine can be tricky if you don't really know where you're going. Using a clean and simple base is essential. You know the KiSS principle ? Keep It Short and Simple : that's the way I do it.

Most of my games are based on a similar canvas, be it a 2D platformer or a top-down game. Actually, it's interesting to note that a platformer is nothing more than a top-down engine with gravity applied to the player on every frame.

In this article, I will use the Haxe language: if you don't know it yet, it's an amazing language that can compile to many targets, including Flash, C, or iOS/Android (using NME). However, the principles here are very generic and simple, meaning that you can easily adapt to any other language.

Those 3 games share the exact same engine.



I use a simple, lightweight, Entity class which does all the basics and I extend it. Pretty classic, but there are a few tricks.

Here is a simple version of this class:
class Entity {
 // Graphical object
 public var sprite : flash.display.Sprite;

 // Base coordinates
 public var cx : Int;
 public var cy : Int;
 public var xr : Float;
 public var yr : Float;

 // Resulting coordinates
 public var xx : Float;
 public var yy : Float;
 
 // Movements
 public var dx : Float;
 public var dy : Float;

 public function new() {
  //...
 }

 public function update() {
  //...
 }
}

Coordinates system

First thing, I use a coordinate system focused on ease of use.

I usually have a grid based logic: the level, for example, is a grid of empty cells (where the player can walk) and wall cells.

Therefore, cx,cy are the grid coordinates. xr,yr are ratios (0 to 1.0) that represent the position inside a grid cell. Finally, xx,yy are resulting coordinates from cx,cy + xr,yr.

Thinking with this system makes lots of things much easier. For example, checking collisions on the right side of an entity is trivial: just read cx+1,cy coordinate. You can also use the xr value to check if the Entity is on the right side of its cell.

We will consider from now on that with have a method hasCollision(cx,cy) in our class that returns true if their his a collision at a given coordinate, false otherwise.

if( hasCollision(cx+1,cy) && xr>=0.7 ) { 
  xr = 0.7; // cap xr 
  // ...
}

The xx,yy coordinates are only updated at the end of the update loop.

Note: in Flash, updating sprite.x and sprite.y has a small cost: lots of things are updated internally when you change these values. That means each time you access them, matrices are updated, objects are rendered..etc. So you probably don't want to work on sprite.x directly, that's the reason I always use an intermediary: xx.
It also makes cross platform dev easier as the Entity class is more about logic than graphics.

// assuming the cell size of your grid system is 16px
xx = Std.int( (cx+xr) * 16 ); 
yy = Std.int( (cy+yr) * 16 );
sprite.x = xx;
sprite.y = yy;


Also, sometimes you will need to initialize cx,cy and xr,yr based on a xx,yy coordinate :

public function setCoordinates(x,y) {
  xx = x;
  yy = y;
  cx = Std.int(xx/16);
  cy = Std.int(yy/16);
  xr = (xx-cx*16) / 16;
  yr = (yy-cy*16) / 16;
}


X movements

On every frame, the value dx is added to xr.
If xr becomes greater than 1 or lower than 0 (ie. the Entity is beyond the bounds of its current cell), the cx coordinate is updated accordingly.

while( xr>1 ) {
  xr -= 1;
  cx ++;
}
while( xr<0 ) {
  xr += 1;
  cx --;
}
You should always apply friction to dx, to smoothly cap its value (much better results than a simple if).
dx *= 0.96;
In your main loop, when the appropriate event is fired (key press or anything), you can simply change dx to move your entity accordingly.
// hero being an Entity
hero.dx = 0.1;
// or
hero.dx += 0.05;

X collisions

Checking and managing collisions is pretty simple:
if( hasCollision(cx+1,cy) && xr>=0.7 ) { 
  xr = 0.7;
  dx = 0; // stop movement
}
if( hasCollision(cx-1,cy) && xr<=0.3 ) { 
  xr = 0.3;
  dx = 0; 
}

X complete !

Here is the complete source code for X management. Couldn't be simpler :)
xr+=dx;
dx*=0.96;

if( hasCollision(cx-1,cy) && xr<=0.3 ) {
 dx = 0;
 xr = 0.3;
}
if( hasCollision(cx+1,cy) && xr>=0.7 ) {
 dx = 0;
 xr = 0.7;
}

while( xr<0 ) {
 cx--;
 xr++;
}
while( xr>1 ) {
 cx++;
 xr--;
}

What about Y?

Mostly copy and paste. There could be a few differences though, depending on the kind of game you're making. For example, in a platformer, you may want the yr value to cap at 0.5 instead of 0.7 when a collision is detected underneath Entity feet.
yr+=dy;
dy+=0.05;
dy*=0.96;

if( hasCollision(cx,cy-1) && yr<=0.4 ) {
 dy = 0;
 yr = 0.4;
}
if( hasCollision(cx,cy+1) && yr>=0.5 ) {
 dy  = 0;
 yr = 0.5;
}

while( yr<0 ) {
 cy--;
 yr++;
}
while( yr>1 ) {
 cy++;
 yr--;
}
Here is a demo of this simple engine running (source code):
Here is Entity class from this demo:
import flash.display.Sprite;

class Entity {
 var man     : Manager;
 
 public var sprite  : Sprite;
 
 public var cx   : Int;
 public var cy   : Int;
 public var xr   : Float;
 public var yr   : Float;
 
 public var dx   : Float;
 public var dy   : Float;
 public var xx   : Float;
 public var yy   : Float;
 
 public function new() {
  man = Manager.ME;
  
  cx = 5;
  cy = 0;
  xr = yr = 0.5;
  dx = dy = 0;
  
  sprite = new Sprite();
  sprite.graphics.beginFill(0xFFFF00,1);
  sprite.graphics.drawCircle(0,0,Const.GRID*0.5);
 }
 
 public inline function hasCollision(cx,cy) {
  return
   if( cx<0 || cx>=man.level.length || cy>=man.level[cx].length )
    true; // out of bounds
   else
    man.level[cx][cy]; // check level data
 }
  
 public function update() {
  var frictX = 0.92;
  var frictY = 0.94;
  var gravity = 0.04;
  
  // X component
  xr+=dx;
  dx*=frictX;
  if( hasCollision(cx-1,cy) && xr<=0.3 ) {
   dx = 0;
   xr = 0.3;
  }
  if( hasCollision(cx+1,cy) && xr>=0.7 ) {
   dx = 0;
   xr = 0.7;
  }
  while( xr<0 ) {
   cx--;
   xr++;
  }
  while( xr>1 ) {
   cx++;
   xr--;
  }
  
  // Y component
  dy+=gravity;
  yr+=dy;
  dy*=frictY;
  if( hasCollision(cx,cy-1) && yr<=0.4 ) {
   dy = 0;
   yr = 0.4;
  }
  if( hasCollision(cx,cy+1) && yr>=0.5 ) {
   dy  = 0;
   yr = 0.5;
  }
  while( yr<0 ) {
   cy--;
   yr++;
  }
  while( yr>1 ) {
   cy++;
   yr--;
  }
   
   
  xx = Std.int((cx+xr)*Const.GRID);
  yy = Std.int((cy+yr)*Const.GRID);
  sprite.x = xx;
  sprite.y = yy;
 }
 
}
Don't hesitate to leave a comment if you have any question :) Read the second part of this article.