WWDC 2017 の SceneKit サンプル Fox 2 を調べる その18
BaseComponent.swift を見てゆく。 GameplayKit の GKComponent を使用したクラスと GKAgent2D の extension で構成されてり、 ここで作成された GKComponent は PlayerComponent、ChaserComponent、ScaredComponent で継承され、一部機能追加されている。
GKComponent と GKAgent2D については過去記事参照。
その前に GameController.swift での呼び出し
今回使用している、GKComponent の値は GameController クラスで設定されているものが多いので、まずこちらを見てみる。
コンポーネントの役割
コンポーネント名 | 説明 |
---|---|
PlayerComponent | プレイヤーキャラ Max の設定 |
ChaserComponent | 追いかけてくる敵キャラの設定 |
ScaredComponent | 遠ざかる敵キャラの設定 |
ざっくり図で説明
ざっくり図で説明するとこんな感じ。
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 を見てゆく。