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.
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
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?
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 Atari in many ways, the intuitive and visceral controls of Super Mario Brothers and fun level design were such a dramatic improvement that it felt like something completely new – and we couldn’t put it down for hours!
In this tutorial, you’re going to recapture the magic and create your own platform jumper game – but since the hero will be a Koala instead of a plumber, we’ll call it “Super Koalio Brothers!” ;]
Also to keep things simple instead of adding enemies we’ll just have a simple challenge — avoid the spiky floors. That way you’ll be able to focus on learning how to implement the heart of a platformer game: the physics engine.
This tutorial assumes you are already familiar with Cocos2D development. If you are new to Cocos2D, check out some of the other tutorials on this site first.
Do you have the necessary koala-fications? Then let’s get hopping!
Getting Started
To get started, go ahead and download the starter project for this tutorial.
Once you’ve downloaded the file, unzip it, open the project in Xcode, and build and run. You should see the following appear on the screen:
That’s right – just a boring empty screen! :] You’ll be filling that out in the rest of the tutorial.
This starter project is pretty bare bones – the main point of giving it to you is so that you’d have all of the images/sounds you’ll need for the project pre-integrated. Take a look through the project and you’ll see it contains the following:
- Game Art. Includes the Koalio free game art pack from Ray’s wife Vicki.
- Level Map. I put together a level map for you, based on the SMB level 1-1 you all know and love!
- Gratuitous Music and Sound Effects. This is a raywenderlich.com tutorial after all! :]
- A CCLayer subclass. A class called GameLevelLayer, that will do a lot of the physics engine work. Right now it’s as empty as a drum. (It’s just waiting for you to come!)
- A CCSprite subclass. A class called Player, which will contain the Koala’s logic. Right now it’s just waiting for you to make it fly away! (Sorry for all the jokes Norah!)
Once you’ve had a chance to look through the project and fully understand what’s there, keep reading and we’ll discuss some philosophy about physics engines!
The Tao of Physics Engines
A platform game revolves around its physics engine, and in this tutorial you’ll be creating your own physics engine from scratch.
There are two main reasons why you’ll be rolling your own, instead of using pre-existing engines such as Box2D or Chipmunk:
- Fine tuning. To get the right feel for a platformer game, you need to be able to fine-tune the feel and response of the engine. In general, platformers created using pre-existing engines don’t feel like the Mario/Sonic/Contra/Russian Attack games that you’re used to.
- Simplicity. Box2D and Chipmunk have a lot of capabilities your game engine doesn’t really need, so your homebrew engine will end up being less resource-intensive overall.
A physics engine does two important things:
- Simulate movement. The first job of a physics engine is to simulate resistive forces like gravity, and applied forces like running, jumping, and friction.
- Detect collisions. The second job of a physics engine is to finds and resolve collisions between the player and other objects in the level.
For example, in your Koalio game you’ll apply an upward force to the Koala to make him jump. Over time, the force of gravity will act against that initial jumping force, which will give you that nice classic parabolic jump pattern.
And as for collision detection, you’ll use that to keep the Koala from falling through the ground, and to detect when our poor Koala collides with some spikes (ouch!)
Let’s see how this will work in practice.
Physics Engineering
In the physics engine you’ll create, the Koala will have its own movement-describing variables: current velocity (speed), acceleration, and position, among others. Using these variables, every movement you apply to the Koala will follow this algorithm:
- Is the jump or move action selected?
- If so, apply a jump or movement force to the Koala.
- Also apply gravity to the Koala.
- Compute the resulting velocity for the Koala.
- Apply this resulting velocity to the Koala and update his position.
- Check for collision between the Koala and other objects.
- If there’s a collision, resolve it either by moving the Koala back enough so that the collision is no longer occurring, or by causing damage to the poor Koala.
You’ll run these steps for every frame. In the game world, gravity is constantly pushing the Koala down and through the ground, but the collision resolution step will put him back on top of the ground in each frame. You can also use this feature to determine if the Koala is still touching the ground, and if not, disallow a jump action if the Koala is already in mid-jump or has just walked off a ledge.
Steps 1-5 will occur solely within the Koala’s object. All the necessary information is contained there, and it makes sense to let the Koala update its own variables.
However, when you reach the sixth step — collision detection — you need to take all of the level features into consideration, such as walls, floors, enemies, and other hazards. The collision detection step will be performed in each frame by the GameLevelLayer – remember, that’s the CCLayer subclass that will do a lot of the physics engine work.
If you allowed the Koala’s class to update his own position, he would move himself into a collision with a wall or ground block, and the GameLevelLayer would then move him back out, repeatedly — which would make him look like he was vibrating. (Had a little too much coffee, Koalio?)
So, you’re not going to allow the Koala to update his own state. Instead, the Koala will have a new variable, desiredPosition, that he will update. The GameLevelLayer will check if this desiredPosition is valid by detecting and resolving any collisions, and then the GameLevelLayer will update the Koala’s position.
Got it? Let’s try it out and see what it looks like in code!
Loading the TMXTiledMap
I’m going to assume you’re familiar with how tile maps work. If you aren’t, you can learn more about them in this tutorial.
Let’s take a look at the level. Start up your Tiled map editor (download it if you don’t have it already) and open level1.tmx from your project directory. You’ll see the following:
If you look in the sidebar, you’ll see that there are three different layers:
- hazards: This layer contains the things the Koala needs to avoid touching to stay alive (gulp).
- walls: This layer contains tiles the Koala cannot move through, which mostly consist of floor tiles.
- background: This layer contains things that are there for aesthetics only, such as clouds or hills.
Now time to code! Open up GameLevelLayer.m and add the following after the #import but before the @implementation:
@interface GameLevelLayer()
{
CCTMXTiledMap *map;
}
@end
This adds a private instance variable for the tile map into your class.
Next you’ll load this map into your layer by loading it in the init section. Add the following code to init:
CCLayerColor *blueSky = [[CCLayerColor alloc] initWithColor:ccc4(100, 100, 250, 255)];
[self addChild:blueSky];
map = [[CCTMXTiledMap alloc] initWithTMXFile:@"level1.tmx"];
[self addChild:map];
First, you add a background color (a CCLayerColor) to the layer, which will be the blue sky. The next two lines of code simply load the tile map (a CCTMXTiledMap) and add it to the layer.
Next, in GameLevelLayer.m, add the import for Player.h:
#import "Player.h"
Still in GameLevelLayer.m, add the following instance variable to the @interface section:
Player * player;
Then add the Koala to the level with the following code in the init method:
player = [[Player alloc] initWithFile:@"koalio_stand.png"];
player.position = ccp(100, 50);
[map addChild:player z:15];
This code loads the Koala sprite object, gives it a position, and adds it to the map object.
You may be wondering why you added the Koala to the map object instead of the layer. Well, you want to control exactly which TMX layers are in front of and behind the Koala sprite, so the Koala needs to be a child of the map. You want your hero Koalio in front, so you give him a Z-order of 15. Also, this makes it so that if you scroll the tile map, the Koala still stays in the same relative position within the tile map.
OK, let’s check it out! Build and run and you should see the following:
It looks like a game, but Koalio is defying gravity! It’s time to kiss him goodbye and bring him down – with a physics engine :]
The Gravity of Koalio’s Situation
To build a physics simulation, you could write a complex set of branching logic that takes the Koala’s state into account and decides which forces to apply based on that state. But, this would quickly become very complicated — and it isn’t really how physics works. In the real world, gravity is always pulling things towards the earth. So you’ll add the constant force of gravity to the Koala in every frame.
Other forces don’t just switch on and off. In the real world, a force is applied to an object and the momentum continues to move the object through space until some other force acts on that object to change the momentum.
For example, a vertical force like a jump doesn’t turn gravity off; it momentarily overcomes gravity, but gravity slows the ascent, and ultimately brings the object back to the ground. Similarly, a force that pushes an object is countered by friction, which gradually slows down the object until it stops.
This is how you’ll model your physics engine. You won’t constantly check whether your Koala is on the ground and decide whether to apply gravity; gravity will always be applied in this world.
Playing God
The logic of your physics engine will mean that when a force is applied to an object, the object will continue to move until another force counteracts it. When Koalio walks off a ledge, he’ll continue to move down at an accelerating rate until he collides with something else. When you move Koalio, he won’t stop moving as soon as you stop applying force; friction will slow him down gradually until he stops.
As you proceed with your platform game, you’ll see that this logic will make it easier to handle complex situations, such as an ice floor where the Koala doesn’t stop on a dime, or a free-fall over a cliff. This model of cumulative forces will make for a fun, dynamic-feeling game.
It will also make the implementation easier, because you won’t have to constantly query the state of your objects – they will just follow the natural laws of your world and their behavior will emerge from the application of those laws!
Sometimes, you do get to play God! :]
The Law of the Land: CGPoints and Forces
Let’s define a few terms:
- Velocity describes how fast an object is moving in a given direction.
- Acceleration is the rate of change in velocity – how an object’s speed and direction change over time.
- A force is an influence that causes a change in speed or direction.
In a physics simulation, a force applied to an object will accelerate that object to a certain velocity, and that object will continue moving at that velocity, until acted upon by another force. Velocity is a value that persists from one frame to the next and only changes by the application of new forces.
You’re going to be representing three things with CGPoint structures: velocity (speed), force/acceleration (change in speed), and position. There are two reasons for using CGPoint structures:
- They’re 2D. Velocity, force/acceleration, and position are all 2D values for a 2D game. “What?” you might say. “Gravity only acts in one dimension!” However, you could easily imagine a game with changing gravity where you’d need the second dimension. Think Super Mario Galaxy!
- It’s convenient. By using CGPoints, you can rely on the various built-in functions within Cocos2D. You’ll be making heavy use of functions such as ccpAdd (add two points), ccpSub (subtract them), and ccpMult (multiply a point by a float to scale it up or down). This will make your code much easier to write — and debug!
Your Koala object will have a velocity variable that will be acted upon in each frame by a number of forces, including gravity, forward/jumping forces supplied by the user, and friction, which will slow and stop the Koala.
In each frame, you’ll add all these forces together, and the accumulated force that results will be added to the previous velocity of the Koala object. That will give you the current velocity. The current velocity will be scaled down to match the fractional time amount between each frame, and finally, that scaled value will be used to move the Koala’s position for that frame.
Note: If any of this still sounds confusing, Daniel Shiffman wrote an excellent tutorial on vectors that explains the accumulation of forces structure that you’ll be using. The tutorial is designed for Processing, a language for creative designers similar to Java, but the concepts are applicable in any programming language. It’s a great and accessible read and I highly recommend you check it out!
Let’s start with gravity. Set up the run loop, where you’ll be applying these forces. In the GameLevelLayer.m, add the following to init before the close of the if block:
[self schedule:@selector(update:)];
Then add the method to the class:
-(void)update:(ccTime)dt
{
[player update:dt];
}
Next open up Player.h and add modify it to look like the following:
#import <Foundation/Foundation.h>
#import "cocos2d.h"
@interface Player : CCSprite
@property (nonatomic, assign) CGPoint velocity;
-(void)update:(ccTime)dt;
@end
Next add the implementation to Player.m:
#import "Player.h"
@implementation Player
@synthesize velocity = _velocity;
// 1
-(id)initWithFile:(NSString *)filename
{
if (self = [super initWithFile:filename]) {
self.velocity = ccp(0.0, 0.0);
}
return self;
}
-(void)update:(ccTime)dt
{
// 2
CGPoint gravity = ccp(0.0, -450.0);
// 3
CGPoint gravityStep = ccpMult(gravity, dt);
// 4
self.velocity = ccpAdd(self.velocity, gravityStep);
CGPoint stepVelocity = ccpMult(self.velocity, dt);
// 5
self.position = ccpAdd(self.position, stepVelocity);
}
@end
Let’s go through the above code section by section.
- Here you created a new init method to initialize the velocity variable to 0.0.
- Here you declared the value of the gravity vector (vector meaning the change in position). For each second in time, you’re accelerating the velocity of the Koala 450 pixels towards the floor. If the Koala starts from a standstill, at the one second mark he’ll be moving at 450 pixels/second, at two seconds he’ll be moving at 900 pixels/second, and so forth. Clear enough!
- Here, you used the ccpMult to scale the acceleration down to the size of the current timestep. Recall that ccpMult multiplies a CGPoint’s values by a float value, and returns the CGPoint result. Even when you’re faced with a variable frame rate, you’ll still get consistent acceleration.
- Here once you’ve calculated the gravity for the current step, you add it to your current velocity. With the new velocity you’ve calculated, you then got the velocity for a single timestep. Again, you’re doing these calculations in order to get consistent velocity, no matter what the frame rate is.
- Finally, with the velocity you calculated for this single step, you use the ccpAdd function to get the updated position for the Koala.
Congratulations! You are well on your way to writing your first physics engine! Build and run now to see the result!
Whoops — Koalio is falling through the floor! Let’s fix that up.
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.
- 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).
- Next, you create a new array that you will return with all the tile information.
- 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.
- 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.”
- 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.
- 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).
- 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.
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.
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.
- 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.
- 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.
- 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.
- 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.
- 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.
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.
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.
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.
Back to the Code!
Returning to the monster checkForAndResolveCollisions: method…
- Section 6 has you getting the index of the current tile. You use the index to determine the position of the tile. You are going to deal with the adjacent tiles individually, moving the Koala by subtracting or adding the width or height of the collision as appropriate. Simple enough. However, once you get to the diagonal tiles, you're going to implement the algorithm described in the previous section.In section 7, you determine whether the collision is wide or tall. If it's wide, you resolve vertically. You'll either be moving the Koala up or down, which you determine next by seeing if the tile index is greater than five (tiles six and seven are beneath the Koala). Based on that, you know whether you need to add or subtract the collision height from the Koala. The horizontal collision resolution follows the same logic.
- Finally, you set the position of the Koala to the final collision-detection fixed result.
That method is the guts of your collision detection system. It's a basic system, and you may find that if your game moves very quickly or has other goals, you need to alter it to get consistent results. At the end of this article, there are a couple of really great resources with more about how to handle collision detection.
Let's put it to use! Make the following change to the update method (still in GameLevelLayer:)
// Replace this line: "[self getSurroundingTilesAtPosition:player.position forLayer:walls];" with:
[self checkForAndResolveCollisions:player];
Also, you can remove or comment out the log statement in the getSurroundingTilesAtPosition:forLayer:
/*
for (NSDictionary *d in gids) {
NSLog(@"%@", d);
} //8 */
Build and run! Are you surprised by the results?
Koalio is stopped by the floor, but sinks into it eventually! What gives?
Can you guess what’s been missed? Remember — you are adding the force of gravity to the Koala’s velocity with each frame. This means that Koalio is constantly accelerating downward.
You are constantly adding speed to the Koala's downward trajectory until it is greater than the size of the tile — you’re moving through an entire tile in a single frame, which was a problem discussed earlier.
When you resolve a collision, you also need to reset the velocity of the Koala to zero for that dimension! The Koala has stopped moving, so the velocity value should reflect it.
If you don't do this, you'll get weird behaviors, both moving through tiles as you saw above, and also in situations where your Koala jumps into a low ceiling and he floats against it longer than he should. This is the kind of weirdness you want to avoid in your game.
It was mentioned before that you need a good way to determine when the Koala is on the ground so he can't jump off of thin air. You'll set that flag up now. Add the indicated lines to the checkForAndResolveCollisions:
-(void)checkForAndResolveCollisions:(Player *)p {
NSArray *tiles = [self getSurroundingTilesAtPosition:p.position forLayer:walls ]; //1
p.onGround = NO; //////Here
for (NSDictionary *dic in tiles) {
CGRect pRect = [p collisionBoundingBox]; //3
int gid = [[dic objectForKey:@"gid"] intValue]; //4
if (gid) {
CGRect tileRect = CGRectMake([[dic objectForKey:@"x"] floatValue], [[dic objectForKey:@"y"] floatValue], map.tileSize.width, map.tileSize.height); //5
if (CGRectIntersectsRect(pRect, tileRect)) {
CGRect intersection = CGRectIntersection(pRect, tileRect);
int tileIndx = [tiles indexOfObject:dic];
if (tileIndx == 0) {
//tile is directly below player
p.desiredPosition = ccp(p.desiredPosition.x, p.desiredPosition.y + intersection.size.height);
p.velocity = ccp(p.velocity.x, 0.0); //////Here
p.onGround = YES; //////Here
} else if (tileIndx == 1) {
//tile is directly above player
p.desiredPosition = ccp(p.desiredPosition.x, p.desiredPosition.y - intersection.size.height);
p.velocity = ccp(p.velocity.x, 0.0); //////Here
} else if (tileIndx == 2) {
//tile is left of player
p.desiredPosition = ccp(p.desiredPosition.x + intersection.size.width, p.desiredPosition.y);
} else if (tileIndx == 3) {
//tile is right of player
p.desiredPosition = ccp(p.desiredPosition.x - intersection.size.width, p.desiredPosition.y);
} else {
if (intersection.size.width > intersection.size.height) {
//tile is diagonal, but resolving collision vertially
p.velocity = ccp(p.velocity.x, 0.0); //////Here
float resolutionHeight;
if (tileIndx > 5) {
resolutionHeight = intersection.size.height;
p.onGround = YES; //////Here
} else {
resolutionHeight = -intersection.size.height;
}
p.desiredPosition = ccp(p.desiredPosition.x, p.desiredPosition.y + resolutionHeight);
} else {
float resolutionWidth;
if (tileIndx == 6 || tileIndx == 4) {
resolutionWidth = intersection.size.width;
} else {
resolutionWidth = -intersection.size.width;
}
p.desiredPosition = ccp(p.desiredPosition.x + resolutionWidth, p.desiredPosition.y);
}
}
}
}
}
p.position = p.desiredPosition; //8
}
Each time the Koala has a tile under him (either adjacently or diagonally) you set the p.onGround to YES and set his velocity to zero. Also, if the Koala has a tile adjacently above him, you set his velocity to zero. This will make the velocity variable properly reflect the Koala’s actual movement and speed.
You set the onGround flag to NO at the beginning of the loop. This way, the only time the onGround returns a YES is if you have detected a collision underneath the Koala. You can use this to know when the Koala can and cannot jump. You need to add this property to the Koala class, so do that now.
Add the following property to the header file (and then synthesize it in the implementation). In Player.h:
@property (nonatomic, assign) BOOL onGround;
And in Player.m:
@synthesize onGround = _onGround;
Build and run! Is it working as expected? Yes! O frabjous day! Callooh! Callay!
Where to Go From Here?
Congratulations! You've built yourself a working physics engine! If you're through to this point, you can breathe a sigh of relief and pat yourself on the back. That was the hard part – nothing but smooth sailing in Part 2 of this tutorial!
Here's the complete project you have built so far.
In Part 2, you'll make your hero Koalio run and jump. You'll also make the spikes on the floor dangerous, and handling winning/losing the game.
If you want more information about platformers and physics engines, here are a few resources I recommend:
- The Sonic the Hedgehog Wiki has a great section describing how Sonic interacts with solid tiles.
- Perhaps the best guide to implementing platformers, from Higher-Order Fun.
- The creators of N have a great tutorial that goes well beyond the basics of tile-based collision systems.
Let me know how it’s going so far by submitting your comments to the forums!
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!