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


この記事について

GameplayKitフレームワークが実現する機能の一つであるステートマシンについて解説します。
Appleが提供しているサンプルコードDispenser: GameplayKit State Machine Basicsをフルスクラッチで再現することで、理解していくカタチです。なお、サンプルコードでは、macOSiOStvOSをターゲットにしていますが、本記事ではiOSだけに絞っています。

環境

Xcode 8.2
Swift 3.0.1
iOS 10.2

サンプルコードの概要

Dispenser、つまり給水機を再現したアプリです。SpriteKitによって2Dのグラフィックで給水器を表示しています。
SpriteKitの基本については別記事にて解説していますので、そちらを参考にしてもらえればと思います。
給水機は、水を放出するとタンクに補給されている水の量が減少します。
この給水機の状態をステートマシンで管理することになります。
状態は、以下の通りです。

  • 満タン: Full
  • 水あり: Partially Full
  • カラ : Empty
  • 要補給: Refill
  • 補給中: Serve

世にある多くのゲームが、そのゲームの進行状態やキャラクターの状態を管理することで効率よく開発しています。
例えば、スーパーマリオを考えてみましょう。
マリオは基本状態として静止しています。
方向キーとボタンの組み合わせやアイテム取得時には、移動状態・ダッシュ状態・ジャンプ状態・無敵状態など色々な状態になります。
キャラクターのクラスに全ての機能を実装するよりも、状態(State)ごとにクラスを定義しておき、変化するたびにStateを組み合わせる方が効率的な開発や改善が期待できると言うわけです。

ステートマシンパターンのクラス

GoFデザインパターンのStateパターンを簡潔に言うと「状態をクラスで表現する」となります。(参考Webサイト

手順

大まかな流れは以下のようになります。
プロジェクト準備
シーンファイル作成
アクションファイル作成
シーン表示
シーンのクラス設定

プロジェクト準備

プロジェクト新規作成

各種画像はダウンロードしたサンプルコードのプロジェクトからコピーしておきます。
プロジェクト名はReproductionDispenserとする
Device Orientation: Landscape LeftLandscape Right のみにチェックする
Main.storyboardファイルから、ビューコントローラのルートビューをSKViewクラスに変更しておく
ビューコントローラのクラス名をGameViewControllerに変更し、ファイル名もGameViewController.swiftに変更しておく

使用するイメージ素材

給水機背景(back-1)

給水機背景(back-2)

ボトル(bottle)

給水機(dispenser)

補給ボタンパネル(refill_button_chrome)

補給ボタン(refill_button)

ステート遷移図(statemachine_icons)

水流(stream)

テーブル(table)

水(water)

シーンファイル作成

メニューバーから「File > New > File... > iOS > Resource > SpriteKit Scene」を選択する(GameScene.sks)
アトリビュートインスペクタからファイルの設定を変更
Size: Width = 1024, Height = 768
Anchor Point: X = 0, Y = 0

スプライトを配置

スプライトの各種プロパティについて解説します

  • Position: SpriteKitのシーンでは、左下が原点(X:0, Y:0)となります。
  • Z: Positionは0を起点に+値が手前、-値が奥方向です。
  • Size: 幅と高さの値です。
  • Anchor Point: スプライトの起点位置です。四角いスプライトであれば(X:0, Y:0)は左下になります。起点を中央にしたければ(X:0.5, Y:0.5)と指定することになります。

各種スプライトは、メディアライブラリからドラッグ&ドロップで配置できます。

テーブルの画像(table.png)
Position: X = 512, Y = 0
Z: Position = 4
Size: Width = 2000, Height = 62
Anchor Point: X = 0.5, Y = 0

給水機の画像(dispenser.png)
Position: X = 512, Y = 60
Z: Position = 4
Anchor Point: X = 0.5, Y = 0

給水機背景の画像1(back-1.png)
このスプライトはdispenserスプライトの子ノードになります。
Position: X = 0, Y = 0
Z: Position = -1
Anchor Point: X = 0.5, Y = 0

給水機背景の画像2(back-2.png)
このスプライトはdispenserスプライトの子ノードになります。
Position: X = 0, Y = 0
Z: Position = -4
Anchor Point: X = 0.5, Y = 0

水の画像(water.png)
このスプライトはdispenserスプライトの子ノードになります。
Position: X = -2, Y = 323
Z: Position = -1
Anchor Point: X = 0.5, Y = 0

図. 給水機のスプライト

スプライトの階層に注意してください。このように配置されていれば問題ありません。

続いて、補給ボタンのスプライトを配置します。

補給ボタンパネルの画像(refill_button_chrome)
dispenserの子ノードになります。
Position: X = 265, Y = 45
Z: Position = 1
Size: Width = 90, Height = 150
Anchor Point: X = 0.5, Y = 0.5
Scale: X = 0.123, Y = 0.123

補給ボタンの画像(refill_button)
refillButtonChromeの子ノードになります。
Position: X = -0.186, Y = -0.836
Z: Position = 2
Size: Width = 405, Height = 405
Anchor Point: X = 0.5, Y = 0.5
Scale: X = 8.118, Y = 5.054

水流とボトルのスプライトを配置します。

水流のスプライト(stream)
dispenserの子ノードです。
Position: X = 0, Y = 220
Z: Position = -3
Anchor Point: X = 0.5, Y = 0

ボトルのスプライト(bottle)
dispenserの子ノードです。
補給ボタンをタップすると、スライドして給水機にセットされます。
Position: X = 0, Y =
Z: Position = -1
Anchor Point: X = 0.5, Y = 0

給水機の表示ランプ
dispenserの子ノードです。
給水機の状態によって任意の色に変化します。
position: X = 0, Y = 684
Z: position = -4
Anchor Point: X = 0.5, Y = 1
Color: Licorice

図.補給ボタンとボトルのスプライトを配置したシーン

このようなシーンになっていれば問題ありません。

最後にステート遷移を表示するスプライトを配置します。

ステート遷移図のスプライト(Statemachine Chart)
Scale: X: 0.063, Y = 0.063
Position: X = 192, Y = 526
Z: Position = 1
Size: Width = 360, Height = 303

満タンのスプライト(FullState)
Statemachine Chartの子ノードになります。
Scale: X = 19, Y = 14
Position: X = 20,Y = 1588
Z: Position = -1
Size: Width = 1900, Height = 1400
Color: Iron

給水のスプライト(ServeState)
Statemachine Chartの子ノードになります。
Scale: X = 14, Y = 46
Position: X = -2008,Y = 20
Z: Position = -1
Size: Width = 1400, Height = 4600
Color: Iron

給水中のスプライト(RefillingState)
Statemachine Chartの子ノードになります。
Scale: X = 14, Y = 46
Position: X = 10,Y = 2046
Z: Position = -1
Size: Width = 1400, Height = 4500
Color: Iron

カラのスプライト(EmptyState)
Statemachine Chartの子ノードになります。
Scale: X = 19, Y = 14
Position: X = -4.4,Y = 1606
Z: Position = -1
Size: Width = 1900, Height = 1400
Color: Iron

水ありのスプライト(PartiallyState)
Statemachine Chartの子ノードになります。
Scale: X = 19, Y = 17
Position: X = 2.6, Y = 23
Z: Position = -1
Size: Width = 1900, Height = 1700
Color: Iron

シーンが完成すると、このようになります。
図. シーン

アクションファイル作成

スプライトのアニメーションをGUIで作成します。
メニューバーから「File > New > File... > iOS > Resource > SpriteKit Action」を選択する(Actions.sks)
このActions.sksの中に、全部で7つ(fillCup, buttonPressed, slideCup, drainWater, resetStream, resetCup, refillDispenser)のアニメーションを作成します。

fillCup

Move

  • Start Time: 1
  • Duration: 1
  • Timing Function: Linear
  • Offset: X = 0, Y = 300

buttonPressed

Fade out

  • Start Time: 0
  • Duration: 1
  • Timing Function: Linear

Fade in

  • Start Time: 2
  • Duration: 1
  • Timing Function: Linear

slideCup

Move to

  • Start Time: 0
  • Duration: 1
  • Timing Function: Ease Out
  • Position: X = 0, Y = 0

Move to

  • Start Time: 2
  • Duration: 1
  • Timing Function: Ease In
  • Position: X = 1000, Y = 0

drainWater

Resize

  • Start Time: 0
  • Duration: 1
  • Timing Function: Linear
  • Amount: Width = 0, Height = -70

resetStream

Move to

  • Start Time: 0
  • Duration: 1
  • Timing Function: Linear
  • Position: X = 0, Y = 280

resetCup

Move to

  • Start Time: 0
  • Duration: 1
  • Timing Function: Linear
  • Position: X = -1000, Y = 0

refillDispenser

Resize to Height

  • Start Time: 0
  • Duration: 1
  • Timing Function: Linear
  • Height: 280

アクションのタイムラインはこのようになります。
図. Actions.sksのタイムライン

ゲームシーンのクラスを定義

メニューバーから「File > New > File... > iOS > Swift File」を選択する(GameScene.swift
GameScene.swiftファイルにSpriteKitフレームワークをインポートして、SKSceneを継承したGameSceneクラスを定義する

GameScene.swift
import SpriteKit

class GameScene: SKScene {

}

ゲームシーンを表示

アプリ起動時にGameSceneを表示させます。

GameViewController.swift
import UIKit
import SpriteKit

class GameViewController: UIViewController {

    let scene = SKScene(fileNamed: "GameScene")!

    override func viewDidLoad() {
        super.viewDidLoad()

        let scaleFactor = scene.size.height / view.bounds.height
        scene.scaleMode = .aspectFit
        scene.size.width = view.bounds.width * scaleFactor

        let skView = view as! SKView
        skView.presentScene(scene)
        skView.ignoresSiblingOrder = true
    }
}

ignoreSiblingOrderプロパティにtrueを設定することで、階層化されたノードのうちの兄弟間の順序を無視することでSpriteKitの描画パフォーマンスを向上させます。

ビルド

シミュレータでビルドすると、このように表示されます。

図. シミュレータでビルド

給水機のスプライトが左に寄っていますが、現時点ではこれで問題ありません。

ゲームシーンにGameSceneクラスを設定

ゲームシーンをGameSceneクラスのプログラムで操作できるようにするため、GameScene.sksファイルのカスタムクラスインスペクタのCustom ClassをGameSceneに変更する
さらに、ゲームシーンのdispenserスプライトが中央に表示されるようにコードを記述しておく

GameScene.swift
class GameScene: SKScene {

    override func didChangeSize(_ oldSize: CGSize) {
        let dispenser = childNode(withName: "dispenser")
        dispenser?.position.x = size.width / 2
    }

}

ビルド

これで、給水機は常に画面中央に表示されるようになりました。
シミュレータで確認しましょう。

給水ボタンは給水機の子ノードなので、給水機とともに移動しています。

続き

本記事では、Statemachineを扱うための準備をしました。
次回、【GameplayKit】iOSゲームアプリにおけるStateパターン実践 Part_2では以降の記事で、本格的にStamemachineを実現するクラスを扱っていく予定です。