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

< Previous: Enemy or bomb: AVAudioPlayer   Next: Slice to win >

Follow the sequence

You've come so far already, and really there isn't a lot to show for your work other than being able to draw glowing slice shapes when you move touches around the screen. But that's all about to change, because we're now about to create the interesting code – we're going to make the game actually create some enemies.

Now, you might very well be saying, "but Paul, we just wrote the enemy creating code, and I never want to see it again!" You're right (and I never want to see it again either!) but it's a bit more complicated: the createEnemy() method creates one enemy as required. The code we're going to write now will call createEnemy() in different ways so that we get varying groups of enemies.

For example, sometimes we want to create two enemies at once, sometimes we want to create four at once, and sometimes we want to create five in quick sequence. Each one of these will call createEnemy() in different ways.

There's a lot to cover here, so let's get started: add this new enum before the ForceBomb enum you added a few minutes ago:

enum SequenceType: Int {
    case oneNoBomb, one, twoWithOneBomb, two, three, four, chain, fastChain
}

That outlines the possible types of ways we can create enemy: one enemy that definitely is not a bomb, one that might or might not be a bomb, two where one is a bomb and one isn't, then two/three/four random enemies, a chain of enemies, then a fast chain of enemies.

The first two will be used exclusively when the player first starts the game, to give them a gentle warm up. After that, they'll be given random sequence types from twoWithOneBomb to fastChain.

We're going to need quite a few new properties in order to make the plan work, so please add these now:

var popupTime = 0.9
var sequence: [SequenceType]!
var sequencePosition = 0
var chainDelay = 3.0
var nextSequenceQueued = true

And here's what they do:

  • The popupTime property is the amount of time to wait between the last enemy being destroyed and a new one being created.
  • The sequence property is an array of our SequenceType enum that defines what enemies to create.
  • The sequencePosition property is where we are right now in the game.
  • The chainDelay property is how long to wait before creating a new enemy when the sequence type is .chain or .fastChain. Enemy chains don't wait until the previous enemy is offscreen before creating a new one, so it's like throwing five enemies quickly but with a small delay between each one.
  • The nextSequenceQueued property is used so we know when all the enemies are destroyed and we're ready to create more.

Whenever we call our new method, which is tossEnemies(), we're going to decrease both popupTime and chainDelay so that the game gets harder as they play. Sneakily, we're always going to increase the speed of our physics world, so that objects move rise and fall faster too.

Nearly all the tossEnemies() method is a large switch/case statement that looks at the sequencePosition property to figure out what sequence type it should use. It then calls createEnemy() correctly for the sequence type, passing in whether to force bomb creation or not.

The one thing that will need to be explained is the way enemy chains are created. Unlike regular sequence types, a chain is made up of several enemies with a space between them, and the game doesn't wait for an enemy to be sliced before showing the next thing in the chain.

The best thing for you to do is to put this source code into your project, and we can talk about the chain complexities in a moment:

func tossEnemies() {
    popupTime *= 0.991
    chainDelay *= 0.99
    physicsWorld.speed *= 1.02

    let sequenceType = sequence[sequencePosition]

    switch sequenceType {
    case .oneNoBomb:
        createEnemy(forceBomb: .never)

    case .one:
        createEnemy()

    case .twoWithOneBomb:
        createEnemy(forceBomb: .never)
        createEnemy(forceBomb: .always)

    case .two:
        createEnemy()
        createEnemy()

    case .three:
        createEnemy()
        createEnemy()
        createEnemy()

    case .four:
        createEnemy()
        createEnemy()
        createEnemy()
        createEnemy()

    case .chain:
        createEnemy()

        DispatchQueue.main.asyncAfter(deadline: .now() + (chainDelay / 5.0)) { [unowned self] in self.createEnemy() }
        DispatchQueue.main.asyncAfter(deadline: .now() + (chainDelay / 5.0 * 2)) { [unowned self] in self.createEnemy() }
        DispatchQueue.main.asyncAfter(deadline: .now() + (chainDelay / 5.0 * 3)) { [unowned self] in self.createEnemy() }
        DispatchQueue.main.asyncAfter(deadline: .now() + (chainDelay / 5.0 * 4)) { [unowned self] in self.createEnemy() }

    case .fastChain:
        createEnemy()

        DispatchQueue.main.asyncAfter(deadline: .now() + (chainDelay / 10.0)) { [unowned self] in self.createEnemy() }
        DispatchQueue.main.asyncAfter(deadline: .now() + (chainDelay / 10.0 * 2)) { [unowned self] in self.createEnemy() }
        DispatchQueue.main.asyncAfter(deadline: .now() + (chainDelay / 10.0 * 3)) { [unowned self] in self.createEnemy() }
        DispatchQueue.main.asyncAfter(deadline: .now() + (chainDelay / 10.0 * 4)) { [unowned self] in self.createEnemy() }
    }

    sequencePosition += 1
    nextSequenceQueued = false
}

That looks like a massive method, I know, but in reality it's just the same thing being called in different ways. The interesting parts are the .chain and .fastChain cases, and also I want to explain in more detail the nextSequenceQueued property.

Each sequence in our array creates one or more enemies, then waits for them to be destroyed before continuing. Enemy chains are different: they create five enemies with a short break between, and don't wait for each one to be destroyed before continuing.

To handle these chains, we have calls to asyncAfter() with a timer value. If we assume for a moment that chainDelay is 10 seconds, then:

  • That makes chainDelay / 10.0 equal to 1 second.
  • That makes chainDelay / 10.0 * 2 equal to 2 seconds.
  • That makes chainDelay / 10.0 * 3 equal to three seconds.
  • That makes chainDelay / 10.0 * 4 equal to four seconds.

So, it spreads out the createEnemy() calls quite neatly.

The nextSequenceQueued property is more complicated. If it's false, it means we don't have a call to tossEnemies() in the pipeline waiting to execute. It gets set to true only in the gap between the previous sequence item finishing and tossEnemies() being called. Think of it as meaning, "I know there aren't any enemies right now, but more will come shortly."

We can make our game come to life with enemies with two more pieces of code. First, add this just before the end of didMove(to:):

sequence = [.oneNoBomb, .oneNoBomb, .twoWithOneBomb, .twoWithOneBomb, .three, .one, .chain]

for _ in 0 ... 1000 {
    let nextSequence = SequenceType(rawValue: RandomInt(min: 2, max: 7))!
    sequence.append(nextSequence)
}

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

That code fills the sequence array with seven pre-written sequences to help players warm up to how the game works, then adds 1001 (the ... operator means "up to and including") random sequence types to fill up the game. Finally, it triggers the initial enemy toss after two seconds.

The way we generate random sequence type values is new and quite interesting. If you cast your mind back, this is how we defined the SequenceType enum:

enum SequenceType: Int {
    case oneNoBomb, one, twoWithOneBomb, two, three, four, chain, fastChain
}

Note that it says enum SequenceType: Int. We didn't have that for the ForceBomb enum – it's new here, and it means "I want this enum to be mapped to integer values," and means we can reference each of the sequence type options using so-called "raw values" from 0 to 7.

For example, to create a twoWithOneBomb sequence type we could use SequenceType(rawValue: 2). Swift doesn't know whether that number exists or not (we could have written 77), so it returns an optional type that you need to unwrap.

The second change we're going to make is to remove enemies from the game when they fall off the screen. This is required, because our game mechanic means that new enemies aren't created until the previous ones have been removed. The exception to this rule are enemy chains, where multiple enemies are created in a batch, but even then the game won't continue until all enemies from the chain have been removed.

We're going to modify the update() method so that:

  1. If we have active enemies, we loop through each of them.
  2. If any enemy is at or lower than Y position -140, we remove it from the game and our activeEnemies array.
  3. If we don't have any active enemies and we haven't already queued the next enemy sequence, we schedule the next enemy sequence and set nextSequenceQueued to be true.

Put this code first in the update() method:

if activeEnemies.count > 0 {
    for node in activeEnemies {
        if node.position.y < -140 {
            node.removeFromParent()

            if let index = activeEnemies.index(of: node) {
                activeEnemies.remove(at: index)
            }
        }
    }
} else {
    if !nextSequenceQueued {
        DispatchQueue.main.asyncAfter(deadline: .now() + popupTime) { [unowned self] in
            self.tossEnemies()
        }

        nextSequenceQueued = true
    }
}

And now the part you've been waiting for extremely patiently: press Cmd+R to run the game, because it should now be getting close to useful!

Now that the game has bombs as well as penguins, it's almost starting to come together.

Learn Server-Side Swift now!

Take your Swift code to the server and become a full-stack developer with my latest book: Server-Side Swift!

< Previous: Enemy or bomb: AVAudioPlayer   Next: Slice to win >
Click here to visit the Hacking with Swift store >>