Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4396

Sprite Kit Tutorial: How to Make a Platform Game Like Super Mario Brothers – Part 2

$
0
0
Learn how to make a game like Super Mario!

Learn how to make a game like Super Mario!

Update 2/2/14: I have updated this tutorial for Sprite Kit to celebrate the launch of the second edition of the Platformer Game Starter Kit, which is fully updated for Sprite Kit. Enjoy!

Welcome back to our 2-part Sprite Kit tutorial series on making a game like Super Mario!

In the first part of the series, you learned how to create a simple, tile-based physics engine that controls how the hero of your game, Koalio, moves around his world.

In this second and final part of the series, you’ll learn how to make Koalio run and jump – the fun part of the game!

You’ll also add collisions with those scary spikey floors, handle winning and losing, and of course add some gratuitous sound effects and music.

This second part is WAY simpler (and shorter) than the first tutorial, a reward for the hard work you put in last time. So turn your coding mojo on, and enjoy!

Moving Koalio Around

The controls you’re going to implement are very simple. There will be forward and jump controls only — much like 1-bit Ninja. If you touch the left half of the screen, Koalio will run forward. Touching the right half of the screen will make Koalio jump.

You heard me right – Koalio can’t move backwards! True Koalas don’t back down from danger.

Since Koalio will be moved forward by the user, rather than by the GameLevelScene, you need some values that you can check in the Player class to update his forward velocity. Add the following properties to the Player class:

In Player.h:

@property (nonatomic, assign) BOOL forwardMarch;
@property (nonatomic, assign) BOOL mightAsWellJump;

Now add the following touch-handling methods to the GameLevelScene.m:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
  for (UITouch *touch in touches) {
    CGPoint touchLocation = [touch locationInNode:self];
    if (touchLocation.x > self.size.width / 2.0) {
      self.player.mightAsWellJump = YES;
    } else {
      self.player.forwardMarch = YES;
    }
  }
}
 
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
  for (UITouch *touch in touches) {
 
    float halfWidth = self.size.width / 2.0;
    CGPoint touchLocation = [touch locationInNode:self];
 
    //get previous touch and convert it to node space
    CGPoint previousTouchLocation = [touch previousLocationInNode:self];
 
    if (touchLocation.x > halfWidth && previousTouchLocation.x <= halfWidth) {
      self.player.forwardMarch = NO;
      self.player.mightAsWellJump = YES;
    } else if (previousTouchLocation.x > halfWidth && touchLocation.x <= halfWidth) {
      self.player.forwardMarch = YES;
      self.player.mightAsWellJump = NO;
    }
  }
}
 
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
 
  for (UITouch *touch in touches) {
    CGPoint touchLocation = [touch locationInNode:self];
    if (touchLocation.x < self.size.width / 2.0) {
      self.player.forwardMarch = NO;
    } else {
      self.player.mightAsWellJump = NO;
    }
  }
}

These changes should be pretty straightforward. If the user creates a touch event that has an X-coordinate of less than half the screen’s width, you turn on the forwardMarch boolean for the player. Otherwise (if the location of the touch event is greater than that), simply turn on the mightAsWellJump boolean.

touchesMoved is a little more complicated, because you only want to change the boolean values above if the touch crosses mid screen, so you have to calculate the previousTouch location as well. Other than that, you’re just checking to see which direction the touch crosses and turning the appropriate boolean on or off. Finally, if the user stops touching the one side of the screen or the other, you want to turn the appropriate boolean value off.

There are a few changes that need to be made to detect touches. First, add this line to initWithSize:

self.userInteractionEnabled = YES;

Now that you are passing the touches through to your player class booleans, you can add some code to the update method so that Koalio can move. Start with the forward movement. Change the update method in Player.m to the following:

- (void)update:(NSTimeInterval)delta
{
  CGPoint gravity = CGPointMake(0.0, -450.0);
  CGPoint gravityStep = CGPointMultiplyScalar(gravity, delta);
  //1
  CGPoint forwardMove = CGPointMake(800.0, 0.0);
  CGPoint forwardMoveStep = CGPointMultiplyScalar(forwardMove, delta);
 
  self.velocity = CGPointAdd(self.velocity, gravityStep);
  //2
  self.velocity = CGPointMake(self.velocity.x * 0.9, self.velocity.y);
  //3
  //Jumping code goes here
  if (self.forwardMarch) {
    self.velocity = CGPointAdd(self.velocity, forwardMoveStep);
  }
  //4
  CGPoint minMovement = CGPointMake(0.0, -450);
  CGPoint maxMovement = CGPointMake(120.0, 250.0);
  self.velocity = CGPointMake(Clamp(self.velocity.x, minMovement.x, maxMovement.x), Clamp(self.velocity.y, minMovement.y, maxMovement.y));
 
  CGPoint velocityStep = CGPointMultiplyScalar(self.velocity, delta);
 
  self.desiredPosition = CGPointAdd(self.position, velocityStep);
}

Let’s break these new lines down section-by-section:

  1. You add a forwardMove force that will come into play while the user is touching the screen. As a reminder, you are scaling that force (800 points per second) to the appropriate amount for the current frame’s time step (delta) in order to have consistent acceleration.
  2. Here you apply a damping force to the horizontal velocity to simulate friction. You’re applying physics here just as you did with gravity. In each frame, additional movement force will be applied.


    When the force is removed, you want the player to come to a stop, but not immediately. Here you apply a 0.90 damping; in other words, reducing the overall horizontal force by ten percent each frame.

  3. In section three, you check for the boolean (meaning that the screen is being touched) and add the forwardMove force if appropriate.
  4. In section four, you apply the clamping. This limits the player’s maximum movement speed in both the horizontal (running top speed), upward (jumping speed) and downward (falling) directions.

    These damping and clamping values put limits on how quickly things happen in the game. It also prevents the buildup of velocity problem that you experienced in the first part of the tutorial.

    You want the player to have a maximum speed and to reach that speed within a second or less. This way your player’s movements still feel natural, and provide a level of control. The maximum force that you’ll allow is a positive 120 value, which would be one quarter the screen width per second.

    If you want to increase the rate of acceleration of your player, increase the forwardMove variable and the damping value 0.90 respectively. If you want to increase your player’s maximum speed, simply increase the 120 value. You’re also capping the jumping velocity at 250 and falling velocity at 450.

Build and run. You should be able to make Koalio run forward by pressing the left half of the screen. Watch that Koala go!

IMG_2644

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.”

IMG_2645

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!

IMG_2646

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]];
}
  1. 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.
  2. Next, you assign a string indicating whether the player has won or lost.
  3. 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.
  4. Finally, you create a UIButton that the user can tap to restart the level over again (calling the replay: method). This should look familiar to you if you’ve use UIButtons. 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.
  5. 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.
  6. Finally, you call presentScene on a newly allocated instance of the GameLevelScene, 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:

IMG_2647

Don’t repeat this too much though, or the animal protection agency might come after you! :]

The Pit of Doom

Now for the scenario where Koalio falls down a hole. In this case, you’ll end the game.

As the code is now, it will crash. (Horrors!) The code throws a TMXLayerInfo assertion error. That’s where you need to intervene. This occurs in the checkForAndResolveCollisionsForPlayer:forLayer: method when you call tileGIDAtTileCoord:.

Add the following code in GameLevelScene.m, in the checkForAndResolveCollisionsForPlayer:forLayer: method, after the CGPoint playerCoord = [layer coordForPoint:player.desiredPosition]; line;

if (playerCoord.y >= self.map.mapSize.height - 1) {
  [self gameOver:0];
  return;
}

This code will run the gameOver routine and abandon the process of building the tile array. You’ll abandon the collision loop as well and avoid any issues that might result from it.

The handleHazardCollisions also loops through the tiles. You need to add some code to that method to prevent it from doing so if the Koala has jumped down a hole. Add the following line to the beginning of that method:

if (self.gameOver) return;

Build and run now. Find a pit to jump into, and . . . no crashing! The game ends as it should.

IMG_2651

Winning!

Now, handle the case where your hero Koalio wins the game!

All you’re going to do is monitor the X-position of the player and trigger the “win” condition when he crosses the threshold (which will be at the end of the level). This level is about 3400 pixels wide. You’re going to trigger a “win” condition when the player gets to pixel 3130.

Add a new method in GameLevelScene.m as follows:

-(void)checkForWin {
  if (self.player.position.x > 3130.0) {
    [self gameOver:1];
  }
}

Add the checkForWin method after the handleHazardCollisions call in update:

[self checkForWin];

Build and run now. Navigate your hero Koalio through the level, and if you can make it to the end, you’ll have this message:

IMG_2652

Gratuitous Music and Sound Effects

You know what time it is – time for gratuitous music and sound effects!

Let’d dive right into it. You’ll be using the built in Sprite Kit methods for sound effects and the SKTAudio engine for the background music. Add this at the top of GameLevelScene.m:

#import "SKTAudio.h"

Then add this line to the initWithSize method of the GameLevelScene (before self.userInteractionEnabled = YES;):.

[[SKTAudio sharedInstance] playBackgroundMusic:@"level1.mp3"];

This gives you some nice gaming music. Thanks to Kevin Macleod of Incompetech.com for composing the music (Brittle Reel). He has tons of CC licensed music there!

Now add a jumping sound. Go to the Player.m and add the following to the update method inside the jumping code:

if (self.mightAsWellJump && self.onGround) {
  self.velocity = CGPointAdd(self.velocity, jumpForce);
  [self runAction:[SKAction playSoundFileNamed:@"jump.wav" waitForCompletion:NO]];
} else if (!self.mightAsWellJump && self.velocity.y > jumpCutoff) {
  self.velocity = CGPointMake(self.velocity.x, jumpCutoff);
}

Finally, play a sound when Koalio falls down a hole or when he hits a hazard. Do that in the gameOver method in GameLevelScene.m:

-(void)gameOver:(BOOL)won {
  self.gameOver = YES;
  [self runAction:[SKAction playSoundFileNamed:@"hurt.wav" waitForCompletion:NO]];
 
  NSString *gameText;

Build and run, and enjoy your groovy new tunes.

And, that’s it. You’ve written a platformer. You. Are. Awesome!

Where to Go From Here?

Here’s the final SuperKoalioPt2Fin with all of the code from this Sprite Kit tutorial.

There are a lot more things that could have been covered: everything from enemy collisions and AI, to enhanced movement abilities (wall slide, double jump, etc.), to level design guidelines.

And speaking of that… great news about that!

The Platformer Game Starter Kit

We just released the second edition of the Platformer Game Starter Kit, which is fully updated for Sprite Kit!

Here’s what you’ll learn in the Platformer Game Starter Kit:

  • How to manage and load multiple levels
  • How to make a scrollable, unlockable level selection screen
  • How easily Sprite Kit works with UIKit Storyboards
  • How to efficiently use sprite sheets, animations, tilesets, and work with pixel art!
  • How to create a state machine to handle character/enemy animations and behaviors
  • More about how to make amazing and fun tile-based physics engines!
  • How to create an on-screen joystick and HUD
  • How to add hardware controller support
  • Level design for platformers
  • How to build Enemy AI and dynamic behaviors.
  • Interviews with the developers of several top iOS platformer games with tips and tricks
  • . . . and lots, lots more!

You can check out the second edition of the Platformer Game Starter Kit on the raywenderlich.com store.

In the meantime, don’t forget the resources recommended at the end of Part 1 of this tutorial.

I hope you enjoyed getting your physics on and are more inspired than ever about building your own platformer!

Sprite Kit Tutorial: How to Make a Platform Game Like Super Mario Brothers – Part 2 is a post from: Ray Wenderlich

The post Sprite Kit Tutorial: How to Make a Platform Game Like Super Mario Brothers – Part 2 appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4396

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>