3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

【GameplayKit】効率的にゲームをつくるためのフレームワーク④

Last updated at Posted at 2016-12-07

#前回の記事
効率的にゲームをつくるためのフレームワーク③

#この投稿の概要
シーンにあるノード(各色ボックス)にパーティクルによる演出を表示します。
そのために、パーティクルコンポーネントを定義して任意のエンティティに追加していきます。

#手順
大まかな流れは以下の通りです。
パーティクルエミッタファイルを新規作成
パーティクルコンポーネントを定義
コンポーネントシステムにコンポーネントを追加
シーン描画のデリゲート先を指定する
エンティティ生成メソッドを修正
ビルド

##パーティクルエミッタファイルを新規作成
エミッタが使用するパーティクル画像は以下の2つです。
starburst.png~~~
starburst.png←Sparkleになります。
spark.png~~~~~
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`とは別のアルゴリズムになっているので、省略しました。
「クラス継承」アーキテクチャでは困難になりがちなゲーム設計として、覚えておきたいと思います。
3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?