Sprite Kit Tutorial: How to Make a Platform Game Like Super Mario Brothers – Part 1

Learn how to make a platform game like Super Mario Brothers in this Sprite Kit tutorial! By Jake Gundersen.

Leave a rating/review
Save for later
Share
You are currently viewing page 4 of 5 of this article. Click here to view the first page.

Taking Away Your Koala’s Privileges

Up to this point, the Koala got to set his own position. But now you’re taking that privilege away.

If the Koala updates his position and then the GameLevelScene finds a collision, you’ll want your Koala to get moved back. You don't want him bouncing all over like a cat on catnip!

So, he needs a new variable that he can update, but that will stay a secret between himself and the GameLevelScenedesiredPosition.

You want the Koala class to calculate and store his desired position. But the GameLevelScene will update your Koala’s position after that position is validated for collisions. The same applies to the collision detection tile loop — you don't want the collision detector updating the actual sprite until after all the tiles have been checked for collisions and resolved.

You'll need to change a few things. First, add this new property to Player.h

@property (nonatomic, assign) CGPoint desiredPosition;

Now, modify the collisionBoundingBox method in Player.m to the following:

- (CGRect)collisionBoundingBox {
  CGRect boundingBox = CGRectInset(self.frame, 2, 0);
  CGPoint diff = CGPointSubtract(self.desiredPosition, self.position);
  return CGRectOffset(boundingBox, diff.x, diff.y);
}

This computes a bounding box based on the desired position, which the layer will use for collision detection.

Next, make the following change to the update method so that it's updating the desiredPosition property instead of the position property:

// Replace this line 'self.position = CGPointAdd(self.position, velocityStep);' with:
self.desiredPosition = CGPointAdd(self.position, velocityStep);

Finally, in GameLevelScene.m's checkForAndResolveCollisionsForPlayer:forLayer: method, there's a reference to player.position. Change it to desiredPosition as well:

CGPoint playerCoord = [layer coordForPoint:player.desiredPosition];

Let’s Resolve Some Collisions!

Now it's time for the real deal. This is where you’re going to tie it all together.

Inside checkForAndResolveCollisionsForPlayer:forLayer:, replace the code from the comment "collision resolution goes here" until the end of the method (including the final curly brace) with the following code:

      //1
      if (CGRectIntersectsRect(playerRect, tileRect)) {
        CGRect intersection = CGRectIntersection(playerRect, tileRect);
        //2
        if (tileIndex == 7) {
          //tile is directly below Koala
          player.desiredPosition = CGPointMake(player.desiredPosition.x, player.desiredPosition.y + intersection.size.height);
        } else if (tileIndex == 1) {
          //tile is directly above Koala
          player.desiredPosition = CGPointMake(player.desiredPosition.x, player.desiredPosition.y - intersection.size.height);
        } else if (tileIndex == 3) {
          //tile is left of Koala
          player.desiredPosition = CGPointMake(player.desiredPosition.x + intersection.size.width, player.desiredPosition.y);
        } else if (tileIndex == 5) {
          //tile is right of Koala
          player.desiredPosition = CGPointMake(player.desiredPosition.x - intersection.size.width, player.desiredPosition.y);
          //3
        } else {
          if (intersection.size.width > intersection.size.height) {
            //tile is diagonal, but resolving collision vertically
            //4
            float intersectionHeight;
            if (tileIndex > 4) {
              intersectionHeight = intersection.size.height;
            } else {
              intersectionHeight = -intersection.size.height;
            }
            player.desiredPosition = CGPointMake(player.desiredPosition.x, player.desiredPosition.y + intersection.size.height );
          } else {
            //tile is diagonal, but resolving horizontally
            float intersectionWidth;
            if (tileIndex == 6 || tileIndex == 0) {
              intersectionWidth = intersection.size.width;
            } else {
              intersectionWidth = -intersection.size.width;
            }
            player.desiredPosition = CGPointMake(player.desiredPosition.x  + intersectionWidth, player.desiredPosition.y);
          }
        }
      }
    }
  }
  //5
  player.position = player.desiredPosition;
}

Okay! Let’s look at the code you’ve just implemented.

  1. To check for the collision, you use CGRectIntersectsRect to see if the player's (desired) rectangle and the tile's rectangle intersect. Just because there's a tile adjacent to the Koala's position, doesn't necessarily mean that they are overlapping.
    If there is a collision, then you get the CGRect that describes the overlapping section of the two CGRects with the CGRectIntersection() function.

Pausing to Consider a Dilemma...

Here’s the tricky bit. You need to determine how to resolve this collision.

You might think the best way to do so is to move your Koala backwards out of the collision, or in other words, to reverse the last move until a collision no longer exists with the tile. That's the way some physics engines work, but you’re going to implement a better solution.

Consider this: gravity is constantly pulling the Koala into the tiles underneath him, and those collisions are constantly being resolved.

If you imagine that the Koala is moving forward, the Koala is also going to be moving downward at the same time due to gravity. If you choose to resolve that collision by reversing the last move (forward and down), the Koala would need to move upward and backward — but that's not what you want!

Your Koala needs to move up enough to stay on top of those tiles, but continue to move forward at the same pace.

Illustration of good vs. bad ways to move up from the wall.

The same problem would also present itself if the Koala were sliding down a wall. If the user is pressing the Koala into the wall, then the Koala’s desired trajectory is diagonally downward and into the wall. Reversing this trajectory would move him upward and away from the wall — again, not the motion you want! You want the Koala to stay on the outside of the wall without slowing or reversing his downward speed.

Illustration of good vs. bad ways to move away from the wall.

Therefore, you need to decide when to resolve collisions vertically, when to resolve them horizontally, and to handle both events as mutually exclusive cases. Some physics engines always resolve one way first, but you really want to make the decision based on the location of the tile relative to the Koala. So, for example, when the tile is directly beneath the Koala, you will always resolve that collision by moving the Koala upward.

What about when the tile is diagonally opposite to the Koala's position? In this case, you'll use the intersecting CGRect as a guess as to how you should move him. If the intersection of the two rects is wider than it is deep, you'll assume that the correct resolution in this case is vertical. If the intersecting rect is taller than it is wide, you’ll resolve it horizontally.

Detecting collision direction from intersection rectangle.

This process will work reliably as long as the Koala's velocity stays within certain bounds and your game runs at a reasonable frame rate. Later on, you'll include some clamping code for the Koala so that he doesn't fall too quickly, which could cause problems, such as moving through an entire tile in one step.

Once you've determined whether you need a horizontal or vertical collision resolution, you will use the intersecting CGRect size in dimension to move the Koala back out of a collision state. Look at the height or width, as appropriate, of the collision CGRect and use that value as the distance to move the Koala.

By now, you may have suspected why you need to resolve tiles in a certain order. You'll always do the adjacent tiles before the diagonal ones. If you check the collision for the tile that is below and to the right of the Koala, you'll want to resolve this collision vertically.

However, it's possible that in this position the collision CGRect would be taller than it is wide, such as in the case where the Koala is barely colliding with the tile.

Refer again to the figure to the right. The blue area (kinda hard to see; it's to the right of the red area) is tall and skinny, because that collision intersection only represents a small portion of the whole collision. However, if you've already resolved the tile directly beneath the Koala, then you're no longer be in a collision state with the tile below and to the right, thereby avoiding the problem altogether.

Jake Gundersen

Contributors

Jake Gundersen

Author

Over 300 content creators. Join our team.