Apple Engine

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

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

前回の続きで、Character クラス の関数を見てゆく。

 

Character クラスの初期化

init(scene: SCNScene) {
    super.init()

    loadCharacter()
    loadParticles()
    loadSounds()
    loadAnimations()
}

セットアップのため4つの変数が呼ばれる。
scene で SCNScene が設定されているが、ご覧のように使用されていない。

 

loadCharacter()

ジオメトリと物理判定用のコライダー設定。

変数の方でも紹介しているが、シーンに配置される Max のノードは characterNode > characterOrientation > model の階層になっている。

model がジオメトリとコリジョン。
その上に model のノードを回転させる characterOrientation。
characterOrientation の上に位置を設定する characterNode 構成されている。

private func loadCharacter() {
    let scene = SCNScene( named: "Art.scnassets/character/max.scn")!
    model = scene.rootNode.childNode( withName: "Max_rootNode", recursively: true)
    model.simdPosition = Character.modelOffset

    characterNode = SCNNode()
    characterNode.name = "character"
    characterNode.simdPosition = Character.initialPosition

    characterOrientation = SCNNode()
    characterNode.addChildNode(characterOrientation)
    characterOrientation.addChildNode(model)

    let collider = model.childNode(withName: "collider", recursively: true)!
    collider.physicsBody?.collisionBitMask = Int(([ .enemy, .trigger, .collectable ] as Bitmask).rawValue)

    let (min, max) = model.boundingBox
    let collisionCapsuleRadius = CGFloat(max.x - min.x) * CGFloat(0.4)
    let collisionCapsuleHeight = CGFloat(max.y - min.y)

    let collisionGeometry = SCNCapsule(capRadius: collisionCapsuleRadius, height: collisionCapsuleHeight)
    characterCollisionShape = SCNPhysicsShape(geometry: collisionGeometry, options:[.collisionMargin: Character.collisionMargin])
    collisionShapeOffsetFromModel = float3(0, Float(collisionCapsuleHeight) * 0.51, 0.0)
}

 

scene に max.scn ファイルを読み込み、そこから model のノードに「Max_rootNode」の名前のノードを設定。自身の変数 modelOffset を simdPosition へ渡し Max のオフセット位置を設定している。

characterNode に "character" という名前と simdPosition に initialPosition を渡し初期位置を設定。

characterNode に characterOrientation を addChildNode させて、 characterOrientation に model を addChildNode をさせている。

"collider" の名前を持つコライダーを model から探して、敵、鍵が出現する宝石、鍵の collisionBitMask を設定。 ( .enemy, .trigger, .collectable は GameController.swift で設定されている)

model の boundingBox と collisionMargin から SCNPhysicsShape 作成、characterCollisionShape に設定。

また collisionShapeOffsetFromModel で collisionCapsuleHeight に 0.51 かけたコリジョンのオフセットの float3 を設定している。

 

loadParticles()

キャラクターのパーティクル設定の関数。

private func loadParticles() {
    ...
}

 

以下、関数の内容
var particleScene = SCNScene( named: "Art.scnassets/character/jump_dust.scn")!
let particleNode = particleScene.rootNode.childNode(withName: "particle", recursively: true)!
jumpDustParticle = particleNode.particleSystems!.first!

シーンファイルの読み込みを使い回すため、変数 particleScene で用意し読み込んでいる。
この後の burn.scn と particles_spin.scn でも使用する。

jump_dust.scn からチルドノードの particle ノードを particleNode 渡し、 jumpDustParticle へ particleNode の SCNParticleSystem 渡している。

ジャンプのアクションが起きるたびに model(ジオメトリのあるノード)の dustEmitter の Particle System に新しいパーティクル(Particle System)が追加される。

 

particleScene = SCNScene( named: "Art.scnassets/particles/burn.scn")!
let burnParticleNode = particleScene.rootNode.childNode(withName: "particles", recursively: true)!

let particleEmitter = SCNNode()
characterOrientation.addChildNode(particleEmitter)

burn.scn から particles のノードを burnParticleNode に追加し、 particleEmitter ていう空ノードを作成し、Max の回転用のノード characterOrientation へ追加。 後ほど、particleEmitter へ3つのパーティクルとこのノードの移動を行う。

ちなみに、burn.scn は particles の下に fire、smoke、whiteSmoke という3つの Particle Systems が設定されている。

 

fireEmitter = burnParticleNode.childNode(withName: "fire", recursively: true)!.particleSystems![0]
fireEmitterBirthRate = fireEmitter.birthRate
fireEmitter.birthRate = 0

smokeEmitter = burnParticleNode.childNode(withName: "smoke", recursively: true)!.particleSystems![0]
smokeEmitterBirthRate = smokeEmitter.birthRate
smokeEmitter.birthRate = 0

whiteSmokeEmitter = burnParticleNode.childNode(withName: "whiteSmoke", recursively: true)!.particleSystems![0]
whiteSmokeEmitterBirthRate = whiteSmokeEmitter.birthRate
whiteSmokeEmitter.birthRate = 0

fireEmitter、smokeEmitter、whiteSmokeEmitter に fire、smoke、whiteSmoke の Particle Systems を burnParticleNode から取得している。 各 Particle Systems の birthRate を 0 にすることでパーティクルの再生を止めているため、 元の設定を fireEmitterBirthRate、smokeEmitterBirthRate、whiteSmokeEmitterBirthRate に保存している。

 

particleScene = SCNScene(named:"Art.scnassets/particles/particles_spin.scn")!
spinParticle = (particleScene.rootNode.childNode(withName: "particles_spin", recursively: true)?.particleSystems?.first!)!
spinCircleParticle = (particleScene.rootNode.childNode(withName: "particles_spin_circle", recursively: true)?.particleSystems?.first!)!

攻撃用のパーティクルを particles_spin.scn から particleScene に読み込み、spinParticle と spinCircleParticle に Particle System を渡している。

spinCircleParticle は衝撃波のようなパーティクルで、spinParticle は光の玉が下に流れていくようなパーティクル。 spinParticle は Fox2 では使用されていない。

 

particleEmitter.position = SCNVector3Make(0, 0.05, 0)
particleEmitter.addParticleSystem(fireEmitter)
particleEmitter.addParticleSystem(smokeEmitter)
particleEmitter.addParticleSystem(whiteSmokeEmitter)

particleEmitter ノードの位置を Y に 0.05 移動させてパーティクルの噴射位置を変え、 fireEmitter、smokeEmitter、whiteSmokeEmitter をこのノードのパーティクルとして追加している。

 

spinParticleAttach = model.childNode(withName: "particles_spin_circle", recursively:  true)

Max のジオメトリがあるノードにあるからノード particles_spin_circle を取得して spinParticleAttach に設定している。 攻撃の際 spinParticleAttach へ spinCircleParticle のパーティクルを追加する。

 

loadSounds()

各種 SE を SCNAudioSource で読み込み、ヴォリューム設定と位置による音のミキシングを false でオフにし、ロードしている。
また、足跡のみ複数の音を使っているため配列に入れている。

デフォルトでは SCNAudioSource の isPositional は true になっており、位置による音のミキシングが自動的に行われる。 オーディオソースの音量やリバーブ効果がリスナー(ここではカメラ)までの距離によって自動的に変化するが、 今回はリスナーが移動し発生する SE がリスナー付近なので false で設定している模様。

private func loadSounds() {
    aahSound = SCNAudioSource( named: "audio/aah_extinction.mp3")!
    aahSound.volume = 1.0
    aahSound.isPositional = false
    aahSound.load()

    catchFireSound = SCNAudioSource(named: "audio/panda_catch_fire.mp3")!
    catchFireSound.volume = 5.0
    catchFireSound.isPositional = false
    catchFireSound.load()

    ouchSound = SCNAudioSource(named: "audio/ouch_firehit.mp3")!
    ouchSound.volume = 2.0
    ouchSound.isPositional = false
    ouchSound.load()

    hitSound = SCNAudioSource(named: "audio/hit.mp3")!
    hitSound.volume = 2.0
    hitSound.isPositional = false
    hitSound.load()

    hitEnemySound = SCNAudioSource(named: "audio/Explosion1.m4a")!
    hitEnemySound.volume = 2.0
    hitEnemySound.isPositional = false
    hitEnemySound.load()

    explodeEnemySound = SCNAudioSource(named: "audio/Explosion2.m4a")!
    explodeEnemySound.volume = 2.0
    explodeEnemySound.isPositional = false
    explodeEnemySound.load()

    jumpSound = SCNAudioSource(named: "audio/jump.m4a")!
    jumpSound.volume = 0.2
    jumpSound.isPositional = false
    jumpSound.load()

    attackSound = SCNAudioSource(named: "audio/attack.mp3")!
    attackSound.volume = 1.0
    attackSound.isPositional = false
    attackSound.load()

    for i in 0..<Character.stepsCount {
        steps[i] = SCNAudioSource(named: "audio/Step_rock_0\(UInt32(i)).mp3")!
        steps[i].volume = 0.5
        steps[i].isPositional = false
        steps[i].load()
    }
}

 

loadAnimations()

各シーンファイルには Max のボーン構造で設定されたアニメーションが入っており、これを SCNAnimationPlayer に渡して、アイドル状態のアニメーションや、任意の関数でアニメーションをさせている。
その初期設定をここで行なっている。

private func loadAnimations() {
    ...
}

 

以下、関数の内容
let idleAnimation = Character.loadAnimation(fromSceneNamed: "Art.scnassets/character/max_idle.scn")
model.addAnimationPlayer(idleAnimation, forKey: "idle")
idleAnimation.play()

 

idleAnimation に max_idle.scn を読み込み、
Max のジオメトリがある model の AnimationPlayer へ idleAnimation を "idle" として設定している。

アイドルアニメーションのみ、この場で再生を行なっている。

loadAnimation 関数はこのファイルの終わりの方に定義されている。

 

let walkAnimation = Character.loadAnimation(fromSceneNamed: "Art.scnassets/character/max_walk.scn")
walkAnimation.speed = Character.speedFactor
walkAnimation.stop()

if Character.enableFootStepSound {
    walkAnimation.animation.animationEvents = [
        SCNAnimationEvent(keyTime: 0.1, block: { _, _, _ in self.playFootStep() }),
        SCNAnimationEvent(keyTime: 0.6, block: { _, _, _ in self.playFootStep() })
    ]
}
model.addAnimationPlayer(walkAnimation, forKey: "walk")

walkAnimation に max_walk.scn を読み込み、
再生スピードを歩行用に設定した変数を渡し、アニメーションをストップさせて、
enableFootStepSound が true の場合、SCNAnimationEvent で足跡を鳴らしている。

最後に model の AnimationPlayer へ walkAnimation を "walk" として設定している。

 

let jumpAnimation = Character.loadAnimation(fromSceneNamed: "Art.scnassets/character/max_jump.scn")
jumpAnimation.animation.isRemovedOnCompletion = false
jumpAnimation.stop()
jumpAnimation.animation.animationEvents = [SCNAnimationEvent(keyTime: 0, block: { _, _, _ in self.playJumpSound() })]
model.addAnimationPlayer(jumpAnimation, forKey: "jump")

jumpAnimation に max_jump.scn を読み込み、このアニメーションの再生終了時にレンダリングツリーからの削除を防ぐため、isRemovedOnCompletion を false にしている。

再生を止め、SCNAnimationEvent でジャンプ時のサウンド再生し、 最後に model の AnimationPlayer へ jumpAnimation を "jump" として設定している。

ちなみに、isRemovedOnCompletion の初期値は true になっており、再生が止まると、シーンから消える。 max_idle.scn と max_walk.scn は scn ファイルで無限に再生する設定にされているためこの設定は必要ない。

 

let spinAnimation = Character.loadAnimation(fromSceneNamed: "Art.scnassets/character/max_spin.scn")
spinAnimation.animation.isRemovedOnCompletion = false
spinAnimation.speed = 1.5
spinAnimation.stop()
spinAnimation.animation.animationEvents = [SCNAnimationEvent(keyTime: 0, block: { _, _, _ in self.playAttackSound() })]
model!.addAnimationPlayer(spinAnimation, forKey: "spin")

spinAnimation に max_spin.scn を読み込み、このアニメーションの再生終了時にレンダリングツリーからの削除を防ぐため、isRemovedOnCompletion を false にしている。

再生を止め、SCNAnimationEvent で攻撃時のサウンド再生し、 最後に model の AnimationPlayer へ spinAnimation を "spin" として設定している。

 

次回は、他の関数やプロパティを見てゆく。