Demolition Page 3 - Adding State with GKStateMachine

We continue our exploration of Swift 2.0, GameplayKit, and SpriteKit by using GKStateMachine to make our game behave a little more like a real game without making our standard code more complex. 

You might have noticed that in the last update that the players started moving before the level had animated in. That really didn't look very good. Traditionally games would display a Get Ready message while the level assembles itself and then starts once everything is in place. 

GameplayKit provides a class called GKStateMachine which we'll be using for a range of different purposes, but here we want to model the different states a level can be in. It's useful to list those out. 

  • Preparing the level is building in and we want to give human players a chance to decide what they want to do. We always go from this state to Playing.  
  • Playing Most of the time the level will be in this state, the game as we normally think of it will be running. When exit this state that means it's....
  • Game Over there are two cases one where one player killed all the others... Or one where there was no winner (a draw). We want two different states to capture each of these as we may want to do something else depending on which happens. 

We can draw these as a simple diagram. 

Building a State Machine

Something isn't immediately obvious is that all the GKStates in a GKStateMachine have an update method. This is really key, and augments their other job of managing transitions between themselves. Each GKState instance is sub-class (you'll see why shortly), so we will need 4 sub-classes for each state. I have also created an initial sub-class so that we can reference the LevelScene. In a very real sense you can think about the GKState sub classes as taking control of the update behaviour when they are active. 

Here is our parent sub-class

class LevelSceneState : GKState{
    var levelScene : LevelScene
    init( levelScene:LevelScene){
        self.levelScene = levelScene
    }
}

Let's focus on the Preparing state first and give it everything it needs. 

Preparing 

Let's start with the update method, the animations will be done after 4 seconds, so we keep track of how many seconds have passed since we entered the state. When it's greater than 4 we tell the parent state machine to enter the next state. 

override func updateWithDeltaTime(seconds: NSTimeInterval) {
    time += seconds
    if time >= 4{
        stateMachine?.enterState(Playing.self)
    }
}

How does the state machine know that is a valid transition?  Each state has the responsibility to override the isValidNextState method which is passed the class of a candidate next state, and then returns true or false if that is a valid next state. For preparing we have only one valid state to move to

override func isValidNextState(stateClass: AnyClass) -> Bool {
    return stateClass is Playing.Type
}

From a purely mechanical perspective that's all we need to do. However there are two other methods that are really useful here, didEnterWithPreviousState is called straight after the state becomes the state machine's current state, and willExitWithNextState is called just before a new state becomes active. 

We can use these to provide a Get Ready message that animates in

override func didEnterWithPreviousState(previousState: GKState?) {
  let getReady = SKLabelNode(text: "Get Ready!")
  getReady.fontName = "Chalkduster"
  getReady.name = "getReady"
  getReady.zPosition = LevelScene.Layer.Foreground.rawValue
  getReady.position = CGPoint(x: levelScene.size.width / 2.0, y: levelScene.size.height-50.0)
  levelScene.addChild(getReady)
  getReady.runAction(
      SKAction.sequence([
          SKAction.moveToY(levelScene.size.height / 2.0, duration: 0.5),
          SKAction.repeatActionForever(SKAction.sequence([
              SKAction.fadeAlphaTo(0.5, duration: 0.1),
              SKAction.fadeAlphaTo(1.0, duration: 0.1)
              ]))
          ])
  )
}

And then when we leave the state we want to animate out the message

override func willExitWithNextState(nextState: GKState) {
    if let getReady = levelScene.childNodeWithName("getReady"){
        getReady.runAction(SKAction.sequence([
            SKAction.group([
                SKAction.scaleTo(4.0, duration: 0.5),
                SKAction.fadeAlphaTo(0, duration: 0.5)
                ]),
            SKAction.removeFromParent()
            ]))
    }
}

That's it. The beauty of GameplayKit here is that it has allowed us to have a very clear decomposition of responsibility. Let's apply that to the next state

Playing

This is really easy, it's what we used to do in the LevelScene in the last instalment. 

override func updateWithDeltaTime(seconds: NSTimeInterval) {
    if let arena = levelScene.childNodeWithName("Arena"){
        for child in arena.children{
            if let updateable = child as? UpdateableLevelEntity{
                updateable.update(0.0, inLevel: levelScene)
            }
        }
    }
}

Finally just make sure we can go to the two game over states (although neither do anything yet). 

override func isValidNextState(stateClass: AnyClass) -> Bool {
    if stateClass is PlayerWon.Type || stateClass is PlayersDrew.Type{
        return true
    }
    return false
}

Creating the State Machine

A state machine really just needs to set up its states. Here is the entire state machine sub-class.  

public class LevelStateMachine: GKStateMachine {
  init(forLevel:LevelScene) {
      super.init(states: [
          Preparing(levelScene: forLevel),
          Playing(levelScene: forLevel),
          PlayerWon(levelScene: forLevel),
          PlayersDrew(levelScene: forLevel)])
  }
}

Now we have that class we need to bake it into our LevelScene. 

LevelScene Redux

Really there a very small change. We add a variable, and initialise it in the constructor. 

public var stateMachine : LevelStateMachine!
public var lastUpdateTime : NSTimeInterval?
public init(levelData:LevelData){
    self.levelData = levelData
    gridGraph = GKGridGraph(fromGridStartingAt: int2(0,0), width: Int32(levelData.gridWidth), height: Int32(levelData.gridHeight), diagonalsAllowed: false)
    super.init(size: levelData.size)
    stateMachine = LevelStateMachine(forLevel: self)
}

We will enter the Preparing state in the begin method. We don't need to do anything else, the state will handle it. 

public func begin(){
    removeAllChildren()
    //Add destructable blocks
    processTilesWithBlock(randomlyAddDestructable)
    addChild(levelSprite())
    stateMachine.enterState(LevelStateMachine.Preparing.self)
}

Finally we just need to change update to hand the gameplay updating off to the current state.  

public override func update(currentTime: NSTimeInterval) {
    if lastUpdateTime == nil{
        lastUpdateTime = currentTime
    }
    stateMachine.updateWithDeltaTime(currentTime - lastUpdateTime!)
    lastUpdateTime = currentTime
    ...
}

Get Ready!

And that's it. Now our level animates in and then the game gets started. The logic for each state is parcelled into that state, avoiding methods like update becoming a massive mess of all modes.  

Next time we are going to focus on the players a little more...