Handling Collisions

Sprite Kit provides a full-featured physics engine that makes it easy for us to define how the nodes in a scene interact with each other. In Adventure, we use the physics simulation for a variety of tasks:

Sprite Kit does most of the hard work for us; all we have to do is configure the physics body information on each node, and wait for notifications when collisions occur.

By default, an SKNode object doesn’t have any related physics body, which means it doesn’t take part in the physics simulation. We use this default behavior for all the trees in Adventure, because trees are in the scene purely to enhance the look of the environment and don’t have direct physical interaction with characters.

In contrast, we do want the Adventure characters to interact with each other and with the walls of the maze. We create physics bodies for each character and wall node, and configure them to indicate the types of object we want each node to collide with.

Creating Physics Bodies

When we create the scene, we walk through the data in the level map and create the starting locations of various characters, and then add nodes to represent the walls in the maze, as described in “Adding the Collision Walls.”

Each wall is configured with a rectangular physics body, with the dynamic property set to NO.

  1. Adventure: APAAdventureScene.m -addCollisionWallAtWorldPoint:withWidth:height:
  2. - (void)addCollisionWallAtWorldPoint:(CGPoint)worldPoint withWidth:(CGFloat)width height:(CGFloat)height {
  3. ... (Create a rectangle with the specified size, and a basic node)
  4. wallNode.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:rect.size];
  5. wallNode.physicsBody.dynamic = NO;
  6. wallNode.physicsBody.categoryBitMask = APAColliderTypeWall;
  7. }
5

We set dynamic to NO because the wall’s position is unaffected by other physics bodies—it doesn’t move, even if other physics bodies collide with it.

We set the categoryBitMask property on every physics body in the scene to one of the APAColliderType values:

  1. Adventure: APACharacter.h
  2. typedef enum : uint8_t {
  3. APAColliderTypeHero = 1,
  4. APAColliderTypeGoblinOrBoss = 2,
  5. APAColliderTypeProjectile = 4,
  6. APAColliderTypeWall = 8,
  7. APAColliderTypeCave = 16
  8. } APAColliderType;

We use these values as a bit mask to indicate the types of collision we want Sprite Kit to detect for any physics body. We want a hero, for example, to collide with other heroes, goblins, the level boss, walls, and goblin caves, but not with projectiles.

All other physics bodies in the game belong to the characters—we create and configure these during character initialization.

Configuring Physics Bodies

The APACharacter class has two initialization methods, one for animated characters such as heroes, goblins, and the level boss, and one for the parallax goblin caves. Both methods call through to a shared initialization method, which in turn calls configurePhysicsBody.

Each of the character subclasses overrides configurePhysicsBody to create and configure a suitable physics body for the sprite. The APAHeroCharacter class, for example, creates a circular body and configures it to collide with everything other than a projectile.

  1. Adventure: APAHeroCharacter.m -configurePhysicsBody
  2. - (void)configurePhysicsBody {
  3. self.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:kCharacterCollisionRadius];
  4. self.physicsBody.categoryBitMask = APAColliderTypeHero;
  5. self.physicsBody.collisionBitMask =
  6. APAColliderTypeGoblinOrBoss | APAColliderTypeHero |
  7. APAColliderTypeWall | APAColliderTypeCave;
  8. self.physicsBody.contactTestBitMask = APAColliderTypeGoblinOrBoss;
  9. }

Each of the character subclasses set a contactTestBitMask, which means we get a callback whenever a physics body makes contact with another physics body whose categoryBitMask is set to one of the specified types. For example, when a goblin collides with a projectile, we want to know so that we perform tasks such as applying damage or increasing a player’s score.

Responding to Collisions

Collision callbacks in Sprite Kit are the responsibility of the physics delegate; in Adventure, this is the APAAdventureScene class. When one body makes contact with another, the didBeginContact: method is called, which we implement to check for collisions involving characters, and collisions involving projectiles.

  1. Adventure: APAAdventureScene.m -didBeginContact:
  2. - (void)didBeginContact:(SKPhysicsContact *)contact {
  3. SKNode *node = contact.bodyA.node;
  4. if ([node isKindOfClass:[APACharacter class]]) {
  5. [(APACharacter *)node collidedWith:contact.bodyB];
  6. }
  7. ... (Repeat for bodyB)
  8. if (contact.bodyA.categoryBitMask & APAColliderTypeProjectile ||
  9. contact.bodyB.categoryBitMask & APAColliderTypeProjectile) {
  10. SKNode *projectile = ... (projectile is either bodyA.node or bodyB.node)
  11. [projectile runAction:[SKAction removeFromParent]];
  12. ... (Show the projectile sparks)
  13. }
  14. }
10

If the collision involves a projectile, the projectile has reached its destination, and so we remove it from the scene. We also run a one-shot particle emitter to display sparks at the collision point.

Any side effects from collisions involving one or more characters are handled by the character subclasses. For example, the APAGoblin class implements the collidedWith: method to deal with projectile hits by applying damage, requesting the “get hit” animation, and increasing the relevant player’s score if the goblin dies.

  1. Adventure: APAGoblin.m -collidedWith:
  2. - (void)collidedWith:(SKPhysicsBody *)other {
  3. if (self.dying) {
  4. return;
  5. }
  6. if (other.categoryBitMask & APAColliderTypeProjectile) {
  7. self.requestedAnimation = APAAnimationStateGetHit;
  8. CGFloat damage = 100.0f;
  9. if ((arc4random_uniform(2)) == 0) {
  10. damage = 50.0f;
  11. }
  12. BOOL killed = [self applyDamage:damage fromProjectile:other.node];
  13. if (killed) {
  14. [[self characterScene] addToScore:10 afterEnemyKillWithProjectile:other.node];
  15. }
  16. }
  17. }
3

We return early if the goblin is already dying, because the projectile can do no further damage.

10

We use arc4random_uniform() to return a value of 0 or 1, which decides whether a projectile should inflict 50% or 100% damage.

This adds a twist to the game—goblins randomly take either one or two projectile hits to die.

The applyDamage:fromProjectile: method is implemented by APACharacter. Projectiles fade out after traveling a certain distance, so we want the potency of the projectile to decrease as it begins to fade. We use applyDamage:fromProjectile: to multiply the supplied value by the projectile’s alpha value, before calling the applyDamage: method to handle decreasing the health of the character:

  1. Adventure: APACharacter.m -applyDamage:fromProjectile:
  2. - (BOOL)applyDamage:(CGFloat)damage fromProjectile:(SKNode *)projectile {
  3. return [self applyDamage:damage * projectile.alpha];
  4. }