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

#前回
[【GameplayKit】iOSゲームアプリにおけるStateパターン実践 Part_6](http://qiita.com/chino_tweet/items/357ca36f82280b907abe)は、給水機の表示ランプ点滅をレンダリングで実装しました。

#この記事について
給水機の状態を管理するステートマシンを完成させます。

#手順
補給状態を定義
補給アニメーションと自動的なステート遷移
水あり状態からの補給


##補給状態（RefillingState）を定義
タンクが空になったら、水を補給できるようにします。
補給中の状態を表現するステート`RefillingState`を定義します。
メニューバーから「File > New > File... > iOS > Swift File」を選択する。（RefillingState.swift）
`GameplayKit`フレームワークをインポートして、`DispenserState`クラスを継承した`RefillingState`クラスを定義します。

```ruby:RefillingState.swift
import GameplayKit

class RefillingState: DispenserState {

    static let waterNodeHeight: CGFloat = 280
        
    init(game: GameScene) {
        super.init(game: game, associatedNodeName: "RefillingState")
    }
    
    override func didEnter(from previousState: GKState?) {
        super.didEnter(from: previousState)
    }
    
    override func isValidNextState(_ stateClass: AnyClass) -> Bool {
        return (stateClass is FullState.Type)
    }
    
}
```

必要となるイニシャライザとイベントメソッドを実装しておきます。
`RefillingState`の次のステートは`FullState`のみ遷移できるようにします。
`waterNodeHeight`プロパティは、満タン時のタンク水量です。

##補給のアニメーションと自動的なステート遷移
リフィル（補給）のアニメーション後、自動的にフル状態`FullState`へ遷移するようにします。
上記の機能を`playRefillAnimationThenExit()`メソッドに実装します。

```ruby:RefillingState.swift
import GameplayKit

class RefillingState: DispenserState {
    
    static let waterNodeHeight: CGFloat = 280
    
    init(game: GameScene) {
        super.init(game: game, associatedNodeName: "RefillingState")
    }
    
    override func didEnter(from previousState: GKState?) {
        super.didEnter(from: previousState)
    }
    
    override func isValidNextState(_ stateClass: AnyClass) -> Bool {
        return (stateClass is FullState.Type)
    }
    
    func playRefillAnimationThenExit() {
        let waterNode = game.scene?.childNode(withName: "//water") as! SKSpriteNode
        let consumptionOfwater = RefillingState.waterNodeHeight - waterNode.size.height
        let waterRatio = consumptionOfwater / RefillingState.waterNodeHeight
        let refillTime = TimeInterval(waterRatio)
        
        let refillAction = SKAction(named: "refillDispenser", duration: refillTime)!
        waterNode.run(refillAction, completion: {
            self.stateMachine?.enter(FullState.self)
        })
    }
    
}
```

###ステートマシンに`RefillingState`を登録
ゲームシーンのステートマシンに補給状態を登録します。

```ruby:GameScene.swift
class GameScene: SKScene {
    
    var stateMachine: GKStateMachine!
    
    override func didMove(to view: SKView) {
        let fullState = FullState(game: self)
        let serveState = ServeState(game: self)
        let partiallyFullState = PartiallyFullState(game: self)
        let emptyState = EmptyState(game: self)
        let refillingState = RefillingState(game: self)
        
        stateMachine = GKStateMachine(states: [fullState,
                                               serveState,
                                               partiallyFullState,
                                               emptyState,
                                               refillingState])
        stateMachine.enter(FullState.self)
    }
    
    override func didChangeSize(_ oldSize: CGSize) {
        let dispenser = childNode(withName: "dispenser")
        dispenser?.position.x = size.width / 2
    }
    
    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.")
            refillButton?.run(SKAction(named: "buttonPressed", duration: 0.6)!)
            stateMachine.enter(RefillingState.self)
        } else {
            print("attempt to dispense")
            stateMachine.enter(ServeState.self)
        }
    }
}
```

`touchedEnded`メソッドも修正しています。
リフィルボタンの場所がタップされていれば、`buttonPressed`アクションを見せた後にステートマシンは`RefillingState`に遷移します。
続いて、`EmptyState`クラスの`isValidNextState`メソッドで、`RefillingState`クラスのみに遷移するようにしておきます。

```ruby:EmptyState
class EmptyState: DispenserState {

    init(game: GameScene) {
        super.init(game: game, associatedNodeName: "EmptyState")
    }
    
    override func didEnter(from previousState: GKState?) {
        super.didEnter(from: previousState)
        
        let red = SKColor.red
        changeIndicatorLightToColor(red)
    }
    
    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 RefillingState.Type)
    }
    
}
```

##水あり状態`PartiallyFullState`からの補給
タンクがカラになる前、水が残っている状態`PartiallyFullState`でも補給できるようにします。
`isValidNextState()`メソッドで、`ServeState`か`RefillingState`に遷移できるように記述します。

```ruby:PartiallyFullState.swift
class PartiallyFullState: DispenserState {
    
    init(game: GameScene) {
        super.init(game: game, associatedNodeName: "PartiallyFullState")
    }
    
    override func didEnter(from previousState: GKState?) {
        super.didEnter(from: previousState)
        print("water is partially full...")
    }
    
    override func isValidNextState(_ stateClass: AnyClass) -> Bool {
        switch stateClass {
        case is ServeState.Type, is RefillingState.Type:
            return true
        default:
            return false
        }
    }
    
}
```

#完成
7回にわたって`GameplayKit`フレームワークのStateパターン実装を解説しました。
給水機の状態はとてもわかりやすい教材でした。
Stateパターンを駆使すれば、野球やサッカーのようなゲームも開発しやすくなるのかもしれません。
野球なら攻撃状態と守備状態・サッカーならボールホルダーとオフザボールプレイヤーを表現できます。
Entity-Componentパターンと組み合わせれば、より本格的なゲームも効率よく開発できそうですね。
[【GameplayKit】iOSゲームアプリにおけるStateパターン実践 Part_1](http://qiita.com/chino_tweet/items/7037b6c7a7c530c91935)
[【GameplayKit】iOSゲームアプリにおけるStateパターン実践 Part_2](http://qiita.com/chino_tweet/items/449e54581ebb9bd5ae31)
[【GameplayKit】iOSゲームアプリにおけるStateパターン実践 Part_3](http://qiita.com/chino_tweet/items/20e15ea58b9f17d51f4d)
[【GameplayKit】iOSゲームアプリにおけるStateパターン実践 Part_4](http://qiita.com/chino_tweet/items/35c62dc17e93e5b8b335)
[【GameplayKit】iOSゲームアプリにおけるStateパターン実践 Part_5](http://qiita.com/chino_tweet/items/75745930649285efe0c8)
[【GameplayKit】iOSゲームアプリにおけるStateパターン実践 Part_6](http://qiita.com/chino_tweet/items/357ca36f82280b907abe)
[【GameplayKit】iOSゲームアプリにおけるStateパターン実践  Final]()