#前回の記事
効率的にゲームをつくるためのフレームワーク③
#この投稿の概要
シーンにあるノード(各色ボックス)にパーティクルによる演出を表示します。
そのために、パーティクルコンポーネントを定義して任意のエンティティに追加していきます。
#手順
大まかな流れは以下の通りです。
パーティクルエミッタファイルを新規作成
パーティクルコンポーネントを定義
コンポーネントシステムにコンポーネントを追加
シーン描画のデリゲート先を指定する
エンティティ生成メソッドを修正
ビルド
##パーティクルエミッタファイルを新規作成
エミッタが使用するパーティクル画像は以下の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`を定義する。
イニシャライザを実装する。
```ruby:ParticleComponent.swift
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`に追加しています。
```ruby:Game.swift
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`は秒指定です。
```ruby:ParticleComponent
override func update(deltaTime _: TimeInterval) {
if let geometryComponent = geometryComponent {
geometryComponent.geometryNode.addParticleSystem(particleEmitter)
}
}
```
ただし、この実装では毎フレーム必ず、`update(_:deltaAtTime:)`メソッドが実行されて、ノードに`SCNParticleSystem`オブジェクトが追加されていまします。
メンバプロパティにフラグを用意して、すでに`SCNParticleSystem`オブジェクトが追加されている場合には、何もしないように変更します。
```ruby:ParticleControlComponent.swift
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`クラスがコンポーネントを操作するため)
1. `Game`クラスに`SCNSceneRendererDelegate`プロトコルを採用します。
(`SCNView`クラスのオブジェクトは`SCNSceneRenderer`プロトコルに適合している)
2. ビューコントローラのルートビュー(`SCNView`オブジェクト)の`delegate`プロパティに`Game`のインスタンスを格納します。
図. シーンの描画サイクル
![SCNSceneRendererDelegate_2x_19b65ac5-da1b-4ef2-85c3-84d0d2ad8ed8.png](https://qiita-image-store.s3.amazonaws.com/0/70476/c086ea8b-dd28-bf08-c57b-bf40f213bcf9.png)
`SCNSceneRendererDelegate`プロトコルを採用する。
```ruby:Game.swift
class Game: NSObject, SCNSceneRendererDelegate {
let scene = SCNScene(named: "GameScene.scn")!
var entities = [GKEntity]()
...
}
```
`Game`クラスが、`SCNView`型オブジェクトから描画サイクルの通知を受けるには`SCNSceneRendererDelegate`プロトコルに適合していなければいけません。
ビューコントローラ側では、`Game`型オブジェクトをシーンの`delegate`プロパティに格納します。
```GameViewController.swift
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`クラスに実装します。
```ruby:Game.swift
func renderer(_: SCNSceneRenderer, updateAtTime time: TimeInterval) {
let timeSincePreviousUpdate = time - previousUpdateTime
particleComponentSystem.update(deltaTime: timeSincePreviousUpdate)
previousUpdateTime = time
}
```
コンポーネントシステムの`update(_:deltaTime)`メソッドは、登録されているコンポーネントの`update(_:deltaTime)`メソッドを実行します。
`previousUpdateTime`プロパティで最終更新時間を追跡していますが、特に使用していません。
##ファクトリメソッドを修正
```ruby:Game.swift
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`のパーティクルを演出します。
```ruby:Game.swift
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`とは別のアルゴリズムになっているので、省略しました。
「クラス継承」アーキテクチャでは困難になりがちなゲーム設計として、覚えておきたいと思います。