iOS で SceneKit を試す(Swift 3) その71 - SCNPhysicsContact と SCNPhysicsContactDelegate
PhysicsWorld 上で、2つ以上の PhysicsBody の接触が起こった場合に SCNPhysicsContactDelegate を呼ぶことができる。
注意点
PhysicsBody は contactTestBitMask が 0 以外でないと SCNPhysicsContactDelegate へ情報が送られないので注意。
当然だが、SCNPhysicsContact は SCNPhysicsContactDelegate の内部で情報が更新される。
そのため、それ以外の場所で SCNPhysicsContact を呼んでも接触の情報は取得できない。
追記: SCNView へ SCNScene を設定後、physicsWorld.contactDelegate = self を行ってもデリゲートが動作しないことが判明。コードは修正済み
SCNPhysicsContact で取得できるもの
プロパティ名 | 説明 |
---|---|
nodeA | 1個目のノードを返す |
nodeB | 2個目のノードを返す |
contactPoint | 接地点の座標 |
contactNormal | 接点の法線ベクトル。どの方向から衝突されているかを示す |
collisionImpulse | ニュートン秒で表された衝突時の力。弱く衝突しているか、激しく衝突しているかなど調べることができる |
penetrationDistance | 互いのノードが重なっている距離 |
SCNPhysicsContactDelegate のメソッド
用意されているメソッドは、接触した時、接触状態が更新された時、接触が終わった時の3つ
- physicsWorld(SCNPhysicsWorld, didBegin: SCNPhysicsContact)
- physicsWorld(SCNPhysicsWorld, didUpdate: SCNPhysicsContact)
- physicsWorld(SCNPhysicsWorld, didEnd: SCNPhysicsContact)
設定手順
- ViewController などのクラスで SCNPhysicsContactDelegate を呼ぶ。
- シーンのメソッドで physicsWorld.contactDelegate を設定する
- 調べたい PhysicsBody の contactTestBitMask を 0 以外にする
- SCNPhysicsContactDelegate メソッドを追加する
つくってみる
いつも通り、XCode で SceneKit の Game テンプレートプロジェクトを作成。
今回は GameViewController.swift のみ修整。
その1
GameViewController.swift を開き、13行目に UIViewController の隣に SCNPhysicsContactDelegate を追加。
class GameViewController: UIViewController, SCNPhysicsContactDelegate {
その2
viewDidLoad() の中身を大幅に変えるので以下に変更。 SCNFloor と 最後に physicsWorld.contactDelegate を設定している
override func viewDidLoad() { super.viewDidLoad() let scene = SCNScene() scene.physicsWorld.contactDelegate = self let cameraNode = SCNNode() cameraNode.camera = SCNCamera() cameraNode.position = SCNVector3(x: 12.68, y: 7.445, z: 12.86) cameraNode.eulerAngles = SCNVector3(x: ((Float.pi * -22.129) / 180), y: ((Float.pi * 44.576) / 180), z: 0.0) scene.rootNode.addChildNode(cameraNode) let lightNode = SCNNode() lightNode.light = SCNLight() lightNode.light!.type = .omni lightNode.position = SCNVector3(x: 0, y: 1, z: 0) scene.rootNode.addChildNode(lightNode) let floorNode = SCNNode(geometry: SCNFloor()) floorNode.name = "floor" floorNode.physicsBody = SCNPhysicsBody(type: .static, shape: nil) floorNode.physicsBody?.contactTestBitMask = 1 scene.rootNode.addChildNode(floorNode) let scnView = self.view as! SCNView scnView.scene = scene scnView.allowsCameraControl = true scnView.showsStatistics = true scnView.backgroundColor = UIColor.black }
その3
以下のコードは contactTestBitMask を 1 にした SCNSphere。
floorNode の上に書いておく。
ちなみに floorNode の contactTestBitMask はすでに設定済み。
let ballNode = SCNNode(geometry: SCNSphere()) ballNode.name = "ball" ballNode.position = SCNVector3(x: 0, y: 5, z: 0) ballNode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil) ballNode.physicsBody?.contactTestBitMask = 1 scene.rootNode.addChildNode(ballNode)
その4
contactDelegate のメソッドを書いてみる。
viewDidLoad(){ ... } の下あたりに書くと良いかと。
didEnd で SCNPhysicsContact で調べられるすべての値を出力している
func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) { print("contactDelegate: Begin") } func physicsWorld(_ world: SCNPhysicsWorld, didUpdate contact: SCNPhysicsContact) { print("contactDelegate: Update") } func physicsWorld(_ world: SCNPhysicsWorld, didEnd contact: SCNPhysicsContact) { print("contactDelegate: End") let firstNode = contact.nodeA let secondNode = contact.nodeB print("NodeA: \(String(describing: firstNode.name!))") print("NodeB: \(String(describing: secondNode.name!))") print("contactPoint: \(String(describing: contact.contactPoint))") print("contactNormal: \(String(describing: contact.contactNormal))") print("collisionImpulse: \(String(describing: contact.collisionImpulse))") print("penetrationDistance: \(String(describing: contact.penetrationDistance))") }
設定完了。
ビルドしてみる
print から出力される情報は以下のような感じ。
contactDelegate: Begin contactDelegate: End NodeA: ball NodeB: floor contactPoint: SCNVector3(x: 0.0, y: -9.31322575e-10, z: 0.0) contactNormal: SCNVector3(x: 0.0, y: 0.99999994, z: 0.0) collisionImpulse: 11.4864807128906 penetrationDistance: -0.0325339697301388 contactDelegate: Begin contactDelegate: Update ・ ・ ・ contactDelegate: Update
didEnd で情報を調べているが弾んだ後は didEnd が呼ばれないが、
着地の際は Begin が呼ばれている。
また、PhysicsBody が静止するまで Update が呼び続けられる。
まとめ
今回は SCNPhysicsContact の情報の出力しかしていないが、 SCNPhysicsContact から nodeA と nodeB で接触している SCNNode を比較できる。
nodeA、nodeB の name や contactTestBitMask などを調べたりして applyForce を加えたり、接触したノードを消して爆発のパーティクルを表示するような表現ができることがわかると思う。
あと、今回は2つの PhysicsBody で試したが2つ以上でも同様。
例えば、X軸 1.5 にもう1つ SCNSphere を置くと contactDelegate は1個目の球と床、2個目の球と床の接地した情報を取得す続ける。
GameViewController.swift の全コード
import UIKit import QuartzCore import SceneKit class GameViewController: UIViewController, SCNPhysicsContactDelegate { override func viewDidLoad() { super.viewDidLoad() let scene = SCNScene() scene.physicsWorld.contactDelegate = self let cameraNode = SCNNode() cameraNode.camera = SCNCamera() cameraNode.position = SCNVector3(x: 12.68, y: 7.445, z: 12.86) cameraNode.eulerAngles = SCNVector3(x: ((Float.pi * -22.129) / 180), y: ((Float.pi * 44.576) / 180), z: 0.0) scene.rootNode.addChildNode(cameraNode) let lightNode = SCNNode() lightNode.light = SCNLight() lightNode.light!.type = .omni lightNode.position = SCNVector3(x: 0, y: 1, z: 0) scene.rootNode.addChildNode(lightNode) let ballNode = SCNNode(geometry: SCNSphere()) ballNode.name = "ball" ballNode.position = SCNVector3(x: 0, y: 5, z: 0) ballNode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil) ballNode.physicsBody?.contactTestBitMask = 1 scene.rootNode.addChildNode(ballNode) let floorNode = SCNNode(geometry: SCNFloor()) floorNode.name = "floor" floorNode.physicsBody = SCNPhysicsBody(type: .static, shape: nil) floorNode.physicsBody?.contactTestBitMask = 1 scene.rootNode.addChildNode(floorNode) let scnView = self.view as! SCNView scnView.scene = scene scnView.allowsCameraControl = true scnView.showsStatistics = true scnView.backgroundColor = UIColor.black } func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) { print("contactDelegate: Begin") } func physicsWorld(_ world: SCNPhysicsWorld, didUpdate contact: SCNPhysicsContact) { print("contactDelegate: Update") } func physicsWorld(_ world: SCNPhysicsWorld, didEnd contact: SCNPhysicsContact) { print("contactDelegate: End") let firstNode = contact.nodeA let secondNode = contact.nodeB print("NodeA: \(String(describing: firstNode.name!))") print("NodeB: \(String(describing: secondNode.name!))") print("contactPoint: \(String(describing: contact.contactPoint))") print("contactNormal: \(String(describing: contact.contactNormal))") print("collisionImpulse: \(String(describing: contact.collisionImpulse))") print("penetrationDistance: \(String(describing: contact.penetrationDistance))") } override var shouldAutorotate: Bool { return true } override var prefersStatusBarHidden: Bool { return true } override var supportedInterfaceOrientations: UIInterfaceOrientationMask { if UIDevice.current.userInterfaceIdiom == .phone { return .allButUpsideDown } else { return .all } } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } }
今回はここまで。