Sprite Kit Tutorial: How to Make a Platform Game Like Super Mario Brothers – Part 1
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 1
55 mins
- Getting Started
- The Tao of Physics Engines
- Physics Engineering
- Loading the TMXTiledMap
- The Gravity of Koalio’s Situation
- Playing God
- The Law of the Land: CGPoints and Forces
- Bumps In The Night – Collision Detection
- Heavy Lifting
- I’m Surrounded By Tiles!
- Taking Away Your Koala’s Privileges
- Let’s Resolve Some Collisions!
- Pausing to Consider a Dilemma...
- Back to the Code!
- Where to Go From Here?
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 GameLevelScene
— desiredPosition
.
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.
- 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 theCGRect
that describes the overlapping section of the twoCGRects
with theCGRectIntersection()
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.
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.
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.
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.