NEW! Master Swift design patterns with my latest book! >>

< Previous: Getting up and running: SKCropNode   Next: Whack to win: SKAction sequences >

Penguin, show thyself: SKAction moveBy(x:y:duration:)

We want the slots to manage showing and hiding penguins themselves as needed, which means we need to give them some properties and methods of their own.

The two things a slot needs to know are "am I currently visible to be whacked by the player?" and "have I already been hit?" The former avoids players tapping on slots that are supposed to be invisible; the latter so that players can't whack a penguin more than once.

To track this data, put these two properties at the top of your WhackSlot class:

var isVisible = false
var isHit = false

Showing a penguin for the player to tap on will be handled by a new method called show(). This will make the character slide upwards so it becomes visible, then set isVisible to be true and isHit to be false. The movement is going to be created by a new SKAction, called moveBy(x:y:duration:).

This method will also decide whether the penguin is good or bad – i.e., whether the player should hit it or not. This will be done using a RandomInt() function that I bundled into Helper.swift for you to make random number generation easier: one-third of the time the penguin will be good; the rest of the time it will be bad.

To make it clear to the player which is which, we have two different pictures: penguinGood and penguinEvil. We can change the image inside our penguin sprite by changing its texture property. This takes a new class called SKTexture, which is to SKSpriteNode sort of what UIImage is to UIImageView – it holds image data, but isn't responsible for showing it.

Changing the character node's texture like this is helpful because it means we don't need to keep adding and removing nodes. Instead, we can just change the texture to match what kind of penguin this is, then change the node name to match so we can do tap detection later on.

However, all the above should only happen if the slot isn't already visible, because it could cause havoc. So, the very first thing the method needs to do is check whether isVisible is true, and if so exit.

Enough talk; here's the show() method:

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

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

    if RandomInt(min: 0, max: 2) == 0 {
        charNode.texture = SKTexture(imageNamed: "penguinGood")
        charNode.name = "charFriend"
    } else {
        charNode.texture = SKTexture(imageNamed: "penguinEvil")
        charNode.name = "charEnemy"
    }
}

You may have noticed that I made the method accept a parameter called hideTime. This is for later, to avoid having to rewrite too much code.

The show() method is going to be triggered by the view controller on a recurring basis, managed by a property we're going to create called popupTime. This will start at 0.85 (create a new enemy a bit faster than once a second), but every time we create an enemy we'll also decrease popupTime so that the game gets harder over time.

First, the easy bit: add this property to GameScene.swift:

var popupTime = 0.85

To jump start the process, we need to call createEnemy() once when the game starts, then have createEnemy() call itself thereafter. Clearly we don't want to start creating enemies as soon as the game starts, because the player needs a few moments to orient themselves so they have a chance.

So, in didMove(to:) we're going to call the (as yet unwritten) createEnemy() method after a delay. This requires some new Grand Central Dispatch (GCD) code: asyncAfter() is used to schedule a closure to execute after the time has been reached.

Here's how the code looks to run a closure after a delay:

DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [unowned self] in
    self.doStuff()
}

The deadline parameter to asyncAfter() means “1 second after now,” giving us the 1-second delay.

Now, onto the createEnemy() method. This will do several things:

  • Decrease popupTime each time it's called. I'm going to multiply it by 0.991 rather than subtracting a fixed amount, otherwise the game gets far too fast.
  • Shuffle the list of available slots using the GameplayKit shuffle that we've used previously.
  • Make the first slot show itself, passing in the current value of popupTime for the method to use later.
  • Generate four random numbers to see if more slots should be shown. Potentially up to five slots could be shown at once.
  • Call itself again after a random delay. The delay will be between popupTime halved and popupTime doubled. For example, if popupTime was 2, the random number would be between 1 and 4.

There are only two new things in there. First, I'll be using the *= operator to multiply and assign at the same time, in the same way that += meant "add and assign" in project 2. Second, I'll be using the RandomDouble() function to generate a random Double value, which is what asyncAfter() uses for its delay.

As we need to use GameplayKit, add this import line now:

import GameplayKit

Here's the method to create enemies:

func createEnemy() {
    popupTime *= 0.991

    slots = GKRandomSource.sharedRandom().arrayByShufflingObjects(in: slots) as! [WhackSlot]
    slots[0].show(hideTime: popupTime)

    if RandomInt(min: 0, max: 12) > 4 { slots[1].show(hideTime: popupTime) }
    if RandomInt(min: 0, max: 12) > 8 {  slots[2].show(hideTime: popupTime) }
    if RandomInt(min: 0, max: 12) > 10 { slots[3].show(hideTime: popupTime) }
    if RandomInt(min: 0, max: 12) > 11 { slots[4].show(hideTime: popupTime)  }

    let minDelay = popupTime / 2.0
    let maxDelay = popupTime * 2
    let delay = RandomDouble(min: minDelay, max: maxDelay)

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

Because createEnemy() calls itself, all we have to do is call it once in didMove(to: ) after a brief delay. Put this just before the end of the method:

DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [unowned self] in
    self.createEnemy()
}

From then on, we don't have to worry about it because createEnemy() will call itself.

Before we're done, we need to upgrade the WhackSlot class to include a hide() method. If you run the code now, you'll see that the penguins appear nice and randomly, but they never actually go away. We're already passing a hideTime parameter to the show() method, and we're going to use that so the slots hide themselves after they have been visible for a time.

We could of course just make the slots hide after a fixed time, but that's no fun. By using popupTime as the input for hiding delay, we know the penguins will hide themselves more quickly over time.

First, add this method to the WhackSlot class:

func hide() {
    if !isVisible { return }

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

That just undoes the results of show(): the penguin moves back down the screen into its hole, then its isVisible property is set to false.

We want to trigger this method automatically after a period of time, and, through extensive testing (that is, sitting around playing) I have determined the optimal hide time to be 3.5x popupTime.

So, put this code at end of show():

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

Go ahead and run the app, because it's really starting to come together: the penguins show randomly, sometimes by themselves and sometimes in groups, then hide after a period of being visible. But you can't hit them, which means this game is more Watch-a-Penguin than Whack-a-Penguin. Let's fix that!

Love Hacking with Swift?

Get all 40 projects in PDF and HTML: buy the Hacking with Swift book! It contains over 1300 pages of hands-on Swift coding, and will really help boost your iOS career

< Previous: Getting up and running: SKCropNode   Next: Whack to win: SKAction sequences >
MASTER SWIFT NOW
Buy Practical iOS 12 Buy Pro Swift Buy Swift Design Patterns Buy Practical iOS 11 Buy Swift Coding Challenges Buy Server-Side Swift (Vapor Edition) Buy Server-Side Swift (Kitura Edition) Buy Hacking with macOS Buy Advanced iOS Volume One Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with Swift Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let me know!

Click here to visit the Hacking with Swift store >>