Sprite Kit Tutorial: How to Make a Platform Game Like Super Mario Brothers – Part 2
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 2
25 mins
Your Mac Will Make Him... Jump, Jump!
The jump is the distinguishing feature of the platformer, and the element that leads to most of the fun. You want to make sure that the jumping movement is fluid and feels right. In this tutorial, you'll implement the jump algorithm used in Sonic the Hedgehog, as described here.
Add the following to the update method at the marked (//Jumping code goes here) line:
CGPoint jumpForce = CGPointMake(0.0, 310.0);
if (self.mightAsWellJump && self.onGround) {
self.velocity = CGPointAdd(self.velocity, jumpForce);
}
If you stop here (go ahead and build and run if you like), you'll get old school Atari jumping. Every jump the will be same height. You apply a force to the player, and wait until gravity pulls him back down again.
In modern platform games, users have much more control over the jumping. You want controllable, completely unrealistic (but fun as hell) Mario Bros/Sonic jumping abilities where you can change directions mid air and even stop a jump short.
To accomplish this, you’ll need to add the variable component. There are a couple ways to do it, but you'll do it the Sonic way. Set the jump algorithm to reduce the force of the upward thrust if the user stops pressing the screen. Replace the above code with the following:
CGPoint jumpForce = CGPointMake(0.0, 310.0);
float jumpCutoff = 150.0;
if (self.mightAsWellJump && self.onGround) {
self.velocity = CGPointAdd(self.velocity, jumpForce);
} else if (!self.mightAsWellJump && self.velocity.y > jumpCutoff) {
self.velocity = CGPointMake(self.velocity.x, jumpCutoff);
}
This code performs one extra step. In the event that the user stops pressing the screen (self.mightAsWellJump
will become NO
), it checks the upward velocity of the player. If that value is greater than the cutoff, it will set the velocity to the cutoff value.
This effectively reduces the jump. This way, you'll always get a minimum jump (at least as high as the jumpCutoff
), but if you continue to hold, you'll get the full jump force available.
Build and run now. This is starting to feel like a real game! From this point on, you'll probably need to test on a real device instead of the simulator (if you haven't been already) in order to use both “buttons.”
You got Koalio running and jumping, but eventually he’ll run out of screen real estate. Time to fix that!
Add this snippet of code from the tile-based game tutorial to GameLevelScene.m:
- (void)setViewpointCenter:(CGPoint)position {
NSInteger x = MAX(position.x, self.size.width / 2);
NSInteger y = MAX(position.y, self.size.height / 2);
x = MIN(x, (self.map.mapSize.width * self.map.tileSize.width) - self.size.width / 2);
y = MIN(y, (self.map.mapSize.height * self.map.tileSize.height) - self.size.height / 2);
CGPoint actualPosition = CGPointMake(x, y);
CGPoint centerOfView = CGPointMake(self.size.width/2, self.size.height/2);
CGPoint viewPoint = CGPointSubtract(centerOfView, actualPosition);
self.map.position = viewPoint;
}
You also need to import the SKTUtils.h for this to work:
#import "SKTUtils.h"
This code clamps the screen to the position of the player. In the case where Koalio is at the edge of the level, it stops centering on him, and clamps the edge of the level to the edge of the screen.
There’s one modification here from the original post. In the last line, the map is moved, instead of the layer. This is possible because the player is a child of the map, so when the player moves right, the map moves left and the player remains in the center of the screen.
The touch methods rely on a position within the layer as well. If you moved your layer around instead, you would need to take those calculations into account. This is easier.
For a complete explanation, refer to the tile-based game tutorial.
You need to add that call to the update
method:
[self setViewpointCenter:self.player.position];
Build and run now. You can navigate Koalio through the entire level!
The Agony of Defeat
Now you can move on to handling the winning and losing game scenarios.
Tackle the losing scenario first. There are hazards placed in this level. If the player collides with a hazard, the game will end.
Since these are fixed tiles, you need to handle them like you handled the wall collisions in the previous tutorial. However, instead of resolving collisions, you'll end the game. You're in the home stretch now — there’s only a few things left to do!
Add the following method to GameLevelScene.m:
- (void)handleHazardCollisions:(Player *)player
{
NSInteger indices[8] = {7, 1, 3, 5, 0, 2, 6, 8};
for (NSUInteger i = 0; i < 8; i++) {
NSInteger tileIndex = indices[i];
CGRect playerRect = [player collisionBoundingBox];
CGPoint playerCoord = [self.hazards coordForPoint:player.desiredPosition];
NSInteger tileColumn = tileIndex % 3;
NSInteger tileRow = tileIndex / 3;
CGPoint tileCoord = CGPointMake(playerCoord.x + (tileColumn - 1), playerCoord.y + (tileRow - 1));
NSInteger gid = [self tileGIDAtTileCoord:tileCoord forLayer:self.hazards];
if (gid != 0) {
CGRect tileRect = [self tileRectFromTileCoords:tileCoord];
if (CGRectIntersectsRect(playerRect, tileRect)) {
[self gameOver:0];
}
}
}
}
All of this code should look familiar, since it's copied and pasted from the checkForAndResolveCollisionsForPlayer:forLayer:
method. The only method that's new is gameOver
. This call takes one parameter: 0 if the player has lost, 1 if the player has won.
You're using the hazards layer instead of the walls layer, so you'll need to set that up in the @interface
at the beginning of the implementation file as property
@property (nonatomic, strong) TMXLayer *hazards;
Set it up in initWithSize
(just after the walls setup line):
self.hazards = [self.map layerNamed:@"hazards"];
One last thing you need to do is call the method in update
. Add this line after the call to checkForAndResolveCollisionsForPlayer:forLayer:
:
[self handleHazardCollisions:self.player];
Now, if the player runs into any tile from the hazards layer, you'll call gameOver
. That method is just going to throw up a restart button with a message that you've lost (or won, as the case may be):
-(void)gameOver:(BOOL)won {
//1
self.gameOver = YES;
//2
NSString *gameText;
if (won) {
gameText = @"You Won!";
} else {
gameText = @"You have Died!";
}
//3
SKLabelNode *endGameLabel = [SKLabelNode labelNodeWithFontNamed:@"Marker Felt"];
endGameLabel.text = gameText;
endGameLabel.fontSize = 40;
endGameLabel.position = CGPointMake(self.size.width / 2.0, self.size.height / 1.7);
[self addChild:endGameLabel];
//4
UIButton *replay = [UIButton buttonWithType:UIButtonTypeCustom];
replay.tag = 321;
UIImage *replayImage = [UIImage imageNamed:@"replay"];
[replay setImage:replayImage forState:UIControlStateNormal];
[replay addTarget:self action:@selector(replay:) forControlEvents:UIControlEventTouchUpInside];
replay.frame = CGRectMake(self.size.width / 2.0 - replayImage.size.width / 2.0, self.size.height / 2.0 - replayImage.size.height / 2.0, replayImage.size.width, replayImage.size.height);
[self.view addSubview:replay];
}
- (void)replay:(id)sender
{
//5
[[self.view viewWithTag:321] removeFromSuperview];
//6
[self.view presentScene:[[GameLevelScene alloc] initWithSize:self.size]];
}
- The first line sets a new boolean called
gameOver
. You use this value to stop the update method from allowing the player to continue to move and interact with the level. You’ll see that in just a minute. - Next, you assign a string indicating whether the player has won or lost.
- Then, the code creates a label, and assigns a string based on whether the user has won or lost. Sprite Kit has a handy
SKNode
subclass designed for labels like this. - Finally, you create a
UIButton
that the user can tap to restart the level over again (calling thereplay:
method). This should look familiar to you if you've useUIButtons
. The only thing that may look peculiar is that you set the tag property. This is so that you can look up the button later and remove it when you reload the scene. - In the replay method, you first get the
UIButton
by its tag and remove it from the view. You don't want that button hanging around during the next play session. - Finally, you call
presentScene
on a newly allocated instance of theGameLevelScene
, this reloads the scene with a fresh copy.
The only other thing you need to do is add the gameOver
boolean to the GameLevelScene
class. Add it to the @interface
at the beginning of the GameLevelScene.m:
@property (nonatomic, assign) BOOL gameOver;
Add the following line to the beginning of the update method:
if (self.gameOver) return;
One last step. Drag replay.png and replay@2x.png from Resources\sprites.atlas to Resources in order to make a copy of the files that is outside sprites.atlas. This is necessary so that UIKit can find the image to display the UIButton (UIKit does not know anything about Sprite Kit texture atlases).
Go ahead and build and run now, and find some spikes to jump on! You should see something like this:
Don't repeat this too much though, or the animal protection agency might come after you! :]