How to Make a Game Like Monster Island Tutorial

Brian Broom

Update 10/7/16: This tutorial has been updated for Xcode 8 and Swift 3.

Want to learn how to make a game like Monster Island? Read on to find out.

Want to learn how to make a game like Monster Island? Read on to find out.

In this how to make a game like Monster Island tutorial, you will level up your knowledge of the SpriteKit physics system by creating a game named ZombieCat. Your goal as a zombie, is to defeat evil cats by throwing beakers of zombie goo at them—turning them into zombie cats. To do that, you’ll implement one of the major UI features of Monster Island: A large power meter showing the strength and direction of your throw.

Zombies AND cats—what could be better!

In case you’ve never played it, Monster Island is a fun physics-based puzzle game by Miniclip in which you defeat monsters by flinging bombs at them.

If you haven’t used Apple’s SpriteKit framework before, check out Sprite Kit Swift 2 Tutorial for Beginners. This tutorial assumes you’re familiar with actions, the physics system and the scene editor.

Let’s dive in!

Getting Started

This project uses Swift 3 and requires, at a minimum, Xcode beta 4. Once you have that, go ahead and download a copy of the starter project, and look through it.

Open GameScene.sks to see the basic elements of a physics puzzle level already set up.

The walls, cats and obstacles all have physics bodies defined for them, as well as their Category Mask values set in the Attributes Inspector.

Make a game like Monster Island tutorial. Evil cats up to no good.

Evil cats up to no good.

Creating the Projectile

At this point, all you can do in the game is watch the cats quietly plotting to take over the world. Scary, right? You need a way to fight back. That way, of course, is zombie goo. :]

In GameScene.swift, add these properties just before didMove(to:)

var pinBeakerToZombieArm: SKPhysicsJointFixed?
var beakerReady = false

Add a method to create the node for your beaker of goo.

func newProjectile () {
  let beaker = SKSpriteNode(imageNamed: "beaker")
  beaker.name = "beaker"
  beaker.zPosition = 5
  beaker.position = CGPoint(x: 120, y: 625)
  let beakerBody = SKPhysicsBody(rectangleOf: CGSize(width: 40, height: 40))
  beakerBody.mass = 1.0
  beakerBody.categoryBitMask = PhysicsType.beaker
  beakerBody.collisionBitMask = PhysicsType.wall | PhysicsType.cat
  beaker.physicsBody = beakerBody
  addChild(beaker)
 
  if let armBody = childNode(withName: "player")?.childNode(withName: "arm")?.physicsBody {
    pinBeakerToZombieArm = SKPhysicsJointFixed.joint(withBodyA: armBody, bodyB: beakerBody, anchor: CGPoint.zero)
    physicsWorld.add(pinBeakerToZombieArm!)
    beakerReady = true
  }
}

Add the following line to didMove(to:):

newProjectile()

This is a fairly standard example of creating a new SKSpriteNode, setting its properties, adding a SKPhysicsBody and adding it to the scene. Two properties you might not be familiar with are categoryBitMask and collisionBitMask. These mask values are how the physics system identifies which sprites interact with each other.

Each physics body has a categoryBitMask property you set to one of the values defined in the PhysicsType struct located at the top of GameScene.swift. Sprites will only collide with or bounce off objects where the categoryBitMask matches the other object’s collisionBitMask.

The beaker is attached to the zombie with a SKPhysicsJoint. A fixed joint keeps two physics bodies in the same relative position. In this case, the beaker will move with the arm body, until the joint is removed.

Note: The physics simulation uses two types of body interactions, collision and contact.

Collision: In this type of interaction, the two sprites bounce off of each other like a ball off a wall. The physics simulation automatically takes care of the collision calculation, so you don’t have to deal with angles or differences in mass.

Contact: This interaction alerts you when two objects touch or overlap. The moving object will continue to move through the other object, unless you change its motion manually. You’d use contacts to be informed when an event happens, such as a ball crossing a goal line, without changing the object’s motion.

For more details about these interactions, or bit masks in general, the Making Contact section of How To Create a Breakout Game with Sprite Kit and Swift gives a great explanation.

Build and run to see your new beaker of zombie goo. You’re almost ready to attack those cats!

New beaker appears on screen

bloop

Throwing the Beaker

Now that you’ve got the beaker in the game, you need to be able to use it as a weapon.

You have to beaker-ful with zombie goo. It could create a cat-atrophy.

Meow.

To throw the beaker, all you have to do is remove the SKSKPhysicsJointFixed and give it an impulse in the physics system. An impulse is like a kick, represented by a vector that has both an X and a Y component.

Add the following methods to GameScene.swift:

func tossBeaker(strength: CGVector) {
  if beakerReady == true {
    if let beaker = childNode(withName: "beaker") {
      if let arm = childNode(withName: "player")?.childNode(withName: "arm") {
        let toss = SKAction.run() {
        self.physicsWorld.remove(self.pinBeakerToZombieArm!)
        beaker.physicsBody?.applyImpulse(strength)
        beaker.physicsBody?.applyAngularImpulse(0.1125)
        self.beakerReady = false
      }
      let followTrough = SKAction.rotate(byAngle: -6*3.14, duration: 2.0)
 
      arm.run(SKAction.sequence([toss, followTrough]))
    }
 
    // explosion added later
    }
  }
}
 
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
  tossBeaker(strength: CGVector(dx: 1400, dy: 1150))
}

When using the physics system, you change the motion of a sprite through its physics body, not the sprite itself. SKPhysicsBody has two methods you could use to throw the beaker, applyForce and applyImpulse. Since you want the change to be instantaneous, use applyImpulse. Usually, thrown objects spin while in the air, and applyAngularImpulse simulates that. You can adjust the angular impulse to get the desired amount of spin, or even change its direction by using a negative number.

Don’t forget to remove the SKPhysicsJoint, or the beaker will stay attached to the zombie hand.

Build and run the game. When you tap on the screen, the beaker should fly around. Notice how the beaker bounces off of the walls and cats.

Make a game like Monster Island tutorial. A beaker is in flight.

Wheeee!

But despite all that spinning and bouncing, there’s no explosion. For that, you need the explosion effect.

Adding the Explosion Animation

To display the explosion image, you’ll need a SKSpriteNode. You may be wondering where the best place is to position the explosion. There are several answers, but one of the simplest is to add it as a child of the beaker.

Add the following to the end of newProjectile():

let cloud = SKSpriteNode(imageNamed: "regularExplosion00")
cloud.name = "cloud"
cloud.setScale(0)
cloud.zPosition = 1
beaker.addChild(cloud)

This creates a new sprite and adds it as a child node of the beaker, meaning the two will move and rotate together. You set the scale to zero to hide the sprite, since the explosion happens later. Setting the zPosition ensures the cloud appears on top of the beaker instead of underneath.

To see the explosion, add the following to tossBeaker(strength:) replacing the explosion added later comment:

if let cloud = beaker.childNode(withName: "cloud") {
 
  // 1
  let fuse = SKAction.wait(forDuration: 4.0)
  let expandCloud = SKAction.scale(to: 3.5, duration: 0.25)
  let contractCloud = SKAction.scale(to: 0, duration: 0.25)
 
  // 2
  let removeBeaker = SKAction.run() {
    beaker.removeFromParent()
  }
  let boom = SKAction.sequence([fuse, expandCloud, contractCloud, removeBeaker])
 
  // 3
  let respawnBeakerDelay = SKAction.wait(forDuration: 1.0)
  let respawnBeaker = SKAction.run() {
    self.newProjectile()
  }
  let reload = SKAction.sequence([respawnBeakerDelay, respawnBeaker])
 
  // 4
  cloud.run(boom) {
    self.run(reload)
  }
}

Taking this step-by-step:

  1. The first three actions wait 4 seconds before making the cloud grow and shrink in the form of an explosion. Feel free to experiment with these values for different effects.
  2. Normally, this would be a simple removeFromParent action, but these actions run on the cloud node. You need to remove the beaker node instead, so you define a runBlock action. Then you combine the actions in a sequence.
  3. These actions give you another beaker to toss at the cats.
  4. Finally, you run the sequence of actions from Step 2 on the cloud node. While you could have the reload actions in the main sequence action, the completion block will come in handy later.

Build and run to see how dangerous zombie goo can be.

Make a game like Monster Island tutorial. boom

Down with the ferocious felines!

Animate the Explosion

You’ve got an explosion now, but it’s pretty basic. To really knock those kitties off their paws, animate the explosion during the expansion and contraction step. The starter project includes images regularExplosion00.png through regularExplosion08.png, and here you’ll add an action that cycles through them.

First, add a property to the top of GameScene.swift:

var explosionTextures = [SKTexture]()

Now add the following to the end of didMove(to:):

for i in 0...8 {
  explosionTextures.append(SKTexture(imageNamed: "regularExplosion0\(i)"))
}

This builds the array of SKTextures for the animation.

Next, find section 2 of the tossBeaker(strength:) method and replace the line let boom = SKAction.sequence... with the following:

let animate = SKAction.animate(with: explosionTextures, timePerFrame: 0.056)
 
let expandContractCloud = SKAction.sequence([expandCloud, contractCloud])
let animateCloud = SKAction.group([animate, expandContractCloud])
 
let boom = SKAction.sequence([fuse, animateCloud, removeBeaker])

You calculate the value for timePerFrame by dividing the total duration for the expand and contract animation (0.5 s) by the number of frames (9).

Build and run to see your upgraded explosion.

The explosion animation now uses multiple frames.

Your dog would approve. :]

Setting the Explosion Range

When the beaker explodes, you’ll want to know which cat sprites are within range of the explosion. One way to do this is to create a separate SKPhysicsBody for the explosion and use the contact system to be notified when the explosion touches a cat. However, since each SKSpriteNode can only have one physics body attached to it, you’ll create an invisible node for the explosion radius and add it to the beaker as a child.

Add the following to the end of newProjectile():

let explosionRadius = SKSpriteNode(color: UIColor.clear, size: CGSize(width: 200, height: 200))
explosionRadius.name = "explosionRadius"
 
let explosionRadiusBody = SKPhysicsBody(circleOfRadius: 200)
explosionRadiusBody.mass = 0.01
explosionRadiusBody.pinned = true
explosionRadiusBody.categoryBitMask = PhysicsType.explosionRadius
explosionRadiusBody.collisionBitMask = PhysicsType.none
explosionRadiusBody.contactTestBitMask = PhysicsType.cat
 
explosionRadius.physicsBody = explosionRadiusBody
beaker.addChild(explosionRadius)

After you create the explosion physics body, you set its mass property to 0.01 to minimize the amount of mass the explosionRadiusBody will add to the beakerBody.

Notice how explosionRadiusBody has none for its collisionBitMask. You don’t want this extra “bubble” around the beaker to collide with anything, because then it would look like the beaker bounced before it hit the object. You do want the contactTestBitMask set to cat, so that the system will recognize those sprites overlapping as a contact.

Modify the if let cloud statement in tossBeaker(strength:) to look like:

if let cloud = beaker.childNode(withName: "cloud"),
   let explosionRadius = beaker.childNode(withName: "explosionRadius") {

Next, add the following to section 2 in tossBeaker(strength:), just after the let animate... line:

let greenColor = SKColor(red: 57.0/255.0, green: 250.0/255.0, blue: 146.0/255.0, alpha: 1.0)
let turnGreen = SKAction.colorize(with: greenColor, colorBlendFactor: 0.7, duration: 0.3)
 
let zombifyContactedCat = SKAction.run() {
  if let physicsBody = explosionRadius.physicsBody {
    for contactedBody in physicsBody.allContactedBodies() {
      if (physicsBody.contactTestBitMask & contactedBody.categoryBitMask) != 0  ||
        (contactedBody.contactTestBitMask & physicsBody.categoryBitMask) != 0  {
        contactedBody.node?.run(turnGreen)
        contactedBody.categoryBitMask = PhysicsType.zombieCat
      }
    }
  }
}

This action finds all the physics bodies in contact with the explosionRadius body and uses the colorize(with:colorBlendFactor:duration:) action to turn the new zombie cat a putrid green color.

The if statement with contactTestBitMask and categoryBitMask is there because the allContactedBodies() method returns all the SKPhysicsBody objects touching the given body, instead of only the ones that match the contactTestBitMask. This statement filters out the extra physics bodies.

Change the expandContractCloud action to this:

let expandContractCloud = SKAction.sequence([expandCloud, zombifyContactedCat, contractCloud])

Update the collisionBitMask for the beaker in newProjectile() to:

beakerBody.collisionBitMask = PhysicsType.wall | PhysicsType.cat | PhysicsType.zombieCat

Without this change, the beaker would pass through the zombie cats—which would make sense if the zombie goo turned the cats into ghosts, but sadly, it does not.

Build and run. The beaker should explode next to the cat on the right, turning him into a zombie cat. Finally!

Make a game like Monster Island tutorial. One of the cats has been turned into a zombie.

Meow – Brains!

Making Sprites React

Now that the cats know what’s coming, it would be great to show how afraid they are of your beaker of goo. Since you already have a SKPhysicsBody object to tell when the beaker is close to a cat, you can have the contact system notify you when this body touches a cat node. The system does this by calling delegate methods in your class. You’ll set that up next.

First, since you want to change the image texture for the cat sprites when contact starts, then revert it when it ends, it makes sense to store these textures in a property for easy access. Add these properties to the property section at the top of GameScene.swift:

let sleepyTexture = SKTexture(imageNamed: "cat_sleepy")
let scaredTexture = SKTexture(imageNamed: "cat_awake")

To keep your code nice and organized, add a class extension at the bottom of GameScene.swift to conform to the SKPhysicsContactDelegate protocol:

// MARK: - SKPhysicsContactDelegate
extension GameScene: SKPhysicsContactDelegate {
 
  func didBegin(_ contact: SKPhysicsContact) {
    if (contact.bodyA.categoryBitMask == PhysicsType.cat) {
      if let catNode = contact.bodyA.node as? SKSpriteNode {
        catNode.texture = scaredTexture
      }
    }
 
    if (contact.bodyB.categoryBitMask == PhysicsType.cat) {
      if let catNode = contact.bodyB.node as? SKSpriteNode {
        catNode.texture = scaredTexture
      }
    }
  }
 
  func didEnd(_ contact: SKPhysicsContact) {
    if (contact.bodyA.categoryBitMask == PhysicsType.cat) {
      if let catNode = contact.bodyA.node as? SKSpriteNode {
        catNode.texture = sleepyTexture
      }
    }
 
    if (contact.bodyB.categoryBitMask == PhysicsType.cat) {
      if let catNode = contact.bodyB.node as? SKSpriteNode {
        catNode.texture = sleepyTexture
      }
    }
  }
}

Both methods are almost identical, except that the first changes the texture to the afraid cat, and the second sets it back to the sleepy cat. When this method is called, it is passed a SKPhysicsContact object, which contains the two bodies that generated the contact as bodyA and bodyB. One quirk of this system: there is no guarantee which body will be A and which will be B, so it’s helpful to test both. If the categoryBitMask of that body matches the value for cat, you can reassign the texture of that body.

Next, add the following line to the end of didMove(to:):

physicsWorld.contactDelegate = self

This assigns the GameScene class as the delegate for the physics contact system. The system will call didBegin(_:) and didEnd(_:) on the delegate each time the appropriate physics bodies touch or stop touching. The values of categoryBitMask and contactTestBitMask determine which bodies trigger these method calls.

You’ll need to switch from the afraid cat texture back to sleepy cat if the cat turns into a zombie (zombies aren’t afraid anymore, obviously). Update the zombifyContactedCat action in tossBeaker(strength:) to this:

let zombifyContactedCat = SKAction.run() {
  if let physicsBody = explosionRadius.physicsBody {
    for contactedBody in physicsBody.allContactedBodies() {
      if (physicsBody.contactTestBitMask & contactedBody.categoryBitMask) != 0  ||
        (contactedBody.contactTestBitMask & physicsBody.categoryBitMask) != 0  {
        if let catNode = contactedBody.node as? SKSpriteNode {
          catNode.texture = self.sleepyTexture // set texture to sleepy cat
        }
        contactedBody.node?.run(turnGreen)
        contactedBody.categoryBitMask = PhysicsType.zombieCat
      }
    }
  }
}

It’s a simple addition, but having the enemies in a game react to their surroundings, especially their impending doom, can add a lot of fun and anticipation.

Build and run to see the effect.

Cat is afraid because the exploding beaker is near.

Oh noes

Power Meter

The beaker throw is looking great, but you need to get that pesky remaining cat. It’s time to add the power meter so you can adjust the strength and angle of your throw.

In GameScene.sks, drag a Color Sprite from the object library into your scene for the base arrow. Set the name, texture, position and anchor point attributes to the following values:

Sprite settings for angle meter.

Sprite settings for angle meter.

Next, drag a second Color Sprite into the scene for the power indicator, and set its name, parent, texture, position, anchor point, and zPosition attributes:

Sprite settings for power meter.

Sprite settings for power meter.

The arrow tells you the direction of the thrown beaker, and the size of the green bar is the strength of the throw. Making the arrow the parent of the green bar means that the two will rotate together, and the anchor point setting makes sure that the arrow rotates around its end, instead of the center of the sprite.

Build and run to see how it looks.

Added sprite for the power meter.

Still need more power.

Accessing the Power Meter From Code

Tracking touches as you drag your finger across the screen doesn’t have to be complicated. Instead of calculating based on touch directly, you can add a UIGestureRecognizer to do the heavy lifting.

In this case, a UIPanGestureRecognizer is perfect, since it measures how far your finger has moved from the starting point in either direction.

Add these properties to GameScene.swift:

var previousThrowPower = 100.0
var previousThrowAngle = 0.0
var currentPower = 100.0
var currentAngle = 0.0
var powerMeterNode: SKSpriteNode? = nil
var powerMeterFilledNode: SKSpriteNode? = nil

Here you keep track of the current and previous values for power and angle, along with references to the power meter nodes so they can move.

The way UIPanGestureRecognizer works is that you provide an update method for it to call. Add this to the end of GameScene.swift just after touchesBegan(_:with:):

func handlePan(recognizer:UIPanGestureRecognizer) {
  if recognizer.state == UIGestureRecognizerState.began {
    // do any initialization here
  }
 
  if recognizer.state == UIGestureRecognizerState.changed {
    // the position of the drag has moved
    let viewLocation = recognizer.translation(in: self.view)
    print("x: \(viewLocation.x) y: \(viewLocation.y)")
  }
 
  if recognizer.state == UIGestureRecognizerState.ended {
    // finish up
    tossBeaker(strength: CGVector(dx: 1600, dy: 1100))
  }
}

Shortly after you start sliding your finger on the screen, the UIPanGestureRecognizer recognizes this as a pan and calls your update method, with the UIPanGestureRecognizer as a parameter. The state of the recognizer tells you if the pan has just started, is moving, or has just ended, giving you a chance to respond appropriately in the update method.

Scroll up to didMove(to:) and add the following before the end of the method:

powerMeterNode = childNode(withName: "powerMeter") as? SKSpriteNode
powerMeterFilledNode = powerMeterNode?.childNode(withName: "powerMeterFilled") as? SKSpriteNode
 
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
view.addGestureRecognizer(panRecognizer)

Here you get the power meter nodes from GameScene.sks, then add a UIPanGestureRecognizer to the scene.

Finally, delete the touchesBegan(_:) method.

Build and run the app. As you scroll your finger across the screen, look at Xcode’s console window, which will show you the current displacement, meaning the distance from your initial touch to your finger’s current position.

Console output from gesture recognizer

Sample console output

Take a look at the sign of the numbers in each direction. Notice that dragging your finger upwards makes the y values negative, not positive. This is because the UIPanGestureRecognizer works in the UIKit coordinate system, where the origin is in the upper left, instead of the SpriteKit coordinate system, where the origin is the lower left.

Shows translation values for several touch values

UIPanGestureRecognizer Translation

The power meter isn’t moving, however, so you’ll tackle that next.

Updating the Meter

A meter that doesn’t move isn’t very helpful, so here you’ll update it to reflect a user’s touch.

For this game, the x component of the translation will change the power from 0% to 100%, while the y component will change the angle from 0° (horizontal) to 90° (vertical). To achieve this, add a method to do the calculations:

func updatePowerMeter(translation: CGPoint) {
  // 1
  let changeInPower = translation.x
  let changeInAngle = translation.y
  // 2
  let powerScale = 2.0
  let angleScale = -150.0
  // 3
  var power = Float(previousThrowPower) + Float(changeInPower) / Float(powerScale)
  var angle = Float(previousThrowAngle) + Float(changeInAngle) / Float(angleScale)
  // 4
  power = min(power, 100)
  power = max(power, 0)
  angle = min(angle, Float(M_PI_2))
  angle = max(angle, 0)
  // 5
  powerMeterFilledNode?.xScale = CGFloat(power/100.0)
  powerMeterNode?.zRotation = CGFloat(angle)
  // 6
  currentPower = Double(power)
  currentAngle = Double(angle)
}

Here’s a summary of what’s happening:

  1. Split the translation value into an x part for changing power, and a y part for changing angle.
  2. Define scale factors for power and angle changes. The values need to be scaled down, otherwise a small finger movement will change the power from 0 to 100%. Larger factors mean it takes a longer pan gesture to produce a given change in power or angle. Don’t make the factors too large, or you will run out of screen before you get the value you want. The angle scale value is negative, because of the flipped sign of the values coming from the gesture recognizer.
  3. Compute the new power and angle by taking the previous value and adding the change, dividing by the scale factors from part 2. Starting with the previous values keeps the arrow from having a jump back to 0 power with every new gesture.
  4. Make sure the power and angle values are between the minimum and maximum values that make sense.
  5. Adjust the sprite parameters to match the computed power and angle.
  6. Save these computed values so that when the gesture ends, you can provide the right impulse to the beaker.

Each time the UIPanGestureRecognizer updates, you compute the new power from 0 to 100 and use the xScale property to stretch the green bar accordingly. At the same time, you compute the updated angle and set the zRotation on the entire meter to make it rotate. Since rotating an SKSpriteNode also rotates its internal x and y axes, you can set the xScale property on the power bar to scale it horizontally regardless of the orientation of the power arrow.

Next, find the section of handlePan(recognizer:) where you check for UIGestureRecognizerState.changed, and update it to call this new method:

if recognizer.state == UIGestureRecognizerState.changed {
  // the position of the drag has moved
  let translation = recognizer.translation(in: self.view)
  updatePowerMeter(translation: translation)
}

Build and run. Now, dragging your finger around the screen adjusts both the green power bar as well as the angle of the arrow.

Power meter adjusts based on touch input.

This one is juuusst right.

Throwing With the Correct Impulse

The last thing to do is use the computed power and angle, instead of the hard coded values, to throw the beaker.

In the section of handlePan(recognizer:) where you check for UIGestureRecognizerState.ended, update the if statement to look like:

if recognizer.state == UIGestureRecognizerState.ended {
  // finish up
  let maxPowerImpulse = 2500.0
  let currentImpulse = maxPowerImpulse * currentPower/100.0
 
  let strength = CGVector( dx: currentImpulse * cos(currentAngle),
                           dy: currentImpulse * sin(currentAngle) )
  tossBeaker(strength: strength)
}

Here you put together the strength vector by calculating its x and y values.

If you’re not familiar with turning a vector into a x and y components, you make use of some functions from trigonometry.

X and Y Components of a Vector

X and Y Components of a Vector

You scale a maximum value down to currentImpulse, which is r in the diagram. θ is the angle from the vector to the x-axis, which you have as currentAngle.

Add these lines to tossBeaker(strength:) at the end of section 1:

previousThrowPower = currentPower
previousThrowAngle = currentAngle

Build and run; the beaker will now use your custom values. See if you can get the last remaining cat!

Make a game like Monster Island tutorial. Projectile thrown with a custom angle and power.

Get that cat right meow!

Win or Lose

You’re almost done! All that’s left is to track whether you convert all the cats to zombies before running out of beakers.

Your GameScene.sks file already has labels for the number of beakers and cats remaining, so you just need to add the code. Add the following to the property section of GameScene.swift:

var beakersLeft = 3
var catsRemaining = 2

Now add the following method:

func updateLabels() {
  if let beakerLabel = childNode(withName: "beakersLeftLabel") as? SKLabelNode {
    beakerLabel.text = "\(beakersLeft)"
  }
 
  if let catsLabel = childNode(withName: "catsRemainingLabel") as? SKLabelNode {
    catsLabel.text = "\(catsRemaining)"
  }
}

This method simply updates the labels onscreen to match the current values.

Add this method:

func checkEndGame() {
  if catsRemaining == 0 {
    print("you win")
    if let gameOverScene = GameOverScene(fileNamed: "GameOverScene") {
      gameOverScene.scaleMode = scaleMode
      gameOverScene.won = true
      view?.presentScene(gameOverScene)
    }
    return
  }
 
  if beakersLeft == 0 {
    print("you lose")
    if let gameOverScene = GameOverScene(fileNamed: "GameOverScene") {
      gameOverScene.scaleMode = scaleMode
      view?.presentScene(gameOverScene)
    }
  }
}

The game is over when you run out of beakers or if you convert all the cats to zombies. The files for GameOverScene.sks and GameOver.swift are included in the starter project.

Find the zombifyContactedCat action inside the tossBeaker(strength:) method and change it to:

let zombifyContactedCat = SKAction.run() {
  if let physicsBody = explosionRadius.physicsBody {
    for contactedBody in physicsBody.allContactedBodies() {
      if (physicsBody.contactTestBitMask & contactedBody.categoryBitMask) != 0  ||
        (contactedBody.contactTestBitMask & physicsBody.categoryBitMask) != 0  {
        if let catNode = contactedBody.node as? SKSpriteNode {
          catNode.texture = self.sleepyTexture
        }
        contactedBody.node?.run(turnGreen)
        self.catsRemaining -= 1
        contactedBody.categoryBitMask = PhysicsType.zombieCat
      }
    }
  }
}

The main change you made was subtracting 1 from the catsRemaining property. You do this immediately after turning the cat green to keep track of the number of cats remaining.

Finally, update the completion block in section 4 of tossBeaker(strength:) to:

cloud.run(boom) {
  self.beakersLeft -= 1
  self.run(reload)
  self.updateLabels()
  self.checkEndGame()
}

This subtracts the thrown beaker from the internal count and updates the labels to the current values, then checks to see if you won or lost the game.

Build and run. As you play, you should see the labels displaying the number of cats and beakers remaining. The game also displays whether you won.

Win and lose game splash screen.

When you zombie-fy all the cats, you win!

Finishing Touches

For one last bit of flair, add a particle emitter to the beaker. The starter project includes BeakerSmoke.sks, which looks like a toxic green cloud coming from the beaker, and BeakerSparkTrail.sks, which looks like a burning fuse to ignite the goo. You can add these to the beaker by adding the following lines to tossBeaker(strength:), between sections 1 and 2.

if let sparkNode = SKEmitterNode(fileNamed: "BeakerSparkTrail") {
  beaker.addChild(sparkNode)
}
 
if let smokeNode = SKEmitterNode(fileNamed: "BeakerSmoke") {
  smokeNode.targetNode = self
  beaker.addChild(smokeNode)
}

The targetNode property changes which node the particles themselves are added to, in this case the scene node itself. If the smoke particles are added to the beaker node, the smoke spins around as the beaker spins, which doesn’t look right.

Build and run, then admire your handiwork.

The beaker now leaves a smoke trail and has sparks from a fuse.

Danger! Danger!

Voila! Those cats don’t stand a chance. ;]

Note: No cats were harmed in the implementation of this tutorial!

Where to Go From Here?

Download the completed project here.

In this tutorial, you used SpriteKit’s physics system to create a game similar to Monster Island, only better since it zombie-fies cats! You learned how to create and throw a projectile, animate a contact explosion and make sprites react to their surroundings. You also learned to incorporate a power meter to measure the user’s touch in order to throw the projectile.

There are tons of levels you can create using the tools learned in this tutorial. You can also explore ideas like obstacles that move and walls that can be destroyed. The options are endless!

For more about creating games with SpriteKit, check out 2D iOS & tvOS Games by Tutorials.

I hope you enjoyed this tutorial. If you have comments or questions, please join the forum discussion below.

Artwork by Vicki Wenderlich and kenney.nl.

Brian Broom

Brian has been tinkering with computers since writing basic programs on IBM PC (with Two! floppy drives). He has done web, database, C++, ruby, and now iOS development. Brian has spent the last few years as a computer science teacher and trainer.

You can reach him by email or on Twitter.

Other Items of Interest

Save time.
Learn more with our video courses.

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 19 total!

Swift Team

... 15 total!

iOS Team

... 33 total!

Android Team

... 15 total!

macOS Team

... 10 total!

Apple Game Frameworks Team

... 11 total!

Unity Team

... 11 total!

Articles Team

... 12 total!

Resident Authors Team

... 15 total!