An iOS 7 Sprite Kit Collision Handling Tutorial

From Techotopia
Jump to: navigation, search
PreviousTable of ContentsNext
An iOS 7 Sprite Kit Game TutorialAn iOS 7 Sprite Kit Particle Emitter Tutorial


Purchase the fully updated iOS 10 / Swift 3 / Xcode 8 edition of this book in eBook ($19.99) or Print ($45.99) format.
iOS 10 App Development Essentials Print and eBook (ePub/PDF/Kindle) edition contains over 100 chapters. Learn more...

Buy eBook Buy Print Preview Book


In this chapter, the game created in the previous chapter entitled An iOS 7 Sprite Kit Game Tutorial will be extended to implement collision detection. The objective is to detect when an arrow node collides with a ball node and, in the event of such a collision, implement a join between those two nodes so that the arrow appears to embed into the ball.




Defining the Category Bit Masks

If not already loaded, start Xcode and open the SpriteKitDemo project created in the previous chapter. When detecting collisions within a Sprite Kit scene, a delegate method is called each time a collision is detected. This method will only be called, however, if the colliding nodes are configured appropriately using category bit masks.

For the purposes of this demonstration game, only collisions between arrow and ball sprite nodes are of interest. The first step, therefore, is to declare collision masks for these two node categories. Begin by editing the ArcheryScene.m file and adding these declarations at the top of the @implementation section:

#import "ArcheryScene.h"
#import "WelcomeScene.h"

@interface ArcheryScene ()
@property BOOL sceneCreated;
@property int score;
@property int ballCount;
@property NSArray *archerAnimation;
@end

@implementation ArcheryScene

static const uint32_t arrowCategory = 0x1 << 0;
static const uint32_t ballCategory = 0x1 << 1;
.
.
.
@end

Assigning the Category Masks to the Sprite Nodes

Having declared the masks, these need to be assigned to the respective node objects when they are created within the game. This is achieved by assigning the mask to the categoryBitMask property of the physics body assigned to the node. In the case of the ball node, this code can be added in the createBallNode method as follows:

- (void) createBallNode
{
    SKSpriteNode *ball = [[SKSpriteNode alloc] 
       initWithImageNamed:@"BallTexture.png"];

    ball.position = CGPointMake(randomBetween(200, self.size.width),
                                self.size.height-50);

    ball.name = @"ballNode";
    ball.physicsBody = 
       [SKPhysicsBody bodyWithCircleOfRadius:(ball.size.width/2)-7];
    ball.physicsBody.usesPreciseCollisionDetection = YES;
    ball.physicsBody.categoryBitMask = ballCategory;

    [self addChild:ball];
}

Repeat this step to assign the appropriate category mask to the arrow node in the createArowNode method:

- (SKSpriteNode *) createArrowNode
{
    SKSpriteNode *arrow = [[SKSpriteNode alloc] 
        initWithImageNamed:@"ArrowTexture.png"];

    arrow.position = CGPointMake(CGRectGetMinX(self.frame)+100, 
        CGRectGetMidY(self.frame));

    arrow.name = @"arrowNode";

    arrow.physicsBody = 
        [SKPhysicsBody bodyWithRectangleOfSize:arrow.frame.size];
    arrow.physicsBody.usesPreciseCollisionDetection = YES;
    arrow.physicsBody.categoryBitMask = arrowCategory;

    return arrow;
}

Configuring the Collision and Contact Masks

Having assigned category masks to the arrow and ball nodes, these nodes are ready to be included in collision detection handling. Before this can be implemented however, code needs to be added to indicate whether the application needs to know about collisions, contacts or both. When a contact occurs, two nodes are able to touch or even occupy the same space in a scene. It might be valid, for example, for one sprite node to pass over another node and the game logic needs to be notified when this happens. A collision involves contact between two nodes that cannot occupy the same space in the scene. In such a situation (and subject to prevailing physics body properties) the two nodes will typically bounce away from each other.

The type of contact for which notification is required is specified by assigning contact and collision bit masks to the physics body of one of the node categories involved in the contact. For the purposes of this example, we will specify that notification is required for both contact and collision between the arrow and ball categories:

- (SKSpriteNode *) createArrowNode
{
    SKSpriteNode *arrow = 
        [[SKSpriteNode alloc] initWithImageNamed:@"ArrowTexture.png"];

    arrow.position = CGPointMake(CGRectGetMinX(self.frame)+100, 
        CGRectGetMidY(self.frame));

    arrow.name = @"arrowNode";

    arrow.physicsBody = 
        [SKPhysicsBody bodyWithRectangleOfSize:arrow.frame.size];
    arrow.physicsBody.usesPreciseCollisionDetection = YES;
    arrow.physicsBody.categoryBitMask = arrowCategory;
    arrow.physicsBody.collisionBitMask = arrowCategory | ballCategory;
    arrow.physicsBody.contactTestBitMask = 
                          arrowCategory | ballCategory;

    return arrow;
}

Implementing the Contact Delegate

When the Sprite Kit physics system detects a collision or contact for which appropriate masks have been configured it needs a way to notify the application code that such an event has occurred.

It does this by calling methods on the class instance that has been registered as the contact delegate for the physics world object associated with the scene in which the contact took place. In actual fact, the system is able to notify the delegate at both the beginning and end of the contact if both the didBeginContact and didEndContact methods are implemented. Passed as an argument to these methods is an SKPhysicsContact object containing information about the location of the contact and references to the physical bodies of the two nodes involved in the contact.

For the purposes of this tutorial we will use the ArcheryScene instance as the contact delegate and implement only the didBeginContact method. Begin, therefore, by modifying the didMoveToView method in the ArcheryScene.m file to declare the class as the contact delegate:

- (void)didMoveToView:(SKView *)view
{
    if (!self.sceneCreated)
    {
        self.score = 0;
        self.ballCount = 40;
        self.physicsWorld.gravity = CGPointVectori(0, -1.0);
        self.physicsWorld.contactDelegate = self;
        [self initArcheryScene];
        self.sceneCreated = YES;
    }
}

Purchase the fully updated iOS 10 / Swift 3 / Xcode 8 edition of this book in eBook ($19.99) or Print ($45.99) format.
iOS 10 App Development Essentials Print and eBook (ePub/PDF/Kindle) edition contains over 100 chapters. Learn more...

Buy eBook Buy Print Preview Book

Having made the ArcheryScene class the contact delegate the ArcheryScene.h interface file needs to be modified to indicate that the class now implements the SKPhysicsContactDelegate protocol:

#import <SpriteKit/SpriteKit.h>

@interface ArcheryScene : SKScene
     <SKPhysicsContactDelegate>
@end

Returning to the ArcheryScene.m, implement the didBeginContact method as follows:

- (void) didBeginContact:(SKPhysicsContact *)contact
{
    SKSpriteNode *firstNode, *secondNode;

    firstNode = (SKSpriteNode *)contact.bodyA.node;
    secondNode = (SKSpriteNode *) contact.bodyB.node;

    if ((contact.bodyA.categoryBitMask == arrowCategory) 
         && (contact.bodyB.categoryBitMask == ballCategory))
    {
        CGPoint contactPoint = contact.contactPoint;

        float contact_y = contactPoint.y;
        float target_y = secondNode.position.y;
        float margin = secondNode.frame.size.height/2 - 25;

        if ((contact_y > (target_y - margin)) && 
                   (contact_y < (target_y + margin)))
        {
            NSLog(@"Hit");
            self.score++;
        }
    }
}

The code starts by extracting references to the two nodes that have collided. It then checks that the first node was an arrow and the second a ball (no points are scored if a ball falls onto an arrow). Next, the point of contact is identified and some rudimentary mathematics used to check that the arrow struck the side of the ball (for a game of app store quality more rigorous checking might be required to catch all cases). Assuming that the hit was within the defined parameters, a message is output to the console and the game score variable is incremented.

Run the game and test the collision handling by making sure that the “Hit” message appears in the Xcode console when an arrow hits the side of a ball.

Implementing a Physics Joint Between Nodes

When a valid hit is detected, the arrow needs to appear to embed partway into the ball and stick there as the ball continues its descent. In order to achieve this, a new texture will be applied to the arrow sprite node that makes the arrow appear without a tip and slightly shorter.

The joining of the arrow node and ball will be achieved by implementing a physics joint at the point of contact between the two nodes. A number of different joint types are available, but for the purposes of this game, a fixed joint provides the exact behavior required.

The embedded arrow texture is contained in the ArrowHitTexture.png file. Locate this file in the SpriteImages folder of the sample code download and drag and drop it onto the Supporting Files folder in the project navigator.

Within the ArcheryScene.m file, modify the didBeginContact method to establish a fixed joint between the two nodes and to change the texture of the arrow:

- (void) didBeginContact:(SKPhysicsContact *)contact
{
    SKSpriteNode *firstNode, *secondNode;

    firstNode = (SKSpriteNode *)contact.bodyA.node;
    secondNode = (SKSpriteNode *) contact.bodyB.node;

    if ((contact.bodyA.categoryBitMask == arrowCategory) 
       && (contact.bodyB.categoryBitMask == ballCategory))
    {
        CGPoint contactPoint = contact.contactPoint;

        float contact_x = contactPoint.x;
        float contact_y = contactPoint.y;
        float target_y = secondNode.position.y;

        float margin = secondNode.frame.size.height/2 - 25;

        if ((contact_y > (target_y - margin)) && 
             (contact_y < (target_y + margin)))
        {
            SKPhysicsJointFixed *joint = 
               [SKPhysicsJointFixed jointWithBodyA:contact.bodyA 
                bodyB:contact.bodyB 
                anchor:CGPointMake(contact_x, contact_y)];

            [self.physicsWorld addJoint:joint];

            SKTexture *texture = 
               [SKTexture textureWithImageNamed:@"ArrowHitTexture"];
            firstNode.texture = texture;
            self.score++;
        }
    }
}

Compile and run the application. When an arrow scores a hit on a ball the arrow will now appear to stick into the ball. Note that the attachment of the arrow to the ball causes the ball to rotate and change course as a result of the impact and shift in center of gravity (Figure 58-1). All of this is provided automatically by the Sprite Kit physics world.


An iOS 7 Sprite Kit collision with joint

Figure 58-1


Game Over

All that now remains is to display the score to the user when all of the balls have been released. This will require a new label node and a small change to an action sequence followed by a transition to the welcome scene so the user can start a new game. Begin by adding the method to create the label node in the ArcheryScene.m file:

- (SKLabelNode *) createScoreNode
{
    SKLabelNode *scoreNode = 
      [SKLabelNode labelNodeWithFontNamed:@"Bradley Hand"];

    scoreNode.name = @"scoreNode";

    NSString *newScore = 
      [NSString stringWithFormat:@"Score: %i", self.score];

    scoreNode.text = newScore;
    scoreNode.fontSize = 60;
    scoreNode.fontColor = [SKColor redColor];

    scoreNode.position = 
     CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));

    return scoreNode;
}

Next, implement the gameOver method which will display the score label node and then transition back to the welcome scene:

- (void) gameOver
{
    SKLabelNode *scoreNode = [self createScoreNode];

    [self addChild:scoreNode];

    SKAction *fadeOut = [SKAction sequence:@[[SKAction 
                          waitForDuration:3.0], 
                         [SKAction fadeOutWithDuration:3.0]]];

    SKAction *welcomeReturn = [SKAction runBlock:^{

        SKTransition *transition = 
          [SKTransition revealWithDirection:SKTransitionDirectionDown 
                        duration:1.0];

        WelcomeScene *welcomeScene = 
          [[WelcomeScene alloc] initWithSize:self.size];

        [self.scene.view presentScene: welcomeScene 
                         transition:transition];
    }];

    SKAction *sequence = [SKAction sequence:@[fadeOut, welcomeReturn]];

    [self runAction:sequence];
}

Finally, add a completion handler that calls the gameOver method to the ball release action in the initArcheryScene method:

- (void) initArcheryScene
{
.
.
.
    self.archerAnimation = archerFrames;

    SKAction *releaseBalls = [SKAction sequence:@[
                [SKAction performSelector:@selector(createBallNode) 
                          onTarget:self],
                    [SKAction waitForDuration:1]
                ]];


    [self runAction: [SKAction repeatAction:releaseBalls count:self.ballCount] completion:^{
        [self gameOver];
    }];

}

Compile, run and test. Also feel free to experiment by adding other features to the game to gain familiarity with the capabilities of Sprite Kit. The next chapter, entitled An iOS 7 Particle Emitter Tutorial, will cover the use of the Particle Emitter to add special effects to Sprite Kit games.

Summary

The Sprite Kit physics engine provides a mechanism for detecting when two nodes within a scene come into contact with each other. Collision and contact detection is configured through the use of category masks together with contact and collision masks. When appropriately configured, the didBeginContact and didEndContact methods of a designated delegate class are called at the start and end of a contact between two nodes for which detection is configured. These methods are passed references to the nodes involved in the contact so that appropriate action can be taken within the game.

Sprite Kit also allows joints to be formed between nodes. In this chapter a fixed joint was implemented to attach arrow nodes to the ball nodes in the event of contacts being detected.


Purchase the fully updated iOS 10 / Swift 3 / Xcode 8 edition of this book in eBook ($19.99) or Print ($45.99) format.
iOS 10 App Development Essentials Print and eBook (ePub/PDF/Kindle) edition contains over 100 chapters. Learn more...

Buy eBook Buy Print Preview Book



PreviousTable of ContentsNext
An iOS 7 Sprite Kit Game TutorialAn iOS 7 Sprite Kit Particle Emitter Tutorial