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()
}
}
今回はここまで。