Apple Engine

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

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)

 

設定手順

  1. ViewController などのクラスで SCNPhysicsContactDelegate を呼ぶ。
  2. シーンのメソッドで physicsWorld.contactDelegate を設定する
  3. 調べたい PhysicsBody の contactTestBitMask を 0 以外にする
  4. 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
}   

 

f:id:x67x6fx74x6f:20170816190745p:plain

 

その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))")
}

 

設定完了。

 

ビルドしてみる

f:id:x67x6fx74x6f:20170816190818g:plain

 

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()
    }
}

 

今回はここまで。