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

< Previous: Ready... aim... fire: Timer and follow()   Next: Making things go bang: SKEmitterNode >

Swipe to select

Now that you can see fireworks shooting across your screen, it's time to reveal the difficulty element. You see, every game needs some challenge, and in our case the challenge is to destroy fireworks in groups of the same color. We're going to make it so that players can select only one color of firework at a time, so if they choose two red then touch a green, the two red will become deselected.

So, the challenge will be to select and detonate fireworks based on their color, and as you'll see shortly we're going to heavily bias scores so that players receive many more points for larger groups.

What we're going to code now is the touch handling method, checkTouches(). We're going to call this from touchesBegan() and touchesMoved() so that users can either tap to select fireworks or just swipe across the screen.

The method needs to start by figuring out where in the scene the player touches, and what nodes are at that point. It will then loop through all nodes under the point to find any with the name "firework". When it finds one, it will set its name to be "selected" rather than "firework" and change its colorBlendFactor value to 0. That will disable the color blending entirely, making the firework white.

Here's the checkTouches() method with that functionality in there:

func checkTouches(_ touches: Set<UITouch>) {
    guard let touch = touches.first else { return }

    let location = touch.location(in: self)
    let nodesAtPoint = nodes(at: location)

    for node in nodesAtPoint {
        if node is SKSpriteNode {
            let sprite = node as! SKSpriteNode

            if sprite.name == "firework" {
                sprite.name = "selected"
                sprite.colorBlendFactor = 0
            }
        }
    }
}

You've seen most of that previously, but that's because I missed out the logic to handle ensuring that players select only one color at a time. The above code will let them select all the fireworks, regardless of color.

So, we need to insert a second loop just before the sprite.name = "selected" line. When you place one loop inside another it's called an inner loop, and you need to be careful: if you have one loop that executes 100 times it's OK, and if you have another loop that executes 200 times that's OK too, but if you put one inside the other you now have 20,000 iterations of your loop and that's almost certainly not OK. Here, though, we'll have maybe two or three items in our outer loop and a maximum of 10 or so in the inner, so we're quite safe.

Remember, this inner loop needs to ensure that the player can select only one firework color at a time. So if they select red then another red, both are selected. But if they then select a green, we need to deselect the first two because they are red.

So, the loop will go through every firework in our fireworks array, then find the firework image inside it. Remember, that array holds the container node, and each container node holds the firework image and its spark emitter. If the firework was selected and is a different color to the firework that was just tapped, then we'll put its name back to "firework" and put its colorBlendFactor back to 1 so it resumes its old color.

So, put this code just before the sprite.name = "selected" line:

for parent in fireworks {
    let firework = parent.children[0] as! SKSpriteNode

    if firework.name == "selected" && firework.color != sprite.color {
        firework.name = "firework"
        firework.colorBlendFactor = 1
    }
}

That's the entire method, so all we need to do is make sure it's called. To make that happen, we need to modify the existing touchesBegan() method and add one for touchesMoved() too. All they will do is send the touch information on to checkTouches(), like this:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    super.touchesBegan(touches, with: event)
    checkTouches(touches)
}

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    super.touchesMoved(touches, with: event)
    checkTouches(touches)
}

There's one more thing we need to code before moving on, and that's some additions to the update() method. This is because we need to handle the fireworks that the player doesn't destroy, and our solution is simple enough: if they get past 900 points up vertically, we consider them dead and remove them from the fireworks array and from the scene.

There is one curious quirk here, and it's down to how you remove items from an array. When removing items, we're going to loop through the array backwards rather than forwards. The reason for is that array items move down when you remove an item, so if you have 1, 2, 3, 4 and remove 3 then 4 moves down to become 3. If you're counting forwards, this is a problem because you just checked three and want to move on, but there's now a new 3 and possibly no longer a 4! If you're counting backwards, you just move on to 2.

Note: I chose 900 rather than 800 to mean "off screen vertically" because it's nice to give players a little extra time when making important actions. It's possible that the top firework is at 890 and the bottom one still on screen and being manipulated, so at least this way the player has the best possible window in which to make all their selections.

Here's the new update() method:

override func update(_ currentTime: TimeInterval) {
    for (index, firework) in fireworks.enumerated().reversed() {
        if firework.position.y > 900 {
            // this uses a position high above so that rockets can explode off screen
            fireworks.remove(at: index)
            firework.removeFromParent()
        }
    }
}

Download for free!

Want a free 75-minute video teaching functional programming, protocol-oriented programming, and more? This is your lucky day!

< Previous: Ready... aim... fire: Timer and follow()   Next: Making things go bang: SKEmitterNode >
Click here to visit the Hacking with Swift store >>