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" として設定している。
次回は、他の関数やプロパティを見てゆく。