GO FURTHER, FASTER: Try the Swift Career Accelerator today! >>

Whack to win: SKAction sequences

To bring this project to a close, we still need to do two major components: letting the player tap on a penguin to score, then letting the game end after a while. Right now it never ends, so with popupTime getting lower and lower it means the game will become impossible after a few minutes.

We're going to add a hit() method to the WhackSlot class that will handle hiding the penguin. This needs to wait for a moment (so the player still sees what they tapped), move the penguin back down again, then set the penguin to be invisible again.

We're going to use an SKAction for each of those three things, which means you need to learn some new uses of the class:

  • SKAction.wait(forDuration:) creates an action that waits for a period of time, measured in seconds.
  • SKAction.run(block:) will run any code we want, provided as a closure. "Block" is Objective-C's name for a Swift closure.
  • SKAction.sequence() takes an array of actions, and executes them in order. Each action won't start executing until the previous one finished.

We need to use SKAction.run(block:) in order to set the penguin's isVisible property to be false rather than doing it directly, because we want it to fit into the sequence. Using this technique, it will only be changed when that part of the sequence is reached.

Put this method into the WhackSlot class:

func hit() {
    isHit = true

    let delay = SKAction.wait(forDuration: 0.25)
    let hide = SKAction.moveBy(x: 0, y: -80, duration: 0.5)
    let notVisible = SKAction.run { [unowned self] in self.isVisible = false }
    charNode.run(SKAction.sequence([delay, hide, notVisible]))
}

With that new method in place, we can call it from the touchesBegan() method in GameScene.swift. This method needs to figure out what was tapped using the same nodes(at:) method you saw in project 11: find any touch, find out where it was tapped, then get a node array of all nodes at that point in the scene.

We then need to loop through the list of all nodes that are at that point, and see if they have the name "charFriend" or "charEnemy" and take the appropriate action. Rather than dump all the code on you at once, here's the basic outline of touchesBegan() to start with:

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 {
        if node.name == "charFriend" {
            // they shouldn't have whacked this penguin
        } else if node.name == "charEnemy" {
            // they should have whacked this one
        }
    }
}

Nothing complicated there – this is all stuff you know already.

What is new is what comes in place of those two comments. The first comment marks the code block that will be executed if the player taps a friendly penguin, which is obviously against the point of the game.

When this happens, we need to call the hit() method to make the penguin hide itself, subtract 5 from the current score, then run an action that plays a "bad hit" sound. All of that should only happen if the slot was visible and not hit.

The code for this block is going to do something interesting that you haven't seen before, and it looks like this:

let whackSlot = node.parent?.parent as? WhackSlot

It gets the parent of the parent of the node, and typecasts it as a WhackSlot. This line is needed because the player has tapped the penguin sprite node, not the slot – we need to get the parent of the penguin, which is the crop node it sits inside, then get the parent of the crop node, which is the WhackSlot object, which is what this code does.

You're also going to meet a new piece of code: SKAction's playSoundFileNamed() method, which plays a sound and optionally waits for the sound to finish playing before continuing – useful if you're using an action sequence.

We haven't used sound files in iOS yet, but there isn't really a whole lot to say. The three main sound file formats you'll use are MP3, M4A and CAF, with the latter being a renamed AIFF file. AIFF is a pretty terrible file format when it comes to file size, but it's much faster to load and use than MP3s and M4As, so you'll use them often.

Put this code where the // they shouldn't have whacked this penguin comment was:

guard let whackSlot = node.parent?.parent as? WhackSlot else { continue }
if !whackSlot.isVisible { continue }
if whackSlot.isHit { continue }

whackSlot.hit()
score -= 5

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

When the player taps a bad penguin, the code is similar. The differences are that we want to add 1 to the score (so that it takes five correct taps to offset one bad one), and run a different sound. But we're also going to set the xScale and yScale properties of our character node so the penguin visibly shrinks in the scene, as if they had been hit.

Put this code where the // they should have whacked this one comment was:

guard let whackSlot = node.parent?.parent as? WhackSlot else { continue }
if !whackSlot.isVisible { continue }
if whackSlot.isHit { continue }

whackSlot.charNode.xScale = 0.85
whackSlot.charNode.yScale = 0.85

whackSlot.hit()
score += 1

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

Since we're now potentially modifying the xScale and yScale properties of our character node, we need to reset them to 1 inside the show() method of the slot. Put this just before the run() call inside show():

charNode.xScale = 1
charNode.yScale = 1

Now, looking at our touchesBegan() method in GameScene.swift, you should see we can actually move the code around a little to remove duplication. Specifically, checking isVisible and isHit doesn’t need to be done twice, and neither does calling whackSlot.hit() – a better idea is to move those lines outside of their conditions, like this:

for node in tappedNodes {
    guard let whackSlot = node.parent?.parent as? WhackSlot else { continue }
    if !whackSlot.isVisible { continue }
    if whackSlot.isHit { continue }
    whackSlot.hit()

    if node.name == "charFriend" {
        // they shouldn't have whacked this penguin
        score -= 5

        run(SKAction.playSoundFileNamed("whackBad.caf", waitForCompletion: false))
    } else if node.name == "charEnemy" {
        // they should have whacked this one
        whackSlot.charNode.xScale = 0.85
        whackSlot.charNode.yScale = 0.85
        score += 1

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

This game is almost done. Thanks to the property observer we put in early on the game is now perfectly playable, at least until popupTime gets so low that the game is effectively unplayable.

To fix this final problem and bring the project to a close, we're going to limit the game to creating just 30 rounds of enemies. Each round is one call to createEnemy(), which means it might create up to five enemies at a time.

First, add this property to the top of your game scene:

var numRounds = 0

Every time createEnemy() is called, we're going to add 1 to the numRounds property. When it is greater than or equal to 30, we're going to end the game: hide all the slots, show a "Game over" sprite, then exit the method. Put this code just before the popupTime assignment in createEnemy():

numRounds += 1

if numRounds >= 30 {
    for slot in slots {
        slot.hide()
    }

    let gameOver = SKSpriteNode(imageNamed: "gameOver")
    gameOver.position = CGPoint(x: 512, y: 384)
    gameOver.zPosition = 1
    addChild(gameOver)

    return
}

That uses a positive zPosition so that the game over graphic is placed over other items in our game.

The game is now complete! Go ahead and play it for real and see how you do. If you're using the iOS simulator, bear in mind that it's much hard to move a mouse pointer than it is to use your fingers on a real iPad, so don't adjust the difficulty unless you're testing on a real device!

Hacking with Swift is sponsored by Essential Developer.

SPONSORED Transform your career with the iOS Lead Essentials. This Black Friday, unlock over 40 hours of expert training, mentorship, and community support to secure your place among the best devs. Click for early access to this limited offer and a free crash course.

Save your spot

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.6/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.