SceneKitのSCNPhysicsContactDelegateで衝突判定を行う


大好評の「ペチャバト」にも用いられているSceneKitの物理判定API、Physicsについてを書きたいと思います。

SCNPhysicsContactDelegate

よく使われると思うのが、SCNPhysicsContactDelegateで、当たり判定が発生したときに呼ばれるメソッドが以下のように定義されています。

  • func physicsWorld(SCNPhysicsWorld, didBegin: SCNPhysicsContact)
  • func physicsWorld(SCNPhysicsWorld, didUpdate: SCNPhysicsContact)
  • func physicsWorld(SCNPhysicsWorld, didEnd: SCNPhysicsContact)

didBegin, didUpdate, didEndで呼ばれる3種類のメソッドがあり、それぞれ呼ばれるタイミングが異なります。今回は物体同士の衝突が終わったら呼ばれる、didBeginを例に、実際に2つの物体がぶつかったときのハンドリング方法を簡単に紹介します。

まずはDelegateをセット

まずは、 sceneView.scene.physicsWorldのcontactDelegateにViewControllerをセット

override func viewDidLoad() {
    super.viewDidLoad()

    sceneView.scene.physicsWorld.contactDelegate = self
}

extension ViewController: SCNPhysicsContactDelegate {
    func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {

    }
}

当たったら呼ばれるメソッドを定義

そして、SCNPhysicsContactDelegateのphysicsWorld(didBeginを定義していきます。

引数にはSCNPhysicsWorldとSCNPhysicsContactがあって、前者は環境の情報、後者は当たり判定の情報です。

func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
    let nodeA = contact.nodeA
    let nodeB = contact.nodeB
}

以下のようなケースでは、contact.nodeAかcontact.nodeBに、青いボールか赤いマトのNodeがそれぞれセットされています。

当たり判定を行う

例えば、以下のように的のNodeとボールのNodeが定義されているとしましょう。

let matoNode = SCNNode()
matoNode.name = "mato"

let ballNode = SCNNode()
ballNode.name = "ball"

その場合SCNPhysicsContactDelegateのメソッドにおいて、以下のように当たったかどうか判定します。(もっとスマートな書き方あるかもしれませんが)

func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
    let nodeA = contact.nodeA
    let nodeB = contact.nodeB

    if (nodeA.name == "mato" && nodeB.name == "ball") || (nodeB.name == "mato" && nodeA.name == "ball") {
        // 当たった!!
    }
}

当たったときに、触覚フィードバックをしたり、相手に当たり情報を送信したりして、シューティングバトルを成立させることができます。

当たり判定を行うためのNodeの設定

実は、ただ単にNodeを設定しただけでは、当たり判定は発火しません。先にあげたマトのNodeとボールのNodeをそれぞれ具体的に設定してみましょう。

マトのNode

SCNPhysicsShapeを設定する必要があります。geometryを引数にわたします。

let cylinder = SCNCylinder(radius: 0.1, height: 0.05)
let matoNode = SCNNode(geometry: box)
node.name = "mato"
node.position = SCNVector3Make(0, 0, -1.5)

// add PhysicsShape
let shape = SCNPhysicsShape(geometry: cylinder, options: nil)
node.physicsBody = SCNPhysicsBody(type: .dynamic, shape: shape)
node.physicsBody?.isAffectedByGravity = false

ボールのNode

当てる側のSCNPhysicsBodyにはcontactTestBitMaskを設定するのを忘れずに!
こちらデフォルトは0ですが、そのままだと当たり判定が発火しません。

let ball = SCNSphere(radius: 0.1)
ball.firstMaterial?.diffuse.contents = UIColor.blue

let ballNode = SCNNode(geometry: ball)
ballNode.name = "ball"

// add PhysicsShape
let shape = SCNPhysicsShape(geometry: ball, options: nil)
ballNode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: shape)
ballNode.physicsBody?.contactTestBitMask = 1
ballNode.physicsBody?.isAffectedByGravity = false

以上!

まとめ

  • SceneKitのSCNPhysicsContactDelegateのメソッドには3種類ある
  • SCNPhysicsContactからnodeAとnodeBがあるので、その名前からどのNodeとどのNodeが衝突したか判定できる
  • SCNPhysicsShapeを設定するのを忘れずに
  • 当てる側にはContactTestBitMaskも忘れずに

サンプルコード

https://github.com/kboy-silvergym/ARKit-Emperor のPhysicsにあります!

参考になる記事