WWDC 2017 の SceneKit サンプル Fox 2 を調べる その15
Character.swift 過去の記事
- Character.swift その1 「定数 / 変数 と グローバル関数」
- Character.swift その2 「イニシャライズ関数」
- Character.swift その3 「その他の関数とプロパティ その1」
今回は Character クラスの update 関数を見てゆく。
長いので注意。
update(atTime time: TimeInterval, with renderer: SCNSceneRenderer)
GameController.swift の SCNSceneRenderer デリゲートの updateAtTime で呼ばれ、 フレーム毎にこの関数が呼ばれる。
func update(atTime time: TimeInterval, with renderer: SCNSceneRenderer) { ... }
以下、関数の内容
フレーム毎にカウントする変数後ほど使用。
frameCounter += 1
if shouldResetCharacterPosition { shouldResetCharacterPosition = false resetCharacterPosition() return }
shouldResetCharacterPosition が true なら resetCharacterPosition() を呼び、Max を初期位置に戻し、この関数から抜ける。
var characterVelocity = float3.zero var groundMove = float3.zero if groundNode != nil { let groundPosition = groundNode!.simdWorldPosition groundMove = groundPosition - groundNodeLastPosition }
characterVelocity、groundMove を初期化。 接地面である groundNode が nil なければ、groundPosition に groundNode の位置を渡し、 groundMove を groundPosition から groundNodeLastPosition 引いた値を渡し設定する。
characterVelocity = float3(groundMove.x, 0, groundMove.z) let direction = characterDirection(withPointOfView:renderer.pointOfView)
キャラクタの速度の設定と characterDirection で向きを算出する。
if previousUpdateTime == 0.0 { previousUpdateTime = time }
previousUpdateTime が 0 であるなら、現在時間を previousUpdateTime 渡す。
let deltaTime = time - previousUpdateTime let characterSpeed = CGFloat(deltaTime) * Character.speedFactor * walkSpeed let virtualFrameCount = Int(deltaTime / (1 / 60.0)) previousUpdateTime = time
deltaTime へ現在の時間と過去の時間を引き、差分渡す。
キャラクターのスピードを deltaTime、speedFactor、walkSpeed をかけて渡す。
virtualFrameCount は deltaTime を 1/60 でわり仮想的なフレームカウントの作成し、
previousUpdateTime へ time を渡し過去の時間を設定する。
if !direction.allZero() { characterVelocity = direction * Float(characterSpeed) var runModifier = Float(1.0) #if os(OSX) if NSEvent.modifierFlags.contains(.shift) { runModifier = 2.0 } #endif walkSpeed = CGFloat(runModifier * simd_length(direction)) directionAngle = CGFloat(atan2f(direction.x, direction.z)) isWalking = true } else { isWalking = false }
Max 向きが全て 0 であれば isWalking に false にして止まっている状態。
0 でなければ characterVelocity と direction に characterSpeed をかけて速度を出す。
runModifier は 1.0 を設定しており、数行したの walkSpeed で最終的な歩行スピードを決める。
macOS 用の分岐では Shift キーを押すと runModifier が 2.0 となり倍で、
runModifier はこのための変数でここ以外に使用することはない。
walkSpeed で direction を simd_length 使用し長さを割り出し runModifier とかける。
direction.x、direction.z を atan2f から角度を割り出し、isWalking を true にして歩行している状態にする。
let up = float3(0, 1, 0) var wPosition = characterNode.simdWorldPosition
up で上向のベクトルを設定。 wPosition は Max のワールド座標を SIMD で取得する。
downwardAcceleration -= Character.gravity wPosition.y += downwardAcceleration
重力設定。
downwardAcceleration に gravity で設定した値で常にマイナスし wPosition.y に設定する。
let HIT_RANGE = Float(0.2) var p0 = wPosition var p1 = wPosition p0.y = wPosition.y + up.y * HIT_RANGE p1.y = wPosition.y - up.y * HIT_RANGE
床となるマップにヒットテスト用レンジを決める。 p0, p1 に Max の位置情報を渡し、p0.y に wPosition.y と上向のベクトル 1 を足し HIT_RANGE を渡す。 p1.y は上向のベクトル 1をマイナスしている。
let options: [String: Any] = [ SCNHitTestOption.backFaceCulling.rawValue: false, SCNHitTestOption.categoryBitMask.rawValue: Character.collisionMeshBitMask, SCNHitTestOption.ignoreHiddenNodes.rawValue: false]
コリジョンのオプション。
collisionMeshBitMask で設定した値をビットマスクに設定し、ジオメトリ裏面の判定と Hidden に設定しているノードも物理判定を行う。
let hitFrom = SCNVector3(p0) let hitTo = SCNVector3(p1) let hitResult = renderer.scene!.rootNode.hitTestWithSegment(from: hitFrom, to: hitTo, options: options).first
設定した p0 と p1 使用して hitTestWithSegment でヒットテストを行い hitResult に渡す。
let wasTouchingTheGroup = groundNode != nil groundNode = nil var touchesTheGround = false let wasBurning = isBurning
wasTouchingTheGroup は groundNode が nil でなければ true を設定し、groundNode を初期化。 touchesTheGround を false で初期化。 wasBurning に今燃えているかを渡す。
if let hit = hitResult { let ground = float3(hit.worldCoordinates) if wPosition.y <= ground.y + Character.collisionMargin { wPosition.y = ground.y + Character.collisionMargin if downwardAcceleration < 0 { downwardAcceleration = 0 } groundNode = hit.node touchesTheGround = true isBurning = groundNode?.name == "COLL_lava" } } else { if wPosition.y < Character.minAltitude { wPosition.y = Character.minAltitude queueResetCharacterPosition() } }
ヒットテストから得た結果の hitResult を hit に入れ、hitResult がなく、Max の Y軸が minAltitude 低い場合 queueResetCharacterPosition が呼ばれ、shouldResetCharacterPosition が true になる。
この update 関数が再度呼ばれた時、Max の位置がリセットされる。
hit へ hitResult の代入が成功した場合、ground に hit.worldCoordinates が設定される。
ground.y と collisionMargin を足し合わせたもの(地面)より wPosition.y(Max)が小さければ、
wPosition.y = ground.y + Character.collisionMargin に。
地面についているため downwardAcceleration が 0 より小さくなってしまった場合は 0 に設定。
groundNode を hit.node にして、touchesTheGround を true にして地面についた判定を行う。
そして groundNode の名前が "COLL_lava" なら isBurning を true にする。
groundNodeLastPosition = (groundNode != nil) ? groundNode!.simdWorldPosition: float3.zero
groundNode が空出なければ groundNodeLastPosition に groundNode のワールド座標を入れる。
空なら (0,0,0) を設定する。
if jumpState == 0 { if isJump && touchesTheGround { downwardAcceleration += Character.jumpImpulse jumpState = 1 model.animationPlayer(forKey: "jump")?.play() } } else { if jumpState == 1 && !isJump { jumpState = 2 } if downwardAcceleration > 0 { for _ in 0..<virtualFrameCount { downwardAcceleration *= jumpState == 1 ? 0.99: 0.2 } } if touchesTheGround { if !wasTouchingTheGroup { model.animationPlayer(forKey: "jump")?.stop(withBlendOutDuration: 0.1) if isBurning { // Swift のサンプルのミス。ほんとは "if !isBurning {" model.childNode(withName: "dustEmitter", recursively: true)?.addParticleSystem(jumpDustParticle) } else { if wasBurning { characterNode.runAction(SCNAction.sequence([ SCNAction.playAudio(catchFireSound, waitForCompletion: false), SCNAction.playAudio(ouchSound, waitForCompletion: false) ])) } } } if !isJump { jumpState = 0 } } }
ジャンプした際の処理 1。
以下の isJump は GameController にあるボタン操作でオン・オフが行われる。
jumpState が 0 かつ、isJump と touchesTheGround が true の場合はジャンプ状態となる。
downwardAcceleration に jumpImpulse を足して Max 上昇させ、Animation Player から "jump" のキーでジャンプアニメーションを再生させて、jumpState を 1 にする。
jumpState が 0 ではない場合。
jumpState が 1 で isJump が false の場合は jumpState を 2。
downwardAcceleration が 0 以上なら 仮想フレーム分だけ downwardAcceleration に 値をかける。 jumpState が 1 の場合は 0.99、そうでない場合は 0.2。 この分岐でわかるように、jumpState が 1 で isJump が false の場合は落下状態。
touchesTheGround がある場合で wasTouchingTheGroup がない場合、要するに地面についた時 0.1 秒後にジャンプアニメーションを止める。
また、isBurning が false で着地位置が溶岩ではない場合は、着時の際に砂煙のパーティクル jumpDustParticle を Max の dustEmitter ノードに追加する。
isBurning が true で wasBurning が true の場合、再度溶岩に着地しているので、catchFireSound と ouchSound を再生させる。
if touchesTheGround && !wasTouchingTheGroup && !isBurning && lastStepFrame < frameCounter - 10 { lastStepFrame = frameCounter characterNode.runAction(SCNAction.playAudio(steps[0], waitForCompletion: false)) } if wPosition.y < characterNode.simdPosition.y { wPosition.y = characterNode.simdPosition.y }
ジャンプした際の処理 2。
touchesTheGround が true、wasTouchingTheGroup と isBurning が false かつ、lastStepFrame が frameCounter から 10 引いた数より小さい場合、 lastStepFrame に frameCounter を入れ、足音の SE の配列の 0 番目を再生する。
地面にめり込まないようにするため wPosition.y が characterNode.simdPosition.y よ小さい場合、wPosition.y に現在地の characterNode.simdPosition.y を入れるようにしている。
if touchesTheGround { targetAltitude = wPosition.y } baseAltitude *= 0.95 baseAltitude += targetAltitude * 0.05 characterVelocity.y += downwardAcceleration if simd_length_squared(characterVelocity) > 10E-4 * 10E-4 { let startPosition = characterNode!.presentation.simdWorldPosition + collisionShapeOffsetFromModel slideInWorld(fromPosition: startPosition, velocity: characterVelocity) }
ジャンプした際の処理 3。
地面によってジャンプする高さが変わるため、地面に触れるたびジャンプの高度を更新する。
これまでの処理を通し touchesTheGround がある場合は、targetAltitude 目的となるジャンプ高度を wPosition.y に設定。
基準となる高度 baseAltitude に 0.95 を掛けつつ、targetAltitude * 0.05 を足し、 さらに Max の Y 軸に downwardAcceleration を足す。
simd_length_squared(characterVelocity) が 10E-4 * 10E-4 より大きい場合、
startPosition に characterNode の物理シミュレーション後の位置情報に コリジョンのオフセットである collisionShapeOffsetFromModel を入れて、このファイルに定義されている
slideInWorld で物理シミュレーションを加味下処理をしている。
ちなみに、10E-4 は 10 * 10 ^ -4 なので 0.001。10E-4 * 10E-4 は 0.000001。
simd_length_squared は要素を二乗して足した値。
simd_length_squared(float3(1, 1, 1)) なら 1^2 + 1^2 + 1^2 = 3。
simd_length_squared(float3(1, 2, 3)) なら 1^2 + 2^2 + 3^2 = 14。
次回は 残りの関数やプロパティを見てゆく。