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

#この記事について
GameplayKitフレームワークが実現する機能の一つであるステートマシンについて解説します。
Appleが提供しているサンプルコード[Dispenser: GameplayKit State Machine Basics](https://developer.apple.com/library/content/samplecode/Dispenser_GameplayKit/Introduction/Intro.html#//apple_ref/doc/uid/TP40016460)をフルスクラッチで再現することで、理解していくカタチです。なお、サンプルコードでは、`macOS``iOS``tvOS`をターゲットにしていますが、本記事では`iOS`だけに絞っています。

#環境
Xcode 8.2
Swift 3.0.1
iOS 10.2

#サンプルコードの概要
Dispenser、つまり給水機を再現したアプリです。`SpriteKit`によって2Dのグラフィックで給水器を表示しています。
SpriteKitの基本については[別記事](http://qiita.com/chino_tweet/items/208246bb76506528722e)にて解説していますので、そちらを参考にしてもらえればと思います。
給水機は、水を放出するとタンクに補給されている水の量が減少します。
この給水機の状態をステートマシンで管理することになります。
状態は、以下の通りです。

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

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

#ステートマシンパターンのクラス
GoFデザインパターンのStateパターンを簡潔に言うと「状態をクラスで表現する」となります。（[参考Webサイト](http://www.techscore.com/tech/DesignPattern/State.html/)）


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

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

###使用するイメージ素材
給水機背景（back-1）
![back-1.png](https://qiita-image-store.s3.amazonaws.com/0/70476/04c661d6-547b-b27c-c3d1-788475c6c534.png)
給水機背景（back-2）
![back-2.png](https://qiita-image-store.s3.amazonaws.com/0/70476/c3d4d0aa-73e7-470a-c46c-57141ef1d102.png)
ボトル（bottle）
![bottle.png](https://qiita-image-store.s3.amazonaws.com/0/70476/f134b745-3537-866e-e248-dca1a2a8d636.png)
給水機（dispenser）
![dispenser.png](https://qiita-image-store.s3.amazonaws.com/0/70476/b2f708d0-f9c7-078e-1356-f69847c41fb4.png)
補給ボタンパネル（refill_button_chrome）
![refill_button_chrome.png](https://qiita-image-store.s3.amazonaws.com/0/70476/0f791397-6bf2-839e-c862-3dfef7ff9430.png)
補給ボタン（refill_button）
![refill_button.png](https://qiita-image-store.s3.amazonaws.com/0/70476/0801b52f-06d3-ef00-03d2-2dad84565387.png)
ステート遷移図（statemachine_icons）
![statemachine_icons_truetrans_2x.png](https://qiita-image-store.s3.amazonaws.com/0/70476/d61dffb8-c079-4759-c1e0-5442ee2d764d.png)
水流（stream）
![stream.png](https://qiita-image-store.s3.amazonaws.com/0/70476/e84d79f1-8061-01b1-5f20-0e5af7a3f751.png)
テーブル（table）
![table.png](https://qiita-image-store.s3.amazonaws.com/0/70476/619c04da-8e49-d9f7-8a89-4180f9f9c89a.png)
水（water）
![water.png](https://qiita-image-store.s3.amazonaws.com/0/70476/b2579854-04b2-555b-e281-a27257091ba9.png)



##シーンファイル作成
メニューバーから「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


図. 給水機のスプライト
<img width="883" alt="スクリーンショット 2016-12-20 17.36.01.png" src="https://qiita-image-store.s3.amazonaws.com/0/70476/885df44a-6479-4fec-e790-ce9a331b0431.png">
スプライトの階層に注意してください。このように配置されていれば問題ありません。

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

補給ボタンパネルの画像（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

図.補給ボタンとボトルのスプライトを配置したシーン
<img width="879" alt="スクリーンショット 2016-12-20 18.20.53.png" src="https://qiita-image-store.s3.amazonaws.com/0/70476/2472584a-14a4-71a3-7ed1-96fcf2c86961.png">
このようなシーンになっていれば問題ありません。

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

ステート遷移図のスプライト（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

シーンが完成すると、このようになります。
図. シーン
<img width="1139" alt="スクリーンショット 2016-12-20 18.57.14.png" src="https://qiita-image-store.s3.amazonaws.com/0/70476/068df2d3-a23f-c073-415e-1634a84c2c7f.png">

##アクションファイル作成
スプライトのアニメーションを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のタイムライン
<img width="831" alt="スクリーンショット 2016-12-20 19.27.39.png" src="https://qiita-image-store.s3.amazonaws.com/0/70476/696b0a63-703f-fc92-da1d-4ed02e5a2ca7.png">


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

```ruby:GameScene.swift
import SpriteKit

class GameScene: SKScene {
    
}
```

##ゲームシーンを表示
アプリ起動時に`GameScene`を表示させます。

```ruby: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`の描画パフォーマンスを向上させます。

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

図. シミュレータでビルド
![スクリーンショット 2016-12-20 20.00.48.png](https://qiita-image-store.s3.amazonaws.com/0/70476/0c928513-4bb7-7505-b35e-f085aff4a207.png)
給水機のスプライトが左に寄っていますが、現時点ではこれで問題ありません。

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

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

###ビルド
これで、給水機は常に画面中央に表示されるようになりました。
シミュレータで確認しましょう。
![スクリーンショット 2016-12-20 20.36.38.png](https://qiita-image-store.s3.amazonaws.com/0/70476/c5acbb53-b9fb-945d-b395-7c66d8d9bf1d.png)
給水ボタンは給水機の子ノードなので、給水機とともに移動しています。


#続き
本記事では、Statemachineを扱うための準備をしました。
次回、[【GameplayKit】iOSゲームアプリにおけるStateパターン実践 Part_2](http://qiita.com/chino_tweet/items/449e54581ebb9bd5ae31)では以降の記事で、本格的にStamemachineを実現するクラスを扱っていく予定です。