Here we are, live with the “Trying to catch up on Swift” show!
This is Day 59 of my Swift learning path started on February 1st. Yes, I missed a day as last Friday was an awful day at work, really unbelievable.
Anyways… today we’ll be making our second game using SpriteKit.
Setting up
- Create a new Xcode project within the iOS category and using the Game template. Select iPad as its only target device and deselect Portrait and Upside Down orientations.
- Delete the Action.sks file; inside GameScene.sks delete the “Hello, World” label, change the size to be 1024 x 768 and the anchor point to be 0, 0; inside GameScene.swiftdelete all code except for the
didMove
method and add an emptytouchesBegan
method. - Download the assets for this project from the Hacking with Swift GitHub repository; drag the @2x.png images into the Assets.xcassets folder; drag the two audio .caf files inside the project, just under Info.plist.
Getting up and running: SKCropNode
The nice thing of learning new stuff is that, usually, on the first run, you don’t understand a single thing. Well, that was my feeling today. I understand that we have already faced our first SpriteKit game, but this speed of explaining is really too much. Sure, I can go and grab the book and proceed at my own speed but… yes, never mind my rants.
- Add a
gameScore
property of typeSKLabelNode!
and ascore
property initialised to0
to theGameScene
class. This last property needs a property observer:didSet { gameScore.text = "Score: \(score)"
. Nothing to explain, right? We are iOS veterans… 😒… yeah… - Inside the
didMove
method we need to initialise our background as anSKSpriteNode(imageNamed:)
object, set its position to aCGPoint(x: 512, y: 384)
(that is, in the center of our scene), set its blend mode to.replace
(so that its colour replaces whatever is below it), set itszPosition
to-1
(that is, behind everything else) and add it as a child to the parent view. - After this create the game score with
SKLabelNode(fontNamed: "Chalkduster")
, set its text to “Score: 0”, its position toCGPoint(x: 8, y: 8)
(that is, in the bottom left corner), its horizontal alignment mode to.left
, the font size to48
and, finally, add the child to the parent view. - As the default behaviour of this Xcode template is to stretch the view so that the edges get cut off we need to replace the
.aspectFill
of thescene.scaleMode
inside GameViewController.swift . This will gently stretch our scene so that it fits the device dimensions no matter what aspect ratio we are working with (this would be a problem with the iPad Pro 11-inch). - Create a Cocoa Touch Class that subclasses
SKNode
named “WhackSlot” (for the love of Sparta, please pay attention to what you type!). This base class has the only purpose of sitting on a scene at a determined position and holding other nodes as children. Addimport SpriteKit
at the top of the file. - Add a method to create a hole at its current position:
func configure(at position: CGPoint) {
self.position = position
let sprite = SKSpriteNode(imageNamed: "whackHole")
addChild(sprite)
}
- Add a property to the GameScene class to store all the upcoming slots (
var slots = [WhackSlot]()
). - Create a method that accepts a
CGPoint
position as its only parameter and that creates a slot, calls itsconfigure(at:)
method and then adds it to both the scene and the just created array. - Create the four loops necessary to create the slots at the end of
didMove(to:).
- Familiarise with the concept of an
SKCropNode
, a special kind ofSKNode
subclass that uses an image as a cropping mask. Unfortunately Apple’s Documentation of theSKCropNode
class is only rendered in Objective-C. Understood or not, move on… - In WhatSlot.swift add a
SKSpriteNode!
property. Then, just before the end of theconfigure(at:)
method, initialise anSKCropNode
, position it atCGPoint(x: 0, y: 15)
, give it azPosition
= 1 (that is, in front of everything else) and set itsmaskNode
property tonil
. After this initialise a newSKSpriteNode(imageNamed: "penguinGood")
, position it toCGPoint(x: 0, y: -90)
, call it “character” and add it to the crop node! Finally, add the crop node the the parent scene! Running now will show many penguins just below each hole. - Change the
maskNode
property to be= SKSpriteNode(imageNamed: "whackMask")
. Now all the penguins are hidden.
I really do not understand why this is happening. After all, we are using a red graphic to crop nodes… Paul says:
everything with a color is visible, and everything transparent is invisible
But what is transparent here? Every image has a color, they are there and that’s it… I really do not get it, I’m sorry, and Apple’s Documentation doesn’t make it any easier on me. So… reading this again:
This is a special kind of
SKNode
subclass that uses an image as a cropping mask: anything in the coloured part will be visible, anything in the transparent part will be invisible.
Maybe I have some issues with English language, sure, but now I understand that putting this image on top of the whole will make it sure that if a penguin is in the coloured part it will be visible, otherwise not… but why using the word “transparent”? It is so misleading to me… Also, how does this cropping mask becomes invisible itself, while being red in the beginning?
Fascinatingly irritating, I would say!
Penguin, show thyself: SKAction
– moveBy(x:y:duration:)
- At the top of the
WhackSlot
class add two properties of type Bool, both set tofalse
, to indicate the fact that a penguin is visible or not and is hit or not. - Create the
show()
method inside theWhackSlot
class. First, check if theisVisible
property is set to true, in which case just exit the method. Second, callcharNode.run(SKAction.moveBy(x: 0, y: 80, duration: 0.05))
to run this moving action on the character node so that it moves up by 80 points (being placed down 90 this will look as if it is just popping out of the hole) very fast, 1/20th of a second!. Third: setisVisible
totrue
andisHit
tofalse
. Fourth: once every three times set thecharNode.texture = SKTexture(imageNamed: "penguinGood")
and its.name
property to “charFriend”. In the other two instances, set the name of the image to “penguinEvil” and the node name to “charEnemy”. - In GameScene.swift create a proper to manage the triggering of the
show
method:var popupTime = 0.85
. - Inside
didMove(to:)
call upon the powers of GCD to schedule the execution of the penguins appearance like this:DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in self?.createEnemy() }
- Write the
createEnemy()
method. This will first decrease thepopupTime
slowly (by multiplying and reassigning to itself by0.991
). Second: it will shuffle theslots
array and callshow
on its first element with ahideTime
parameter ofpopupTime
. Third: with an increasingly low probability (determined by a randomness between 0 and 13—why this number?!) extra penguins will be shown at the same time. Fourth: the minimum delay will be set to the half of the popup time while the maximum delay to its double. The real delay will be a random amount between those two maximums. Fifth: now for the genius hit. The method will call itself after the delay! - Update the
WhackSlot
class to include ahide()
method. IfisVisible
is false just get out because there is no reason to hide something that is not visible! Then run the contrary of the move action ran just before. Finally setisVisible = false
. - At the end of the
show()
method insert an asynchronous call to thehide()
method.
Whack to win: SKAction
sequences
- In the
WhackSlot
class add thehit()
method: this will set theisHit
property totrue
, create a delay action in the form ofSKAction.wait(forDuration: 0.25)
, hide the penguin by moving the node down by 80 points in half a second, then set it to be not visible in this sequenceSKAction.run { [weak self] in self.isVisible = false }
. It will then run this sequence of actions one after the other. - Finally write the
touchesBegan
method. First: check that there is a touch and gets its location. Second: capture the nodes found at that location. Third: for every node in those capture node, it will execute something according to whether the player tapped the good penguin or the bad penguin. - At the top of the loop be sure that by tapping a penguin you actually reached its grand-parent node, like this:
guard let whachSlot = node.parent?.parent as? WhackSlot else { continue }
. Then if that slot is invisible or is hit just continue. Assuming those two properties false, just call thewhackSlot.hit()
. Then if the good one was hit, decrease the score by 5, else increase it by 1 and decrease the scale of the node to 85%. At the end of each condition call the action.playSoundFileNamed
to play the correct sound for each occasion. - Inside the
show
method of the GameScene.swift file, before therun()
call, just restore the original size of the sprites. - Last but not list, create a property called
numRounds = 0
to keep track of how many penguins are being called. - Inside the
createEnemy()
method, just before thepopupTime
assignment, increase the number of rounds by 1 and, if that amount passes 30, hide all of the slots and create a game over node! Don’t forget to return at the end of this sub-part.
This game is done.
You can find the finished repository here.
Please don’t forget to drop a hello and a thank you to Paul for all his great work (you can find him on Twitter) and don’t forget to visit the 100 Days Of Swift initiative page.
Thanks for reading!
Till the next one!
If you like what I’m doing here please consider liking this article and sharing it with some of your peers. If you are feeling like being really awesome, please consider making a small donation to support my studies and my writing (please appreciate that I am not using advertisement on my articles).
If you are interested in my music engraving and my publications don’t forget visit my Facebook page and the pages where I publish my scores (Gumroad, SheetMusicPlus, ScoreExchange and on Apple Books).
You can also support me by buying Paul Hudson’s books from this Affiliate Link.
Anyways, thank you so much for reading!
Till the next one!