WWDC 2017 の SceneKit サンプル Fox 2 を調べる その17
BaseComponent.swift の前にそこで使用している GameplayKit の Entities and Components について見てゆく。
Entities and Components とは?
所謂、エンティティ・コンポーネント・システム。
ざっくり説明すると、エンティティはプレイヤーキャラや敵キャラなどの実態で、コンポーネントというエンティティに対して機能を持ったものを割り当てや削除を行う。
システムはコンポーネントを一括での操作などを行う。
詳しくは Wikipedia を参照。
エンティティ・コンポーネント・システム - Wikipedia
GameplayKit では以下のようになっている。
一般名称 | GameplayKit でのクラス名 |
---|---|
entity | GKEntity |
component | GKComponent |
system | GKComponentSystem |
今回、Fox2 では GKEntity と GKComponent を使っているのでそこを掘り下げてみる。
GKEntity と GKComponent で何かつくる
Xcode の Game テンプレートを使い、コンポーネントを設定したノード(ここでは宇宙船)をタップすると消す簡単なものを作成してみる。
下準備
プロジェクトの Build Phases の Link Binary With Libraries の「+」ボタンから GameplayKit.framework を選択してインポートする。
Command + N で新規 Swift ファイルを作成。
TestComponent.swift と名前をつけカスタムのコンポーネントを作成。
GameplayKit と SceneKit をインポートし、GKComponent を継承する。
コンポーネントに delete 関数を設定する
名前はなんでもよいのだが、ひとまず外部か呼び出せるように delete という関数をつくってみる。 この関数では自身の親となる entity が設定した GKSCNNodeComponent を探し、自前で設定した変数 nodeComponent へ渡している。 GKSCNNodeComponent の中身は設定したと想定される SCNNode になる。
nodeComponent.node で SCNNode を調べることができるので removeFromParentNode() でノードを消す処理をしている
TestComponent.swift
import GameplayKit import SceneKit class TestComponent: GKComponent { func delete() { guard let nodeComponent = entity?.component(ofType: GKSCNNodeComponent.self) else { return } nodeComponent.node.removeFromParentNode() } }
GameViewController で GKEntity と TestComponent を紐づける
GameViewController.swift の編集をする。 内容は イニシャライズ時に設定をし、タッチイベントでコンポーネントの delete() を呼ぶようにする。
下準備
GameplayKit を使用するのでひとまずインポートする。
import GameplayKit
"class GameViewController: UIViewController {" の下に追加。
GKScene は GKEntity と GKComponent を保持しているものだと思ってもらってよい。
var gkScene = GKScene() let test = TestComponent()
イニシャライズ時の処理
"ship.runAction(SCNAction..." の下あたりに以下を追加。
GKEntity を作成し、GKSCNNodeComponent を使用して ship (宇宙船) を testEntity ノードとして設定。
さらに testEntity に TestComponent である test をコンポーネントとして設定し、gkScene に作成した GKEntity を追加している
let testEntity = GKEntity() testEntity.addComponent(GKSCNNodeComponent(node: ship)) testEntity.addComponent(test) gkScene.addEntity(testEntity)
タッチイベント
タッチ時に GKComponent の test から delete 関数を呼ぶようにする
@objc func handleTap(_ gestureRecognize: UIGestureRecognizer) { let scnView = self.view as! SCNView let p = gestureRecognize.location(in: scnView) let hitResults = scnView.hitTest(p, options: [:]) if hitResults.count > 0 { test.delete() } }
ビルドして宇宙船をタップすると宇宙船が消える。
シーン再生中にフラグでなんらかの処理を与える
このままだと、ViewController に処理を直で書いた方が早いので、Entities and Components での特有の機能を設定してみる。
update 関数について
GKEntity、GKComponent、GKComponentSystem では update 関数が用意されており、
SceneKit の SCNSceneRenderer でフレーム毎に呼ぶと、自前でつくった Bool 変数からコンポーネントの状態を変えた際に自動で処理をすることもできる。
コンポーネントの修正
以下のように修正。
とりあえず、コンポーネントの外から deleteFlag 変数が見れるよう public で設定。
GKComponent の update 関数をオーバーライドしてフレーム毎に中を処理する。
今回は deleteFlag が true なら、delete 関数が呼ばれるようにしている。
class TestComponent: GKComponent { public var deleteFlag:Bool = false override func update(deltaTime seconds: TimeInterval) { if self.deleteFlag { delete() } super.update(deltaTime: seconds) } func delete() { guard let nodeComponent = entity?.component(ofType: GKSCNNodeComponent.self) else { return } nodeComponent.node.removeFromParentNode() } }
GameViewController の修正
振る舞いとしては、SCNSceneRenderer のフレーム毎の処理でコンポーネントの update を呼び出し続ける。
タップ時にコンポーネントの deleteFlag を true するとコンポーネントの update が反応し、ノードを消す。
SCNSceneRendererDelegate
SCNSceneRenderer でフレーム毎の処理を行うため、UIViewController の横に SCNSceneRendererDelegate を追加。
class GameViewController: UIViewController, SCNSceneRendererDelegate {
また、delegate を動作するように "scnView.delegate = self" を設定する。
let scnView = self.view as! SCNView scnView.delegate = self
SCNSceneRenderer の delegate で描画時に処理する renderer 関数を設定する。
その前にこのクラスの "let test = TestComponent()" 付近に 以下のメンバ変数を設定。
このサンプルでは必要ないのだが経過時間を取るため設定。
var previousUpdateTime: TimeInterval = 0
renderer 関数の設定。
time で与えられる TimeInterval はこれまでの経過時間を与えるため関数内で、
起動した値を previousUpdateTime で 0 とし、time から引いている。
内部の for 文から gkScene にある全ての entity の update を動かしている。
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) { var calcTime: TimeInterval = 0 if previousUpdateTime != 0 { calcTime = time - previousUpdateTime } for entity: GKEntity in gkScene.entities { entity.update(deltaTime: calcTime) } previousUpdateTime = time }
タップイベントの修正
直接、コンポーネントの関数を呼んでいたが、今回のタップイベントでは以下のようにフラグを立てるだけにする。
SCNSceneRenderer の方でコンポーネントの update を動かしているため、タップイベントで deleteFlag を true にすると即時でコンポーネントの関数が呼ばれるようになる。
@objc func handleTap(_ gestureRecognize: UIGestureRecognizer) { let scnView = self.view as! SCNView let p = gestureRecognize.location(in: scnView) let hitResults = scnView.hitTest(p, options: [:]) if hitResults.count > 0 { test.deleteFlag = true } }
サンプルファイル
github.com
まとめ
今回は Entity が1つで、Component も1つしかなく、複数の Entity で使いまわしたりしていないため、あまり便利さがないが、キャラクターや何かの状態が変更したり、何かと影響しあい変更される場合は、割と効率的に動作の設定ができると思われる。
ちなみに今回説明はしていないが、GKComponentSystem は GKEntity から GKComponent を取得して設定し、GKEntity から調べることなく GKComponent の機能にアクセスすることができる。
詳しくは Entities and Components のドキュメントや Boxes のサンプルを参照。
おまけ
Fox2 のサンプルで使用されているが、Xcode 9 から GKComponent の設定値を Scene Editor で設定することができるようになった。
GKComponent を継承したクラスへ @IBInspectable のように @GKInspectable のついた変数を設定。
Scene Editor の Node Inspector (Command + Option + 3) の1番下の Components の「+」ボタンからクラスを選ぶ。
プロジェクト内で GKComponent を継承したものを探すため、一度 Xcode を閉じないとクラスが候補に出ない可能性があるかも?
追加したクラスを選択すると設定した値が列挙されこちらで設定した値が優先される。
Scene Editor で GKComponent を設定すると "シーンファイル名.gks" というバイナリの plist が自動的に生成される。
ファイルを消すと紐づけが消されるので注意。
次回に続く。