I am attempting to detect the contact between two nodes in ARKit SceneKit

1

I am creating an AR UFO invaders game. I have created a UFO with blender and added the .dae file to xCode under art.scnassets and converted it to .scn. Next, I created an orange laser node using the "touches begin" function. I am attempting to detect when the UFO node and the orange laser node come into contact resulting in adding one point to the score. The app builds and runs with the UFO node moving and the orange laser node being created upon touch as well, however, it does not detect a contact or collision and add to the score.

I have tried both enum BodyType and struct OptionSet.

import UIKit
import SceneKit
import ARKit
import Foundation

enum BodyType:Int {

    case UFO = 1
    case orangelaser = 2
    case redlaser = 4

}

extension UIColor {
    convenience init(rgb: UInt) {
        self.init(
            red: CGFloat((rgb & 0xFF0000) >> 16) / 255.0,
            green: CGFloat((rgb & 0x00FF00) >> 8) / 255.0,
            blue: CGFloat(rgb & 0x0000FF) / 255.0,
            alpha: CGFloat(1.0)
        )
    }
}


class ViewController: UIViewController, ARSCNViewDelegate, ARSessionDelegate, SCNPhysicsContactDelegate {

// .........

    var gamerunning: Int = 0

    var score: Int = 0
    var lives: Int = 3
    var ammo: Int = 100

    var coinCount = 0
    var displaylivesammocounter = 0

    var rotationtimer: Int = 0
    var UFOxposition: Float = 0
    var UFOyposition: Float = 0
    var UFOzposition: Float = 0

    var UFONode: SCNNode!
    var orangelaserNode: SCNNode!

    var detectedPlanes: [String : SCNNode] = [:]
    var translation = matrix_identity_float4x4
    var gamePos = SCNVector3Make(0.0, 0.0, 0.0)

   // ......

    override func viewDidLoad() {
        super.viewDidLoad()

        // Set the view's delegate
        sceneView.delegate = self



        // Show statistics such as fps and timing information
        sceneView.showsStatistics = true

        // Create a new scene
        let scene = SCNScene(named: "art.scnassets/UFO final with paint.scn")!

        // Set the scene to the view
        sceneView.scene = scene
        sceneView.scene.physicsWorld.contactDelegate = self

        createUFO()

        // .............

        UFONode.isHidden = true

        // .........


    func createUFO() {


        UFONode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
        UFONode.physicsBody?.restitution = 0.0
        UFONode.physicsBody?.friction = 1.0

        // Set the category masks
        UFONode.physicsBody?.categoryBitMask = BodyType.UFO.rawValue

        // Set the contact masks
        UFONode.physicsBody?.contactTestBitMask = BodyType.orangelaser.rawValue

        // Set the collission masks
        UFONode.physicsBody?.collisionBitMask = BodyType.orangelaser.rawValue

        UFONode = sceneView.scene.rootNode.childNode(withName: "UFO", recursively: true)!
        sceneView.scene.rootNode.addChildNode(UFONode)

    }

    // .............

    func startGame() {

        // .................

        gamerunning = 1

        UFONode.isHidden = false

        // ...................

        let UFOrotationTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { timer in

            if (self.rotationtimer >= 1) {
                timer.invalidate()
            }

            self.UFONode.runAction(SCNAction.repeat(SCNAction.rotateBy(x: 0, y: 1, z: 0, duration: 1), count: 1))

            func random(min: CGFloat, max: CGFloat) -> CGFloat {
                assert(min < max)
                return CGFloat(Float(arc4random()) / 0xFFFFFFFF) * (max - min) + min
            }

            self.UFOyposition = Float(random(
                min: -0.2,
                max: 0.2
            ))

            self.UFOxposition = Float(random(
                min: -0.2,
                max: 0.2
            ))

            self.UFOzposition = Float(random(
                min: -0.2,
                max: 0.2
           ))

            self.UFONode.runAction(SCNAction.moveBy(x: CGFloat(self.UFOxposition), y: CGFloat(self.UFOyposition), z: CGFloat(self.UFOzposition), duration: 1.0))

        })

    }

    func restartSameGame(){

        // ..............

        gamerunning = 1

        UFONode.isHidden = false

        // ................

        let UFOrotationTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { timer in

            if (self.rotationtimer >= 1) {
                timer.invalidate()
            }

            self.UFONode.runAction(SCNAction.repeat(SCNAction.rotateBy(x: 0, y: 1, z: 0, duration: 1), count: 1))

            func random(min: CGFloat, max: CGFloat) -> CGFloat {
                assert(min < max)
                return CGFloat(Float(arc4random()) / 0xFFFFFFFF) * (max - min) + min
            }

            self.UFOyposition = Float(random(
                min: -0.2,
                max: 0.2
            ))

            self.UFOxposition = Float(random(
                min: -0.2,
                max: 0.2
            ))

            self.UFOzposition = Float(random(
                min: -0.2,
                max: 0.2
            ))

            self.UFONode.runAction(SCNAction.moveBy(x: CGFloat(self.UFOxposition), y: CGFloat(self.UFOyposition), z: CGFloat(self.UFOzposition), duration: 1.0))

        })

    }

    func newGame() {

        // ................

        score = 0
        ammo = 100
        lives = 3

        // ......................

        gamerunning = 1

        UFONode.isHidden = false

        // ........................

        let UFOrotationTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { timer in

            if (self.rotationtimer >= 1) {
                timer.invalidate()
            }

            self.UFONode.runAction(SCNAction.repeat(SCNAction.rotateBy(x: 0, y: 1, z: 0, duration: 1), count: 1))

            func random(min: CGFloat, max: CGFloat) -> CGFloat {
                assert(min < max)
                return CGFloat(Float(arc4random()) / 0xFFFFFFFF) * (max - min) + min
            }

            self.UFOyposition = Float(random(
                min: -0.2,
                max: 0.2
            ))

            self.UFOxposition = Float(random(
                min: -0.2,
                max: 0.2
            ))

            self.UFOzposition = Float(random(
                min: -0.2,
                max: 0.2
            ))

            self.UFONode.runAction(SCNAction.moveBy(x: CGFloat(self.UFOxposition), y: CGFloat(self.UFOyposition), z: CGFloat(self.UFOzposition), duration: 1.0))

        })

    }

    @IBAction func PlayGameButton(_ sender: Any) {

        startGame()

    }

    @IBAction func PlayAgainButton(_ sender: Any) {

            if interstitial.isReady {
                interstitial.present(fromRootViewController: self)
            } else {

                self.rotationtimer = 0

                if (self.ammo > 0 && self.lives > 0)  {
                    self.restartSameGame()
                } else {
                    self.newGame()
                }

        }

    }

    func addScore() {

        score = score + 1

        DisplayScore.text = "\(score)"
        DisplayLives.text = "\(lives)"
        DisplayAmmo.text = "\(ammo)"

        if (lives <= 0) {
            gameOver()
        }

        if (ammo <= 0) {
            gameOver()
        }

    }

    func decreaseLives() {

        if (lives > 0) {
            lives = lives - 1
        }

        DisplayScore.text = "\(score)"
        DisplayLives.text = "\(lives)"
        DisplayAmmo.text = "\(ammo)"

        if (lives <= 0) {
            gameOver()
        }

        if (ammo <= 0) {
            gameOver()
        }

    }

    func decreaseAmmo() {

        if (ammo > 0) {
            ammo = ammo - 1
        }

        DisplayScore.text = "\(score)"
        DisplayLives.text = "\(lives)"
        DisplayAmmo.text = "\(ammo)"

        if (lives <= 0) {
            gameOver()
        }

        if (ammo <= 0) {
            gameOver()
        }

    }

    // .....................

    func gameOver() {

        DisplayScore.text = "\(score)"
        DisplayLives.text = "\(lives)"
        DisplayAmmo.text = "\(ammo)"

        UFONode.isHidden = true
        gamerunning = 0


        MoreLivesAmmoButton.isHidden = false
        EarnRewardsButton.isHidden = false
        PlayAgainButton.isHidden = false

        rotationtimer = 1

        // .....................

        GameOverLabel.isHidden = false



    }

    // .............................

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        // Create a session configuration
        let configuration = ARWorldTrackingConfiguration()
        configuration.planeDetection = [.horizontal, .vertical]

        // Run the view's session
        sceneView.session.run(configuration)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        // Pause the view's session
        sceneView.session.pause()
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let frame = sceneView.session.currentFrame else { return } //1
        let camMatrix = SCNMatrix4(frame.camera.transform)
        let direction = SCNVector3Make(-camMatrix.m31 * 5.0, -camMatrix.m32 * 10.0, -camMatrix.m33 * 5.0) //2
        let position = SCNVector3Make(camMatrix.m41, camMatrix.m42, camMatrix.m43) //3
        if ((ammo > 0) && (lives > 0) && (UFONode.isHidden == false) && (gamerunning == 1)) {
            let orangelaser = SCNCapsule(capRadius: 0.25, height: 1.0) //1
            orangelaser.firstMaterial?.diffuse.contents = UIColor(red: 245.0 / 255.0, green: 127.0 / 255.0, blue: 44.0 / 255.0, alpha: 1 )
            orangelaser.firstMaterial?.emission.contents = UIColor(red: 245.0 / 255.0, green: 127.0 / 255.0, blue: 44.0 / 255.0, alpha: 1 ) //2
            orangelaserNode = SCNNode(geometry: orangelaser)
            orangelaserNode.scale = SCNVector3(x: 0.009, y: 0.009, z: 0.009)
            orangelaserNode.position = position //3
            self.orangelaserNode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
            orangelaserNode.physicsBody?.mass = 1.5; // 1.5kg
            orangelaserNode.physicsBody?.restitution = 0.25
            orangelaserNode.physicsBody?.friction = 0.75
            orangelaserNode.physicsBody?.categoryBitMask = BodyType.orangelaser.rawValue
            orangelaserNode.physicsBody?.contactTestBitMask = BodyType.UFO.rawValue
            orangelaserNode.physicsBody?.collisionBitMask = BodyType.UFO.rawValue
            sceneView.scene.rootNode.addChildNode(orangelaserNode)
            orangelaserNode.runAction(SCNAction.sequence([SCNAction.wait(duration: 10.0), SCNAction.removeFromParentNode()])) //5
            orangelaserNode.physicsBody?.applyForce(direction, asImpulse: true) //6

            decreaseAmmo()

        }
    }

    func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
        print("contact happened!")
        if (contact.nodeA.physicsBody?.categoryBitMask == BodyType.UFO.rawValue && contact.nodeB.physicsBody?.categoryBitMask == BodyType.orangelaser.rawValue ){

            print("collision UFO and orangelaser")

            addScore()

        } else if (contact.nodeB.physicsBody?.categoryBitMask == BodyType.UFO.rawValue && contact.nodeA.physicsBody?.categoryBitMask == BodyType.orangelaser.rawValue ){

            print("collision orangelaser and UFO")

            addScore()

        }

    }




    // MARK: - ARSCNViewDelegate

/*
    // Override to create and configure nodes for anchors added to the view's session.
    func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
        let node = SCNNode()

        return node
    }
*/

    func session(_ session: ARSession, didFailWithError error: Error) {
        // Present an error message to the user

    }

    func sessionWasInterrupted(_ session: ARSession) {
        // Inform the user that the session has been interrupted, for example, by presenting an overlay

    }

    func sessionInterruptionEnded(_ session: ARSession) {
        // Reset tracking and/or remove existing anchors if consistent tracking is required

    }
}

App builds and runs. I do not see any error messages. However when orange laser nodes come into contact with or collide with the UFO nothing happens.

swift
xcode
scenekit
arkit
asked on Stack Overflow Jun 11, 2019 by mgm3com

1 Answer

2

To implement collision detection in ARKit between two objects, make sure you have followed each step:

1. Create an enum containing all body types:

enum BodyType : Int {
    case type1 = 1
    case type2 = 2
} 

2. Apply physics body and categoryBitMask to all of your nodes that require collision:

node1.physicsBody = SCNPhysicsBody(type: .static, shape: nil)
node1.physicsBody?.categoryBitMask = BodyType.type1.rawValue

node2.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
node2.physicsBody?.categoryBitMask = BodyType.type2.rawValue

3. Add contactTestBitMask to your moving object to filter the collisions trigger only on your nodes:

node2.physicsBody?.contactTestBitMask = BodyType.type2.rawValue

4. Conform to SCNPhysicsContactDelegate and implement physicsWorld(_: didBegin:):

Add this to viewDidLoad():

self.sceneView.scene.physicsWorld.contactDelegate = self

Following these steps, anytime a collision happens between your nodes, the physicsWorld(_: didBegin:) delegate will be called:

extension ViewController: SCNPhysicsContactDelegate {
    func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
        // Collision happened between contact.nodeA and contact.nodeB
    }
}

answered on Stack Overflow Jun 11, 2019 by M Reza

User contributions licensed under CC BY-SA 3.0