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。
次回は 残りの関数やプロパティを見てゆく。