Day 55 - 100 Days of Swift

5 minute read

Project 14 (part 1)

Day 55 is the first part of the fourteenth project. It will be a whack-a-mole style game where you whack penguins as they pop out of holes. Today you get the scene laid out and implement the logic to hide and show the penguins.

First, you make a new game project, delete the stuff that comes with it, and add a score label and background in GameScene.swift:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var gameScore: SKLabelNode!
var score = 0 {
    didSet {
        gameScore.text = "Score: \(score)"
    }
}

// in didMove(to:)
let background = SKSpriteNode(imageNamed: "whackBackground")
background.position = CGPoint(x: 512, y: 384)
background.blendMode = .replace
background.zPosition = -1
addChild(background)

gameScore = SKLabelNode(fontNamed: "Chalkduster")
gameScore.text = "Score: 0"
gameScore.position = CGPoint(x: 8, y: 8)
gameScore.horizontalAlignmentMode = .left
gameScore.fontSize = 48
addChild(gameScore)

Then you add a new subclass of SKNode to encapsulate one hole, and the penguin hiding inside it. For now you just give it this configure method:

1
2
3
4
5
6
func configure(at position: CGPoint) {
    self.position = position

    let sprite = SKSpriteNode(imageNamed: "whackHole")
    addChild(sprite)
}

Back in GameScene.swift you add an array to hold all the slots, and lay them out:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var slots: [WhackSlot] = []

func createSlot(at position: CGPoint) {
    let slot = WhackSlot()
    slot.configure(at: position)
    addChild(slot)
    slots.append(slot)
}

// In didMove(to:)
for i in 0 ..< 5 { createSlot(at: CGPoint(x: 100 + (i * 170), y: 410)) }
for i in 0 ..< 4 { createSlot(at: CGPoint(x: 180 + (i * 170), y: 320)) }
for i in 0 ..< 5 { createSlot(at: CGPoint(x: 100 + (i * 170), y: 230)) }
for i in 0 ..< 4 { createSlot(at: CGPoint(x: 180 + (i * 170), y: 140)) }

This leads to a configuration that looks like this:

Screenshot of scene laid out.

Next, back in WhackSlot.swift, you add an SKSpriteNode for the penguin, and an SKCropNode to hide the penguin when it is in the hole:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var charNode: SKSpriteNode!

// In configure(at:)
let cropNode = SKCropNode()
cropNode.position = CGPoint(x: 0, y: 15)
cropNode.zPosition = 1
cropNode.maskNode = SKSpriteNode(imageNamed: "whackMask")

charNode = SKSpriteNode(imageNamed: "penguinGood")
charNode.position = CGPoint(x: 0, y: -90)
charNode.name = "character"
cropNode.addChild(charNode)

addChild(cropNode)

Then you add a show method, to animate the penguin in:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Tracks whether the penguin is currently visible
var isVisible = false
// Tracks whether the penguin has been hit
var isHit = false

func show(hideTime: Double) {
    if isVisible { return }

    charNode.run(SKAction.moveBy(x: 0, y: 80, duration: 0.05))
    isVisible = true
    isHit = false

    if Int.random(in: 0...2) == 0 {
        charNode.texture = SKTexture(imageNamed: "penguinGood")
        charNode.name = "charFriend"
    } else {
        charNode.texture = SKTexture(imageNamed: "penguinEvil")
        charNode.name = "charEnemy"
    }

    DispatchQueue.main.asyncAfter(deadline: .now() + (hideTime * 3.5)) { [weak self] in
        self?.hide()
    }
}

And a hide method to animate it back out:

1
2
3
4
5
6
func hide() {
    if !isVisible { return }

    charNode.run(SKAction.moveBy(x: 0, y: -80, duration: 0.05))
    isVisible = false
}

Then in GameScene.swift you add a popupTime variable to keep track of the timing between penguins popping up, and a method to add them:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var popupTime = 0.85

func createEnemy() {
    popupTime *= 0.991

    slots.shuffle()
    slots[0].show(hideTime: popupTime)

    if Int.random(in: 0...12) > 4 { slots[1].show(hideTime: popupTime) }
    if Int.random(in: 0...12) > 8 {  slots[2].show(hideTime: popupTime) }
    if Int.random(in: 0...12) > 10 { slots[3].show(hideTime: popupTime) }
    if Int.random(in: 0...12) > 11 { slots[4].show(hideTime: popupTime)  }

    let minDelay = popupTime / 2.0
    let maxDelay = popupTime * 2.0

    let delay = Double.random(in: minDelay...maxDelay)

    DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
        self?.createEnemy()
    }
}

It calls itself after a delay, so all we need to do it fire it off the first time and it will just keep going on its own from there:

1
2
3
4
// In didMove(to:)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
    self?.createEnemy()
}

After that, the penguins pop up in the holes, and hide themselves over time, with the time decreasing as you go (so the game gets harder). You can’t actually whack an penguins yet, or score any points, but here is what it looks like so far (the animations don’t all translate great in the gif, but they are smooth in the app):

Gif of penguin popping up animation

You can find my version of this project at the end of day 55 on Github here.