Apple Engine

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

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

Character.swift 過去の記事

 

今回は 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。

 

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