Writing a Super Mario Brothers Clone Game (Part 2)
- Transfer
- Tutorial
Welcome to the second part of a series of tutorials on how to write your own platformer like Super Mario Brothers!
In the first part, we wrote a simple physics engine based on the Tiled Map.
In the second (and last) part, we will teach Coalio to move and jump - the most fun part of any platformer!
We will learn to track collisions with dangers at a level, handle victory and defeat; add great sound effects and music!
The second part is much easier (and shorter) than the first - a little rest after hard work last time! So turn on your kodo kung fu and enjoy!
Our movements will be very simple: just running forward and jumping - just like in 1-bit Ninja. If the player touches the left side of the screen, Coalio runs forward. If the player touches the right side of the screen, Coalio jumps.
You got it right: Coalio can't move back! Real Koalas do not step back from danger!
As Coalio moves forward, we need a variable that can be used in the Player class to update the position of the Koala. Add the following to the Player class:
In Player.h :
And synthesize everything in Player.m :
Now add the following touch handling methods to GameLevelLayer :
The code above is pretty easy to understand. If the X-coordinate of the screen touch is less than 240 (half the screen), then we set the forwardMarch variable to YES. Otherwise (if the X-coordinate is greater than 240) we assign the value YES to the variable mightAsWellJump.
touchesMoved is a bit more complicated method. We need to change the value of the variables only when the touch crosses the middle of the screen. So we must also take into account the previous touch - the previousTouch variable. In other words, we simply check from which side the intersection occurred and change the values of the Boolean variables, respectively.
We want that when a player stops touching a certain area of the screen, only the necessary Boolean variables are turned off.
However, it is worth changing a few more things for the proper handling of touches. Add the following line to the init method :
We need to enable multitouch in AppDelegate.m (suddenly our player wants to jump and move forward at the same time). Add the following before the line [director_ pushScene: [GameLevelLayer scene]]; :
Now that we have the value of the variables in the object of the Player class, we can add some code to the update method so that Coalio can move. Let's start by moving forward. Change the update method in Player.m to the following:
Let's take this code step by step:
Run the project. Coalio should start moving if you click on the left side of the screen. Watch our Koala move briskly!
Next, we will make Koala jump!
Jumping is an integral part of any platform game that brings quite a lot of fun. We need to make sure that the jump is smooth and well calibrated. In this tutorial, we use the Sonic the Hedgehog jump algorithm described here .
Add the following to the update method before the line if (self.forwardMarch) { :
If we stop right now and start the project, we will get classic Atari-style jumps. All jumps will be the same height. We use force on the koala and wait for gravity to overcome it.
In modern platformers, users have much more control over jumps. We want controlled, absolutely unrealistic (but damn funny) jumps in the style of Mario Bros or Sonic.
There are several ways to achieve our goal, but we will take Sonic's jumps as a basis. When a player stops touching the screen, the strength of the jump begins to decrease. Replace the code above with the following:
This code takes another additional step. When the player stops touching the screen (the value of self.mightAsWellJump becomes NO), the game checks the speed of Coalio upward. If this value is greater than our threshold, then the game sets the value of the threshold of variable speed.
Thus, with an instant touch of the screen, the Koala will not jump higher than jumpCutoff, but with a long press on the screen, the Koala will jump higher and higher.
Run the project. The game is starting to look like something worthwhile! Now, most likely, you need to test the application on a real device (if you have not already done so) to check the operation of both “buttons”.
We made Coalio run and jump, but he quickly runs off the edge of the screen. It's time to fix it!
Add the following snippet fromcell map tutorial in GameLevelLayer.m :
This code locks the screen to the position of the player. If the Coalio reaches the end of the level, the screen ceases to be centered on the Koale and fixes the level face to the face of the screen.
It is worth paying attention that in the last line we are moving the card, not the player. This is possible because the player is a descendant of the map. So when the player moves to the right, the card moves to the left and the player remains in the center of the screen.
For a more detailed explanation, visit this tutorial .
We need to add the following call to the update method :
Run the project. Koalio can run through the entire level!
There are two outcomes: either victory or defeat.
First, we will deal with the defeat scenario. There are hazards at our level. If the player is in danger, the game ends.
Since dangers are fixed cells, we will work with them as obstacles from the previous tutorial. However, instead of detecting collisions, we will simply end the game. We are on the road to success - just a little bit is left!
Add the following method to GameLevelLayer.m :
All the code above should be familiar to you, as it is just copied from the checkAndResolveCollisions method. The only innovation is the gameOver method. We send him 0 if the player lost, and 1 if he won.
We use the hazards layer instead of the walls layer, so we need to declare the variable CCTMXLayer * hazards; at
The only thing left to do is add the following code to the update method :
Now, if a player flies into any dangerous cell from the hazards layer, we will call gameOver. This method will simply display the “Restart” button with a message that you lost (or won - this also happens):
The first line declares the variable gameOver. We will use this variable in order to make the update method stop making any changes to the position of Coalio. Literally in a minute we will do it.
Next, the code creates the text (label) and gives it the value of won or lost. We also create a “Restart” button.
These block-based CCMenu object methods are pretty handy to use. In our case, we simply replace our level with a copy of it in order to start the level over. We use CCAction and CCMove just to beautifully animate the buttons.
Another thing to do is add the boolean variable gameOver to the GameLevelLayer class. This will be a local variable, since we will not use it outside the class. Add the following to
Change the update method as follows:
Launch the game, find the studded floor and jump on it! You should see something like this:
Just don’t repeat it too often, otherwise problems with animal defenders may appear! :]
Now add a scenario of falling into a hole between cells. In this case, we just end the game.
Right now, when it crashes, the code crashes with the error "TMXLayer: invalid position". (Horror!) This is where we need to intervene.
Add the following code to GameLevelLayer.m , in the getSurroundingTilesAtPosition: method , before the tileGIDat line :
This code will trigger the gameOver action and prevent the process of creating an array of cells. We also need to prevent the loop from passing through all the cells in checkForAndResolveCollisions . Add a block of code after the line NSArray * tiles = [self getSurroundingTilesAtPosition: p.position forLayer: walls]; :
This will prevent the challenge of the loop and unnecessary crash of the game.
Launch the game now. Find the abyss into which you can jump, and ... no sorties! The game works as intended.
Now, let's work with the case when our Coalio wins the game!
All we do is monitor the x-position of our Koala and display a screen with a victory message if it crosses a certain mark. The length of this level is 3400 pixels. We will put the winning mark at a distance of 3130 pixels.
Add a new method to GameLevelLayer.m and update method of the update :
Run, try to win. If it works out, then you should see the following:
It's time for great music and sound effects!
Let's start. Add the following to the beginning of GameLevelLayer.m and Player.m :
Now add the following line to the init method of the GameLevelLayer file .
We just hooked up some nice game music. Thanks to Kevin of Incompetech.com for writing music ( Brittle Reel ). He has a whole cloud of CC licensed music there!
Now add the sound of the jump. Go to Player.m and add the following to the jump code of the update method :
Finally, we play the sound of defeat when Coalio touches a dangerous cell or falls into a hole. Add the method gameOver file GameLevelLayer.m following:
Launch and enjoy new nice tunes.
That's all! You wrote a platformer! You are gorgeous!
And here is the source code of our completed project.
I decided to translate tutorials from raywenderlich.com from time to time .
My next goal is to translate a series of tutorials on how to create your own game like Fruit Ninja based on Box2D and Cocos2D.
Write in the comments if it's worth it. If the transfer is in demand, I will translate.
About all found inaccuracies and typos, please write in the Habrashta or here in the comments.
I will be happy to answer all questions about the tutorial!
In the first part, we wrote a simple physics engine based on the Tiled Map.
In the second (and last) part, we will teach Coalio to move and jump - the most fun part of any platformer!
We will learn to track collisions with dangers at a level, handle victory and defeat; add great sound effects and music!
The second part is much easier (and shorter) than the first - a little rest after hard work last time! So turn on your kodo kung fu and enjoy!
Move Coalio
Our movements will be very simple: just running forward and jumping - just like in 1-bit Ninja. If the player touches the left side of the screen, Coalio runs forward. If the player touches the right side of the screen, Coalio jumps.
You got it right: Coalio can't move back! Real Koalas do not step back from danger!
As Coalio moves forward, we need a variable that can be used in the Player class to update the position of the Koala. Add the following to the Player class:
In Player.h :
@property (nonatomic, assign) BOOL forwardMarch;
@property (nonatomic, assign) BOOL mightAsWellJump;
And synthesize everything in Player.m :
@synthesize forwardMarch = _forwardMarch, mightAsWellJump = _mightAsWellJump;
Now add the following touch handling methods to GameLevelLayer :
Click me
- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *t in touches) {
CGPoint touchLocation = [self convertTouchToNodeSpace:t];
if (touchLocation.x > 240) {
player.mightAsWellJump = YES;
} else {
player.forwardMarch = YES;
}
}
}
- (void)ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *t in touches) {
CGPoint touchLocation = [self convertTouchToNodeSpace:t];
//получаем предыдущее касание
CGPoint previousTouchLocation = [t previousLocationInView:[t view]];
CGSize screenSize = [[CCDirector sharedDirector] winSize];
previousTouchLocation = ccp(previousTouchLocation.x, screenSize.height - previousTouchLocation.y);
if (touchLocation.x > 240 && previousTouchLocation.x <= 240) {
player.forwardMarch = NO;
player.mightAsWellJump = YES;
} else if (previousTouchLocation.x > 240 && touchLocation.x <=240) {
player.forwardMarch = YES;
player.mightAsWellJump = NO;
}
}
}
- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *t in touches) {
CGPoint touchLocation = [self convertTouchToNodeSpace:t];
if (touchLocation.x < 240) {
player.forwardMarch = NO;
} else {
player.mightAsWellJump = NO;
}
}
}
The code above is pretty easy to understand. If the X-coordinate of the screen touch is less than 240 (half the screen), then we set the forwardMarch variable to YES. Otherwise (if the X-coordinate is greater than 240) we assign the value YES to the variable mightAsWellJump.
touchesMoved is a bit more complicated method. We need to change the value of the variables only when the touch crosses the middle of the screen. So we must also take into account the previous touch - the previousTouch variable. In other words, we simply check from which side the intersection occurred and change the values of the Boolean variables, respectively.
We want that when a player stops touching a certain area of the screen, only the necessary Boolean variables are turned off.
However, it is worth changing a few more things for the proper handling of touches. Add the following line to the init method :
self.isTouchEnabled = YES;
We need to enable multitouch in AppDelegate.m (suddenly our player wants to jump and move forward at the same time). Add the following before the line [director_ pushScene: [GameLevelLayer scene]]; :
[glView setMultipleTouchEnabled:YES];
Now that we have the value of the variables in the object of the Player class, we can add some code to the update method so that Coalio can move. Let's start by moving forward. Change the update method in Player.m to the following:
Click me
-(void)update:(ccTime)dt {
CGPoint gravity = ccp(0.0, -450.0);
CGPoint gravityStep = ccpMult(gravity, dt);
CGPoint forwardMove = ccp(800.0, 0.0);
CGPoint forwardStep = ccpMult(forwardMove, dt); //1
self.velocity = ccpAdd(self.velocity, gravityStep);
self.velocity = ccp(self.velocity.x * 0.90, self.velocity.y); //2
if (self.forwardMarch) {
self.velocity = ccpAdd(self.velocity, forwardStep);
} //3
CGPoint minMovement = ccp(0.0, -450.0);
CGPoint maxMovement = ccp(120.0, 250.0);
self.velocity = ccpClamp(self.velocity, minMovement, maxMovement); //4
CGPoint stepVelocity = ccpMult(self.velocity, dt);
self.desiredPosition = ccpAdd(self.position, stepVelocity);
}
Let's take this code step by step:
- We add forwardMove power, which is applied when the player touches the screen. Let me remind you that we change this force (800 points per second) depending on the frame rate per second, in order to get constant acceleration.
- Here we add friction. We use physics just like we did with gravity. Forces will be applied every frame. When the power runs out, we want the player to stop. We apply friction with a force of 0.90. In other words, we reduce the force of movement by 10% each frame.
- In the third section, we check whether the player touches the screen and add, if necessary, the force of forwardStep.
- In section four, we set limits. Limit the maximum player speed. Both horizontal (running speed) and vertical (jump speed, gravity speed).
The friction force and limitations allow us to control the speed of processes in the game. Having these limitations prevents a problem with too high a speed from the first tutorial.
We want the player to reach maximum speed in a second or less. Thus, the movements of the Koala will still look natural and, at the same time, give in to control. We set a limit of 120 (a quarter of the width of the screen per second) for maximum speed.
If we want to increase the acceleration bar of our player, we must increase both values, respectively: forwardMove and friction force (0.90). If we want to increase the maximum speed of the player, we just need to increase the value of 120. We also limit the jump speed to 250 and the fall speed to 450.
Run the project. Coalio should start moving if you click on the left side of the screen. Watch our Koala move briskly!
Next, we will make Koala jump!
Our poppy will make her ... Jump, jump!
Jumping is an integral part of any platform game that brings quite a lot of fun. We need to make sure that the jump is smooth and well calibrated. In this tutorial, we use the Sonic the Hedgehog jump algorithm described here .
Add the following to the update method before the line if (self.forwardMarch) { :
CGPoint jumpForce = ccp(0.0, 310.0);
if (self.mightAsWellJump && self.onGround) {
self.velocity = ccpAdd(self.velocity, jumpForce);
}
If we stop right now and start the project, we will get classic Atari-style jumps. All jumps will be the same height. We use force on the koala and wait for gravity to overcome it.
In modern platformers, users have much more control over jumps. We want controlled, absolutely unrealistic (but damn funny) jumps in the style of Mario Bros or Sonic.
There are several ways to achieve our goal, but we will take Sonic's jumps as a basis. When a player stops touching the screen, the strength of the jump begins to decrease. Replace the code above with the following:
CGPoint jumpForce = ccp(0.0, 310.0);
float jumpCutoff = 150.0;
if (self.mightAsWellJump && self.onGround) {
self.velocity = ccpAdd(self.velocity, jumpForce);
} else if (!self.mightAsWellJump && self.velocity.y > jumpCutoff) {
self.velocity = ccp(self.velocity.x, jumpCutoff);
}
This code takes another additional step. When the player stops touching the screen (the value of self.mightAsWellJump becomes NO), the game checks the speed of Coalio upward. If this value is greater than our threshold, then the game sets the value of the threshold of variable speed.
Thus, with an instant touch of the screen, the Koala will not jump higher than jumpCutoff, but with a long press on the screen, the Koala will jump higher and higher.
Run the project. The game is starting to look like something worthwhile! Now, most likely, you need to test the application on a real device (if you have not already done so) to check the operation of both “buttons”.
We made Coalio run and jump, but he quickly runs off the edge of the screen. It's time to fix it!
Add the following snippet fromcell map tutorial in GameLevelLayer.m :
Click me
- (void)setViewpointCenter:(CGPoint) position {
CGSize winSize = [[CCDirector sharedDirector] winSize];
int x = MAX(position.x, winSize.width / 2);
int y = MAX(position.y, winSize.height / 2);
x = MIN(x, (map.mapSize.width * map.tileSize.width)
- winSize.width / 2);
y = MIN(y, (map.mapSize.height * map.tileSize.height)
- winSize.height/2);
CGPoint actualPosition = ccp(x, y);
CGPoint centerOfView = ccp(winSize.width/2, winSize.height/2);
CGPoint viewPoint = ccpSub(centerOfView, actualPosition);
map.position = viewPoint;
}
This code locks the screen to the position of the player. If the Coalio reaches the end of the level, the screen ceases to be centered on the Koale and fixes the level face to the face of the screen.
It is worth paying attention that in the last line we are moving the card, not the player. This is possible because the player is a descendant of the map. So when the player moves to the right, the card moves to the left and the player remains in the center of the screen.
For a more detailed explanation, visit this tutorial .
We need to add the following call to the update method :
[self setViewpointCenter:player.position];
Run the project. Koalio can run through the entire level!
The agony of defeat
There are two outcomes: either victory or defeat.
First, we will deal with the defeat scenario. There are hazards at our level. If the player is in danger, the game ends.
Since dangers are fixed cells, we will work with them as obstacles from the previous tutorial. However, instead of detecting collisions, we will simply end the game. We are on the road to success - just a little bit is left!
Add the following method to GameLevelLayer.m :
- (void)handleHazardCollisions:(Player *)p {
NSArray *tiles = [self getSurroundingTilesAtPosition:p.position forLayer:hazards ];
for (NSDictionary *dic in tiles) {
CGRect tileRect = CGRectMake([[dic objectForKey:@"x"] floatValue], [[dic objectForKey:@"y"] floatValue], map.tileSize.width, map.tileSize.height);
CGRect pRect = [p collisionBoundingBox];
if ([[dic objectForKey:@"gid"] intValue] && CGRectIntersectsRect(pRect, tileRect)) {
[self gameOver:0];
}
}
}
All the code above should be familiar to you, as it is just copied from the checkAndResolveCollisions method. The only innovation is the gameOver method. We send him 0 if the player lost, and 1 if he won.
We use the hazards layer instead of the walls layer, so we need to declare the variable CCTMXLayer * hazards; at
@ interface
the beginning of the executive file. Add this to the init method (immediately after declaring the variable for the walls layer): hazards = [map layerNamed:@"hazards"];
The only thing left to do is add the following code to the update method :
- (void)update:(ccTime)dt {
[player update:dt];
[self handleHazardCollisions:player];
[self checkForAndResolveCollisions:player];
[self setViewpointCenter:player.position];
}
Now, if a player flies into any dangerous cell from the hazards layer, we will call gameOver. This method will simply display the “Restart” button with a message that you lost (or won - this also happens):
Click me
- (void)gameOver:(BOOL)won {
gameOver = YES;
NSString *gameText;
if (won) {
gameText = @"You Won!";
} else {
gameText = @"You have Died!";
}
CCLabelTTF *diedLabel = [[CCLabelTTF alloc] initWithString:gameText fontName:@"Marker Felt" fontSize:40];
diedLabel.position = ccp(240, 200);
CCMoveBy *slideIn = [[CCMoveBy alloc] initWithDuration:1.0 position:ccp(0, 250)];
CCMenuItemImage *replay = [[CCMenuItemImage alloc] initWithNormalImage:@"replay.png" selectedImage:@"replay.png" disabledImage:@"replay.png" block:^(id sender) {
[[CCDirector sharedDirector] replaceScene:[GameLevelLayer scene]];
}];
NSArray *menuItems = [NSArray arrayWithObject:replay];
CCMenu *menu = [[CCMenu alloc] initWithArray:menuItems];
menu.position = ccp(240, -100);
[self addChild:menu];
[self addChild:diedLabel];
[menu runAction:slideIn];
}
The first line declares the variable gameOver. We will use this variable in order to make the update method stop making any changes to the position of Coalio. Literally in a minute we will do it.
Next, the code creates the text (label) and gives it the value of won or lost. We also create a “Restart” button.
These block-based CCMenu object methods are pretty handy to use. In our case, we simply replace our level with a copy of it in order to start the level over. We use CCAction and CCMove just to beautifully animate the buttons.
Another thing to do is add the boolean variable gameOver to the GameLevelLayer class. This will be a local variable, since we will not use it outside the class. Add the following to
@ interface
at the beginning of the GameLevelLayer.m file :CCTMXLayer *hazards;
BOOL gameOver;
Change the update method as follows:
- (void)update:(ccTime)dt {
if (gameOver) {
return;
}
[player update:dt];
[self checkForAndResolveCollisions:player];
[self handleHazardCollisions:player];
[self setViewpointCenter:player.position];
}
Launch the game, find the studded floor and jump on it! You should see something like this:
Just don’t repeat it too often, otherwise problems with animal defenders may appear! :]
Abyss of death
Now add a scenario of falling into a hole between cells. In this case, we just end the game.
Right now, when it crashes, the code crashes with the error "TMXLayer: invalid position". (Horror!) This is where we need to intervene.
Add the following code to GameLevelLayer.m , in the getSurroundingTilesAtPosition: method , before the tileGIDat line :
if (tilePos.y > (map.mapSize.height - 1)) {
//fallen in a hole
[self gameOver:0];
return nil;
}
This code will trigger the gameOver action and prevent the process of creating an array of cells. We also need to prevent the loop from passing through all the cells in checkForAndResolveCollisions . Add a block of code after the line NSArray * tiles = [self getSurroundingTilesAtPosition: p.position forLayer: walls]; :
if (gameOver) {
return;
}
This will prevent the challenge of the loop and unnecessary crash of the game.
Launch the game now. Find the abyss into which you can jump, and ... no sorties! The game works as intended.
Victory!
Now, let's work with the case when our Coalio wins the game!
All we do is monitor the x-position of our Koala and display a screen with a victory message if it crosses a certain mark. The length of this level is 3400 pixels. We will put the winning mark at a distance of 3130 pixels.
Add a new method to GameLevelLayer.m and update method of the update :
- (void)checkForWin {
if (player.position.x > 3130.0) {
[self gameOver:1];
}
}
- (void)update:(ccTime)dt {
[player update:dt];
[self handleHazardCollisions:player];
[self checkForWin];
[self checkForAndResolveCollisions:player];
[self setViewpointCenter:player.position];
}
Run, try to win. If it works out, then you should see the following:
Great music and sound effects
It's time for great music and sound effects!
Let's start. Add the following to the beginning of GameLevelLayer.m and Player.m :
#import "SimpleAudioEngine.h"
Now add the following line to the init method of the GameLevelLayer file .
[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"level1.mp3"];
We just hooked up some nice game music. Thanks to Kevin of Incompetech.com for writing music ( Brittle Reel ). He has a whole cloud of CC licensed music there!
Now add the sound of the jump. Go to Player.m and add the following to the jump code of the update method :
if (self.mightAsWellJump && self.onGround) {
self.velocity = ccpAdd(self.velocity, jumpForce);
[[SimpleAudioEngine sharedEngine] playEffect:@"jump.wav"];
} else if (!self.mightAsWellJump && self.velocity.y > jumpCutoff) {
self.velocity = ccp(self.velocity.x, jumpCutoff);
}
Finally, we play the sound of defeat when Coalio touches a dangerous cell or falls into a hole. Add the method gameOver file GameLevelLayer.m following:
- (void)gameOver {
gameOver = YES;
[[SimpleAudioEngine sharedEngine] playEffect:@"hurt.wav"];
CCLabelTTF *diedLabel = [[CCLabelTTF alloc] initWithString:@"You have Died!" fontName:@"Marker Felt" fontSize:40];
diedLabel.position = ccp(240, 200);
Launch and enjoy new nice tunes.
That's all! You wrote a platformer! You are gorgeous!
And here is the source code of our completed project.
Translator's Note
I decided to translate tutorials from raywenderlich.com from time to time .
My next goal is to translate a series of tutorials on how to create your own game like Fruit Ninja based on Box2D and Cocos2D.
Write in the comments if it's worth it. If the transfer is in demand, I will translate.
About all found inaccuracies and typos, please write in the Habrashta or here in the comments.
I will be happy to answer all questions about the tutorial!