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

#前回の記事
前回[GameplayKit】iOSゲームアプリにおけるStateパターン実践 Part_4](http://qiita.com/chino_tweet/items/35c62dc17e93e5b8b335)では、ステートマシンによるステート遷移をいくつかの方法を使って実装しました。

#この記事について
`SpriteKit`のスプライトアクションを使って、複雑なアニメーションを実装します。
`SpriteKit`場でのアニメーションは `SKAction`クラスで実現します。
また、ステート遷移先を条件によって分岐させます。

#手順
ボトルのアニメーションを実装
水量減少のアニメーションを実装
水流のアニメーションを実装
条件付きでカラ状態に遷移させる

##ボトルのアニメーション
`waitAndExit()`メソッドの名前を`playDispensingAnimationThenExit()`メソッドに変更しておきます。呼び出し部分も合わせて変更します。
アニメーションは`SKAction`オブジェクトとして、`playDispensingAnimationThenExit()`メソッドに実装します。
まずは、給水中の状態`ServeState`に遷移後、画面端からボトルスプライトがスライドしてくるアニメーションを実装します。スライドしてきたボトルは、1秒間だけ静止した後に画面外へスライドしていきます。
事前に`Actions.sks`ファイルで定義しておいた`SKAction`のタイムラインを基にして、`slideCupAction`オブジェクトを生成します。

```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)
        
        playDispensingAnimationThenExit()
    }

    override func isValidNextState(_ stateClass: AnyClass) -> Bool {
        return stateClass is PartiallyFullState.Type
    }
    
    func playDispensingAnimationThenExit() {
        let slideCupAction = SKAction(named: "slideCup", duration: 3 * ServeState.timeScale)!
        let slideCupActionOnNode = SKAction.run(slideCupAction, onChildWithName: "//bottle")

        //let waitAction = SKAction.wait(forDuration: ServeState.timeScale)

        game.scene?.run(slideCupActionOnNode, completion: {
            self.stateMachine?.enter(PartiallyFullState.self)
        })
    }
}
```

`SKAction(named:duration:)`メソッドでアニメーション動作のオブジェクトを生成し、`run(onChildWithName:)`メソッドを使ってスプライトノードに関連づけています。
ここでは、ボトルのスプライトノード座標を元の場所から(0, 0)にMoveした1秒後、さらに(1000, 0)にMoveしています。

##水量変化のアニメーション
続いて、水量変化のアニメーションを実装します。

```ruby:ServeState.swift
    func playDispensingAnimationThenExit() {
        let slideCupAction    = SKAction(named: "slideCup",   duration: 3 * ServeState.timeScale)!
        let drainWaterAction  = SKAction(named: "drainWater", duration: ServeState.timeScale)!
        let resetStreamAction = SKAction(named: "resetStream", duration: 0)!
        let resetCupAction    = SKAction(named: "resetCup",    duration: 0)!

        let slideCupActionOnNode    = SKAction.run(slideCupAction,    onChildWithName: "//bottle")
        let drainWaterActionOnNode  = SKAction.run(drainWaterAction,  onChildWithName: "//water")
        let resetStreamActionOnNode = SKAction.run(resetStreamAction, onChildWithName: "//stream")
        let resetCupActionOnNode    = SKAction.run(resetCupAction,    onChildWithName: "//bottle")
        
        let waitAction = SKAction.wait(forDuration: ServeState.timeScale)
        let innerSequence = [waitAction, drainWaterActionOnNode,
                             waitAction, resetStreamActionOnNode,
                             waitAction, resetCupActionOnNode]
        let innerSequenceAction = SKAction.sequence(innerSequence)
        
        let group = [slideCupActionOnNode, innerSequenceAction]
        let groupAction = SKAction.group(group)
        
        game.scene?.run(groupAction, completion: {
            self.stateMachine?.enter(PartiallyFullState.self)
        })
    }
```



###ビルド
シミュレータの画面をタップすると、ボトルがスライドしてきた後、給水機タンクの水量が減少してからボトルがさらにスライドしていきます。

##水流のアニメーション
ボトルスライドのアクションとタンク水量減少アクションの間に、水流アニメーションを挿入します。

```ruby:ServeState.swift
    func playDispensingAnimationThenExit() {
        let slideCupAction    = SKAction(named: "slideCup",    duration: 3 * ServeState.timeScale)!
        let drainWaterAction  = SKAction(named: "drainWater",  duration: ServeState.timeScale)!
        let resetStreamAction = SKAction(named: "resetStream", duration: 0)!
        let resetCupAction    = SKAction(named: "resetCup",    duration: 0)!
        let fillCupAction     = SKAction(named: "fillCup",     duration: 2 * ServeState.timeScale)!

        let slideCupActionOnNode    = SKAction.run(slideCupAction,    onChildWithName: "//bottle")
        let drainWaterActionOnNode  = SKAction.run(drainWaterAction,  onChildWithName: "//water")
        let resetStreamActionOnNode = SKAction.run(resetStreamAction, onChildWithName: "//stream")
        let resetCupActionOnNode    = SKAction.run(resetCupAction,    onChildWithName: "//bottle")
        let fillCupActionOnNode     = SKAction.run(fillCupAction,     onChildWithName: "//stream")
        
        let waitAction = SKAction.wait(forDuration: ServeState.timeScale)
        let innerSequence = [waitAction, drainWaterActionOnNode,
                             waitAction, resetStreamActionOnNode,
                             waitAction, resetCupActionOnNode]
        let innerSequenceAction = SKAction.sequence(innerSequence)
        
        let group = [slideCupActionOnNode, fillCupActionOnNode, innerSequenceAction]
        let groupAction = SKAction.group(group)
        
        game.scene?.run(groupAction, completion: {
            self.stateMachine?.enter(PartiallyFullState.self)
        })
    }
```

###ビルド
シミュレータでビルドします。
今度は、水量が減少すると同時に水流がボトルに注ぎ込まれるアニメーションが表示されます。

##条件付きでカラ状態に遷移させる
給水機のタンクが空になったら、ステートマシンを`EmptyState`に遷移させます。
まずは、カラの状態を表現する`EmptyState`を定義します。
メニューバーから「File > New > File... > iOS > Swift Class」を選択します。（EmptyState.swift）

`GameplayKit`フレームワークをインポートして、`DispenserState`クラスを継承した`EmptyState`クラスを定義します。

```ruby:EmptyState.swift
import GameplayKit

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)
    }
    
}
```

イニシャライザ、`didEnter(from:)`メソッドおよび `willExit(to:)`メソッドを実装しておきます。
`EmptyState`クラスを定義したら、ステートマシンに登録しておきます。

```ruby:GameScene.swift
    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)
        
        stateMachine = GKStateMachine(states: [fullState,
                                               serveState,
                                               partiallyFullState,
                                               emptyState])
        stateMachine.enter(FullState.self)
    }
```

###カラ状態への条件

給水機のタンク水量（スプライトノードの`height`プロパティ）が1以下の場合、`EmptyState`に遷移させるようにします。

```ruby:ServeState.swift
    func playDispensingAnimationThenExit() {
        let slideCupAction    = SKAction(named: "slideCup",    duration: 3 * ServeState.timeScale)!
        let drainWaterAction  = SKAction(named: "drainWater",  duration: ServeState.timeScale)!
        let resetStreamAction = SKAction(named: "resetStream", duration: 0)!
        let resetCupAction    = SKAction(named: "resetCup",    duration: 0)!
        let fillCupAction     = SKAction(named: "fillCup",     duration: 2 * ServeState.timeScale)!

        let slideCupActionOnNode    = SKAction.run(slideCupAction,    onChildWithName: "//bottle")
        let drainWaterActionOnNode  = SKAction.run(drainWaterAction,  onChildWithName: "//water")
        let resetStreamActionOnNode = SKAction.run(resetStreamAction, onChildWithName: "//stream")
        let resetCupActionOnNode    = SKAction.run(resetCupAction,    onChildWithName: "//bottle")
        let fillCupActionOnNode     = SKAction.run(fillCupAction,     onChildWithName: "//stream")
        
        let waitAction = SKAction.wait(forDuration: ServeState.timeScale)
        let innerSequence = [waitAction, drainWaterActionOnNode,
                             waitAction, resetStreamActionOnNode,
                             waitAction, resetCupActionOnNode]
        let innerSequenceAction = SKAction.sequence(innerSequence)
        
        let group = [slideCupActionOnNode, fillCupActionOnNode, innerSequenceAction]
        let groupAction = SKAction.group(group)
        
        game.scene?.run(groupAction, completion: {
            let waterNode = self.game.childNode(withName: "//water") as! SKSpriteNode
            let restOfWater = waterNode.size.height
            if restOfWater < 1 {
                self.stateMachine?.enter(EmptyState.self)
            } else {
                self.stateMachine?.enter(PartiallyFullState.self)
            }
        })
    }
```

これでステートマシンは、遷移先の状態を`EmptyState`と`PartiallyFullState`で分岐しますが、`isValidNextState(_:)`メソッドが`PartiallyFullState`以外へのステート遷移を許可していません。
`EmptyState`にも遷移できるように`isValidNextState(_:)`を修正します。

```ruby:ServeState.swift
    override func isValidNextState(_ stateClass: AnyClass) -> Bool {
        switch stateClass {
        case is PartiallyFullState.Type, is EmptyState.Type:
            return true
        default:
            return false
        }
    }
```

###ビルド
給水機のタンク残量が無くなると、ステート遷移パネルが「Empty」になり表示ランプがレッドになります。
<img width="680" alt="スクリーンショット 2016-12-25 1.54.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/70476/0decb3ae-0c4d-7989-2587-dd9b9a9eb095.png">

#次回
複雑なアニメーションのほか、ステート遷移の分岐も実装しました。
[【GameplayKit】iOSゲームアプリにおけるStateパターン実践 Part_6]()では、`SpriteKit`におけるレンダリングを解説します。