Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

前回の記事

前回GameplayKit】iOSゲームアプリにおけるStateパターン実践 Part_4では、ステートマシンによるステート遷移をいくつかの方法を使って実装しました。

この記事について

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

手順

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

ボトルのアニメーション

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

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しています。

水量変化のアニメーション

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

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

ビルド

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

水流のアニメーション

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

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クラスを定義します。

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クラスを定義したら、ステートマシンに登録しておきます。

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に遷移させるようにします。

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

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

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

ビルド

給水機のタンク残量が無くなると、ステート遷移パネルが「Empty」になり表示ランプがレッドになります。
スクリーンショット 2016-12-25 1.54.44.png

次回

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

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away