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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Sprite Kit Tutorial: How to Make a Platform Game Like Super Mario Brothers – Part 1
55 mins
- Getting Started
- The Tao of Physics Engines
- Physics Engineering
- Loading the TMXTiledMap
- The Gravity of Koalio’s Situation
- Playing God
- The Law of the Land: CGPoints and Forces
- Bumps In The Night – Collision Detection
- Heavy Lifting
- I’m Surrounded By Tiles!
- Taking Away Your Koala’s Privileges
- Let’s Resolve Some Collisions!
- Pausing to Consider a Dilemma...
- Back to the Code!
- Where to Go From Here?
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 – Sprite Kit has a helper method to calculate this for you :]
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 – Sprite Kit 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 frame 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 points start to overlap.
Sometimes you’ll even want the points 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.frame, 2, 0);
}
CGRectInset
shrinks a CGRect
by the number of points specified in the second and third arguments. So in this case, the width of your collision bounding box will be four points smaller — two 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 new method will need to perform a number of tasks in your GameLevelScene in order to accomplish the collision detection:
- Return the tile map coordinates of the eight tiles that surround the current location of the Koala.
- 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 looks up the GID of a tile at a given coordinate (more on GIDs later).
- A method that takes a tile’s coordinates and returns the rect in pixel coordinates.
Tackle the helper methods first. Add the following code to GameLevelScene.m:
-(CGRect)tileRectFromTileCoords:(CGPoint)tileCoords {
float levelHeightInPixels = self.map.mapSize.height * self.map.tileSize.height;
CGPoint origin = CGPointMake(tileCoords.x * self.map.tileSize.width, levelHeightInPixels - ((tileCoords.y + 1) * self.map.tileSize.height));
return CGRectMake(origin.x, origin.y, self.map.tileSize.width, self.map.tileSize.height);
}
- (NSInteger)tileGIDAtTileCoord:(CGPoint)coord forLayer:(TMXLayer *)layer {
TMXLayerInfo *layerInfo = layer.layerInfo;
return [layerInfo tileGidAtCoord:coord];
}
The first method finds the pixel origin coordinate by multiplying the tile coordinate by the tile size. You need to invert the coordinate for the height, because the coordinate system of Sprite Kit/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?
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
.
The second method just needs to drill down into the TMXLayerInfo
object property of the layer object. The TMXLayer
class contains a method to find a GID
based on pixel coordinates, called tileGIDAt
:, but you need to find the GID
by the tile coordinate, so you need to access the TMXLayerInfo
object, which has such a method. This is particular to the JSTileMap implementation, if you use another implementation of the TMXTileMap standard, this method will likely exist, but it may be accessed in another way.
I’m Surrounded By Tiles!
Now move on to the first part of your physics engine which will retrieve the surrounding tiles. In this first part you’ll be iterating through the tiles in a particular order to inspect the CGRects
for intersection with the Player CGRect
. You’ll be looking up the GID
as well.
A GID
is a number that represents the index of the image from the tileset. Every TMX layer has a tileset that has a bunch of images arranged in a grid. The GID
is the position in the grid for a particular image. In this implementation, the first image at the top left of the tileset is GID
1, the second (top, to the immediate right of the first tile) is tile 2, etc. Each coordinate position in the map has either a 0, for no tile, or a number that represents the position of the image in the tileset.
You’ll be arranging the order of tiles adjacent to the Koala’s position by priority. 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 GameLevelScene.m:
- (void)checkForAndResolveCollisionsForPlayer:(Player *)player forLayer:(TMXLayer *)layer {
//1
NSInteger indices[8] = {7, 1, 3, 5, 0, 2, 6, 8};
for (NSUInteger i = 0; i < 8; i++) {
NSInteger tileIndex = indices[i];
//2
CGRect playerRect = [player collisionBoundingBox];
//3
CGPoint playerCoord = [layer coordForPoint:player.position];
//4
NSInteger tileColumn = tileIndex % 3;
NSInteger tileRow = tileIndex / 3;
CGPoint tileCoord = CGPointMake(playerCoord.x + (tileColumn - 1), playerCoord.y + (tileRow - 1));
//5
NSInteger gid = [self tileGIDAtTileCoord:tileCoord forLayer:layer];
//6
if (gid) {
//7
CGRect tileRect = [self tileRectFromTileCoords:tileCoord];
//8
NSLog(@"GID %ld, Tile Coord %@, Tile Rect %@, player rect %@", (long)gid, NSStringFromCGPoint(tileCoord), NSStringFromCGRect(tileRect), NSStringFromCGRect(playerRect));
//collision resolution goes here
}
}
}
Phew - there's a lot of code here! Don't worry, we'll go over it in detail.
Before we go section by section, note that you're passing in a layer object here and the Player object. 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.
Passing in the Player object as an argument here is something that I do so this code can be extended. I won't cover it here in this tutorial, but if you wanted to add other moving creatures to your game, you can use this same collision detection routine for them as well.
OK, now let's go through the code above section by section.
Note: For example, if the tileIndex is 3, the value in tileColumn would be 0 (3 % 3 = 0) and the value in tileRow would be 1 (3 / 3 = 1). If the player's position was found to be at tile coordinate 100, 18, then the surrounding tile at tileIndex 3 would be 100 + (0 - 1) and 18 + (1 - 1) or 99, 18, which is the tile directly to the left of the Koala's tile position.
- The first step it to create an array of indices that represent the positions of the tiles that surround the Koala. You then iterate through the 8 tile indices, storing the current index in the
tileIndex
variable.
The Koala is less than two tile widths (a tile is 16 points) high and two tile widths wide. This means that the Koala will only every be encroaching on a 3x3 grid of tiles that directly surround him. If his sprite were larger you'd need to look beyond that 3x3 grid, but to keep things simple, I'm keeping him small.
The index numbers represent the order in which the tiles will be resolved. I'll cover this in more detail a little later, but tile index 7, for example, is the tile directly beneath the Koala and this tile is the tile that you want to resolve first, because it determines if the Koala is on the ground or not. The information about him being on the ground becomes important when he tries to jump. - In step two, you retrieve the
CGRect
(in points) that will trigger a collision with the Player. - Next, you find the tile coordinate of the player's position. This is the starting place from which you'll find the eight other tile coordinates for the surrounding tiles.
- This is the key section, by starting from the player's coordinate, you perform a divide on the tile index to find a row value and a modulo on the index to find a column value. The, using these row and column values, you find a tile coordinate that is around the player's position.
Note: For example, if the tileIndex is 3, the value in tileColumn would be 0 (3 % 3 = 0) and the value in tileRow would be 1 (3 / 3 = 1). If the player's position was found to be at tile coordinate 100, 18, then the surrounding tile at tileIndex 3 would be 100 + (0 - 1) and 18 + (1 - 1) or 99, 18, which is the tile directly to the left of the Koala's tile position.
- In this step, you use the method you created earlier to look up the
GID
value for the tile at the tile coordinate found based on the index. - If the
GID
is 0, it means that for that layer, there is no tile at that coordinate, it's blank space, and you don't need to test for a collision with blank space. - If there is a value in
GID
, then the next step is to get theCGRect
for that tile in points. - At this point, you are ready to log the results of the method so far to validate that you are correctly detecting and retrieving tiles and their positions.
In the next code section, you'll add collision detection and resolution.
Note: For example, if the tileIndex is 3, the value in tileColumn would be 0 (3 % 3 = 0) and the value in tileRow would be 1 (3 / 3 = 1). If the player's position was found to be at tile coordinate 100, 18, then the surrounding tile at tileIndex 3 would be 100 + (0 - 1) and 18 + (1 - 1) or 99, 18, which is the tile directly to the left of the Koala's tile position.
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!
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!
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 guesses about how to best 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 as they exist in the indices array. 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).
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 GameLevelScene class so you can access it there.
Inside GameLevelScene.m, make the following changes:
// Add to the @interface declaration
@property (nonatomic, strong) TMXLayer *walls;
// Add to the init method, after the map is added to the layer
self.walls = [self.map layerNamed:@"walls"];
// Add to the update method
[self checkForAndResolveCollisionsForPlayer:self.player forLayer:self.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. You only see tiles at coordinates 18 and 19, because where Koalio is at, there are no tiles above and to his sides.
Ultimately, this will crash with a TMXLayerInfo Assertion Failure error message though. This happens when the tileGIDat:
method is given a tile position that is outside the boundaries of the tile map.
You're going to stop it from happening by implementing collision detection.