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.
Hi, I think you forgot to insert the check for cell bounds in "X complete" paragraph.
Thank you very much for sharing your work!
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?
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.
Hi, the tutorial is really good, I'm using heaps to follow the tutorial do you think "h2d.Object" can replace "flash.display.Sprite"?
Sure, I updated the tutorial. Any sprite class will do the job here, including h2d.Object, h2d.Graphics, or h2d.Bitmap.
How would you handle objects larger than the tiles themselves?
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!
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
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/
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 =)
Thank you :)
To be honest, I’m not sure what to talk about in part 3 yet, any suggestion?
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?
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
You’re right: the way Haxe3 manages callbacks has changed, you will need to convert them (I didn’t migrate my libs to haxe 3 yet, sorry!).
http://haxe.org/manual/haxe3/migration
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
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;
}
Thank you ! That works very well !
Cool tutorial, thanks for sharing, can’t wait for the part 2 :)
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.
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.
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 ;)
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 :)
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 :)
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;
}
Made a small mistake: friction my be applied outside of the loop! Otherwise, it will be applied too much during big moves.
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.
Hi, What do you think about Component/Entity/System-based aproach? And http://www.ashframework.org/ framework?
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 :)
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?
Good questions: I will post another article about that :)
Had a quick rundown and it looks pretty good, nice live demo.
—
pixelcade.co.uk