Day 56 - 100 Days of Swift

6 minute read

Project 14 (part 2)

Day 56 is the second part of the fourteenth project. Today you get the game to be playable, meaning you can “whack” the penguins and score points. He also gives you a few challenges to extend the game and practice things you learned before.

First, you add this sequence of actions to run when a penguin is “hit” in a method inside of WhackSlot.swift:

1
2
3
4
5
6
7
8
9
10
11
12
func hit() {
    isHit = true

    let smash = SKAction.scaleY(to: 0.3,
                                duration: 0.12)
    let hide = SKAction.move(to: charOrigin,
                             duration: 0.25)
    let notVisible = SKAction.run { [unowned self] in
        self.isVisible = false
    }
    charNode.run(SKAction.sequence([smash, hide, notVisible]))
}

Then, back in GameScene.swift you add some code to detect the hits in touchesBegan(_:with:):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
override func touchesBegan(_ touches: Set<UITouch>,
                           with event: UIEvent?) {
    guard let touch = touches.first else { return }
    let location = touch.location(in: self)
    let tappedNodes = nodes(at: location)

    for node in tappedNodes {
        guard let whackSlot = node.parent?.parent as? WhackSlot else {
            continue
        }

        if !whackSlot.isVisible || whackSlot.isHit { continue }

        if node.name == "charFriend" {
            whackSlot.hit()
            score -= 5

            run(SKAction.playSoundFileNamed("whackBad.caf",
                                            waitForCompletion: false))
        } else if node.name == "charEnemy" {

            whackSlot.hit()
            score += 1

            run(SKAction.playSoundFileNamed("whack.caf",
                                            waitForCompletion: false))
        }
    }
}

You get the first touch, get the location of that touch in the scene, and then get an array of all the nodes at that location. Then you loop through those nodes, check if it’s parent’s parent can be cast as a WhackSlot and verify that it is visible and hasn’t already been hit. Once you get through all that, you check what its name is. If it is a good penguin, you lose five points, and if it is a bad penguin you get one point. You also play different sounds that signify if it was a good or bad hit.

He also has you modify the x and y scale of the node, but I was finding that that messed with animations later, and it didn’t add much to the look/feel so I got rid of it.

Next you create a numRounds variable to keep track of how many rounds have been played, so you can end the game after a while:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var numRounds = 0

// At the beginning of createEnemy()
numRounds += 1

if numRounds >= 30 {
    endGame()
    return
}

func endGame() {
    for slot in slots {
        slot.hide()
    }
    let gameOver = SKSpriteNode(imageNamed: "gameOver")
    gameOver.position = CGPoint(x: 512, y: 384)
    gameOver.zPosition = 1
}

And with that, the game is playable. It is a little lame though, so I made a few modifications (after play-testing). First, I set the end number of rounds equal to 20, but then I subtract 1 from numRounds every time a bad penguin is hit. This allows the player to extend the game indefinitely as long as they can keep up and keep hitting penguins, which means they have a lot more ownership in their score. I also put a limit on how short popUpTime can get. Again, this allows the game to keep going for as long as the player can keep up, rather than the popUpTime getting impossibly small:

1
2
3
4
5
6
7
8
9
// When checking numberOfRounds
if numRounds >= 20 {

// When decreasing popUpTime
if popupTime > 0.18 { popupTime *= 0.991 }

// When the player hits a bad penguin,
// right after increasing the score
numRounds -= 1

The first challenge he gives you is to record your own voice saying “Game Over!” and playing it when the game ends. I did that and added some effects to it in Audition. Then added it to the project and played it like so:

1
2
3
// At the end of endGame()
run(SKAction.playSoundFileNamed("gameOver.aif",
                                waitForCompletion: false))

The second challenge is to add a label with the final score when you present the “Game Over” sprite:

1
2
3
4
5
6
7
8
9
10
11
// In endGame()
let scoreLabel = SKLabelNode(fontNamed: "Chalkduster")
scoreLabel.text = "Final Score: \(score)"
scoreLabel.position = CGPoint(x: 0, y: -72)
scoreLabel.horizontalAlignmentMode = .center
scoreLabel.fontSize = 48
gameOver.addChild(scoreLabel)

gameScore.isHidden = true

addChild(gameOver)

The third challenge took a little more work. It was to add a smoke-like particle emitter when a penguin is hit, and a mud-like particle emitter when they pop out of the hole. In order to do this without there being an increasing number of nodes on the scene, I added this helper function to WhackSlot.swift:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private func addEmitter(called name: String,
                        at position: CGPoint,
                        with zPosition: CGFloat = 0) {

    if let emitter = SKEmitterNode(fileNamed: name) {
        emitter.position = position
        emitter.zPosition = zPosition

        let numParticles = Double(emitter.numParticlesToEmit)
        let lifetime = Double(emitter.particleLifetime)
        let emitterDuration = numParticles * lifetime

        let addEmitterAction = SKAction.run { self.addChild(emitter) }
        let waitAction = SKAction.wait(forDuration: emitterDuration)
        let removeAction = SKAction.run { emitter.removeFromParent() }
        let actions = [addEmitterAction, waitAction, removeAction]
        let sequence = SKAction.sequence(actions)
        run(sequence)
    }

}

Then, when a penguin is added I add the mud-like emitter:

1
2
// In show()
addEmitter(called: "mud", at: CGPoint(x: 10, y: -30))

And, when a penguin is hit, I add the smoke-like one:

1
2
// In hit()
addEmitter(called: "smoke", at: charNode.position, with: 2)

For both of these emitters, I just messed around with settings until they were close enough for me.

With all that, it looks like this:

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