【GameplayKit】iOSゲームアプリにおけるStateパターン実践 Part_4

#前回までの記事
[【GameplayKit】iOSゲームアプリにおけるStateパターン実践 Part_3](http://qiita.com/chino_tweet/items/20e15ea58b9f17d51f4d)では、ステートおよびステートマシンの定義と実装をしました。

#この記事について
以下の方法によるステートの遷移を実装していきます。

- ユーザからのインタラクションによるステート遷移
- ステートに実装された機能による自動的なステート遷移

#手順
1. 給水中状態 `ServeState` の定義
2. 満タン状態 `Full` から給水中状態 `ServeState` への遷移
3. 水あり状態 `PartiallyFullState`の定義
4. 給水中 `ServeState` から水あり `PartiallyFullState` に自動的なステート遷移

##水あり状態の定義
メニューバーから「File > New > File... > iOS > Swift File」を選択します。
ファイル名は`ServeState.swift`とします。
`GameplayKit`フレームワークをインポートして、`DispenserState`クラスを継承した`ServeState`クラスを定義します。

```ruby:ServeState
import GameplayKit

class ServeState: DispenserState {
    
    init(game: GameScene) {
        super.init(game: game, associatedNodeName: "ServeState")
    }
    
    override func didEnter(from previousState: GKState?) {
        super.didEnter(from: previousState)
        print("now is Serve...")
    }

}
```

イニシャライザでは、親クラスのイニシャライザを利用します。
`didEnter(from:)`メソッドも同様に、親クラスのメソッドを利用します。

##満タン状態から給水中状態への遷移
ゲームシーンを`FullState`から`ServeState`へ遷移させます。
まずは、ステートマシンにステートを登録します。
`didMove(to:)`メソッドで、`stateMachine`オブジェクト初期化時に記述します。

```ruby:GameScene.swift
    override func didMove(to view: SKView) {
        let fullState = FullState(game: self)
        let serveState = ServeState(game: self)
        stateMachine = GKStateMachine(states: [fullState, serveState])
        
        stateMachine.enter(FullState.self)
    }
```

続いて、`toucheEnded(with:)`メソッドで、リフィルボタン以外の場所がタップされた場合だけステートマシンを`ServeState`に遷移させます。

```ruby:GameScene.swift
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        let locationInView = touches.first?.location(in: view)
        let locationInScene = convertPoint(fromView: locationInView!)
        
        let refillButton = childNode(withName: "//refillButton")
        let location = locationInScene
        
        if atPoint(location) == refillButton {
            print("attempt to refill.")
        } else {
            print("attempt to dispense")
            stateMachine.enter(ServeState.self)
        }
    }
```

###ビルド
シミュレータで実行します。
画面の適当な場所をタップすると、給水機の表示ランプがグリーン→ブラックに変化します。ただし、リフィルボタンをタップしても変化しません。
つまり、表示ランプがブラックになっていれば状態は`ServeState`に遷移していることがわかります。

ここで、安全に状態遷移するようにして、ステートマシンの堅牢性を確保します。
`GKState`クラスの`isValidNextState(_:)`メソッドで、遷移先ステートをチェックします。

```ruby:FullState.swift
import SpriteKit
import GameplayKit

class FullState: DispenserState {
    
    init(game: GameScene) {
        super.init(game: game, associatedNodeName: "FullState")
    }
    
    override func didEnter(from previousState: GKState?) {
        super.didEnter(from: previousState)

        let green = SKColor.green
        changeIndicatorLightToColor(green)
    }
    
    override func willExit(to nextState: GKState) {
        super.willExit(to: nextState)
        
        let black = SKColor.black
        changeIndicatorLightToColor(black)
    }
    
    override func isValidNextState(_ stateClass: AnyClass) -> Bool {
        return stateClass is ServeState.Type
    }

}
```

給水機の状態が満タンである場合、その遷移先となる状態は給水中`ServeState`の他にはありえません。
`isValidNextState(_:)`メソッドが`false`を返す場合は、ステートマシンは状態を遷移しません。

##水あり状態の定義
メニューバーから「File > New > File... > iOS > Swift Class」を選択して`PartiallyFullState.swift`ファイルを作成します。
`GameplayKit`フレームワークをインポートして、`DispenserState`クラスを継承した`PartiallyFullState`クラスを定義します。

```ruby:PartiallyFullState
import GameplayKit

class PartiallyFullState: DispenserState {
    
    init(game: GameScene) {
        super.init(game: game, associatedNodeName: "PartiallyFullState")
    }
    
}
```

イニシャライザでは、親クラスのイニシャライザを利用します。

##自動的なステート遷移
給水中は一定時間で次の状態（水あり）に遷移すべきです。
画面のタップなど、ユーザからのインタラクションなしで自動的にステートマシンの状態を遷移させます。

まずは、ステートマシンに水あり状態 `PartiallyFullState`を登録します。

```ruby:GameScene.swift
    override func didMove(to view: SKView) {
        let fullState = FullState(game: self)
        let serveState = ServeState(game: self)
        let partiallyFullState = PartiallyFullState(game: self)
        
        stateMachine = GKStateMachine(states: [fullState,
                                               serveState,
                                               partiallyFullState])
        
        stateMachine.enter(FullState.self)
    }
```

ステートマシンの堅牢性を確保するため、`ServeState`からは`PartiallyFullState`にしか遷移できないようにしておきます。

```ruby:ServeState.swift
class ServeState: DispenserState {
    
    init(game: GameScene) {
        super.init(game: game, associatedNodeName: "ServeState")
    }
    
    override func didEnter(from previousState: GKState?) {
        super.didEnter(from: previousState)
        print("now is serve...")
    }

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

###ステートにステート遷移機能を実装する
続いて、`ServeState`に遷移した1秒後に自動で`PartiallyFullState`にステートマシンが遷移するようにします。
自動的な遷移までの時間は、メンバワイズプロパティとして定義した`timeScale`プロパティで設定します。
自動遷移の機能は、`waitAndExit()`メソッドで実装しています。

```ruby:ServeState.swift
class ServeState: DispenserState {
    
    static let timeScale = 1.0
    
    init(game: GameScene) {
        super.init(game: game, associatedNodeName: "ServeState")
    }
    
    override func didEnter(from previousState: GKState?) {
        super.didEnter(from: previousState)
        print("now is serve...")
        
        waitAndExit()
    }

    override func isValidNextState(_ stateClass: AnyClass) -> Bool {
        return stateClass is PartiallyFullState.Type
    }
    
    func waitAndExit() {
        let waitAction = SKAction.wait(forDuration: ServeState.timeScale)
        game.scene?.run(waitAction, completion: {
            self.stateMachine?.enter(PartiallyFullState.self)
        })
    }
}
```

###ビルド
シミュレータで実行してみましょう。
画面上のリフィルボタン以外の場所をタップすると、遷移パネルのハイライトが「Full ~ Serve ~ Partially Full」へと自動的に遷移していきます。

##次回
ステートの自動遷移を実装しました。
次回[【GameplayKit】iOSゲームアプリにおけるStateパターン実践 Part_5](http://qiita.com/chino_tweet/items/75745930649285efe0c8)では、より複雑な`SpriteKit`のスプライトアニメーションと組み合わせます。また、条件によってステート遷移先を分岐させます。