In the previous tutorial, we laid the foundation of our Missile Command game by creating the project, setting up the single-player scene, and adding user interaction. In this tutorial, you'll expand the game experience by adding a multi-player mode as well as physics, collisions, and explosions.
Final Preview
Take a look at the next screenshot to get an idea of what we're aiming for.
Pick Up Where We Left Off
If you haven't already, we strongly advise you to complete the previous tutorial to make sure we can build upon the foundation we laid in the first tutorial. In this tutorial, we zoom in on a number of topics, such as physics, collisions, explosions, and adding a multi-player mode.
1. Enabling Physics
The Sprite Kit framework includes a physics engine that simulates physical objects. The physics engine of the Sprite Kit framework operates through the SKPhysicsContactDelegate
protocol. To enable the physics engine in our game, we need to modify the MyScene
class. Start by updating the header file as shown below to tell the compiler the SKScene
class conforms to the SKPhysicsContactDelegate
protocol.
#import <SpriteKit/SpriteKit.h> @interface MyScene : SKScene <SKPhysicsContactDelegate> @end
The SKPhysicsContactDelegate
protocol enables us to detect if two objects have collided with one another. The MyScene
instance must implement the SKPhysicsContactDelegate
protocol if it wants to be notified of collisions between objects. An object implementing the protocol is notified whenever a collision begins and ends.
Since we will be dealing with explosions, missiles, and monsters, we'll define a category for each type of physical object. Add the following code snippet to the header file of the MyScene
class.
#import <SpriteKit/SpriteKit.h> typedef enum : NSUInteger { ExplosionCategory = (1 << 0), MissileCategory = (1 << 1), MonsterCategory = (1 << 2) } NodeCategory; @interface MyScene : SKScene <SKPhysicsContactDelegate> @end
Before we can start exploring the physics engine of the Sprite Kit framework, we need to set the gravity
property of the physics world as well as its contactDelegate
. Update the initWithSize:
method as shown below.
- (id)initWithSize:(CGSize)size { if (self = [super initWithSize:size]) { self.backgroundColor = [SKColor colorWithRed:(198.0/255.0) green:(220.0/255.0) blue:(54.0/255.0) alpha:1.0]; // ... // // Configure Physics World self.physicsWorld.gravity = CGVectorMake(0, 0); self.physicsWorld.contactDelegate = self; } return self; }
In our game, the physics engine is used to create three types of physics bodies, bullets, missiles, and monsters. When working with the Sprite Kit framework, you use dynamic and static volumes to simulate physical objects. A volume for a group of objects is a volume that contains each object of the group. Dynamic and static volumes are an important element for improving the performance of the physics engine, especially when working with complex objects. In our game, we'll define two type of volumes, circles with a fixed radius and custom objects.
While circles are available through the SKPhysicsBody
class, custom object require a bit of extra work from our part. Because the body of a monster isn't circular, we must create a custom volume for it. To make this task a little bit easier, we'll use a physics body path generator. The tool is straightforward to use. Import your project's sprites and define the enclosing path for each sprite. The Objective-C code to recreate the path is shown below the sprite. As an example, take a look at the following sprite.
The next screenshot shows the same sprite with an overlay of the path generated by the physics body path generator.
If any object touches or overlaps an object's physics boundary, we are notified of this event. In our game, the objects that can touch the monsters are the incoming missiles. Let's start by using the generated paths for the monsters.
To create a physics body, we need to use a CGMutablePathRef
structure, which represents a mutable path. We use it to define the outline of the monsters in the game.
Revisit addMonstersBetweenSpace:
and create a mutable path for each monster type as shown below. Remember that there are two types of monsters in our game.
- (void)addMonstersBetweenSpace:(int)spaceOrder { for (int i = 0; i< 3; i++) { int giveDistanceToMonsters = 60 * i -60; int randomMonster = [self getRandomNumberBetween:0 to:1]; SKSpriteNode *monster; CGMutablePathRef path = CGPathCreateMutable(); if (randomMonster == 0) { monster = [SKSpriteNode spriteNodeWithImageNamed:@"protectCreature4"]; CGFloat offsetX = monster.frame.size.width * monster.anchorPoint.x; CGFloat offsetY = monster.frame.size.height * monster.anchorPoint.y; CGPathMoveToPoint(path, NULL, 10 - offsetX, 1 - offsetY); CGPathAddLineToPoint(path, NULL, 42 - offsetX, 0 - offsetY); CGPathAddLineToPoint(path, NULL, 49 - offsetX, 13 - offsetY); CGPathAddLineToPoint(path, NULL, 51 - offsetX, 29 - offsetY); CGPathAddLineToPoint(path, NULL, 50 - offsetX, 42 - offsetY); CGPathAddLineToPoint(path, NULL, 42 - offsetX, 59 - offsetY); CGPathAddLineToPoint(path, NULL, 29 - offsetX, 67 - offsetY); CGPathAddLineToPoint(path, NULL, 19 - offsetX, 67 - offsetY); CGPathAddLineToPoint(path, NULL, 5 - offsetX, 53 - offsetY); CGPathAddLineToPoint(path, NULL, 0 - offsetX, 34 - offsetY); CGPathAddLineToPoint(path, NULL, 1 - offsetX, 15 - offsetY); CGPathCloseSubpath(path); } else { monster = [SKSpriteNode spriteNodeWithImageNamed:@"protectCreature2"]; CGFloat offsetX = monster.frame.size.width * monster.anchorPoint.x; CGFloat offsetY = monster.frame.size.height * monster.anchorPoint.y; CGPathMoveToPoint(path, NULL, 0 - offsetX, 1 - offsetY); CGPathAddLineToPoint(path, NULL, 47 - offsetX, 1 - offsetY); CGPathAddLineToPoint(path, NULL, 47 - offsetX, 24 - offsetY); CGPathAddLineToPoint(path, NULL, 40 - offsetX, 43 - offsetY); CGPathAddLineToPoint(path, NULL, 28 - offsetX, 53 - offsetY); CGPathAddLineToPoint(path, NULL, 19 - offsetX, 53 - offsetY); CGPathAddLineToPoint(path, NULL, 8 - offsetX, 44 - offsetY); CGPathAddLineToPoint(path, NULL, 1 - offsetX, 26 - offsetY); CGPathCloseSubpath(path); } monster.zPosition = 2; monster.position = CGPointMake(position * spaceOrder - giveDistanceToMonsters, monster.size.height / 2); [self addChild:monster]; } }
With the path ready to use, we need to update the monster's physicsBody
property as well as a number of other properties. Take a look at the following code snippet for clarification.
- (void)addMonstersBetweenSpace:(int)spaceOrder { for (int i = 0; i< 3; i++) { // ... // monster.physicsBody = [SKPhysicsBody bodyWithPolygonFromPath:path]; monster.physicsBody.dynamic = YES; monster.physicsBody.categoryBitMask = MonsterCategory; monster.physicsBody.contactTestBitMask = MissileCategory; monster.physicsBody.collisionBitMask = 1; monster.zPosition = 2; monster.position = CGPointMake(position * spaceOrder - giveDistanceToMonsters, monster.size.height / 2); [self addChild:monster]; } }
The categoryBitMask
and contactTestBitMask
properties of the physicsBody
object are an essential part and may need some explaining. The categoryBitMask
property of the physicsBody
object defines to which categories the node belongs. The contactTestBitMask
property defines which categories of bodies cause intersection notifications with the node. In other words, these properties define which objects can collide with which objects.
Because we are configuring the monster nodes, we set the categoryBitMask
to MonsterCategory
and contactTestBitMask
to MissileCategory
. This means that monsters can collide with missiles and this enables us to detect when a monster is hit by a missile.
We also need to update our implementation of addMissilesFromSky:
. Defining the physics body for the missiles is much easier since each missile is circular. Take a look at the updated implementation below.
- (void)addMissilesFromSky:(CGSize)size { int numberMissiles = [self getRandomNumberBetween:0 to:3]; for (int i = 0; i < numberMissiles; i++) { SKSpriteNode *missile; missile = [SKSpriteNode spriteNodeWithImageNamed:@"enemyMissile"]; missile.scale = 0.6; missile.zPosition = 1; int startPoint = [self getRandomNumberBetween:0 to:size.width]; missile.position = CGPointMake(startPoint, size.height); missile.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:missile.size.height/2]; missile.physicsBody.dynamic = NO; missile.physicsBody.categoryBitMask = MissileCategory; missile.physicsBody.contactTestBitMask = ExplosionCategory | MonsterCategory; missile.physicsBody.collisionBitMask = 1; int endPoint = [self getRandomNumberBetween:0 to:size.width]; SKAction *move =[SKAction moveTo:CGPointMake(endPoint, 0) duration:15]; SKAction *remove = [SKAction removeFromParent]; [missile runAction:[SKAction sequence:@[move,remove]]]; [self addChild:missile]; } }
At this point, the monsters and missiles in our game should have a physics body that will enable us to detect when any of them collide with one another.
Challenge: The challenges for this section are as follows.
- Read and understand the
SKPhysicsBody
class. - Create different physics bodies for the monsters.
2. Collisions and Explosions
Collisions and explosions are two elements that are closely associated. Every time a bullet shot by a flower reaches its destination, the user's touch, it explodes. That explosion can cause a collision between the explosion and any missiles in the vicinity.
To create the explosion when a bullet reaches its target, we need another SKAction
instance. That SKAction
instance is in charge of two aspects of the game, define the explosion's properties and the explosion's physics body.
To define an explosion, we need to focus on the explosion's SKSpriteNode
, its zPosition
, scale
, and position
. The position
is the location of the user's touch.
To create the explosion's physics body, we need to set the node's physicsBody
property as we did earlier. Don't forget to correctly set the categoryBitMask
and contactTestBitMask
properties of the physics body. We create the explosion in touchesBegan:
as shown below.
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { for (UITouch *touch in touches) { // ... // SKSpriteNode *bullet = [SKSpriteNode spriteNodeWithImageNamed:@"flowerBullet"]; bullet.zPosition = 1; bullet.scale = 0.6; bullet.position = CGPointMake(bulletBeginning,110); bullet.color = [SKColor redColor]; bullet.colorBlendFactor = 0.5; float duration = (2 * location.y)/sizeGlobal.width; SKAction *move =[SKAction moveTo:CGPointMake(location.x,location.y) duration:duration]; SKAction *remove = [SKAction removeFromParent]; // Explosion SKAction *callExplosion = [SKAction runBlock:^{ SKSpriteNode *explosion = [SKSpriteNode spriteNodeWithImageNamed:@"explosion"]; explosion.zPosition = 3; explosion.scale = 0.1; explosion.position = CGPointMake(location.x,location.y); explosion.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:explosion.size.height/2]; explosion.physicsBody.dynamic = YES; explosion.physicsBody.categoryBitMask = ExplosionCategory; explosion.physicsBody.contactTestBitMask = MissileCategory; explosion.physicsBody.collisionBitMask = 1; SKAction *explosionAction = [SKAction scaleTo:0.8 duration:1.5]; [explosion runAction:[SKAction sequence:@[explosionAction,remove]]]; [self addChild:explosion]; }]; [bullet runAction:[SKAction sequence:@[move,callExplosion,remove]]]; [self addChild:bullet]; } }
In touchesBegan:
, we've updated the bullet
's action. The new action must call the callExplosion
action before it's removed from the scene. To accomplish this, we've updated the following line of code in touchesBegan:
.
[bullet runAction:[SKAction sequence:@[move,remove]]];
The sequence of the action now contains callExplosion
as shown below.
[bullet runAction:[SKAction sequence:@[move,callExplosion,remove]]];
Build the project and run the application to see the result of our work. As you can see, we still need to detect collisions between the explosions and the incoming missiles. This is where the SKPhysicsContactDelegate
protocol comes into play.
There's one delegate method that is of special interest to us, the didBeginContact:
method. This method will tell us when a collision between an explosion and a missile is taking place. The didBeginContact:
method takes one argument, an instance of the SKPhysicsContact
class, which tells us everything we need to know about the collision. Let me explain how this works.
An SKPhysicsContact
instance has a bodyA
and a bodyB
property. Each body points to a physics body that's involved in the collision. When didBeginContact:
is invoked, we need to detect what type of collision we're dealing with. It can be (1) a collision between an explosion and a missile or (2) a collision between a missile and a monster. We detect the collision type by inspecting the categoryBitmask
property of the physics bodies of the SKPhysicsContact
instance.
Finding out which type of collision we're dealing with is pretty easy thanks to the categoryBitmask
property. If bodyA
or bodyB
has a categoryBitmask
of type ExplosionCategory
, then we know it's a collision between an explosion and a missile. Take a look at the code snippet below for clarification.
- (void)didBeginContact:(SKPhysicsContact *)contact { if ((contact.bodyA.categoryBitMask & ExplosionCategory) != 0 || (contact.bodyB.categoryBitMask & ExplosionCategory) != 0) { NSLog(@"EXPLOSION HIT"); } else { NSLog(@"MONSTER HIT"); } }
If we've encountered a collision between an explosion and a missile, then we grab the node that is associated with the missile's physics body. We also need to assign an action to the node, which will be executed when the bullet hits the missile. The task of the action is to remove the missile from the scene. Note that we don't immediately remove the explosion from the scene as it may be able to destroy other missiles in its vicinity.
When a missile is destroyed, we increment the missileExploded
instance variable and update the label that displays the number of missiles the player has destroyed so far. If the player has destroyed twenty missiles, they win the game.
- (void)didBeginContact:(SKPhysicsContact *)contact { if ((contact.bodyA.categoryBitMask & ExplosionCategory) != 0 || (contact.bodyB.categoryBitMask & ExplosionCategory) != 0) { // Collision Between Explosion and Missile SKNode *missile = (contact.bodyA.categoryBitMask & ExplosionCategory) ? contact.bodyB.node : contact.bodyA.node; [missile runAction:[SKAction removeFromParent]]; //the explosion continues, because can kill more than one missile NSLog(@"Missile destroyed"); // Update Missile Exploded missileExploded++; [labelMissilesExploded setText:[NSString stringWithFormat:@"Missiles Exploded: %d",missileExploded]]; if(missileExploded == 20){ SKLabelNode *ganhou = [SKLabelNode labelNodeWithFontNamed:@"Hiragino-Kaku-Gothic-ProN"]; ganhou.text = @"You win!"; ganhou.fontSize = 60; ganhou.position = CGPointMake(sizeGlobal.width/2,sizeGlobal.height/2); ganhou.zPosition = 3; [self addChild:ganhou]; } } else { // Collision Between Missile and Monster } }
If we're dealing with a collision between a missile and a monster, we remove the missile and monster node from the scene by adding an action [SKAction removeFromParent]
to the list of actions executed by the node. We also increment the monstersDead
instance variable and check if it's equal to 6
. If it is, the player has lost the game and we display a message telling them the game is over.
- (void)didBeginContact:(SKPhysicsContact *)contact { if ((contact.bodyA.categoryBitMask & ExplosionCategory) != 0 || (contact.bodyB.categoryBitMask & ExplosionCategory) != 0) { // Collision Between Explosion and Missile // ... // } else { // Collision Between Missile and Monster SKNode *monster = (contact.bodyA.categoryBitMask & MonsterCategory) ? contact.bodyA.node : contact.bodyB.node; SKNode *missile = (contact.bodyA.categoryBitMask & MonsterCategory) ? contact.bodyB.node : contact.bodyA.node; [missile runAction:[SKAction removeFromParent]]; [monster runAction:[SKAction removeFromParent]]; NSLog(@"Monster killed"); monstersDead++; if(monstersDead == 6){ SKLabelNode *perdeu = [SKLabelNode labelNodeWithFontNamed:@"Hiragino-Kaku-Gothic-ProN"]; perdeu.text = @"You Lose!"; perdeu.fontSize = 60; perdeu.position = CGPointMake(sizeGlobal.width/2,sizeGlobal.height/2); perdeu.zPosition = 3; [self addChild:perdeu]; [self moveToMenu]; } } }
Before running the game on your iPad, we need to implement the moveToMenu
method. This method is invoked when the player loses the game. In moveToMenu
, the game transitions back to the menu scene so the player can start a new game. Don't forget to add an import statement for the MenuScene
class.
- (void)moveToMenu { SKTransition* transition = [SKTransition fadeWithDuration:2]; MenuScene* myscene = [[MenuScene alloc] initWithSize:CGSizeMake(CGRectGetMaxX(self.frame), CGRectGetMaxY(self.frame))]; [self.scene.view presentScene:myscene transition:transition]; }
#import "MyScene.h" #import "MenuScene.h" @interface MyScene () { // ... // } @end
It's time to build the project and run the game to see the final result.
Challenge: The challenges for this section are as follows.
- Change the rules of the game by modifying the number of monsters and bullets.
- Make the game more challenging by modifying the game's dynamics. You could, for example, increase the speed of the missiles once you've used five bullets.
3. Multi-Player
In the game's multi-player mode, two players can challenge each other through a split-screen mode. The multi-player mode doesn't change the game itself. The main differences between the single-player and multi-player modes are listed below.
- We need two sets of assets.
- The position and orientation of the assets need to be updated.
- We need to implement game logic for the second player.
- Explosions need to be tested and caught on a per-explosion basis.
- Only one player can win the game.
In multi-player mode, the game should look like the screenshot below.
This is the final challenge of this tutorial. It isn't as complicated as it may seem. The goal of the challenge is to recreate Missile Command by enabling multi-player mode. The source files of this tutorial contain two Xcode projects, one of which (Missile Command Multi-Player) contains an incomplete implementation of the multi-player mode to get you started with this challenge. Note that the MultiScene
class is incomplete and it is your task to finish its implementation to successfully complete the challenge. You will find hints and comments (/* Work HERE - CODE IS MISSING */
) to help you with this challenge.
The next screenshot shows you the current state of the multi-player mode.
Conclusion
We've covered a lot of ground in this short series on Sprite Kit. You should now be able to create games that are similar to Missile Command using the Sprite Kit framework. If you have any questions or comments, feel free to drop us a line in the comments.
Comments