PAGE_CONTENT = other_viewer.php
Home > Other content > platformer

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:

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!