前回の記事
この投稿の概要
シーンにあるノード(各色ボックス)にパーティクルによる演出を表示します。
そのために、パーティクルコンポーネントを定義して任意のエンティティに追加していきます。
手順
大まかな流れは以下の通りです。
パーティクルエミッタファイルを新規作成
パーティクルコンポーネントを定義
コンポーネントシステムにコンポーネントを追加
シーン描画のデリゲート先を指定する
エンティティ生成メソッドを修正
ビルド
パーティクルエミッタファイルを新規作成
エミッタが使用するパーティクル画像は以下の2つです。
starburst.png~~~
←Sparkleになります。
spark.png~~~~~
←白くて見えませんがココです。(Fireになります)
~~~~~~~~~~~~~
メニューバーから「File > New > File... > iOS > Resource > SceneKit Particle System File」を選択して2つのパーティクル演出ファイル(Fire.scnp, Sparkle.scnp)を作ります。
Fire.scnp(Reactor)
燃え上がる炎のような演出効果です。
Particle system templateは「Reactor」を選択する。
Attribute Inspectorの設定は以下のように変更する。
- Emitter
- Birth rage: 300
- Direction: x=0, y=0, z=0
- Initial ange: 0°
- Shape: Box
- Shape width: 1.1
- Shape height: 1.1
- Shape length: 1.1
- Simulation
- Life span: 1.5
- Acceleration: x=0, y=2, z=0
- Image
- Image: spark.png
- Size: 0.5(誤差0.5)
- Animation: Shrink Liner
Sparkle.scnp
キラキラ輝くような演出効果です。
Particle system templateは「Smoke」を選択する。
Attribute Inspectorの設定は以下のように変更する。
- Emitter
- Birth rate: 50
- Warmup duration: 5
- Location: Surface
- Shape width: 1
- Shape height: 1
- Shape length: 1
- Life span: 1(誤差0.5)
- Liner velocity: 1(誤差0.5)
- Angular velocity: 0(誤差180)
- Acceleration: x, y, z = 0
- Image
- Image: starbrust.png
- Size: 0.5(誤差0.1)
- Animation: Growth then Shrink
- Rendering
- Blending: Additive
パーティクルコンポーネントを定義
GKComponentクラスのサブクラスファイルを新規作成(ParticleComponent.swift)
SpriteKit
をimportする。
GKComponent
をimportする。
ParticleComponent
クラスを定義する。
メンバプロパティとしてgeometryComponent
を定義する。
メンバプロパティとしてparticleEmitter
を定義する。
イニシャライザを実装する。
import GameplayKit
import SceneKit
class ParticleComponent: GKComponent {
var geometryComponent: GeometryComponent? {
return entity?.component(ofType: GeometryComponent.self)
}
let particleEmitter: SCNParticleSystem
init(particleName: String) {
particleEmitter = SCNParticleSystem(named: particleName, inDirectory: "/")!
super.init()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
ParticleComponent
クラスが生成する一つのインスタンスは、一つのパーティクルを演出することになります。
インスタンスが演出するパーティクルはparticleEmitter
プロパティが保持し、イニシャライザで初期化されます。
geometryComponent
プロパティは、自身が所属しているエンティティにアクセスして、そのエンティティが所有しているジオメトリコンポーネントを取得します。
コンポーネントシステムにコンポーネントを登録
Game
クラスにパーティクルコンポーネントを管理するためのparticleComponentSystem
プロパティを定義します。
addComponentsToComponentSystems()
メソッドで、particleComponentSystem
に追加しています。
class Game: NSObject, SCNSceneRendererDelegate {
let scene = SCNScene(named: "GameScene.scn")!
let playerControlComponentSystem = GKComponentSystem(componentClass: PlayerControlComponent.self)
let particleControlComponentSystem = GKComponentSystem(componentClass: ParticleComponent.self)
var entities = [GKEntity]()
override init() {
super.init()
setUpEntities()
addComponentsToComponentSystems()
}
func setUpEntities() {
...
entities = [redBoxEntity, yellowBoxEntity, greenBoxEntity, blueBoxEntity, purpleBoxEntity]
}
func addComponentsToComponentSystems() {
for box in entities {
playerControlComponentSystem.addComponent(foundIn: box)
particleControlComponentSystem.addComponent(foundIn: box)
}
}
func jumpBoxes() {
...
}
func makeBoxEntity(forNodeWithName name: String, wantsPlayerControlComponent: Bool = false) -> GKEntity {
...
return box
}
}
GKComponent
インスタンスのaddComponent(foundIn)
メソッドは、引数のエンティティが所有しているコンポーネントから適切なコンポーネントだけをコンポーネントシステムに追加してくれます。
パーティクルを実装
ParticleComponent
クラス、update(_:deltaAtTime:)
メソッドをオーバーライドします。
update(_:deltaAtTime:)
メソッドは、引数deltaAtTime
毎に実行されることになります。このメソッドに周期的に実行させたい処理を記述しておくことで、コンポーネントを稼働させます。なお、引数deltaAtTime
は秒指定です。
override func update(deltaTime _: TimeInterval) {
if let geometryComponent = geometryComponent {
geometryComponent.geometryNode.addParticleSystem(particleEmitter)
}
}
ただし、この実装では毎フレーム必ず、update(_:deltaAtTime:)
メソッドが実行されて、ノードにSCNParticleSystem
オブジェクトが追加されていまします。
メンバプロパティにフラグを用意して、すでにSCNParticleSystem
オブジェクトが追加されている場合には、何もしないように変更します。
class ParticleComponent: GKComponent {
var geometryComponent: GeometryComponent? {
return entity?.component(ofType: GeometryComponent.self)
}
let particleEmitter: SCNParticleSystem
var boxHasParticleEffect = false // フラグ
init(particleName: String) {
particleEmitter = SCNParticleSystem(named: particleName, inDirectory: "/")!
super.init()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func update(deltaTime _: TimeInterval) {
if let geometryComponent = geometryComponent, !boxHasParticleEffect {
geometryComponent.geometryNode.addParticleSystem(particleEmitter)
}
boxHasParticleEffect = (geometryComponent != nil)
}
}
boxHasParticleEffect
プロパティは、geometryComponent
オブジェクトが所属しているノード(実際にはエンティティ)がパーティクルを与えられるとtrue
になります。
update(_:deltaAtTime:)
メソッドでパーティクルを追加する前に、boxHasParticleEffect
プロパティを確認するようにしています。
シーン描画のデリゲート先を指定
パーティクルを描画するためには、シーンビューの描画サイクルをGame
クラスが把握する必要があります。(Game
クラスがコンポーネントを操作するため)
-
Game
クラスにSCNSceneRendererDelegate
プロトコルを採用します。 (SCNView
クラスのオブジェクトはSCNSceneRenderer
プロトコルに適合している) - ビューコントローラのルートビュー(
SCNView
オブジェクト)のdelegate
プロパティにGame
のインスタンスを格納します。
SCNSceneRendererDelegate
プロトコルを採用する。
class Game: NSObject, SCNSceneRendererDelegate {
let scene = SCNScene(named: "GameScene.scn")!
var entities = [GKEntity]()
...
}
Game
クラスが、SCNView
型オブジェクトから描画サイクルの通知を受けるにはSCNSceneRendererDelegate
プロトコルに適合していなければいけません。
ビューコントローラ側では、Game
型オブジェクトをシーンのdelegate
プロパティに格納します。
override func viewDidLoad() {
super.viewDidLoad()
guard let scnView = view as? SCNView else { fatalError("シーンビュー生成失敗") }
scnView.backgroundColor = UIColor.lightGray
scnView.scene = game.scene
scnView.delegate = game // デリゲート先を指定
}
これでgame
オブジェクトが、シーンビューの描画サイクルイベントのデリゲート先になりました。
描画更新メソッドを実装
Game
クラスはSCNSceneRendererDelegate
プロトコルに準拠しているので、デリゲートメソッドを実装できるようになっています。
フレームごとに呼ばれるデリゲートメソッドrenderer(_:updateTime)
をGame
クラスに実装します。
func renderer(_: SCNSceneRenderer, updateAtTime time: TimeInterval) {
let timeSincePreviousUpdate = time - previousUpdateTime
particleComponentSystem.update(deltaTime: timeSincePreviousUpdate)
previousUpdateTime = time
}
コンポーネントシステムのupdate(_:deltaTime)
メソッドは、登録されているコンポーネントのupdate(_:deltaTime)
メソッドを実行します。
previousUpdateTime
プロパティで最終更新時間を追跡していますが、特に使用していません。
ファクトリメソッドを修正
func makeBoxEntity(forNodeWithName name: String,
wantsPlayerControlComponent: Bool = false,
withParticleComponentNamed particleComponentName: String? = nil) -> GKEntity {
let node = scene.rootNode.childNode(withName: name, recursively: false)
guard let boxNode = node else { fatalError("no exist \(name) node.") }
let box = GKEntity()
let geometryComponent = GeometryComponent(geometryNode: boxNode)
box.addComponent(geometryComponent)
if wantsPlayerControlComponent {
let playerControlComponent = PlayerControlComponent()
box.addComponent(playerControlComponent)
}
if let particleComponentName = particleComponentName {
let particleComponent = ParticleComponent(particleName: particleComponentName)
box.addComponent(particleComponent)
}
return box
}
ファクトリメソッドを呼び出している部分も引数に合わせて修正しておきます。
ここではブルーにSparkle
、イエローにFire
のパーティクルを演出します。
func setUpEntities() {
let redBoxEntity = makeBoxEntity(forNodeWithName: "redBox")
let yellowBoxEntity = makeBoxEntity(forNodeWithName: "yellowBox",
withParticleComponentNamed: "Fire")
let greenBoxEntity = makeBoxEntity(forNodeWithName: "greenBox",
wantsPlayerControlComponent: true)
let blueBoxEntity = makeBoxEntity(forNodeWithName: "blueBox",
wantsPlayerControlComponent: true,
withParticleComponentNamed: "Sparkle")
let purpleBoxEntity = makeBoxEntity(forNodeWithName: "purpleBox")
entities = [redBoxEntity, yellowBoxEntity, greenBoxEntity, blueBoxEntity, purpleBoxEntity]
}
ビルド
Fire
パーティクルがイエローボックスに、Sparkle
パーティクルがブルーボックスに演出されるようになりました。
おわり
以上、GameplayKitの「Entity-Component」アーキテクチャを解説しました。
サンプルプロジェクトでは、ここからさらにSCNLight
クラスを実装していますがGameplayKit
とは別のアルゴリズムになっているので、省略しました。
「クラス継承」アーキテクチャでは困難になりがちなゲーム設計として、覚えておきたいと思います。