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

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

More than 1 year has passed since last update.

前回までの記事

【GameplayKit】iOSゲームアプリにおけるStateパターン実践 Part_3では、ステートおよびステートマシンの定義と実装をしました。

この記事について

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

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

手順

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

水あり状態の定義

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

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オブジェクト初期化時に記述します。

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

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(_:)メソッドで、遷移先ステートをチェックします。

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

PartiallyFullState
import GameplayKit

class PartiallyFullState: DispenserState {

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

}

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

自動的なステート遷移

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

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

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にしか遷移できないようにしておきます。

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()メソッドで実装しています。

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では、より複雑な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