How to Make a Platform Game Like Super Mario Brothers – Part 2

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! Welcome back to our 2-part tutorial series on making a game like Super Mario! In the first part of the series, you learned how to […] By Jake Gundersen.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

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 before the if (self.forwardMarch) { line:

CGPoint jumpForce = ccp(0.0, 310.0);

if (self.mightAsWellJump && self.onGround) {
    self.velocity = ccpAdd(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 = 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 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 (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 GameLevelLayer.m:

-(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 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: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 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 of this code should look familiar, since it's copied and pasted from the checkAndResolveCollisions 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 an instance variable CCTMXLayer *hazards;. Set it up in init (just after the walls setup line):

	hazards = [map layerNamed:@"hazards"];

One last thing you need to do is call the method in your update method:

-(void)update:(ccTime)dt {
  [player update:dt];
  
  [self handleHazardCollisions:player];
  [self checkForAndResolveCollisions:player];
  [self setViewpointCenter:player.position];
}

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 {
	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 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, the code creates a label, and assigns a string based on whether the user has won or lost. It also creates a button that allows the user to start over.

These block-based methods with the CCMenu objects are really nice to use. In this case you're just replacing the scene with a new copy of itself in order to start the level over. You also use a CCAction, CCMoveBy, to animate the replay button into place, just for fun.

The only other thing you need to do is add the gameOver boolean to the GameLevelLayer class. This can just be an instance variable, since you won't need to access it outside of the calls. Add it to the @interface at the beginning of the GameLevelLayer.m, along with the variable we need to track the walls layer:

CCTMXLayer *hazards;
BOOL gameOver;

Edit 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];
}

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! :]

Jake Gundersen

Contributors

Jake Gundersen

Author

Over 300 content creators. Join our team.