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 3 of 5 of this article. Click here to view the first page.

Bumps In The Night – Collision Detection

Collision detection is a fundamental part of any physics engine. There are many different kinds of collision detection, from simple bounding box detection, to complex 3D mesh collision detection. Lucky for you, a platformer like this needs only a very simple collision detection engine.

In order to detect collisions for your Koala, you’ll need to query the TMXTileMap for the tiles that directly surround the Koala. Then, you’ll use a few built-in iOS functions to test whether your Koala’s bounding box is intersecting with a tile’s bounding box.

Note: Forgot what a bounding box is? It’s simply the smallest axis-aligned rectangle that a sprite fits inside. Usually this is straightforward and is the same as the frame of the sprite (including transparent space), but when a sprite is rotated it gets a little tricky. Don’t worry – Cocos2D has a helper method to calculate this for you :]

The functions CGRectIntersectsRect and CGRectIntersection make these kinds of tests very simple. CGRectIntersectsRect tests if two rectangles intersect, and CGRectIntersection returns the intersecting CGRect.

First, you need to find the bounding box of your Koala. Every sprite loaded has a bounding box that is the size of the texture and is accessible with the boundingBox property. However, you’ll usually want to use a smaller bounding box.

Why? The texture usually has some transparent space near the edges, depending on the Koala sprite. You don’t want to register a collision when this transparent space overlaps, but only when actual pixels start to overlap.

Sometimes you’ll even want the pixels to overlap a little bit. When Mario is unable to further move into a block, is he just barely touching it, or do his arms and nose encroach just a little bit into the block?

Let’s try it out. In Player.h, add:

-(CGRect)collisionBoundingBox;

And in Player.m, add:

-(CGRect)collisionBoundingBox {
  return CGRectInset(self.boundingBox, 2, 0);
}

CGRectInset shrinks a CGRect by the number of pixels specified in the second and third arguments. So in this case, the width of your collision bounding box will be six pixels smaller — three on each side — than the bounding box based on the image file you’re using.

Heavy Lifting

Now it’s time to do the heavy lifting. (“Hey, are you calling me fat?” says Koalio).

You’re going to need a number of methods in your GameLevelLayer in order to accomplish the collision detection. You’ll need:

  • A method that returns the coordinates of the eight tiles that surround the current location of the Koala.
  • A method to determine which, if any of these eight tiles is a collision tile. Some of your tiles won’t have physical properties, like clouds in the background, and therefore your Koala won’t collide with them.
  • A method to resolve those collisions in a prioritized way.

You’ll create two helper methods that will make accomplishing the above methods easier.

  • A method that calculates the tile position of the Koala.
  • A method that takes a tile’s coordinates and returns the rect in Cocos2D coordinates.

Tackle the helper methods first. Add the following code to GameLevelLayer.m:

- (CGPoint)tileCoordForPosition:(CGPoint)position 
{
  float x = floor(position.x / map.tileSize.width);
  float levelHeightInPixels = map.mapSize.height * map.tileSize.height;
  float y = floor((levelHeightInPixels - position.y) / map.tileSize.height);
  return ccp(x, y);
}

-(CGRect)tileRectFromTileCoords:(CGPoint)tileCoords 
{
  float levelHeightInPixels = map.mapSize.height * map.tileSize.height;
  CGPoint origin = ccp(tileCoords.x * map.tileSize.width, levelHeightInPixels - ((tileCoords.y + 1) * map.tileSize.height));
  return CGRectMake(origin.x, origin.y, map.tileSize.width, map.tileSize.height);
}

This first method gives you the coordinate of the tile based on the position you pass in. In order to get a tile position, you just divide the coordinate value by the size of the tile.

You need to invert the coordinate for the height, because the coordinate system of Cocos2D/OpenGL has an origin at the bottom left of the world, but the tile map coordinate system starts at the top left of the world. Standards – aren’t they great?

The second method reverses the process of calculating the coordinate. It multiplies the tile coordinate by tile size. Once again, you have to reverse coordinate systems, so you’re calculating the total height of the map (map.mapSize.height * map.tileSize.height) and then subtracting the height of the tiles.

Why do you add one to the tile height coordinate? Remember, the tile coordinate system is zero-based, so the 20th tile has an actual coordinate of 19. If you didn’t add one to the coordinate, the point it returned would be 19 * tileHeight.

I’m Surrounded By Tiles!

Now move on to the method that will retrieve the surrounding tiles. In this method you’ll be building an array that will be returned to the next method on the list. This array will contain the GID of the tile, the tiled map coordinate for that tile, and information about the CGRect origin for that tile.

You’ll be arranging this array by the order of priority that you’ll use later to resolve collisions. For example, you want to resolve collisions for the tiles directly left, right, below, and above your Koala before you resolve any collisions on the diagonal tiles. Also, when you resolve the collision for a tile below the Koala, you’ll need to set the flag that tells you whether the Koala is currently touching the ground.

Add the following method, still in GameLevelLayer.m:

-(NSArray *)getSurroundingTilesAtPosition:(CGPoint)position forLayer:(CCTMXLayer *)layer {
  
  CGPoint plPos = [self tileCoordForPosition:position]; //1
  
  NSMutableArray *gids = [NSMutableArray array]; //2

  for (int i = 0; i < 9; i++) { //3
    int c = i % 3;
    int r = (int)(i / 3);
    CGPoint tilePos = ccp(plPos.x + (c - 1), plPos.y + (r - 1));
    
    int tgid = [layer tileGIDAt:tilePos]; //4
    
    CGRect tileRect = [self tileRectFromTileCoords:tilePos]; //5
    
    NSDictionary *tileDict = [NSDictionary dictionaryWithObjectsAndKeys:
                 [NSNumber numberWithInt:tgid], @"gid",
                 [NSNumber numberWithFloat:tileRect.origin.x], @"x",
                 [NSNumber numberWithFloat:tileRect.origin.y], @"y",
                 [NSValue valueWithCGPoint:tilePos],@"tilePos",
                 nil];
    [gids addObject:tileDict]; //6
    
  }
  
  [gids removeObjectAtIndex:4];
  [gids insertObject:[gids objectAtIndex:2] atIndex:6];
  [gids removeObjectAtIndex:2];
  [gids exchangeObjectAtIndex:4 withObjectAtIndex:6];
  [gids exchangeObjectAtIndex:0 withObjectAtIndex:4]; //7
  
  for (NSDictionary *d in gids) {
    NSLog(@"%@", d);
  } //8
  
  return (NSArray *)gids;
}

Phew - there's a lot of code here! Don't worry, we'll go over it in detail now.

Before we go section by section, note that you're passing in a layer object here. In your tiled map, you have the three layers we discussed earlier - hazards, walls, and backgrounds.

Having separate layers allows you to handle the collision detection differently depending on the layer.

  • Koala vs. hazards. If it's a collision with a block from the hazard layer, you'll kill the poor Koala (rather brutal, aren't you?).
  • Koala vs. walls. If there's a collision with a block on the wall layer, then you’ll resolve that collision by preventing further movement in that direction. "Halt, beast!"
  • Koala vs. backgrounds. If the Koala collides with a block from the background layer, you’ll do nothing. A lazy programmer is the best kind, or so they say ;]

There are other ways to distinguish between different types of blocks, but for your needs, the layer separation is efficient.

OK, now let's go through the code above section by section.

  1. The first thing you do in this new method is retrieve the tile coordinates for the input position (which will be the position of the Koala).
  2. Next, you create a new array that you will return with all the tile information.
  3. Then you start a loop that will run nine times - because there are 9 possible spaces around (and including) the Koala's space. The next few lines calculate the positions of the nine tile positions and store them in the tilePos variable.

Note: You only need information for eight tiles, because you should never need to resolve a collision with the tile space in the center of the 3 by 3 grid.

You should always have caught that collision and resolved it in a surrounding tile position. If there is a collidable tile in the center of the grid, Koalio has moved at least half his width in a single frame. He shouldn't move this fast, ever - at least in this game!

To make iterating through those eight tiles easy, just include the tile position of the Koala and remove it at the end.

  1. The fourth section calls the tileGIDAt: method. This method will return the GID of the tile at a specific tile coordinate. If there's no tile at that coordinate, it returns zero. Later on, you'll make use of the fact that zero means “no tile found.”
  2. Next, you use the helper method to calculate the Cocos2D world space coordinates for each tile's CGRect, and then you store all that information in an NSDictionary. The collection of dictionaries is put into the return array.
  3. In section seven, you are removing the Koala tile from the array and sorting the tiles into a priority order. You will want to resolve collisions with the tiles directly adjacent (above, below, left, right) to the Koala first.

Often in the case of the tile directly under the Koala, resolving the collision will also resolve the collisions for the diagonal tiles. See the figure to the right. By resolving the collision beneath Koalio, shown in red, you also resolve the collision with block #2, shown in blue.

Your collision detection routine will make certain assumptions about how to resolve collisions. Those assumptions are valid more often for adjacent tiles than for diagonal tiles, so you want to avoid collision resolution with diagonal tiles as much as possible.

Here's an image that shows the order of the tiles in the array before, and after, the resorting. You can see that after the resorting, the bottom, top, left, and right tiles are resolved first. Knowing this order will also help you know when to set the flag that the Koala is touching the ground (so you know if he can jump or not, which you’ll cover later).

  1. The loop in section eight provides us with an output of the tiles, so you can make sure that you are doing everything correctly.

You're almost ready for the next build to verify that everything is correct! However, there are a few things to do first. You need to add the walls layer as an instance variable to the GameLevelLayer class so you can access it there.

Inside GameLevelLayer.m, make the following changes:

// Add to the @interface declaration
CCTMXLayer *walls;

// Add to the init method, after the map is added to the layer
walls = [map layerNamed:@"walls"];

// Add to the update method
[self getSurroundingTilesAtPosition:player.position forLayer:walls];

Build and run! But unfortunately it crashes, as you will see in the console:

First you'll see you're getting information about tile positions, and every so often a GID value (but mostly zeroes, because it's mostly open space).

Ultimately, this will crash with a TMXLayer: invalid position error message though. This happens when the tileGIDat: method is given a tile position that is outside the boundaries of the tile map.

You will catch and prevent this error a little later on — but first, you're going to stop it from happening by implementing collision detection.

Jake Gundersen

Contributors

Jake Gundersen

Author

Over 300 content creators. Join our team.