#この記事について
GameplayKitフレームワークが実現する機能の一つであるステートマシンについて解説します。
Appleが提供しているサンプルコードDispenser: GameplayKit State Machine Basicsをフルスクラッチで再現することで、理解していくカタチです。なお、サンプルコードでは、macOS``iOS``tvOS
をターゲットにしていますが、本記事では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 Left
と Landscape 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クラスを定義する
import SpriteKit
class GameScene: SKScene {
}
##ゲームシーンを表示
アプリ起動時にGameScene
を表示させます。
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
スプライトが中央に表示されるようにコードを記述しておく
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を実現するクラスを扱っていく予定です。