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


この記事について

GameplayKitフレームワークが提供するStatemachineアーキテクチャを使います。
詳細に関しては、前回の記事を参照してください。

前回の投稿

【GameplayKit】iOSゲームアプリにおけるStateパターン実践 Part_1が前回の記事です。
プロジェクト作成~ゲームシーンの構築までを行いました。

動作環境

Xcode 8.2
Swift 3.01
iOS 10.2

手順

大まかな流れは以下のようになります。

  1. タッチを検出する
  2. リフィルボタンのタップを識別する
  3. 給水機の状態を表現するDispenserStateを定義する

タッチを検出する

GameSceneクラスのtouchesEnded(_:with:)メソッドをオーバーライドします。
タッチ座標を検出してコンソールに出力しておきます。

GameScene.swift
class GameScene: SKScene {

    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!)
        print("Point in scene: ", locationInScene)
    }
}

ビルド

シミュレータでビルドしたら、画面の適当な場所をタップしてコンソールを確認します。
座標系は、画面左下が原点になっています。

リフィルボタンのタップを識別する

リフィルボタンをタップされたら、補給(リフィル)しなければいけません。
また、タップされた座標がリフィルボタン以外の場所であれば給水(ディスペンス)しましょう。
touchesEnded(_:with:)メソッドに編集します。

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

atPoint()メソッドは、引数の座標オブジェクトにある最も手前のSKNodeオブジェクトを返します。

ビルド

シミュレータ上の画面をタップして、その座標にボタンがあるかないかで給水機の動作が変化するかをコンソールで確認します。
ボタンをタップした場合だけ「attempt to refill」と表示されれば問題ありません。

給水機の状態DispenserStateを定義する

ここから、各種状態(state)を表現するクラスを定義していきます。状態を表現する全てのクラスはGKStateクラスを継承します。
まずは、給水機が変化しうる全ての状態の親クラスとなるDispenserStateクラスを定義します。
このDispenserStateクラスには、全Stateに共通する以下のようなプロパティ・機能を実装します。

  • プロパティ ゲームシーン 自身が従属する状態名
  • 機能 自身の状態に突入した時に、スプライトをハイライト 自身の状態から脱出した時には、スプライトをハイライト終了

メニューバーから「File > New > File... > iOS > Cocoa Touch Class」を選択する
GKStateのサブクラスで、ファイル名はDispenserState.swiftとしておきます。

DispenserState.swift
import SpriteKit
import GameplayKit

class DispenserState: GKState {

}

SpriteKitGameplayKitをインポートしておきます。

プロパティとイニシャライザを実装

ゲームシーンにある状態遷移図から、変化しうる状態名を取得できるようにします。

DispenserState.swift
class DispenserState: GKState {

    let game: GameScene
    let associatedNodeName: String

    init(game: GameScene, associatedNodeName: String) {
        self.game = game
        self.associatedNodeName = associatedNodeName
    }
}

初期値がないメンバワイズプロパティは、イニシャライザで初期化する必要があります。
続いて、機能を実装します。
状態の突入時と脱出時の振る舞いはそれぞれ、didEnter(from:)メソッドとwillExit(to:)メソッドをオーバーライドします。
associatedNodeNameプロパティには、イニシャライザで状態クラスが生成された時に自身がどの状態名であるかが格納されます。

DispenserState.swift
class DispenserState: GKState {

    let game: GameScene
    let associatedNodeName: String

    init(game: GameScene, associatedNodeName: String) {
        self.game = game
        self.associatedNodeName = associatedNodeName
    }

    override func didEnter(from previousState: GKState?) {
        let associatedNode = game.childNode(withName: "//\(associatedNodeName)") as? SKSpriteNode
        guard let node = associatedNode else { return }
        node.color = SKColor.lightGray
    }

    override func willExit(to nextState: GKState) {
        let associatedNode = game.childNode(withName: "//\(associatedNodeName)") as? SKSpriteNode
        guard let node = associatedNode else { return }
        node.color = SKColor.darkGray
    }

    func changeIndicatorLightToColor(_ color: SKColor) {
        let indicator = game.childNode(withName: "//indicator") as! SKSpriteNode
        indicator.color = color
    }
}

イニシャライザで取得しておいたゲームシーンや状態名から、操作したいスプライトをその都度取得しています。
changeIndicatorLightToColor()メソッドは、給水機が動作中にその表示ランプを任意の色に変化させます。

次回

状態を表現するGKStateクラスを継承して、給水機が変化しうる全ての状態で実装すべき機能を持った親クラスを定義しました。
GKStateクラスには、状態が変化するごとに呼ばれるdidEnter(from:)メソッドやwillExit(to:)メソッドがあることがわかります。

【GameplayKit】iOSゲームアプリにおけるStateパターン実践 Part_3
給水機の状態を表現するDispenserStateクラスを継承して、各状態のクラスを定義していきます。