Apple Engine

Apple, iPhone, iOS, その周辺のことについて

WWDC 2017 の SceneKit サンプル Fox 2 を調べる その18

BaseComponent.swift を見てゆく。 GameplayKit の GKComponent を使用したクラスと GKAgent2D の extension で構成されてり、 ここで作成された GKComponent は PlayerComponent、ChaserComponent、ScaredComponent で継承され、一部機能追加されている。

GKComponent と GKAgent2D については過去記事参照。

 

appleengine.hatenablog.com

appleengine.hatenablog.com

 

その前に GameController.swift での呼び出し

今回使用している、GKComponent の値は GameController クラスで設定されているものが多いので、まずこちらを見てみる。

 

コンポーネントの役割

コンポーネント名 説明
PlayerComponent プレイヤーキャラ Max の設定
ChaserComponent 追いかけてくる敵キャラの設定
ScaredComponent 遠ざかる敵キャラの設定

 

ざっくり図で説明

ざっくり図で説明するとこんな感じ。

f:id:x67x6fx74x6f:20180530184655p:plain

BaseComponent を継承した PlayerComponent、ChaserComponent、ScaredComponent が呼ばれ、ChaserComponent、ScaredComponent の player に PlayerComponent を渡している。

 

GameController でのコード

setupEnemies 関数で的に関する振る舞いの設定がされている。

func setupEnemies() {
    ...
}

この関数の中身を見てゆく

 

敵キャラのノードの取得

enemy1 は Max が近づくと追いかける敵で、enemy2 は遠ざかる敵。

self.enemy1 = self.scene?.rootNode.childNode(withName: "enemy1", recursively: true)
self.enemy2 = self.scene?.rootNode.childNode(withName: "enemy2", recursively: true)

 

GKScene の設定

この関数内で使用する GKScene を設定。

let gkScene = GKScene()

 

PlayerComponent

PlayerComponent の GKEntity と Max のノード情報を設定。

let playerEntity = GKEntity()
gkScene.addEntity(playerEntity)
playerEntity.addComponent(GKSCNNodeComponent(node: character!.node))

 

PlayerComponent を設定。
自力で移動する必要がないの isAutoMoveNode を false。
character に Max を渡し、エンティティに playerComponent を追加、 BaseComponent で設定されている positionAgentFromNode 関数を使って GKAgent の X、Y 軸にノードの X、Z を渡している。

let playerComponent = PlayerComponent()
playerComponent.isAutoMoveNode = false
playerComponent.character = self.character
playerEntity.addComponent(playerComponent)
playerComponent.positionAgentFromNode()

 

ChaserComponent、ScaredComponent

PlayerComponent と同様にそれぞれに GKEntity を設定して、
ChaserComponent に enemy1 のノード、ScaredComponent に enemy2 のノード情報を設定。 上で設定している playerComponent をかく player 変数に渡している。

let chaserEntity = GKEntity()
gkScene.addEntity(chaserEntity)
chaserEntity.addComponent(GKSCNNodeComponent(node: self.enemy1!))
let chaser = ChaserComponent()
chaserEntity.addComponent(chaser)
chaser.player = playerComponent
chaser.positionAgentFromNode()

let scaredEntity = GKEntity()
gkScene.addEntity(scaredEntity)
scaredEntity.addComponent(GKSCNNodeComponent(node: self.enemy2!))
let scared = ScaredComponent()
scaredEntity.addComponent(scared)
scared.player = playerComponent
scared.positionAgentFromNode()

 

敵キャラのアニメーション

enemy1、enemy2 に1.4秒で Y 軸 -0.1 〜 0.1 間で上下し続けるアニメーションを設定している。
アニメーションタイミングは EaseInEaseOut(徐々に加速し始め、終わりぎわ徐々に減速する)。

let anim = CABasicAnimation(keyPath: "position")
anim.fromValue = NSValue(scnVector3: SCNVector3(0, 0.1, 0))
anim.toValue = NSValue(scnVector3: SCNVector3(0, -0.1, 0))
anim.isAdditive = true
anim.repeatCount = .infinity
anim.autoreverses = true
anim.duration = 1.2
anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)

self.enemy1!.addAnimation(anim, forKey: "")
self.enemy2!.addAnimation(anim, forKey: "")

 

GKScene の適応

クラス内で設定されている gkScene へここで作成した gkScene を渡してこの関数を終える。

self.gkScene = gkScene

 

ひとまずこんなところ。 今回の本題に移る。

 

 

BaseComponent.swift の中身

import

import GameplayKit
import SceneKit
import simd

GameplayKit は標準のフレームワークではないので、リンクとしても追加する。

 

extension GKAgent2D

今回のクラス内部で GKAgent2D 使用しているのでファイル最後らへんに書かれている extension から紹介。
GKAgent2D を使用しているが Fox2 は 3次元の座標を使用しているため、4x4 の行列 transform の実装を設定している。
キャラクターの位置と回転の状態をやりとりするため。

get では simd_quaternion で上向のベクトルと GKAgent2D の rotation を使用して Y 軸で回転する simd_quatf のクォータニオンを作成し、simd_matrix4x4 で simd_float4x4 に格納している。
simd_float4x4 を格納した transform の X、Z 座標に GKAgent2D の X Y を渡し、Y 座標は EnemyAltitude で設定した敵キャラクターの配置する高度にし return で transform を返す。

set は引数の newTransform から simd_quaternion を使用して simd_quatf にし、simd_angle と π/2 を足しマイナスの符号をつけ回転に渡している。 エージェントの X、Y の移動座標はそのまま newTransform.columns.3.x, newTransform.columns.3.z から取得している。

extension GKAgent2D {
    
    var transform: matrix_float4x4 {
        get {
            let quat = simd_quaternion(-Float(rotation - (.pi / 2)), simd_make_float3(0, 1, 0))
            var transform: simd_float4x4 = simd_matrix4x4(quat)
            transform.columns.3 = simd_make_float4(self.position.x, BaseComponent.EnemyAltitude, self.position.y, 1)
            return transform
        }
        set(newTransform) {
            let quatf: simd_quatf = simd_quaternion(newTransform)
            self.rotation = -(simd_angle(quatf) + (.pi / 2))
            self.position = simd_float2(newTransform.columns.3.x, newTransform.columns.3.z)
        }
    }

}

 

BaseComponent クラス

class BaseComponent: GKComponent {
    ...
}

 

GKComponent を継承した BaseComponent で設定されているものは以下のもの。

  • 敵キャラクタの現在位置の高度
  • エージェント用の GKAgent2D
  • 自動的に動くか判別するフラグ isAutoMoveNode
  • 敵キャラが死んでいるか調べる isDead 関数
  • エージェントからのノードまたはその逆を設定する2つの関数
  • 敵キャラの動きを制限する constrainPosition 関数
  • コンポーネントのアップデート関数
  • 敵キャラが攻撃を受けた際の爆発処理

 

メンバー変数

敵の高度、このコンポーネンに紐づいたエージェント、敵キャラクターが自動で動くようにするフラグ。
プレイヤーキャラクター Max に設定する BaseComponent を継承した PlayerComponent は isAutoMoveNode を false に設定している

public static let EnemyAltitude: Float = -0.46

private(set) var agent = GKAgent2D()
public var isAutoMoveNode = true

 

isDead()

Bool で false を返す。

ChaserComponent、ScaredComponent では状態を調べで生きているか死んでいるかを返す。

func isDead() -> Bool {
    return false
}

 

positionAgentFromNode()

このコンポーネントに紐づいているノードの simdTransform をこのファイルで拡張した agent(GKAgent2D) の transform に渡している。

 

positionNodeFromAgent()

このファイルで拡張した agent(GKAgent2D) の transform をこのコンポーネントに紐づいているノードの simdTransform へ渡している。

 

constrainPosition()

このクラスで設定している agent(GKAgent2D)の移動範囲の上限をこの関数で決めている。
敵が配置されている範囲から出て行ってしまわないため。

func constrainPosition() {
    var position = agent.position
    if position.x > 2 {
        position.x = 2
    }
    if position.x < -2 {
        position.x = -2
    }
    if position.y > 12.5 {
        position.y = 12.5
    }
    if position.y < 8.5 {
        position.y = 8.5
    }
    agent.position = position
}

 

override func update(deltaTime seconds: TimeInterval)

SCNSceneRendererDelegate の renderer 関数がフレーム毎にコンポーネントを読んだ時の処理。
ちなみに継承の際、オーバーライドで書き換えられるため、ここの処理は使われることはない。

以下、処理内容。

  • isDead 関数が true を返す場合はこの処理を抜ける
  • agent の update 関数を呼ぶ
  • isAutoMoveNode が true なら positionNodeFromAgent() を渡し agent の移動座標をノードに渡す。
  • スーパークラスのアップデートを呼ぶ

 

override func update(deltaTime seconds: TimeInterval) {
    if self.isDead() {
        return
    }

    agent.update(deltaTime: seconds)
    constrainPosition()
    if isAutoMoveNode {
        positionNodeFromAgent()
    }
    super.update(deltaTime: seconds)
}

 

internal func performEnemyDieWithExplosion(_ enemy: SCNNode, direction: simd_float3)

敵が攻撃を喰らい爆発する際の処理。

explositionScene に爆発のパーティクルを呼ぶ。

SCNTransaction で 0.4 秒の EaseOut(終わりの方がゆっくりになる)でアニメーションを設定。
以下、アニメーションの処理。

  • 引数の direction はキャラクタの進行方向で Y 座標を 0 に設定
  • 攻撃が当たった的である 引数 enemy ノードのアニメーションを全て消す
  • enemy の eulerAngles の Y 軸 に現在の回転に対して π*4 で2周回転させる
  • direction を simd_normalize 正規化し 1.5 をかけ、Max の進行方向に敵を押し出すように移動させる
  • enemy の移動位置を agent に渡す。

 

アニメーション完了時に、敵キャラへ爆発のパーティクルを設定とともに、敵キャラの透明度を 0 にする。

 

internal func performEnemyDieWithExplosion(_ enemy: SCNNode, direction: simd_float3) {
    guard let explositionScene = SCNScene(named: "Art.scnassets/enemy/enemy_explosion.scn") else {
        print("Missing enemy_explosion.scn")
        return
    }

    SCNTransaction.begin()
    SCNTransaction.animationDuration = 0.4
    SCNTransaction.animationTimingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)

    SCNTransaction.completionBlock = {
        explositionScene.rootNode.enumerateHierarchy({ (node: SCNNode, _: UnsafeMutablePointer<ObjCBool>) in
            guard let particles = node.particleSystems else { return }
            for particle in particles {
                enemy.addParticleSystem(particle)
            }
        })

        // Hide
        enemy.childNodes.first?.opacity = 0.0
    }

    var direction = direction
    direction.y = 0
    enemy.removeAllAnimations()
    enemy.eulerAngles =
        SCNVector3Make(enemy.eulerAngles.x, enemy.eulerAngles.x + .pi * 4.0, enemy.eulerAngles.z)
    enemy.simdWorldPosition += simd_normalize(direction) * 1.5
    positionAgentFromNode()

    SCNTransaction.commit()
}

 

次回は PlayerComponent.swift を見てゆく。