Tutorial: Physics in a 2D platformer game
With our feet firmly planted on the floor, we need to do something about the walls. The player can still just move through walls like a ghost and this is not desirable in our game. The same is true for the ceilings. We can't jump in the game yet, but if we could, we could jump through ceilings. In most games, that is not desirable either. Let's do something about this.
Solid walls and ceilings
For walls and ceilings, we basically do the same as we did for the floor. Instead of just checking point A for collisions, we'll check points B, C and D as well. To do so, we expand the code of our handleCollisions() method to include the other points as well:
private void handleCollisions() { //bottom if (Level.isSolid(getTileCenterX(), getTileBottom())) { position.Y = (getTileBottom() - 1) * Level.TILE_SIZE; } //top if (Level.isSolid(getTileCenterX(), getTileTop())) { position.Y = (getTileCenterY() + 1) * Level.TILE_SIZE; } //left if (Level.isSolid(getTileLeft(), getTileCenterY())) { position.X = (getTileLeft() + 1) * Level.TILE_SIZE; } //right if (Level.isSolid(getTileRight(), getTileCenterY())) { position.X = ((getTileRight() - 1) * Level.TILE_SIZE); } }
Checking diagonal tiles
Having the above code gets us a long way but not all the way there. If you've been moving around the level, you may have noticed that our player character sometimes has a tendency to just instantly jump from one spot to another. The animation below shows what I mean in slow motion.
What happends here is that the player falls down but point A is just outside of the solid tile. As the player moves down, the physics system will keep moving the player down because point A isn't hitting any solid tiles. Eventually, point C will enter the solid tile and the physics system will detect the collision and move the player to the right side of the solid tile to resolve the collision. What we really want is for the player to land on the tile and stay there, not to be pushed to the right.
We'll need to add four more points to perform collision checks for: AC, AD, BC and BD. These are the four corners of our bounding box. In the example above, we'll need to check point AC for a collision. Point AC is inside the solid tile and so we can detect the collision. There is one problem however. How are we going to resolve this collision? Do we move the player up or do we move the player to the right? By just looking at the picture above, we can't be sure whether or not point AC entered the tile from above or from the side and thus we don't know how to resolve this collision.
The solution to this problem is fairly straightforward though. What we do is we calculate the penetration depth along each axis. This means that we look how far point AC has moved into the solid tile along the X axis, and how far it has moved into the tile along the Y axis. We then resolve along the axis that has the least penetration. For most situations, this will be correct.
In our case, the Y axis has the least penetration, so we'll resolve the collision by moving the player upwards. To implement this, we'll add the following code to our handleCollisions() method.
//left top if (Level.isSolid(getTileLeft(), getTileTop())) { float xDepth = ((getTileLeft() + 1) * Level.TILE_SIZE) - getLeft(); float yDepth = ((getTileTop() + 1) * Level.TILE_SIZE) - getTop(); if (yDepth < xDepth) { position.Y = (getTileTop() + 1) * Level.TILE_SIZE; } else { position.X = (getTileLeft() + 1) * Level.TILE_SIZE; } } //right top if (Level.isSolid(getTileRight(), getTileTop())) { float xDepth = getRight() - (getTileRight() * Level.TILE_SIZE); float yDepth = ((getTileTop() + 1) * Level.TILE_SIZE) - getTop(); if (yDepth < xDepth) { position.Y = (getTileTop() + 1) * Level.TILE_SIZE; } else { position.X = (getTileRight() - 1) * Level.TILE_SIZE; } } //left bottom if (Level.isSolid(getTileLeft(), getTileBottom())) { float xDepth = ((getTileLeft() + 1) * Level.TILE_SIZE) - getLeft(); float yDepth = getBottom() - (getTileBottom() * Level.TILE_SIZE); if (yDepth < xDepth) { position.Y = (getTileBottom() - 1) * Level.TILE_SIZE; } else { position.X = (getTileLeft() + 1) * Level.TILE_SIZE; } } //right bottom if (Level.isSolid(getTileRight(), getTileBottom())) { float xDepth = getRight() - (getTileRight() * Level.TILE_SIZE); float yDepth = getBottom() - (getTileBottom() * Level.TILE_SIZE); if (yDepth < xDepth) { position.Y = (getTileBottom() - 1) * Level.TILE_SIZE; } else { position.X = (getTileRight() - 1) * Level.TILE_SIZE; } }
There are a few things to note here:
- Knowing when the player is touching the ground is important for most 2D platformer games. It'll tell you, for instance, when it's okay for the player to initiate a jump. Basically, if your bottom (point A) check detects a collision, or your left bottom (point AC) or right bottom (point AD) checks resolve a collision along the Y axis, you know you've hit a floor.
- The order in which you check the points and resolve their collisions is important. Stick to: bottom (A), top (B), left (C), right (D), left top (BC), right top (BD), left bottom (AC), right bottom (AD).
- There are situations where our penetration depth check resolves in the wrong direction. However, these situations normally only occur as edge cases (both literally and figuratively) and the play will most likely not even notice this happening.
- When the player moves at very high speeds collision detection might fail. Imagine our player moving at a rate of 40 pixels per frame towards a 32 pixel wide wall. In one frame, the player is in front of the wall, the next frame, the player has moved beyond the wall and passed straight through it. It's also possible that due to very fast movement, the penetration depth check fails. If a player moves very fast along the X axis and hits a wall while falling down, the Y axis penetration might be less than the X axis penetration and the game decides to resolve along the Y axis by moving the player down, which is the wrong direction. The most straightforward way to counter these problems is by simply preventing your player from achieving these speeds (by setting a maximum speed)
Conclusion
The collision detection system for our game is done. It's a fairly simple system, but sufficient for a great wide variety of cases. If you are going to build a real 2D platformer game, now is the point where you'll be creating more elegant implementations for gravity and player movement (using acceleration and deceleration by friction) and implement the ability to jump. Making player movement feel "right" is essential for any succesful platformer, so I suggest you find some tutorials on how to implement these things properly. I hope this tutorial helped you set up the basics for world collision detection in a 2D platformer game. Have fun building the rest!