How to Make a Game Like Monster Island Tutorial
Learn how to make a game like Monster Island. By Brian Broom.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How to Make a Game Like Monster Island Tutorial
30 mins
- Getting Started
- Creating the Projectile
- Throwing the Beaker
- Adding the Explosion Animation
- Animate the Explosion
- Setting the Explosion Range
- Making Sprites React
- Power Meter
- Accessing the Power Meter From Code
- Updating the Meter
- Throwing With the Correct Impulse
- Win or Lose
- Finishing Touches
- Where to Go From Here?
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:
- 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.
- 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 arunBlock
action. Then you combine the actions in a sequence. - These actions give you another beaker to toss at the cats.
- 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.
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 SKTexture
s 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.
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!
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.