Apple Engine

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

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

引き続き GameController クラスの残りの関数を見てゆく。

今回はプレイヤーキャラクター Max の色違いのレッサーパンダの仲間の初期設定と助け出した時の処理。
鍵を開けて助ける処理は次回。

 

助け出した仲間のフレーム毎の処理

friendsAreFree が true になり仲間を助けた際に、SCNSceneRenderer のデリゲートで毎フレーム呼ばれる処理。

全ての仲間に対して、pathCurve と Z 座標を元に X 方向のオフセットを作成。 addFriends 関数で作成される仲間のスピードと経過時間、0.5をかけ Z 座標に設定し全身させる。

ensureNoPenetrationOfIndex 関数で各キャラの重なりを防ぐ。

func updateFriends(deltaTime: CFTimeInterval) {
    let pathCurve: Float = 0.4

    for i in 0..<friendCount {
        let friend = friends[i]

        var pos = friend.simdPosition
        let offsetx = pos.x - sinf(pathCurve * pos.z)

        pos.z += friendsSpeed[i] * Float(deltaTime) * 0.5
        pos.x = sinf(pathCurve * pos.z) + offsetx

        friend.simdPosition = pos

        ensureNoPenetrationOfIndex(i)
    }
}

 

助け出した仲間のアニメーション処理

仲間の Max のアニメーション設定。

シーンファイル max_walk.scn から歩行のアニメーションを取得し、 SCNAnimationPlayer の speed に各 friendsSpeed を入れアニメーションさせる。

func animateFriends() {
    let walkAnimation = Character.loadAnimation(fromSceneNamed: "Art.scnassets/character/max_walk.scn")

    SCNTransaction.begin()
    for i in 0..<friendCount {
        //unsynchronize
        let walk = walkAnimation.copy() as! SCNAnimationPlayer
        walk.speed = CGFloat(friendsSpeed[i])
        friends[i].addAnimationPlayer(walk, forKey: "walk")
        walk.play()
    }
    SCNTransaction.commit()
}

 

画面上に仲間を追加する

addFriends はイニシャライズに呼ばれ、3 が設定されている。 その後 unlockDoor 関数で NumberOfFiends が設定される。

func addFriends(_ count: Int) {
    ...
}

中身が多いなため分けてみてゆく。

 

関数内の変数 count を設定

count に friendCount を足し NumberOfFiends 大きければ処理をする。

初期状態は friendCount は 0 なのでこの命令は通らず、unlockDoor 関数が呼ばれた時初めて動く。

var count = count
if count + friendCount > GameController.NumberOfFiends {
    count = GameController.NumberOfFiends - friendCount
}

関数内の変数 count を設定

3種類の異なる色を持つ仲間の Max の設定。

シーンファイル max.scn から Max を取り出し、friend という名前を設定後、コピーした3つジオメトリを配列に設定。 テクスチャを3つ設定し、元の Max のジオメトリからマテリアルをコピーし割り当てる。

自動で動き、何も接触しないので、ジオメトリを入れた配列の全てノードを調べ物理シミュレーション処理を省く。

let friendScene = SCNScene(named: "Art.scnassets/character/max.scn")
guard let friendModel = friendScene?.rootNode.childNode(withName: "Max_rootNode", recursively: true) else { return }
friendModel.name = "friend"

var textures = [String](repeating: "", count: 3)
textures[0] = "Art.scnassets/character/max_diffuseB.png"
textures[1] = "Art.scnassets/character/max_diffuseC.png"
textures[2] = "Art.scnassets/character/max_diffuseD.png"

var geometries = [SCNGeometry](repeating: SCNGeometry(), count: 3)
guard let geometryNode = friendModel.childNode(withName: "Max", recursively: true) else { return }

geometryNode.geometry!.firstMaterial?.diffuse.intensity = 0.5

geometries[0] = geometryNode.geometry!.copy() as! SCNGeometry
geometries[1] = geometryNode.geometry!.copy() as! SCNGeometry
geometries[2] = geometryNode.geometry!.copy() as! SCNGeometry

geometries[0].firstMaterial = geometries[0].firstMaterial?.copy() as? SCNMaterial
geometryNode.geometry?.firstMaterial?.diffuse.contents = "Art.scnassets/character/max_diffuseB.png"

geometries[1].firstMaterial = geometries[1].firstMaterial?.copy() as? SCNMaterial
geometryNode.geometry?.firstMaterial?.diffuse.contents = "Art.scnassets/character/max_diffuseC.png"

geometries[2].firstMaterial = geometries[2].firstMaterial?.copy() as? SCNMaterial
geometryNode.geometry?.firstMaterial?.diffuse.contents = "Art.scnassets/character/max_diffuseD.png"

friendModel.enumerateHierarchy({(_ node: SCNNode, _ _: UnsafeMutablePointer<ObjCBool>) -> Void in
    node.physicsBody = nil
})

 

初期設定

friendPosition で初期位置、 FRIEND_AREA_LENGTH で各仲間の Z 軸方向の配置を設定。

上の方で friend とつけたジオメトリをシーン内から探し friendsNode へ渡す。 friendsNode がない場合は空の SCNNode を設定する

let friendPosition = simd_make_float3(-5.84, -0.75, 3.354)
let FRIEND_AREA_LENGTH: Float = 5.0

var friendsNode: SCNNode? = scene!.rootNode.childNode(withName: "friends", recursively: false)
if friendsNode == nil {
    friendsNode = SCNNode()
    friendsNode!.name = "friends"
    scene!.rootNode.addChildNode(friendsNode!)
}

 

仲間の設定

シーンファイル max_idle.scn からアイドル状態(立った状態)のアニメーションを取得。

for を count 分だけ以下の処理をする。

  • friend に friendModel をコピー。
  • geometryIndex に 0〜2 の幅でランダムに値を入れる
  • このループの geometryNode に friend のチルドノード Max を渡し friend の中身を編集する。
  • この関数の中でつくられた geometries の配列からランダムの数を入れている geometryIndex を添字にして、先ほどの geometryNode.geometry に入れる
  • ランダムで friend ノードの位置を設定。
  • idle にアイドル状態のアニメーションを複製し、speed をランダム値を入れ再生。
  • friendsSpeed 配列に idle の speed を入れる
  • friendsNode 配列に friend を入れる
  • friendCount をインクリメントして数を増やす
let idleAnimation = Character.loadAnimation(fromSceneNamed: "Art.scnassets/character/max_idle.scn")

for _ in 0..<count {
    let friend = friendModel.clone()

    let geometryIndex = Int(arc4random_uniform(UInt32(3)))
    guard let geometryNode = friend.childNode(withName: "Max", recursively: true) else { return }
    geometryNode.geometry = geometries[geometryIndex]

    //place our friend
    friend.simdPosition = simd_make_float3(
        friendPosition.x + (1.4 * (Float(arc4random_uniform(UInt32(RAND_MAX))) / Float(RAND_MAX)) - 0.5),
        friendPosition.y,
        friendPosition.z - (FRIEND_AREA_LENGTH * (Float(arc4random_uniform(UInt32(RAND_MAX))) / Float(RAND_MAX))))

    //unsynchronize
    let idle = (idleAnimation.copy() as! SCNAnimationPlayer)
    idle.speed = CGFloat(Float(1.5) + Float(1.5) * Float(arc4random_uniform(UInt32(RAND_MAX))) / Float(RAND_MAX))

    friend.addAnimationPlayer(idle, forKey: "idle")
    idle.play()
    friendsNode?.addChildNode(friend)

    self.friendsSpeed[friendCount] = Float(idle.speed)
    self.friends[friendCount] = friend
    self.friendCount += 1
}

 

ぶつからない様に仲間を並べる

friendCount の数まで ensureNoPenetrationOfIndex を呼び、 ぶつからない様に仲間を並べる。

for i in 0..<friendCount {
    ensureNoPenetrationOfIndex(i)
}

 

addFriends はここでおしまい。

 

ぶつからない様に位置を変更する関数

引数の index 元に friends の配列からノードの位置を設定する。 for 文から friends の配列を指定し、Max のジオメトリの有効範囲の直径を元に、全ての friends 配列から位置を比較する。 Z 舳 が 3.354 より少ない場合は pos.x 軸を調整する。

func ensureNoPenetrationOfIndex(_ index: Int) {
    var pos = friends[index].simdPosition

    let pandaRadius: Float = 0.15
    let pandaDiameter = pandaRadius * 2.0
    for j in 0..<friendCount {
        if j == index {
            continue
        }

        let otherPos = float3(friends[j].position)
        let v = otherPos - pos
        let dist = simd_length(v)
        if dist < pandaDiameter {
            let pen = pandaDiameter - dist
            pos -= simd_normalize(v) * pen
        }
    }

    if friends[index].position.z <= 3.354 {
        pos.x = max(pos.x, -6.662)
        pos.x = min(pos.x, -4.8)
    }
    friends[index].simdPosition = pos
}

次回は鍵を開けるなどゲームでのアクションの関数を見てゆく。

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

今回は GameController クラスの残りの関数を見てゆく。

 

プレイヤーキャラクター Max の初期位置リセット

Max がマップから落ちた時など、resetPlayerPosition() から Character クラスの queueResetCharacterPosition() を呼ぶ。

func resetPlayerPosition() {
    character!.queueResetCharacterPosition()
}

 

カメラ処理

宝石のアイテム取得、仲間を助けた時の画面処理と Max の停止を行う関数。

startCinematic() は playingCinematic を true にして isPaused で Max の動作を止める。
startCinematic() その反対をする。

func startCinematic() {
    playingCinematic = true
    character!.node!.isPaused = true
}

func stopCinematic() {
    playingCinematic = false
    character!.node!.isPaused = false
}

   

パーティクル処理

particleSystems(with kind: ParticleKind) で ParticleKind no enum から particleSystems 呼び出す処理で、次の addParticles 関数で使用。

addParticle はアイテム取得や仲間を助ける際に表示するパーティクルで、引数の ParticleKind と 座標値を元にシーンに配置している。

func particleSystems(with kind: ParticleKind) -> [SCNParticleSystem] {
    return particleSystems[kind.rawValue]
}

func addParticles(with kind: ParticleKind, withTransform transform: SCNMatrix4) {
    let particles = particleSystems(with: kind)
    for ps: SCNParticleSystem in particles {
        scene!.addParticleSystem(ps, transform: transform)
    }
}

 

トリガー

Max が透明度 0 で見えなくなっている立方体に接触するとトリガーが弾かれカメラ変更が行われ視点が変わるようになっている。   基本的には、実行部分である execTrigger 関数が trigger 関数から呼ばれ動作する。
鍵のアイテム出現後に元の位置へ戻る際、キャラクターが止まっておりトリガーが弾けないので、その時だけ execTrigger 関数を直接叩いている。

 

execTrigger

カメラ視点を買えるものと仲間を助けるトリガーが設定している。

トリガーのノード、アニメーション時間を引数とし、トリガーノード名が trigCam〜 の場合は "trigCam" を消したものをカメラの名前として setActiveCamera からアニメーション時間とともに呼び出す。

トリガーノード名が trigAction〜 で鍵のアイテムが取得されている場合は アクションの名前から "trigAction" 取り除く。
取り除いた文字列が "unlockDoor" なら unlockDoor() を呼び出し、仲間を助ける。

func execTrigger(_ triggerNode: SCNNode, animationDuration duration: CFTimeInterval) {
    if triggerNode.name!.hasPrefix("trigCam_") {
        let cameraName = (triggerNode.name as NSString?)!.substring(from: 8)
        setActiveCamera(cameraName, animationDuration: duration)
    }
    if triggerNode.name!.hasPrefix("trigAction_") {
        if collectedKeys > 0 {
            let actionName = (triggerNode.name as NSString?)!.substring(from: 11)
            if actionName == "unlockDoor" {
                unlockDoor()
            }
        }
    }
}

 

trigger(_ triggerNode: SCNNode)

playingCinematic が true の場合はトリガーの処理は行わない。

保存した最後のトリガーノード保持している lastTrigger と 現在のトリガーノードが異なっていたら lastTrigger に現在のトリガーノードを保存。

execTrigger を実行し、トリガーが引かれたこと判別する firstTriggerDone を true する。

func trigger(_ triggerNode: SCNNode) {
    if playingCinematic {
        return
    }
    if lastTrigger != triggerNode {
        lastTrigger = triggerNode

        execTrigger(triggerNode, animationDuration: firstTriggerDone ? GameController.DefaultCameraTransitionDuration: 0)
        firstTriggerDone = true
    }
}

 

次回に続く。

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

今回は GameController クラスのイニシャライズ関数を見てゆく。
コードのほとんどがこれまで紹介した関数を呼ぶ形となっている。

 

init(scnView: SCNView)

各プラットフォームで SCNView を受け取り、それに変更を加えてゆく。

init(scnView: SCNView) {
    super.init()
    
    ...
}

 

中身を見てゆく  

sceneRenderer

sceneRenderer に scnView を渡し、SCNSceneRenderer のデリゲートを設定し、 FPS など詳細を表示する showsStatistics はコメントアウトされている。

sceneRenderer = scnView
sceneRenderer!.delegate = self

//scnView.showsStatistics = true

 

最前面に表示されるオーバーレイに Overlay クラスへ画面サイズと controller 用に自身を渡す。

overlay = Overlay(size: scnView.bounds.size, controller: self)
scnView.overlaySKScene = overlay

 

scene.scn をこれを使用する SCNView のシーンへ渡す。

self.scene = SCNScene(named: "Art.scnassets/scene.scn")

 

物理シミュレーション、コリジョン、キャラクター設定、敵キャラ、仲間、動く橋、パーティクルの設定を行う。

setupPhysics()
setupCollisions()
setupCharacter()
setupEnemies()
addFriends(3)
setupPlatforms()
setupParticleSystem()

 

ライトの設定。
ディレクショナルライトのノードを取得し、512x512 のシャドウマップを作成し、shadowCascade の設定をする。

let light = scene!.rootNode.childNode(withName: "DirectLight", recursively: true)!.light
light!.shadowCascadeCount = 3
light!.shadowMapSize = CGSize(width: CGFloat(512), height: CGFloat(512))
light!.maximumShadowDistance = 20
light!.shadowCascadeSplittingFactor = 0.5

 

カメラとゲームコントローラーの設定をする

setupCamera()
setupGameController()

 

このファイルで設定されている configureRenderingQuality 関数を呼ぶ。
tvOS での動作時で表示のクオリティを下げている。

configureRenderingQuality(scnView)

 

sceneRenderer のシーンにここで設定されているシーンを渡すことで、
プラットフォーム別の SCNView 側の scene と GameController の scene が同じものとなる。

sceneRenderer!.scene = self.scene

 

オーディオの設定、このシーンのカメラである cameraNode を SCNView の pointOfView に適応。

scene の physicsWorld の contactDelegate を設定している。

setupAudio()

sceneRenderer!.pointOfView = self.cameraNode

sceneRenderer!.scene!.physicsWorld.contactDelegate = self

 

次回は GameController クラスのその他の関数を見てゆく。

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

GameController クラスのセットアップ関数を見てゆく。
今回は音を設定しているオーディオ設定。

 

playSound(_ audioName: AudioSourceKind)

AudioSourceKind の enum で指定されているオーディオソースの添字を playSound 関数に渡す。

指定した添字から音を audioSources の配列から探し、 SCNAudioPlayer をシーンに addAudioPlayer で追加するし音を再生する。

func playSound(_ audioName: AudioSourceKind) {
    scene!.rootNode.addAudioPlayer(SCNAudioPlayer(source: audioSources[audioName.rawValue]))
}

 

setupAudio()

BGM や SE の音を設定する。

  • ここで設定している node に rootNode を渡す
  • audioSource に ambience.mp3 を渡し、BGM として再生する
  • volcanoNode に particles_volcanoSmoke_v2 の名前のノードを渡し、audioSource として volcano.mp3 を設定する。isPositional の設定をしていないため位置によって音が変化する。
  • collect.mp3、collectBig.mp3、unlockTheDoor.m4a、hitEnemy.wav を audioSources として audioSources の配列に入れ、各種設定する。
func setupAudio() {
    let node = scene!.rootNode

    if let audioSource = SCNAudioSource(named: "audio/ambience.mp3") {
        audioSource.loops = true
        audioSource.volume = 0.8
        audioSource.isPositional = false
        audioSource.shouldStream = true
        node.addAudioPlayer(SCNAudioPlayer(source: audioSource))
    }
    
    if let volcanoNode = scene!.rootNode.childNode(withName: "particles_volcanoSmoke_v2", recursively: true) {
        if let audioSource = SCNAudioSource(named: "audio/volcano.mp3") {
            audioSource.loops = true
            audioSource.volume = 5.0
            volcanoNode.addAudioPlayer(SCNAudioPlayer(source: audioSource))
        }
    }

    audioSources[AudioSourceKind.collect.rawValue] = SCNAudioSource(named: "audio/collect.mp3")!
    audioSources[AudioSourceKind.collectBig.rawValue] = SCNAudioSource(named: "audio/collectBig.mp3")!
    audioSources[AudioSourceKind.unlockDoor.rawValue] = SCNAudioSource(named: "audio/unlockTheDoor.m4a")!
    audioSources[AudioSourceKind.hitEnemy.rawValue] = SCNAudioSource(named: "audio/hitEnemy.wav")!

    audioSources[AudioSourceKind.unlockDoor.rawValue].isPositional = false
    audioSources[AudioSourceKind.collect.rawValue].isPositional = false
    audioSources[AudioSourceKind.collectBig.rawValue].isPositional = false
    audioSources[AudioSourceKind.hitEnemy.rawValue].isPositional = false

    audioSources[AudioSourceKind.unlockDoor.rawValue].volume = 0.5
    audioSources[AudioSourceKind.collect.rawValue].volume = 4.0
    audioSources[AudioSourceKind.collectBig.rawValue].volume = 4.0
}

 

次回は GameController クラスのイニシャライズの関数を見てゆく。

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

GameController クラスのセットアップ関数を見てゆく。 今回は敵キャラとパーティクルの設定

 

setupEnemies()

敵の設定。

こちらは以前に紹介しているので割愛。

appleengine.hatenablog.com

 

loadParticleSystems(atPath path: String)

パーティクルセットアップの際に .scn(シーンファイル) や .scnp(パーティクルシステムファイル) から読み込みを容易にするための関数。ファイルパスを指定し呼び出すとパーティクルシステムが入った配列を返す。

引数 path でファイルパスを文字列でとって、ファイル名とディレクトリを使用し、拡張子がパーティクルシステムファイルの場合はそのまま1つのパーティクルが入った配列として返し、シーンファイルの場合はしシーンを全て調べ、見つかったパーティクルを全て配列に入れ返す。

func loadParticleSystems(atPath path: String) -> [SCNParticleSystem] {
    let url = URL(fileURLWithPath: path)
    let directory = url.deletingLastPathComponent()

    let fileName = url.lastPathComponent
    let ext: String = url.pathExtension

    if ext == "scnp" {
        return [SCNParticleSystem(named: fileName, inDirectory: directory.relativePath)!]
    } else {
        var particles = [SCNParticleSystem]()
        let scene = SCNScene(named: fileName, inDirectory: directory.relativePath, options: nil)
        scene!.rootNode.enumerateHierarchy({(_ node: SCNNode, _ _: UnsafeMutablePointer<ObjCBool>) -> Void in
            if node.particleSystems != nil {
                particles += node.particleSystems!
            }
        })
        return particles
    }
}

 

setupParticleSystem()

パーティクルのセットアップ関数。

particleSystems に ParticleKind の enum を添字として、上の loadParticleSystems 関数からパーティクルを格納する。

func setupParticleSystem() {
    particleSystems[ParticleKind.collect.rawValue] = loadParticleSystems(atPath: "Art.scnassets/particles/collect.scnp")
    particleSystems[ParticleKind.collectBig.rawValue] = loadParticleSystems(atPath: "Art.scnassets/particles/key_apparition.scn")
    particleSystems[ParticleKind.enemyExplosion.rawValue] = loadParticleSystems(atPath: "Art.scnassets/particles/enemy_explosion.scn")
    particleSystems[ParticleKind.keyApparition.rawValue] = loadParticleSystems(atPath: "Art.scnassets/particles/key_apparition.scn")
    particleSystems[ParticleKind.unlockDoor.rawValue] = loadParticleSystems(atPath: "Art.scnassets/particles/unlock_door.scn")
}

 

func setupPlatforms()

閉じ込められている仲間の小屋の前の溶岩にある動く橋(mobilePlatform)のアニメーションとパーティクル(particles_platform)の設定。

SCNAction で橋を動かし、パーティクルの orientationDirection を Y 軸 1 の方向に向かせるようにしている。

func setupPlatforms() {
    let PLATFORM_MOVE_OFFSET = Float(1.5)
    let PLATFORM_MOVE_SPEED = Float(0.5)

    var alternate: Float = 1
    
    scene!.rootNode.enumerateHierarchy({(_ node: SCNNode, _ _: UnsafeMutablePointer<ObjCBool>) -> Void in
        if node.name == "mobilePlatform" && !node.childNodes.isEmpty {
            node.simdPosition = simd_float3(
                node.simdPosition.x - (alternate * PLATFORM_MOVE_OFFSET / 2.0), node.simdPosition.y, node.simdPosition.z)

            let moveAction = SCNAction.move(by: SCNVector3(alternate * PLATFORM_MOVE_OFFSET, 0, 0),
                                            duration: TimeInterval(1 / PLATFORM_MOVE_SPEED))
            moveAction.timingMode = .easeInEaseOut
            node.runAction(SCNAction.repeatForever(SCNAction.sequence([moveAction, moveAction.reversed()])))

            alternate = -alternate

            node.enumerateChildNodes({ (_ child: SCNNode, _ _: UnsafeMutablePointer<ObjCBool>) in
                if child.name == "particles_platform" {
                    child.particleSystems?[0].orientationDirection = SCNVector3(0, 1, 0)
                }
            })
        }
    })
}

 

mobilePlatform

particles_platform

 

次回は GameController クラスのオーディオ設定を見てゆく。

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

GameController クラスのセットアップ関数を見てゆく。
今回はカメラ設定

 

Fox2 では固定カメラの他に、プレイヤーキャラクターである Max を追尾するカメラがある。

追尾するカメラは、カメラ視点が変えられるものと、変えられないものの2つある。

 

紹介順の変更

GameController.swift で書かれている内容が前後するが、setupCamera、setupCameraNode、setActiveCamera 関数を先に見てゆく。

 

setupCamera()

イニシャライズに呼ばれるカメラ設定。

self を weakSelf として weak で設定し、lookAtTarget のコンストレイントで、カスタムのコンストレイント作成。

strongSelf に weakSelf を渡し、self がなければ、そのままの position を返し weakSelf が破棄される。
self があった場合は、character の baseAltitude でプレイヤーキャラクター Max の基準となるの高度 に 0.5 足したものに固定するように返すコンストレイントをつくる。

カメラは Max に対して SCNLookAtConstraint などのコンストレインとが設定されている。
ここで作成したコンストレイントは Max がジャンプした際に他のコンストレイントによってカメラが追従してしまうのを防ぐための処置。 このコンストレイントを使用せずに、そのままの Max の座標を渡すとジャンプ時にカメラも Y 軸方向に動いてしまう。

あとは、lookAtTarget をシーンに追加し、シーン中の全てのカメラに setupCameraNode() を適応、 cameraNode の設定とシーンに追加、最初に表示するカメラを setActiveCamera 関数でアニメーション処理をしている  

func setupCamera() {
    weak var weakSelf = self

    self.lookAtTarget.constraints = [ SCNTransformConstraint.positionConstraint(
                                    inWorldSpace: true, with: { (_ node: SCNNode, _ position: SCNVector3) -> SCNVector3 in
        guard let strongSelf = weakSelf else { return position }

        guard var worldPosition = strongSelf.character?.node?.simdWorldPosition else { return position }
        worldPosition.y = strongSelf.character!.baseAltitude + 0.5
        return SCNVector3(worldPosition)
    })]

    self.scene?.rootNode.addChildNode(lookAtTarget)

    self.scene?.rootNode.enumerateHierarchy({(_ node: SCNNode, _ _: UnsafeMutablePointer<ObjCBool>) -> Void in
        if node.camera != nil {
            self.setupCameraNode(node)
        }
    })

    self.cameraNode.camera = SCNCamera()
    self.cameraNode.name = "mainCamera"
    self.cameraNode.camera!.zNear = 0.1
    self.scene!.rootNode.addChildNode(cameraNode)

    setActiveCamera("camLookAt_cameraGame", animationDuration: 0.0)
}

 

setupCameraNode(_ node: SCNNode)

この関数はカメラのノードを受け取った際に、そのノードの名前を取得する。

名前が camTrav〜 で始まる場合は setupAxisAlignedCamera 関数を呼び、
camLookAt〜 で始まる場合は setupFollowCamera 関数を呼ぶ。

func setupCameraNode(_ node: SCNNode) {
    guard let cameraName = node.name else { return }

    if cameraName.hasPrefix("camTrav") {
        setupAxisAlignedCamera(node)
    } else if cameraName.hasPrefix("camLookAt") {
        setupFollowCamera(node)
    }
}

 

setActiveCamera

setActiveCamera は現在のカメラから新しいカメラへの変更と移動のアニメーション、HDR がらみのカメラ設定の変更を行なっている。1つの引数と、2つの引数のものが用意されている、

1つの引数のものはアニメーションの初期値を GameController の DefaultCameraTransitionDuration 固定し、2つの引数のものを再度呼び出している。

func setActiveCamera(_ cameraName: String) {
    setActiveCamera(cameraName, animationDuration: GameController.DefaultCameraTransitionDuration)
}

 

でもって、この関数の実態と処理。

  • 引数のカメラの名前から新しいカメラを設定(camera)
  • 現在アクティブなカメラと引数から得た新しいカメラが同じなら関数を抜ける
  • 最終後に使用したカメラとして現在アクティブなカメラを lastActiveCamera に設定
  • アクティブなカメラがあれば lastActiveCameraFrontDirection にアクティブなカメラの前面方向のベクトルを渡す。
  • activeCamera に引数から得た新しいカメラ camera を渡して、camera をアクティブにする
  • oldTransform に現在のカメラの位置を渡す
  • camera に cameraNode を追加
  • camera の presentation の位置を parentTransform に渡し、parentInv に逆行列を渡す
  • cameraNode を SCNMatrix4Mult を使って過去のカメラの親に移動
  • SCNTransaction でアニメーション設定し、cameraNode.transform を 0 に戻す
  • 被写界深度やブルームなどの新しいカメラ設定を渡す
  • SCNTransaction の設定を終了しアニメーションを開始する
func setActiveCamera(_ cameraName: String, animationDuration duration: CFTimeInterval) {
    guard let camera = scene?.rootNode.childNode(withName: cameraName, recursively: true) else { return }
    if self.activeCamera == camera {
        return
    }

    self.lastActiveCamera = activeCamera
    if activeCamera != nil {
        self.lastActiveCameraFrontDirection = (activeCamera?.presentation.simdWorldFront)!
    }
    self.activeCamera = camera

    let oldTransform: SCNMatrix4 = cameraNode.presentation.worldTransform

    camera.addChildNode(cameraNode)

    let parentTransform = camera.presentation.worldTransform
    let parentInv = SCNMatrix4Invert(parentTransform)

    cameraNode.transform = SCNMatrix4Mult(oldTransform, parentInv)

    SCNTransaction.begin()
    SCNTransaction.animationDuration = duration
    SCNTransaction.animationTimingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
    cameraNode.transform = SCNMatrix4Identity

    if let cameraTemplate = camera.camera {
        cameraNode.camera!.fieldOfView = cameraTemplate.fieldOfView
        cameraNode.camera!.wantsDepthOfField = cameraTemplate.wantsDepthOfField
        cameraNode.camera!.sensorHeight = cameraTemplate.sensorHeight
        cameraNode.camera!.fStop = cameraTemplate.fStop
        cameraNode.camera!.focusDistance = cameraTemplate.focusDistance
        cameraNode.camera!.bloomIntensity = cameraTemplate.bloomIntensity
        cameraNode.camera!.bloomThreshold = cameraTemplate.bloomThreshold
        cameraNode.camera!.bloomBlurRadius = cameraTemplate.bloomBlurRadius
        cameraNode.camera!.wantsHDR = cameraTemplate.wantsHDR
        cameraNode.camera!.wantsExposureAdaptation = cameraTemplate.wantsExposureAdaptation
        cameraNode.camera!.vignettingPower = cameraTemplate.vignettingPower
        cameraNode.camera!.vignettingIntensity = cameraTemplate.vignettingIntensity
    }
    SCNTransaction.commit()
}

 

func setupFollowCamera(_ cameraNode: SCNNode)

キャラに付いてゆくカメラ。
以下の中身を見ていく。

func setupFollowCamera(_ cameraNode: SCNNode) {
    ...
}

 

Max へカメラを振り向かせるコンストレイント。
lookAtTarget を元に SCNLookAtConstraint 設定する。isGimbalLockEnabled が true であるためロール方向の回転を制限する。

let lookAtConstraint = SCNLookAtConstraint(target: self.lookAtTarget)
lookAtConstraint.influenceFactor = 0.07
lookAtConstraint.isGimbalLockEnabled = true

 

位置のコンストレイント。
follow に lookAtTarget を元に SCNDistanceConstraint 設定。
distance へ simd_length 使用して cameraNode.simdPosition から距離を設定し follow の最大値と最小値の距離を設定する。

let follow = SCNDistanceConstraint(target: self.lookAtTarget)
let distance = CGFloat(simd_length(cameraNode.simdPosition))
follow.minimumDistance = distance
follow.maximumDistance = distance

 

Max に対して一定の高度を維持するようにコンストレイントを設定する。
cameraNode.simdWorldPosition の Y 軸の絶対値を desiredAltitude に渡し、 カスタムのコンストレイント keepAltitude で desiredAltitude を使用し高さを固定する 。

let desiredAltitude = abs(cameraNode.simdWorldPosition.y)
weak var weakSelf = self

let keepAltitude = SCNTransformConstraint.positionConstraint(inWorldSpace: true, with: {(_ node: SCNNode, _ position: SCNVector3) -> SCNVector3 in
        guard let strongSelf = weakSelf else { return position }
        var position = float3(position)
        position.y = strongSelf.character!.baseAltitude + desiredAltitude
        return SCNVector3( position )
    })

 

Max 移動した際にカメラの加速度を設定するコンストレイント

let accelerationConstraint = SCNAccelerationConstraint()
accelerationConstraint.maximumLinearVelocity = 1500.0
accelerationConstraint.maximumLinearAcceleration = 50.0
accelerationConstraint.damping = 0.05

 

ユーザー Max を操作するとそれに合わせて徐々に回り込むカスタムのコンストレインとを設定する。

  • activeCamera のノードを取り、上で設定した accelerationConstraint の influenceFactor で加速度の影響を上限 1 で 0.01 ずつ増加させる。
  • targetPosition で lookAtTarget の presentation の simdWorldPosition、cameraDirection で cameraDirection を取得。
  • cameraDirection.allZero() で座標を調べ、全て 0 ならカメラ位置を返し関数を抜ける。
  • accelerationConstraint の influenceFactor を 0 にして、accelerationConstraint 止める。
  • characterWorldUp に character の presentation の simdWorldUp を渡す。
  • transformNode の transform に、このコンストレイントの transform 渡す。
  • characterWorldUp を元に CameraOrientationSensitivity に cameraDirection.x にかけたものを simd_quaternion として適応。
  • transformNode.simdWorldRight を元に CameraOrientationSensitivity に cameraDirection.x にかけたものを simd_quaternion として適応。
  • simd_quaternion 2つで積を出し、transformNode の simdRotate で targetPosition を中心に回転させる
  • transformNode.transform をこのコンストレイントの値として返す。
let transformNode = SCNNode()
let orientationUpdateConstraint = SCNTransformConstraint(inWorldSpace: true) { (_ node: SCNNode, _ transform: SCNMatrix4) -> SCNMatrix4 in
    guard let strongSelf = weakSelf else { return transform }
    if strongSelf.activeCamera != node {
        return transform
    }

    accelerationConstraint.influenceFactor = min(1, accelerationConstraint.influenceFactor + 0.01)

    let targetPosition = strongSelf.lookAtTarget.presentation.simdWorldPosition
    let cameraDirection = strongSelf.cameraDirection
    if cameraDirection.allZero() {
        return transform
    }

    accelerationConstraint.influenceFactor = 0

    let characterWorldUp = strongSelf.character?.node?.presentation.simdWorldUp

    transformNode.transform = transform

    let q = simd_mul(
        simd_quaternion(GameController.CameraOrientationSensitivity * cameraDirection.x, characterWorldUp!),
        simd_quaternion(GameController.CameraOrientationSensitivity * cameraDirection.y, transformNode.simdWorldRight)
    )

    transformNode.simdRotate(by: q, aroundTarget: targetPosition)
    return transformNode.transform
}

 

カメラのコンストレイントとして、設定した follow, keepAltitude, accelerationConstraint, orientationUpdateConstraint, lookAtConstraint を適応する。

cameraNode.constraints = [follow, keepAltitude, accelerationConstraint, orientationUpdateConstraint, lookAtConstraint]

 

func setupAxisAlignedCamera(_ cameraNode: SCNNode)

キャラに付いてゆくカメラ。 こちらはキャラの移動でカメラの回転が行われない。

  • distance へ simd_length を使い cameraNode.simdPosition でカメラの距離を設定
  • originalAxisDirection で前面の方向を cameraNode の simdWorldFront で取得
  • lastActiveCameraFrontDirection で最後に取得した前面の方向のベクトルを設定
  • symetricAxisDirection で X, Z がマイナスになる originalAxisDirection の座標を取得
  • axisAlignConstraint で一定の回転軸に合わせたカスタムのコンストレインとを作成
  • SCNAccelerationConstraint、SCNLookAtConstraint の設定を行い 、axisAlignConstraint 共に cameraNode のコンストレイントを設定する
func setupAxisAlignedCamera(_ cameraNode: SCNNode) {
    let distance: Float = simd_length(cameraNode.simdPosition)
    let originalAxisDirection = cameraNode.simdWorldFront

    self.lastActiveCameraFrontDirection = originalAxisDirection

    let symetricAxisDirection = simd_make_float3(-originalAxisDirection.x, originalAxisDirection.y, -originalAxisDirection.z)

    weak var weakSelf = self

    let axisAlignConstraint = SCNTransformConstraint.positionConstraint(
        inWorldSpace: true, with: {(_ node: SCNNode, _ position: SCNVector3) -> SCNVector3 in
            guard let strongSelf = weakSelf else { return position }
            guard let activeCamera = strongSelf.activeCamera else { return position }

            let axisOrigin = strongSelf.lookAtTarget.presentation.simdWorldPosition
            let referenceFrontDirection =
                strongSelf.activeCamera == node ? strongSelf.lastActiveCameraFrontDirection : activeCamera.presentation.simdWorldFront

            let axis = simd_dot(originalAxisDirection, referenceFrontDirection) > 0 ? originalAxisDirection: symetricAxisDirection

            let constrainedPosition = axisOrigin - distance * axis
            return SCNVector3(constrainedPosition)
        })

    let accelerationConstraint = SCNAccelerationConstraint()
    accelerationConstraint.maximumLinearAcceleration = 20
    accelerationConstraint.decelerationDistance = 0.5
    accelerationConstraint.damping = 0.05

    // look at constraint
    let lookAtConstraint = SCNLookAtConstraint(target: self.lookAtTarget)
    lookAtConstraint.isGimbalLockEnabled = true // keep horizon horizontal

    cameraNode.constraints = [axisAlignConstraint, lookAtConstraint, accelerationConstraint]
}

次回は引き続き GameController クラスのセットアップの関数を見てゆく。

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

GameController クラスのセットアップ関数を見てゆく。
量が多いため何回かに分ける予定。

 

セットアップ

GameController クラスのイニシャライズ時にいくつかの関数が呼ばれる。

 

setupGameController()

ゲームコントローラー設定。

Bluetooth で接続や切断されるため、NotificationCenter の addObserver で接続状態と切断状態を監視する。

接続されれば、GameController で設定している handleControllerDidConnect 関数が呼ばれ、切断されれば handleControllerDidDisconnect が呼ばれゲームコントローラーでの操作の処理が行われる。

イニシャライズ時に setupGameController() が呼ばれ、ゲームコントローラーがすでに接続されている場合は、このファイルの最後の方にある registerGameController 関数が呼ばれゲームコントローラーが使用できるよう処理をする。

func setupGameController() {
    NotificationCenter.default.addObserver(
            self, selector: #selector(self.handleControllerDidConnect),
            name: NSNotification.Name.GCControllerDidConnect, object: nil)

    NotificationCenter.default.addObserver(
        self, selector: #selector(self.handleControllerDidDisconnect),
        name: NSNotification.Name.GCControllerDidDisconnect, object: nil)

    guard let controller = GCController.controllers().first else {
        return
    }

    registerGameController(controller)
}

 

setupCharacter()

プレイヤーキャラクター Max の設定。

クラス内の変数 character に Character クラスを scene と共に渡す。

キャラクターは物理判定を行うため、キャラクターの physicsWorld にシーンの physicsWorld の設定を渡す。

character の中身の Character クラスにある node の返り値から、Character クラスで設定した Max のノードを返す。 そのノードを addChildNode して GameController のシーンの追加する。

func setupCharacter() {
    character = Character(scene: scene!)

    character!.physicsWorld = scene!.physicsWorld
    scene!.rootNode.addChildNode(character!.node!)
}

 

setupPhysics()

物理シミュレーション設定。

enumerateHierarchy で階層の全てのノードを調べ Physics Body の collisionBitMask へ character のビットマスクを渡す。

func setupPhysics() {
    self.scene?.rootNode.enumerateHierarchy({(_ node: SCNNode, _ _: UnsafeMutablePointer<ObjCBool>) -> Void in
        node.physicsBody?.collisionBitMask = Int(Bitmask.character.rawValue)
    })
}

 

setupCollisions()

障害物の判定のノードを設定する。

collision.scn を読み込み、階層の全てのノードを調べる。
見つかった全ての透明度を 0 にして、そのノードを addChildNode して GameController のシーンの追加する。

func setupCollisions() {
    let collisionsScene = SCNScene( named: "Art.scnassets/collision.scn" )
    collisionsScene!.rootNode.enumerateChildNodes { (_ child: SCNNode, _ _: UnsafeMutablePointer<ObjCBool>) in
        child.opacity = 0.0
        self.scene?.rootNode.addChildNode(child)
    }
}

 

次回は GameController クラスのカメラ設定を見てゆく。