In this article, we go through the implementation of a basic tile-based 2D platformer. The mechanics are moving left/right, jumping and falling. There are many different ways to do this, Rodrigo Monteiro has written an exhaustive analysis on the subject and more here. We highly recommend you read it if you are new to making platformers, as it contains plenty of valuable information. We will go into a bit more detail on a few of the methods described and how to implement them in Defold. Everything should however be easy to port to other platforms and languages (we use Lua in Defold).
We assume that you’re familiar with a bit of vector math (linear algebra). If you’re not, it’s a good idea to read up on it since it’s insanely useful for game development. David Rosen at Wolfire has written a very good series about it here.
If you are already using Defold, you can create a new project based on the Article: Platformer template-project and play around with that while reading this article.
We would love to hear your feedback, so please comment at the bottom of the page!
|Some readers has brought up that our suggested method is not possible with the default implementation of Box2D. This is absolutely true and we are sorry for not mentioning that before. See the bottom of the page for info on what modifications we made to Box2D to make this work.|
Collision detection is needed to keep the player from moving through the level geometry. There are a number of ways to deal with this depending on your game and its specific requirements. One of the easiest ways, if possible, is to let a physics engine take care of it. In Defold we use the physics engine Box2D for 2D games. The default implementation of Box2D does not have all the features needed, see the bottom of this article for how we modified it.
A physics engine stores the states of the physics objects along with their shapes in order to simulate physical behaviour. It also reports collisions while simulating, so the game can react as they happen. In most physics engines there are three types of objects: static, dynamic and kinematic objects (these names might be different in other physics engines). There are other types of objects too, but let’s ignore them for now. A static object will never move (e.g. level geometry). A dynamic object is influenced by forces and torques which are transformed into velocities during the simulation. A kinematic object is controlled by the application logic, but still affects other dynamic objects.
In a game like this, we are looking for something that resembles physical real-world behaviour, but having responsive controls and balanced mechanics is far more important. A jump that feels good does not need to be physically accurate or act under real-world gravity. This analysis shows however that the gravity in Mario games gets closer to a gravity of 9.8 m/s^2 for each version. :-)
It’s important that we have full control of what’s going on so we can design and tweak the mechanics to achieve the intended experience. This is why we choose to model the player character by a kinematic object. Then we can move the player character around as we please, without having to deal with physical forces. This means that we will have to solve separation between the character and level geometry ourselves (more about this later), but that’s a drawback we are willing to accept. We will represent the player character by a box shape in the physics world.
Now that we have decided that the player character will be represented by a kinematic object, we can move it around freely by setting the position. Let’s start with moving left/right.
The movement will be acceleration-based, to give a sense of weight to the character. Like for a regular vehicle, the acceleration defines how fast the player character can reach the max speed and change direction. The acceleration is acting over the frame time-step (dt) and then added to the velocity. Similarly, the velocity acts over the frame and the resulting translation is added to the position. In maths, this is called integration over time.
The two vertical bars marks the beginning and end of the frame. The height of the bars is the velocity the player character has at these two points in time, lets call these velocities v0 and v1. v1 is given by applying the acceleration (the slope of the curve) for the time-step dt:
v1 = v0 + acceleration * dt
The colored area is the translation we are supposed to apply to the player character during the current frame. Geometrically, we can approximate the area as:
translation = (v0 + v1) * dt * 0.5
This is how we integrate the acceleration and velocity to move the character in the update-loop:
Determine the target speed based on input
Calculate the difference between our current speed and the target speed
Set the acceleration to work in the direction of the difference
Calculate the velocity change this frame (dv is short for delta-velocity), as above:
local dv = acceleration * dt
Check if dv exceeds the intended speed difference, clamp it in that case
Save the current velocity for later use (self.velocity, which right now is the velocity used the previous frame):
local v0 = self.velocity
Calculate the new velocity by adding the velocity change:
self.velocity = self.velocity + dv
Calculate the x-translation this frame by integrating the velocity, as above:
local dx = (v0 + self.velocity) * dt * 0.5
Apply it to the player character
If you are unsure how to handle input in Defold, there’s a guide about that here.
At this stage, we can move the character left and right and have a weighted and smooth feel to the controls. Let’s add gravity!
Gravity is also an acceleration, but it affects the player along the y-axis. This means that it will be applied in the same manner as the movement acceleration described above. If we just change the calculations above to vectors and make sure we include gravity in the y-component of the acceleration at step 3), it will just work. Gotta love vector-math! :-)
Now our player character can move and fall, so it’s time to look at collision responses. We obviously need to land and move along the level geometry. We will use the contact points provided by the physics engine to make sure we never overlap anything.
A contact point carries a normal of the contact (pointing out from the object we collide with, but might be different in other engines) as well as a distance, which measures how far we have penetrated the other object. This is all we need to separate the player from the level geometry. Since we are using a box, we might get multiple contact points during a frame. This happens for example when two corners of the box intersect the horizontal ground, or the player is moving into a corner.
To avoid making the same correction multiple times, we accumulate the corrections in a vector to make sure we don’t over-compensate. This would make us end up too far away from the object we collided with. In the image above, you can see that we currently have two contact points, visualized by the two arrows (normals). The penetration distance is the same for both contacts, if we would use that blindly each time we would end up moving the player twice the intended amount.
|It’s important to reset the accumulated corrections each frame to the 0-vector. Put something like this in the update-loop:|
self.corrections = vmath.vector3()
Assuming there is a callback-function that will be called for each contact point, here’s how to do the separation in that function:
Project the correction vector onto the contact normal (the correction vector is the 0-vector for the first contact point):
local proj = vmath.dot(self.correction, normal)
Calculate the compensation we need to make for this contact point:
local comp = (distance - proj) * normal
Add it to the correction vector:
self.correction = self.correction + comp
Apply the compensation to the player character:
go.set_position(go.get_position() + comp)
We also need to cancel out the part of the player velocity that moves towards the contact point:
Project the velocity onto the normal:
proj = vmath.dot(self.velocity, message.normal)
If the projection is negative, it means that some of the velocity points towards the contact point; remove that component in that case:
if proj < 0 then self.velocity = self.velocity - proj * message.normal end
Now that we can run on the level geometry and fall down, it’s time to jump! Platformer-jumping can be done in many different ways. In this game we are aiming for something similar to Super Mario Bros and Super Meat Boy. When jumping, the player character is thrusted upwards by an impulse, which is basically a fixed speed. Gravity will continuously pull the character down again, resulting in a nice jump arc. While in the air, the player can still control the character. If the player lets go of the jump button before the peak of the jump arc, the upwards speed is scaled down to halt the jump prematurely.
When the input is pressed, do:
-- jump_takeoff_speed is a constant defined elsewhere self.velocity.y = jump_takeoff_speedNote
This should only be done when the input is pressed, not each frame it is continuously held down.
When the input is released, do:
-- cut the jump short if we are still going up if self.velocity.y > 0 then -- scale down the upwards speed self.velocity.y = self.velocity.y * 0.5 end
We have not yet talked about the level geometry, i.e. the collision shapes of the environment. In Defold, there are two ways to do this. Either you create separate collision shapes on top of the levels you build. This is basically what they did in Braid. This works well if you want softer slopes in the game.
Another option is to use the image data in the tiles and generate collision shapes from that. This means that the level geometry will be automatically updated when you change the levels. We do this in Defold and merge the shapes of neighboring tiles to one, if they align. This eliminates the gaps that can make your player character stop or bump when sliding across several horizontal tiles. We do this by replacing the tile polygons with edge shapes in Box2D at load-time.
Above are four neighboring ledge-tiles. In the image of the corresponding shapes (green block), you can see that the tile shapes have been stitched to one by the green contour.
The tile-based approach is what we used for the level geometry in this game. Below is a screen-shot of how the level looked in the Defold editor.
If you want more information about platformer mechanics, here is an impressively huge amount of info about the physics in Sonic.
If you try our template project on an iOS device or with a mouse, the jump can feel really awkward. That’s just our feeble attempt at platforming with one-touch-input. :-)
We never talked about how we handled the animations in this game. You can get an idea by checking out the player.script, look for the update_animations-function.
We hope you found this information useful! Please make a great platformer so we all can play it! <3
Update: Box2D Modifications
Collisions between kinematic and static objects are ignored. Change the checks in b2Body::ShouldCollide and b2ContactManager::Collide.
The contact distance (called separation in Box2D) is not supplied to the callback-function. Add a distance-member to b2ManifoldPoint and make sure it’s updated in the b2Collide* functions.