2016年5月2日月曜日

SceneKit: プログラムで衝突を制御

SceneKitシリーズ 第五弾。
前回の記事では、Dynamicを使って自動的に動かす方法を紹介しました。今回はKinematicを使って、動きをプログラムで制御する方法をご紹介します。

描画処理の流れ

Dynamicでは、SceneKitに全部処理をお任せできていましたが、自分で制御するには、そもそもどのように処理が進んでいくかのフローを をまず理解しておきましょう。

SceneKitでは、iPhoneデフォルトで60fps...つまり、1秒間に60frameの描画処理をしています。
その1frame毎に以下の処理が行われています。




rendererと書かれているのは、SCNSceneRendererDelegate のことで、SCNViewはこのDelegateを実装しています。

この図、おおまかにいうと以下の流れ。
  • モノを動かしてみる (renderer:updateAtTime )
  • SceneKitが衝突があるかを計算 (Scene Kit simulates physics) 
  • 衝突結果から跳ね返った位置に移動させる(renderer:didSimurlatePhysicsAtTime)
  • 描画処理を開始 (renderer:willRenderScene:atTime:)

"まず動かして、衝突をSceneKitに判断してもらう" という方向になるわけです。

ではでは、Standlandで自由に動き回るGemmy達の実装を例にご紹介していきます。



青緑色の子がGemmy達
自動でウロウロ動き回ります。
このままGemmyが歩くとぶつかるね...

1. 何と衝突させるのかを指定

前回に少し説明をした、physicsBodyをKinematicで作成します。

let shape = SCNPhysicsShape(node: bodyNode, options: nil)
let physicsBody =  SCNPhysicsBody(type: .Kinematic, shape: shape)

そして、何と衝突させるのか? の設定もします。

physicsBody.categoryBitMask = FLSPhysicsCategory.Gemmy
physicsBody.contactTestBitMask = FLSPhysicsCategory.GemmyHits

categoryBitMask が自分自身のBitMask。
contactTestBitMask が衝突する対象のBitMask。

Gemmy同士や、キャラ、看板には衝突するけど、床との衝突判定はいらない など設定ができるわけです。
幽霊キャラの場合は壁は衝突せずにすり抜けれる とか。
飛んでいるキャラは水の上にいけるけど、歩いているキャラは行けないとか。
パターンはいろいろ!

この2つのBitMaskはKinematicに限らず、Dynamic/Staticでも同様に設定します。
その他、physicsBodyにapplyForceの設定もあり、指定の向きに力を入れることもできます。
使っていないので試せていないのですが、重力以外の力、例えばボールを投げるとかそういう時に使えるものと思います。

2. 動かす

renderer:updateAtTime 

Gemmyが歩くのは、ぴょんぴょん跳ねているアニメーションに加え、updateAtTimeのタイミングで以下のようなコードで位置移動 することで実装しています。

let radians = node.rotation.z * node.rotation.w
let direction:float3 = float3(sin(radians), -cos(radians), 0.0)
let position = SCNVector3(float3(node.position) + direction * speed)
node.position = position

※Standlandではyとzは反対で扱っているのに注意

Gemmyが向いている方向へ移動する というコードです。
やっていることとしては、Nodeの位置(x, y, z)を変えているだけというシンプルなもの。

3. 衝突後の位置を判定

Gemmyを歩かせるようにしたら、衝突が発生するはず。
衝突の情報は、SCNPhysicsContactDelegate で取得します。
このDelegateは、SCNView.scene!physicsWorld.contactDelegate で設定します。


physicsWorld:didBeginContact: 

衝突した時にcallされます。
引数に情報として渡される SCNPhysicsContact が衝突の情報です。
  • nodeA , nodeB が衝突したノード
  • contactPoint が衝突した位置
  • contactNormal は衝突して跳ね返ったりした方向
  • collisionImpulse は衝突の勢い
  • penetrationDistance は衝突で重なった距離
physicBodyに設定した重量や反発係数などを元にして、SceneKitが衝突の物理計算をして結果を返してくれているわけですね。素敵です。
これらの情報から、衝突して跳ね返ったらどこの位置になるかを計算します。
Gemmyでは、contactNormal × penetrationDistanceの値を、元のpositionから動かすようにしました。

physicsWorld:didUpdateContact:

衝突情報が変更された時に呼ばれます。
なので、didBeginContactと同じように衝突の処理を組み込んでおきます。

4. 衝突の位置を設定

renderer:didSimurlatePhysicsAtTime 

衝突後の位置(position)をSCNPhysicsContactDelegateで計算しましたが、実際にNodeに設定するのは、didSimuratePhysicsAtTime で行います。


これで、処理は完成。

分かってしまうと簡単ですが、最初はなかなかスムーズには実装できませんでした。
Gemmyが壁をすり抜けてしまう...脱走事件が発生したりして!
衝突の跳ね返りの位置の設定が漏れている子がいたり、障害物がおかしくなっていたり という問題を一つ一つ見直していくことで解決していきました。

こうやってプログラムで制御することで、
「ランダムで歩いたり、向き変えたり、アニメーションいれる」
「何かにあたった後は、立ち止まって、向きを変える」
という自動制御Gemmyが出来上がっています。


Gemmy達がワヤワヤ動いている様子は是非アプリで見てみてくださいね!


作成したアプリはこちら↓


Standland - 座りすぎ解消!スタンドランド


• • •