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

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

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

Learn how to make a game like Super Mario!

Update 2/1/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!

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 the basics of Sprite Kit development. If you are new to Sprite Kit, check out some of the other Sprite Kit 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:

Starter Project for Super Mario Tutorial

That’s right – just an empty screen for now! :] You’ll be filling this 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. Inside Resources\TMX Files\tileSet.png and Resources\sprites.atlas, you’ll find the Koalio free game art pack from Ray’s wife Vicki.
  • Level Map. Inside Resources\TMX Files\level1.tmx, you’ll find a level map I put together using Tiled, based on the SMB level 1-1 you know and love. You’ll learn more about this later in this tutorial.
  • Gratuitous Music and Sound Effects. Inside Resources\Audio, you’ll find some fun background music and sound effects. This is a raywenderlich.com tutorial after all! :]
  • An SKSceneLayer subclass. The scene that appears on startup is called GameLevelScene, which is empty as a drum right now; it’s waiting for you to come! In this tutorial, you’ll be adding some code to create a physics engine and gameplay into this class.
  • An SKSpriteNode subclass. Finally, you’ll see an empty class called Player, which will contain the Koala’s logic. It’s 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 Sprite Kit’s built-in physics engine:

  1. 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 physics engines don’t feel like the Mario/Sonic/Contra games that you’re used to.
  2. Simplicity. Sprite Kit’s built-in physics engine has 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:

Forces acting on Koalio.

Forces acting on Koalio.

  1. Simulate movement. The first job of a physics engine is to simulate resistive forces like gravity, and applied forces like running, jumping, and friction.
  2. 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:

  1. Is the jump or move action selected?
  2. If so, apply a jump or movement force to the Koala.
  3. Also apply gravity to the Koala.
  4. Compute the resulting velocity for the Koala.
  5. Apply this resulting velocity to the Koala and update his position.
  6. Check for collision between the Koala and other objects.
  7. 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.

Forces fighting over Koalio.

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 GameLevelScene – remember, that’s the SKScene 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 GameLevelScene 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 GameLevelScene will check if this desiredPosition is valid by detecting and resolving any collisions, and then the GameLevelScene 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. Currently, Sprite Kit doesn’t include support for TMXTileMaps, but not to worry, you’ll be relying on the JSTileMap open source project for this tutorial, which is already added to your starter project.

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:

A platformer game level made with Tiled

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 GameLevelScene.m and add the following import:

#import "JSTileMap.h"

This imports the header file for the JSTileMap library that adds support for TMX files in Sprite Kit. Next add a private interface with the following private property right before the @implementation:

@interface GameLevelScene()
@property (nonatomic, strong) JSTileMap *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 initWithSize: section. Add the following code to initWithSize: in the if (self = [super initWithSize:size]) {:

self.backgroundColor = [SKColor colorWithRed:.4 green:.4 blue:.95 alpha:1.0];
 
self.map = [JSTileMap mapNamed:@"level1.tmx"];
[self addChild:self.map];

First, you set a background color to the scene, which will be the blue sky. The next two lines of code simply load the tile map (a JSTiledMap) and add it to the layer.

Next, in GameLevelScene.m, add the import for Player.h:

#import "Player.h"

Still in GameLevelScene.m, add the following property to the @interface section:

@property (nonatomic, strong) Player *player;

Then add the Koala to the level with the following code in initWithSize:

self.player = [[Player alloc] initWithImageNamed:@"koalio_stand"];
self.player.position = CGPointMake(100, 50);
self.player.zPosition = 15;
[self.map addChild:self.player];

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 scene. 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 zPosition 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:

Player and Tilemap

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

The gravity force is always on!

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

I control all the forces!

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:

  1. 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!
  2. It’s convenient. By using CGPoints, you can rely on the various well established functions that deal with CGPoints. In this tutorial, you’ll be using the SKTUtils library that was built by the Tutorial Team for our book iOS Games By Tutorials. You’ll be making heavy use of functions such as CGPointAdd (add two points), CGPointSubtract (subtract them), and CGPointMultiplyScalar (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 GameLevelScene.m, add a new property that will help you keep track of the time that passes in between each frame, in the @interface section:

@property (nonatomic, assign) NSTimeInterval previousUpdateTime;

Then, add the following method:

//1
- (void)update:(NSTimeInterval)currentTime
{
  //2
  NSTimeInterval delta = currentTime - self.previousUpdateTime;
  //3
  if (delta > 0.02) {
    delta = 0.02;
  }
  //4
  self.previousUpdateTime = currentTime;
  //5
  [self.player update:delta];
}
  1. First, the update method is built into every SKScene object and all you need to do is implement it and every frame you will get called before the scene is rendered. It provides an NSTimerInterval value that represents the current timestamp of your program.
  2. By subtracting the current time from the last time stamp, you will get the delta time, or the interval, since the last time the method was called. This time interval will be used to scale movement and other forces (like gravity) in order to achieve smooth, consistent animations.
  3. Sometimes delta may spike. This occurs at the beginning of the game (for the first few frames as things are still being loaded into memory) and occasionally when something else happens on the device (like when a system notification comes in). By capping it at .02, you reduce the chance of getting a time step that is too large (which can result in the physics engine behaving in unexpected ways, like Koalio moving through an entire tile).
  4. Here you set the previousUpdateTime to the currentTime which will be used next frame to determine delta.
  5. Finally, you call a method on Player called update. You’ll implement that next.

Next open up Player.h and add modify it to look like the following:

#import <SpriteKit/SpriteKit.h>
 
@interface Player : SKSpriteNode
@property (nonatomic, assign) CGPoint velocity;
- (void)update:(NSTimeInterval)delta;
@end

Next add the implementation to Player.m:

#import "Player.h"
//1
#import "SKTUtils.h"
 
@implementation Player
//2
- (instancetype)initWithImageNamed:(NSString *)name {
  if (self == [super initWithImageNamed:name]) {
    self.velocity = CGPointMake(0.0, 0.0);
  }
  return self;
}
 
- (void)update:(NSTimeInterval)delta {
  //3
  CGPoint gravity = CGPointMake(0.0, -450.0);
  //4
  CGPoint gravityStep = CGPointMultiplyScalar(gravity, delta);
  //5
  self.velocity = CGPointAdd(self.velocity, gravityStep);
  CGPoint velocityStep = CGPointMultiplyScalar(self.velocity, delta);
  //6
  self.position = CGPointAdd(self.position, velocityStep);
}
 
@end

Let’s go through the above code section by section.

  1. First, you import SKUtils. You’ll need those CGPoint convenience methods frequently here.
  2. Next, you create a new initWithImageNamed method and initialize the velocity variable to 0.0.
  3. 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 points towards the floor. If the Koala starts from a standstill, at the one second mark he’ll be moving at 450 points/second, at two seconds he’ll be moving at 900 points/second, and so forth. Clear enough!
  4. Here, you used the CGPointMulitplyScalar to scale the acceleration down to the size of the current time step. Recall that CGPointMulitplyScalar 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.
  5. 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.
  6. Finally, with the velocity you calculated for this single step, you use the CGPointAdd 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!

Gravity

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 – Sprite Kit 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 frame 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 points start to overlap.

Sometimes you’ll even want the points 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.frame, 2, 0);
}

CGRectInset shrinks a CGRect by the number of points specified in the second and third arguments. So in this case, the width of your collision bounding box will be four points smaller — two 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 new method will need to perform a number of tasks in your GameLevelScene in order to accomplish the collision detection:

  • Return the tile map coordinates of the eight tiles that surround the current location of the Koala.
  • 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 looks up the GID of a tile at a given coordinate (more on GIDs later).
  • A method that takes a tile’s coordinates and returns the rect in pixel coordinates.

Tackle the helper methods first. Add the following code to GameLevelScene.m:

-(CGRect)tileRectFromTileCoords:(CGPoint)tileCoords {
  float levelHeightInPixels = self.map.mapSize.height * self.map.tileSize.height;
  CGPoint origin = CGPointMake(tileCoords.x * self.map.tileSize.width, levelHeightInPixels - ((tileCoords.y + 1) * self.map.tileSize.height));
  return CGRectMake(origin.x, origin.y, self.map.tileSize.width, self.map.tileSize.height);
}
 
- (NSInteger)tileGIDAtTileCoord:(CGPoint)coord forLayer:(TMXLayer *)layer {
  TMXLayerInfo *layerInfo = layer.layerInfo;
  return [layerInfo tileGidAtCoord:coord];
}

The first method finds the pixel origin coordinate by multiplying the tile coordinate by the tile size. You need to invert the coordinate for the height, because the coordinate system of Sprite Kit/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?

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.

The second method just needs to drill down into the TMXLayerInfo object property of the layer object. The TMXLayer class contains a method to find a GID based on pixel coordinates, called tileGIDAt:, but you need to find the GID by the tile coordinate, so you need to access the TMXLayerInfo object, which has such a method. This is particular to the JSTileMap implementation, if you use another implementation of the TMXTileMap standard, this method will likely exist, but it may be accessed in another way.

I’m Surrounded By Tiles!

Now move on to the first part of your physics engine which will retrieve the surrounding tiles. In this first part you’ll be iterating through the tiles in a particular order to inspect the CGRects for intersection with the Player CGRect. You’ll be looking up the GID as well.

A GID is a number that represents the index of the image from the tileset. Every TMX layer has a tileset that has a bunch of images arranged in a grid. The GID is the position in the grid for a particular image. In this implementation, the first image at the top left of the tileset is GID 1, the second (top, to the immediate right of the first tile) is tile 2, etc. Each coordinate position in the map has either a 0, for no tile, or a number that represents the position of the image in the tileset.

You’ll be arranging the order of tiles adjacent to the Koala’s position by priority. 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 GameLevelScene.m:

- (void)checkForAndResolveCollisionsForPlayer:(Player *)player forLayer:(TMXLayer *)layer {
  //1
  NSInteger indices[8] = {7, 1, 3, 5, 0, 2, 6, 8};
  for (NSUInteger i = 0; i < 8; i++) {
    NSInteger tileIndex = indices[i];
 
    //2
    CGRect playerRect = [player collisionBoundingBox];
    //3
    CGPoint playerCoord = [layer coordForPoint:player.position];
    //4
    NSInteger tileColumn = tileIndex % 3;
    NSInteger tileRow = tileIndex / 3;
    CGPoint tileCoord = CGPointMake(playerCoord.x + (tileColumn - 1), playerCoord.y + (tileRow - 1));
    //5
    NSInteger gid = [self tileGIDAtTileCoord:tileCoord forLayer:layer];
    //6
    if (gid) {
      //7
      CGRect tileRect = [self tileRectFromTileCoords:tileCoord];
      //8
      NSLog(@"GID %ld, Tile Coord %@, Tile Rect %@, player rect %@", (long)gid, NSStringFromCGPoint(tileCoord), NSStringFromCGRect(tileRect), NSStringFromCGRect(playerRect));
      //collision resolution goes here
    }
 
  }
}

Phew – there’s a lot of code here! Don’t worry, we’ll go over it in detail.

Before we go section by section, note that you’re passing in a layer object here and the Player object. 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.

Passing in the Player object as an argument here is something that I do so this code can be extended. I won’t cover it here in this tutorial, but if you wanted to add other moving creatures to your game, you can use this same collision detection routine for them as well.

OK, now let’s go through the code above section by section.

  1. The first step it to create an array of indices that represent the positions of the tiles that surround the Koala. You then iterate through the 8 tile indices, storing the current index in the tileIndex variable.

    The Koala is less than two tile widths (a tile is 16 points) high and two tile widths wide. This means that the Koala will only every be encroaching on a 3×3 grid of tiles that directly surround him. If his sprite were larger you’d need to look beyond that 3×3 grid, but to keep things simple, I’m keeping him small.

    The index numbers represent the order in which the tiles will be resolved. I’ll cover this in more detail a little later, but tile index 7, for example, is the tile directly beneath the Koala and this tile is the tile that you want to resolve first, because it determines if the Koala is on the ground or not. The information about him being on the ground becomes important when he tries to jump.
  2. In step two, you retrieve the CGRect (in points) that will trigger a collision with the Player.
  3. Next, you find the tile coordinate of the player’s position. This is the starting place from which you’ll find the eight other tile coordinates for the surrounding tiles.
  4. This is the key section, by starting from the player’s coordinate, you perform a divide on the tile index to find a row value and a modulo on the index to find a column value. The, using these row and column values, you find a tile coordinate that is around the player’s position.

    Note: For example, if the tileIndex is 3, the value in tileColumn would be 0 (3 % 3 = 0) and the value in tileRow would be 1 (3 / 3 = 1). If the player’s position was found to be at tile coordinate 100, 18, then the surrounding tile at tileIndex 3 would be 100 + (0 – 1) and 18 + (1 – 1) or 99, 18, which is the tile directly to the left of the Koala’s tile position.

  5. In this step, you use the method you created earlier to look up the GID value for the tile at the tile coordinate found based on the index.
  6. If the GID is 0, it means that for that layer, there is no tile at that coordinate, it’s blank space, and you don’t need to test for a collision with blank space.
  7. If there is a value in GID, then the next step is to get the CGRect for that tile in points.
  8. At this point, you are ready to log the results of the method so far to validate that you are correctly detecting and retrieving tiles and their positions.
    In the next code section, you’ll add collision detection and resolution.

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!

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 guesses about how to best 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 as they exist in the indices array. 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).

Order Index

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 GameLevelScene class so you can access it there.

Inside GameLevelScene.m, make the following changes:

// Add to the @interface declaration
@property (nonatomic, strong) TMXLayer *walls;
 
// Add to the init method, after the map is added to the layer
self.walls = [self.map layerNamed:@"walls"];
 
// Add to the update method
[self checkForAndResolveCollisionsForPlayer:self.player forLayer:self.walls];

Build and run! But unfortunately it crashes, as you will see in the console:

Logging Tiles

First you’ll see you’re getting information about tile positions. You only see tiles at coordinates 18 and 19, because where Koalio is at, there are no tiles above and to his sides.

Ultimately, this will crash with a TMXLayerInfo Assertion Failure error message though. This happens when the tileGIDat: method is given a tile position that is outside the boundaries of the tile map.

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 GameLevelScene 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 GameLevelScenedesiredPosition.

You want the Koala class to calculate and store his desired position. But the GameLevelScene 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;

Now, modify the collisionBoundingBox method in Player.m to the following:

- (CGRect)collisionBoundingBox {
  CGRect boundingBox = CGRectInset(self.frame, 2, 0);
  CGPoint diff = CGPointSubtract(self.desiredPosition, self.position);
  return CGRectOffset(boundingBox, diff.x, diff.y);
}

This computes a bounding box based on the desired position, which the layer will use for collision detection.

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 = CGPointAdd(self.position, velocityStep);' with:
self.desiredPosition = CGPointAdd(self.position, velocityStep);

Finally, in GameLevelScene.m‘s checkForAndResolveCollisionsForPlayer:forLayer: method, there’s a reference to player.position. Change it to desiredPosition as well:

CGPoint playerCoord = [layer coordForPoint:player.desiredPosition];

Let’s Resolve Some Collisions!

Now it’s time for the real deal. This is where you’re going to tie it all together.

Inside checkForAndResolveCollisionsForPlayer:forLayer:, replace the code from the comment “collision resolution goes here” until the end of the method (including the final curly brace) with the following code:

      //1
      if (CGRectIntersectsRect(playerRect, tileRect)) {
        CGRect intersection = CGRectIntersection(playerRect, tileRect);
        //2
        if (tileIndex == 7) {
          //tile is directly below Koala
          player.desiredPosition = CGPointMake(player.desiredPosition.x, player.desiredPosition.y + intersection.size.height);
        } else if (tileIndex == 1) {
          //tile is directly above Koala
          player.desiredPosition = CGPointMake(player.desiredPosition.x, player.desiredPosition.y - intersection.size.height);
        } else if (tileIndex == 3) {
          //tile is left of Koala
          player.desiredPosition = CGPointMake(player.desiredPosition.x + intersection.size.width, player.desiredPosition.y);
        } else if (tileIndex == 5) {
          //tile is right of Koala
          player.desiredPosition = CGPointMake(player.desiredPosition.x - intersection.size.width, player.desiredPosition.y);
          //3
        } else {
          if (intersection.size.width > intersection.size.height) {
            //tile is diagonal, but resolving collision vertically
            //4
            float intersectionHeight;
            if (tileIndex > 4) {
              intersectionHeight = intersection.size.height;
            } else {
              intersectionHeight = -intersection.size.height;
            }
            player.desiredPosition = CGPointMake(player.desiredPosition.x, player.desiredPosition.y + intersection.size.height );
          } else {
            //tile is diagonal, but resolving horizontally
            float intersectionWidth;
            if (tileIndex == 6 || tileIndex == 0) {
              intersectionWidth = intersection.size.width;
            } else {
              intersectionWidth = -intersection.size.width;
            }
            player.desiredPosition = CGPointMake(player.desiredPosition.x  + intersectionWidth, player.desiredPosition.y);
          }
        }
      }
    }
  }
  //5
  player.position = player.desiredPosition;
}

Okay! Let’s look at the code you’ve just implemented.

  1. To check for the collision, you use CGRectIntersectsRect to see if the player’s (desired) rectangle and the tile’s rectangle intersect. Just because there’s a tile adjacent to the Koala’s position, doesn’t necessarily mean that they are overlapping.
    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.

Illustration of good vs. bad ways to move up from the wall.

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 Koala’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.

Illustration of good vs. bad ways to move away from the wall.

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.

Detecting collision direction from intersection rectangle.

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 (kinda hard to see; it’s to the right of the red 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 checkForAndResolveCollisionsForPlayer:forLayer: method…

  1. In section 2 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.
  2. In section 3, you determine whether the collision is wide or tall. If it’s wide, you resolve vertically.
  3. Next you create a variable to hold the value of the distance that you need to move the Player to no longer be in a collision state with the tile.
  4. You’ll either be moving the Koala up or down, which you determine next by seeing if the tile index is greater than five (tile indices 6 and 8 are both beneath). 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.
  5. Finally, you set the position of the Koala to the final collision-detection resolved 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! You can remove or comment out the log statement in the checkForAndResolveCollisionsForPlayer:forLayer:

//NSLog(@"GID %ld, Tile Coord %@, Tile Rect %@, player rect %@", (long)gid, NSStringFromCGPoint(tileCoord), NSStringFromCGRect(tileRect), NSStringFromCGRect(playerRect));

Build and run. Are you surprised by the results?

Eventual Gravity

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.

Here’s the solution: You are constantly increasing the speed of 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 checkForAndResolveCollisionsForPlayer:forLayer:

- (void)checkForAndResolveCollisionsForPlayer:(Player *)player forLayer:(TMXLayer *)layer
{
  NSInteger indices[8] = {7, 1, 3, 5, 0, 2, 6, 8};
  player.onGround = NO;  ////Here
  for (NSUInteger i = 0; i < 8; i++) {
    NSInteger tileIndex = indices[i];
 
    CGRect playerRect = [player collisionBoundingBox];
    CGPoint playerCoord = [layer 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:layer];
    if (gid != 0) {
      CGRect tileRect = [self tileRectFromTileCoords:tileCoord];
      //NSLog(@"GID %ld, Tile Coord %@, Tile Rect %@, player rect %@", (long)gid, NSStringFromCGPoint(tileCoord), NSStringFromCGRect(tileRect), NSStringFromCGRect(playerRect));
      //1
      if (CGRectIntersectsRect(playerRect, tileRect)) {
        CGRect intersection = CGRectIntersection(playerRect, tileRect);
        //2
        if (tileIndex == 7) {
          //tile is directly below Koala
          player.desiredPosition = CGPointMake(player.desiredPosition.x, player.desiredPosition.y + intersection.size.height);
          player.velocity = CGPointMake(player.velocity.x, 0.0); ////Here
          player.onGround = YES; ////Here
        } else if (tileIndex == 1) {
          //tile is directly above Koala
          player.desiredPosition = CGPointMake(player.desiredPosition.x, player.desiredPosition.y - intersection.size.height);
        } else if (tileIndex == 3) {
          //tile is left of Koala
          player.desiredPosition = CGPointMake(player.desiredPosition.x + intersection.size.width, player.desiredPosition.y);
        } else if (tileIndex == 5) {
          //tile is right of Koala
          player.desiredPosition = CGPointMake(player.desiredPosition.x - intersection.size.width, player.desiredPosition.y);
          //3
        } else {
          if (intersection.size.width > intersection.size.height) {
            //tile is diagonal, but resolving collision vertically
            //4
            player.velocity = CGPointMake(player.velocity.x, 0.0); ////Here
            float intersectionHeight;
            if (tileIndex > 4) {
              intersectionHeight = intersection.size.height;
              player.onGround = YES; ////Here
            } else {
              intersectionHeight = -intersection.size.height;
            }
            player.desiredPosition = CGPointMake(player.desiredPosition.x, player.desiredPosition.y + intersection.size.height );
          } else {
            //tile is diagonal, but resolving horizontally
            float intersectionWidth;
            if (tileIndex == 6 || tileIndex == 0) {
              intersectionWidth = intersection.size.width;
            } else {
              intersectionWidth = -intersection.size.width;
            }
            //5
            player.desiredPosition = CGPointMake(player.desiredPosition.x  + intersectionWidth, player.desiredPosition.y);
          }
        }
      }
    }
  }
  //6
  player.position = player.desiredPosition;
}

Each time the Koala has a tile under him (either adjacently or diagonally) you set the player.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 in Player.h:

@property (nonatomic, assign) BOOL onGround;

Build and run. Is it working as expected? Yes! O frabjous day! Callooh! Callay!

Solid Ground

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:

Let me know how it’s going so far by submitting your comments to the forums!

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

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


Viewing all articles
Browse latest Browse all 4400

Trending Articles



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