Back A simple and generic 2D engine, part 1 31 | ♥ 112

Introduction

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. Even Dead Cells uses this exact base engine. By the way, 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. However, the principles here are very generic and simple, meaning that you can easily adapt to any other language.

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 : YourSpriteClass;

	// 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: sometimes, 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 modify 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 --;	cx ++;}
while( xr<0 ) {	xr ++;	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.7 ) {
  xr = 0.7;
  dx = 0;
}
if( hasCollision(cx-1,cy) && xr<=0.3 ) {
  xr = 0.3;
  dx = 0;
}

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.3 ) {
  dy = 0;
  yr = 0.3;
}
if( hasCollision(cx,cy+1) && yr>=0.5 ) {
  dy = 0;
  yr = 0.5;
}

while( yr>1 ) { cy++; yr--;}
while( yr<0 ) {	cy--; yr++;}

Don’t hesitate to leave a comment if you have any question :)

Read the second part of this article.

Leave a Reply

Your email address will not be published. Required fields are marked *

  1. Damiano:

    Hi, I think you forgot to insert the check for cell bounds in "X complete" paragraph.
    Thank you very much for sharing your work!

    April 3, 2021 at 16:13
  2. Aaron:

    This is a great concept. Having done some testing in JavaScript, it seems faster to replace the while loops:

    “`javascript
    while(xr > 1) { xr –; cx++ }
    while(xr < 0) { xr ++; cx– }
    “`

    With this instead:
    “`javascript
    cx += xr | 0; xr %= 1
    “`

    Is there anything I'd be missing out on by taking this approach?

    October 13, 2020 at 16:18
    • Sébastien Bénard:

      This should work fine :) Note that in reality, most of the time there's no actual loop, because xr is rarely greater than 1 (except if you're moving something really fast). So in this case, you just have one pass.

      October 13, 2020 at 16:48
  3. Yaymaha:

    Hi, the tutorial is really good, I'm using heaps to follow the tutorial do you think "h2d.Object" can replace "flash.display.Sprite"?

    February 25, 2020 at 15:22
    • Sébastien Bénard:

      Sure, I updated the tutorial. Any sprite class will do the job here, including h2d.Object, h2d.Graphics, or h2d.Bitmap.

      February 25, 2020 at 15:40
  4. Paul:

    How would you handle objects larger than the tiles themselves?

    February 15, 2015 at 18:58
  5. Rapito:

    Jordan, is reallly not that hard.

    Here:

    The system basically works by constantly tracking an entity's location on a specific cell on your grid system.

    In chess, you know this by knowing in which XY cell is a piece located, this goes further by tracking other values: the distance from the cell's center in which the sprite is located; Enter ratio (xr & yr). These variables allow you to know if the sprite is at the farthest right/left/top/down side of the cell it is currently sitting on.

    This last technique is impressively fast in comparison to others, for collision detecting. The author, Sebastien, just needs to check if there's an object on the surrounding cells, and after comparing the cell position ratio within a defined threshold he takes an action.

    Other techniques involve having to constantly check for virtual polygons/rectangle collisions using much more complicated math, then try adding sprite height and width calculations.

    A simple enough game could be tracked on two int[][] arrays!!

    I particularly loved this article and I'm more than grateful to the author.
    Thanks, and great work!

    February 13, 2015 at 19:39
  6. Jordan:

    As a beiginner i find the calculations you have done to work things out very hard. Can somebody explain how this type of grid system works?
    Thanks

    May 1, 2014 at 21:42
  7. Xavier Belanche:

    Haha… you can do so much if you follow this:

    http://higherorderfun.com/blog/2012/05/20/the-guide-to-implementing-2d-platformers/

    otherwise check out this platform game development to get some ideas: http://katyscode.wordpress.com/tag/platform-game/

    July 15, 2013 at 17:59
  8. Xavier Belanche:

    Hi Sébastien!

    I updated the Key.hx and now it runs well. Here is the source code if you want to replace the old one:

    http://snk.to/f-cdn32jj1

    On the other hand, I hope you got time to write the three part of the platform tutorial =)

    July 11, 2013 at 11:07
    • Sébastien Bénard:

      Thank you :)
      To be honest, I’m not sure what to talk about in part 3 yet, any suggestion?

      July 11, 2013 at 17:47
    • Pierre:

      I have some suggestions for a next part:
      – Monsters and shots: how to add monsters, how they move (patrols? pathfinding?), and how we can fire at each others?
      – Animating sprites: how to add animations that respond to key inputs, behaviors and interactions?
      – Particles system: how to make a simple particle system as the one you used in Atomic Creep Spawner (door smashing) or in Proletarian Ninja (blood effects) for instance?

      August 27, 2013 at 02:30
  9. Xavier Belanche:

    Awesome and helpful job! But I’m stuck trying to compile the source code on Haxe3. Is there any workaround? Here is the err output (I guess it needs to upgrade the callback function):

    $ haxe 2dEngine.hxml
    src/Key.hx:17: characters 61-81 : callback syntax has changed to func.bind(args)
    src/Key.hx:17: characters 61-69 : Unknown identifier : callback
    src/Key.hx:17: characters 61-81 : First parameter of callback is not a function
    src/Key.hx:18: characters 59-80 : callback syntax has changed to func.bind(args)
    src/Key.hx:18: characters 59-67 : Unknown identifier : callback
    src/Key.hx:18: characters 59-80 : First parameter of callback is not a function

    July 11, 2013 at 07:52
  10. undefined:

    This is an awesome way to manage coordinates of an entity ! Thank you for sharing !
    I am studying Hammerfest physics, it’s really hard (for me at least) to understand all the mechanics behind it, but I love so much how !

    I have a small question about the physics of the game : when Igor can’t go high enough to jump over a platform, but if his feet reach between the top and the middle of this platform, and so his “yr” goes over 0.5, why isn’t he teleported on the top of the platform ?

    http://i.imgur.com/JvEEGcg.png

    Thanks

    May 20, 2013 at 17:36
    • Sébastien Bénard:

      That’s because in this case, there is an extra check to allow this “teleport” (makes controls smoother):

      if( !hasCollision(cx,cy-1) && hasCollision(cx,cy) && dy>0 && xr<=0.3 ) {
      dy = 0;
      cy–;
      xr = 0.5;
      }

      May 21, 2013 at 19:44
    • undefined:

      Thank you ! That works very well !

      May 23, 2013 at 20:11
  11. Quentin Dreyer:

    Cool tutorial, thanks for sharing, can’t wait for the part 2 :)

    May 16, 2013 at 09:28
  12. Pierre:

    So the engine is frame-rate-dependent since nothing relies on a dt (delta time) between frames? How it copes with slow devices?
    I also recommend the Entity system approach for games since it permit heavy reusability and a sort of multiple inheritance which is a must-have dealing with games. As an example, Unity3d engine is based upon it.

    May 16, 2013 at 07:51
    • Sébastien Bénard:

      Totally right. I used to use delta time, but I switched to a system where I simply call updates() more than once in a frame if the framerate drops. Almost the same result (user will generally not notice that) but simpler code: you don’t have to multiply everything with dt and don’t have to care about big “jumps” on slow frames.

      May 16, 2013 at 08:41
    • Pierre:

      Mmh interesting! I can’t yet figure out how it would slow down the whole thing (since multiple calls to update() happen when the framerate is already low), but it feels the Kiss paradigm underneath compared to a rather complex continuous collision detection system as Speculative Contacts & co.
      Thanks for sharing and not to mention I loved every game you made for LD48 ;)

      May 16, 2013 at 13:00
    • Sébastien Bénard:

      Technically, I have a flag in my update which is TRUE if it’s a render (normal) frame, or FALSE if it’s a “skipped” frame (ie. more than 1 frame in the current iteration). Only the logic is ran during skipped frames, absolutely no graphic update .
      The important thing is that this way to handle lags doesn’t force you to use a dt variable in every calculation. Ex:
      xr += dx*dt; // versus: xr += dx;

      Not sure if it’s really clear :)

      May 16, 2013 at 13:27
    • Pierre:

      That was perfectly clear :) and quite clever.
      I have a remark on the collision detection code. Since this relies on a “step by step” detection, you avoided a step from being great enough to cross over an entire wall keeping the update rate high enough (which is not altered by any uncontrollable factor as FPS).
      But actually, if dx is really big, say, 3.1 (because you just received a big slap in your face by a giant enemy, or whatever), you can possibly pass through the wall which is 2 squares behind you:
      hasCollision(cx-1,cy) would return false since the square just behind is free, then the while loop would substract 3 to cx… which deport you 3 cells behind..; on the other side of the wall (or IN the wall maybe :s).. What a big slap, isn’t it? :D
      From my point of view, hasCollision should be checked inside the while loop, no?

      And my second point, (I promise I’ll stop annoying you after that one :p) is that I had issues with the demo trying to reach a recess in a wall when I fall from the wall or when I jump from its base. I thought it was because the X collision was processed before the Y one, which pushed me out and avoided me to get in. But in fact this was probably because my horizontal speed was not enough to close the 0.3 gap between me and the wall during my move in front of the recess. I then realized that it was also this constant which gives this pretty effect when falling from the corner of a tile (as if we descended some stairs).
      I don’t know if it was intentional but it’s very nice :)
      I wonder how to manage entities bigger than a cell since it’s related (the actual entity has a radius of 0.4, so there are “margins” of 0.3), and so, I can’t wait for your next post on it :)

      May 16, 2013 at 21:23
    • Sébastien Bénard:

      In a game where dx (or dy) could have really high values, you will need to do this:

      // divides dx in sub steps, so it is never bigger than 0.5
      var steps = Math.max(1, Math.ceil(dx/0.5));
      var subDx = dx/steps;
      while( steps>0 ) {
      xr+=subDx;
      dx*=friction;
      // check X collision
      //…
      // do the “while xr<0" thing
      //…
      // do the “while xr>1” thing
      //…
      steps–;
      }

      Same goes for Y. You should not do this if your game doesn’t require it, as adding 2 loops here will have a (small) performance cost. In most games I made, I actually didn’t have to do that.

      Second point: your analysis is right :) It’s very easy to make the hero grab the corner (like in Last Breath, see Games section):

      // Collision on left side, NO collision on upper left side, hero is actually moving left, he is on top of its cell –> grab!
      if( hasCollision(cx-1, cy) && !hasCollision(cx-1,cy-1) && dx<0 && yr<0.3 ) {
      grabbing = true; // this will disable gravity
      dy = 0;
      dx = 0;
      }

      May 17, 2013 at 07:23
    • Sébastien Bénard:

      Made a small mistake: friction my be applied outside of the loop! Otherwise, it will be applied too much during big moves.

      May 17, 2013 at 07:24
    • Pierre:

      Thank you for your reply. Capping xr/yr to an arbitrary low value (<=1) as you did in the part 2 of this tuto is also a good KiSS solution if we don't need that fast moves.

      May 20, 2013 at 09:54
  13. Eugene Krevenets:

    Hi, What do you think about Component/Entity/System-based aproach? And http://www.ashframework.org/ framework?

    May 15, 2013 at 22:41
    • Sébastien Bénard:

      I will read more about this approach, but right now, I’m not sure it’s really necessary for small projects (like game jams). Nevertheless, that looks really interesting :)

      May 16, 2013 at 06:42
  14. Shizou:

    Very nice approach. how do you handle collisions between entities? and what do you do when the radius of one entity is bigger than the cellsize?

    May 15, 2013 at 05:52
    • Sébastien Bénard:

      Good questions: I will post another article about that :)

      May 15, 2013 at 08:40
  15. Greg Worrall:

    Had a quick rundown and it looks pretty good, nice live demo.


    pixelcade.co.uk

    May 14, 2013 at 21:51