How to Make a Platform Game Like Super Mario Brothers – Part 1

This is a blog post by iOS Tutorial Team member Jacob Gundersen, an indie game developer who runs the Indie Ambitions blog. Check out his latest app – Factor Samurai! For many of us, Super Mario Brothers was the first game that made us drop our jaws in gaming excitement. Although video games started with […] 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 GameLevelLayer 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 GameLevelLayer — desiredPosition.

You want the Koala class to calculate and store his desired position. But the GameLevelLayer 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;

And synthesize it in Player.m:

@synthesize desiredPosition = _desiredPosition;

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

-(CGRect)collisionBoundingBox {
  CGRect collisionBox = CGRectInset(self.boundingBox, 3, 0);
  CGPoint diff = ccpSub(self.desiredPosition, self.position);
  CGRect returnBoundingBox = CGRectOffset(collisionBox, diff.x, diff.y);
  return returnBoundingBox;
}

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

Note: There are many different ways you could have calculated this new bounding box. You could have implemented code similar to that inside CCNode's boundingBox and transform methods, but this way was a lot easier, even if it is a slightly roundabout way to get what you want.

Note: There are many different ways you could have calculated this new bounding box. You could have implemented code similar to that inside CCNode's boundingBox and transform methods, but this way was a lot easier, even if it is a slightly roundabout way to get what you want.

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 = ccpAdd(self.position, stepVelocity);' with:
self.desiredPosition = ccpAdd(self.position, stepVelocity);

Let’s Resolve Some Collisions!

Now it's time for the real deal. This is where you’re going to tie it all together. Add the following method to GameLevelLayer.m:

-(void)checkForAndResolveCollisions:(Player *)p {  
  NSArray *tiles = [self getSurroundingTilesAtPosition:p.position forLayer:walls ]; //1

  for (NSDictionary *dic in tiles) {
    CGRect pRect = [p collisionBoundingBox]; //2
    
    int gid = [[dic objectForKey:@"gid"] intValue]; //3
    
    if (gid) {
      CGRect tileRect = CGRectMake([[dic objectForKey:@"x"] floatValue], [[dic objectForKey:@"y"] floatValue], map.tileSize.width, map.tileSize.height); //4
      if (CGRectIntersectsRect(pRect, tileRect)) {
        CGRect intersection = CGRectIntersection(pRect, tileRect); //5
        
        int tileIndx = [tiles indexOfObject:dic]; //6

        if (tileIndx == 0) {
          //tile is directly below Koala
          p.desiredPosition = ccp(p.desiredPosition.x, p.desiredPosition.y + intersection.size.height);
        } else if (tileIndx == 1) {
          //tile is directly above Koala
          p.desiredPosition = ccp(p.desiredPosition.x, p.desiredPosition.y - intersection.size.height);
        } else if (tileIndx == 2) {
          //tile is left of Koala
          p.desiredPosition = ccp(p.desiredPosition.x + intersection.size.width, p.desiredPosition.y);
        } else if (tileIndx == 3) {
          //tile is right of Koala
          p.desiredPosition = ccp(p.desiredPosition.x - intersection.size.width, p.desiredPosition.y);
        } else {
          if (intersection.size.width > intersection.size.height) { //7
            //tile is diagonal, but resolving collision vertically
            float intersectionHeight;
            if (tileIndx > 5) {
              intersectionHeight = intersection.size.height;
            } else {
              intersectionHeight = -intersection.size.height;
            }
            p.desiredPosition = ccp(p.desiredPosition.x, p.desiredPosition.y + intersection.size.height );
          } else {
          	//tile is diagonal, but resolving horizontally
            float resolutionWidth;
            if (tileIndx == 6 || tileIndx == 4) {
              resolutionWidth = intersection.size.width;
            } else {
              resolutionWidth = -intersection.size.width;
            }
            p.desiredPosition = ccp(p.desiredPosition.x , p.desiredPosition.y + resolutionWidth);
          } 
        } 
      }
    } 
  }
  p.position = p.desiredPosition; //7
}

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

  1. First you retrieve the set of tiles that surround the Koala. Next you loop through each tile in that set. Each time you iterate through a tile, you check if there's a collision. If there is a collision, you resolve it by changing the desiredPosition attribute of the Koala.
  2. Inside each loop, you first get the current bounding box for the Koala. As I've mentioned previously, this desiredPosition variable is the basis for the collisionBoundingBox. Each time a collision is found, the desiredPosition variable is changed so that it's no longer colliding with that tile. Often, that will mean that other tiles surrounding the Koala are no longer in collision either, and when the loops arrive at those tiles, you won't need to resolve those collisions again.
  3. The next step is to retrieve the GID you stored for the tile in the dictionary. There may not be an actual tile in that position. If there isn't, you'll have stored a zero in the dictionary. In that case, the current loop is finished and it will move on to the next tile.
  4. If there is a tile in that position, you need to get the CGRect for that tile. There may or may not be a collision there. You do that with the next line of code and store it in the tileRect variable. Now that you have a CGRect for the Koala and for the tile, you can check them for collisions.
  5. To check for the collision, you run the CGRectIntersectsRect. 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 Kolala’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 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.