Video icon 64
Learning to code? Skill up faster with our practical video courses. Start your free trial today.
Advertisement

Build Missile Command with Sprite Kit: User Interaction

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.

The game's multi-player mode.

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.

You don't need to add additional methods or instance variables to complete the challenge. You only need to focus on implementing the logic for the multi-player mode.

The next screenshot shows you the current state of the multi-player mode.

The incomplete 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.

Advertisement