Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

前回の記事

効率的にゲームをつくるためのフレームワーク③

この投稿の概要

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

手順

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

パーティクルエミッタファイルを新規作成

エミッタが使用するパーティクル画像は以下の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を定義する。
イニシャライザを実装する。

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に追加しています。

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は秒指定です。

ParticleComponent
    override func update(deltaTime _: TimeInterval) {
        if let geometryComponent = geometryComponent {
            geometryComponent.geometryNode.addParticleSystem(particleEmitter)
        }
    }

ただし、この実装では毎フレーム必ず、update(_:deltaAtTime:)メソッドが実行されて、ノードにSCNParticleSystemオブジェクトが追加されていまします。
メンバプロパティにフラグを用意して、すでにSCNParticleSystemオブジェクトが追加されている場合には、何もしないように変更します。

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

SCNSceneRendererDelegateプロトコルを採用する。

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クラスに実装します。

Game.swift
    func renderer(_: SCNSceneRenderer, updateAtTime time: TimeInterval) {
        let timeSincePreviousUpdate = time - previousUpdateTime
        particleComponentSystem.update(deltaTime: timeSincePreviousUpdate)

        previousUpdateTime = time
    }

コンポーネントシステムのupdate(_:deltaTime)メソッドは、登録されているコンポーネントのupdate(_:deltaTime)メソッドを実行します。
previousUpdateTimeプロパティで最終更新時間を追跡していますが、特に使用していません。

ファクトリメソッドを修正

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のパーティクルを演出します。

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とは別のアルゴリズムになっているので、省略しました。
「クラス継承」アーキテクチャでは困難になりがちなゲーム設計として、覚えておきたいと思います。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away