How do I handle collision detection in Scenekit with Swift? - swift

I have been trying to set up a simple Scenekit scene with some physics so I could learn about how SCNPhysicsContactDelegate, categoryBitMask, collisionBitMask and the physicsWorld func work. Not sure if I need to set up a contactTestBitMask as well.
Learning about contact detection sent me down a long path of bitwise operators and the concept of bit masking. Adding in binary is fun! However, this is all still very foggy and I am trying to cobble together several tutorials I've found in both SpriteKit and SceneKit. This is the most comprehensive but it is in Obj-C and I don't understand it how to translate to Swift.
Here is what I have created. Any insights would be much appreciated. Can you see what I have set up incorrectly? I would like to have a simple Print statement occur when the red rolling ball hits the blue target. The floor, ramp and target are .static, while the rolling ball is .dynamic.
import UIKit
import SceneKit
class ViewController: UIViewController, SCNPhysicsContactDelegate {
//category bit masks for ball node and target node
// ball = 0001 -> 1 and target = 0010 ->2
let collisionRollingBall: Int = 1 << 0
let collsionTarget: Int = 1 << 1
//declare variables
var sceneView: SCNView!
var cameraNode: SCNNode!
var groundNode: SCNNode!
var lightNode: SCNNode!
var rampNode: SCNNode!
var rollingBallNode: SCNNode!
var targetNode: SCNNode!
override func viewDidLoad() {
super.viewDidLoad()
//set up sceneview and scene. Define the physicsworld contact delegate as self
sceneView = SCNView(frame: self.view.frame)
sceneView.scene = SCNScene()
sceneView.scene!.physicsWorld.contactDelegate = self
self.view.addSubview(sceneView)
//add floor
let groundGeometry = SCNFloor()
groundGeometry.reflectivity = 0
let groundMaterial = SCNMaterial()
groundMaterial.diffuse.contents = UIColor.greenColor()
groundGeometry.materials = [groundMaterial]
groundNode = SCNNode(geometry: groundGeometry)
//add ramp
let rampGeometry = SCNBox(width: 4, height: 1, length: 18, chamferRadius: 0)
rampNode = SCNNode(geometry: rampGeometry)
rampNode.position = SCNVector3(x: 0, y: 2.0, z: 1.0)
rampNode.rotation = SCNVector4(1, 0, 0, 0.26)
//add rolling ball
let rollingBallGeometry = SCNSphere(radius: 0.5)
let sphereMaterial = SCNMaterial()
sphereMaterial.diffuse.contents = UIColor.redColor()
rollingBallGeometry.materials = [sphereMaterial]
rollingBallNode = SCNNode(geometry: rollingBallGeometry)
rollingBallNode.position = SCNVector3(0, 6, -6)
//add target box
let targetBoxGeometry = SCNBox(width: 4, height: 1, length: 4, chamferRadius: 0)
let targetMaterial = SCNMaterial()
targetMaterial.diffuse.contents = UIColor.blueColor()
targetBoxGeometry.materials = [targetMaterial]
targetNode = SCNNode(geometry: targetBoxGeometry)
targetNode.position = SCNVector3(x: 0, y: 0.5, z: 11.5)
targetNode.rotation = SCNVector4(-1,0,0,0.592)
//add a camera
let camera = SCNCamera()
self.cameraNode = SCNNode()
self.cameraNode.camera = camera
self.cameraNode.position = SCNVector3(x: 13, y: 5, z: 12)
let constraint = SCNLookAtConstraint(target: rampNode)
self.cameraNode.constraints = [constraint]
constraint.gimbalLockEnabled = true
//add a light
let spotLight = SCNLight()
spotLight.type = SCNLightTypeSpot
spotLight.castsShadow = true
spotLight.spotInnerAngle = 70.0
spotLight.spotOuterAngle = 90.0
spotLight.zFar = 500
lightNode = SCNNode()
lightNode.light = spotLight
lightNode.position = SCNVector3(x: 0, y: 25, z: 25)
lightNode.constraints = [constraint]
//define physcis bodies
let groundShape = SCNPhysicsShape(geometry: groundGeometry, options: nil)
let groundBody = SCNPhysicsBody(type: .Static, shape: groundShape)
groundNode.physicsBody = groundBody
let rampShape = SCNPhysicsShape(geometry: rampGeometry, options: nil)
let rampBody = SCNPhysicsBody(type: .Static, shape: rampShape)
rampNode.physicsBody = rampBody
let sphereShape = SCNPhysicsShape(geometry: rollingBallGeometry, options: nil)
let sphereBody = SCNPhysicsBody(type: .Dynamic, shape: sphereShape)
rollingBallNode.physicsBody?.categoryBitMask = collisionRollingBall
rollingBallNode.physicsBody?.collisionBitMask = collsionTarget
rollingBallNode.physicsBody = sphereBody
let targetShape = SCNPhysicsShape(geometry: targetBoxGeometry, options: nil)
let targetBody = SCNPhysicsBody(type: .Static, shape: targetShape)
targetNode.physicsBody?.categoryBitMask = collsionTarget
targetNode.physicsBody?.collisionBitMask = collisionRollingBall
targetNode.physicsBody = targetBody
//add nodes to view
sceneView.scene?.rootNode.addChildNode(groundNode)
sceneView.scene?.rootNode.addChildNode(rampNode)
sceneView.scene?.rootNode.addChildNode(rollingBallNode)
sceneView.scene?.rootNode.addChildNode(targetNode)
sceneView.scene?.rootNode.addChildNode(self.cameraNode)
sceneView.scene?.rootNode.addChildNode(lightNode)
}
func physicsWorld(world: SCNPhysicsWorld, didBeginContact contact: SCNPhysicsContact) {
print("contact")
// let contactMask = contact.nodeA.categoryBitMask |
//contact.nodeB.categoryBitMask
//if contactMask == collsionTarget | collisionRollingBall {
// print("The ball hit the target")
// }
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}

I think you're having to reset the "catagoryBitMask" value in the delegate function because you're trying to set the "catagoryBitMask" and "collisionBitMask" values when the physicsBody is still nil.
rollingBallNode.physicsBody?.categoryBitMask = collisionRollingBall
rollingBallNode.physicsBody?.collisionBitMask = collsionTarget
rollingBallNode.physicsBody = sphereBody
Try putting that 3rd line 1st.

Related

How to change SCNView pivot point

Question
How does the SceneView/camera work out around which point to rotate, and how do I force the scene to rotate around the red sphere (-2, 0, 0)?
Demo
In this example SceneView I have placed
a camera at (-2, 0, 5)
a red sphere at (-2, 0, 0)
a blue sphere at (-1, 0, 0)
a white sphere at (0, 0, 0)
So the camera is in the same position along the x-axis as the red sphere
In the image you see the scene rotates around the blue sphere.
Playground example
Here is a working playground to produce the above example
import UIKit
import SceneKit
import PlaygroundSupport
var scene = SCNScene()
var sceneView: SCNView = {
let s = SCNView(
frame: CGRect(x: 0, y: 0, width: 600, height: 600)
)
s.scene = scene
s.backgroundColor = UIColor.black
s.allowsCameraControl = true
return s
}()
PlaygroundPage.current.liveView = sceneView
let redXPosition: simd_float1 = -2.0
let blueXPosition: simd_float1 = -1.0
let originXPosition: simd_float1 = 0.0
// MARK: Scene Nodes
let cameraNode: SCNNode = {
let n = SCNNode()
n.camera = SCNCamera()
n.camera?.contrast = 0.0
n.camera?.wantsHDR = false
return n
}()
cameraNode.simdPosition = simd_float3(redXPosition, 0, 5)
scene.rootNode.addChildNode(cameraNode)
let ambientLightNode: SCNNode = {
let n = SCNNode()
n.light = SCNLight()
n.light!.type = SCNLight.LightType.ambient
n.light!.color = UIColor(white: 0.75, alpha: 1.0)
return n
}()
ambientLightNode.simdPosition = simd_float3(0,5,0)
scene.rootNode.addChildNode(ambientLightNode)
// MARK: Spheres
// MARK: Origin - White
let originNode: SCNNode = {
let sphere = SCNSphere(radius: 0.5)
let node = SCNNode(geometry: sphere)
let mat = SCNMaterial()
mat.diffuse.contents = UIColor.white
sphere.materials = [mat]
return node
}()
// MARK: Red
let redNode: SCNNode = {
let sphere = SCNSphere(radius: 0.3)
let node = SCNNode(geometry: sphere)
let mat = SCNMaterial()
mat.diffuse.contents = UIColor.red
sphere.materials = [mat]
return node
}()
// MARK: Blue
let blueNode: SCNNode = {
let sphere = SCNSphere(radius: 0.3)
let node = SCNNode(geometry: sphere)
let mat = SCNMaterial()
mat.diffuse.contents = UIColor.blue
sphere.materials = [mat]
return node
}()
// MARK: Place nodes in scene
originNode.simdPosition = simd_float3(originXPosition,0,0)
redNode.simdPosition = simd_float3(redXPosition,0,0)
blueNode.simdPosition = simd_float3(blueXPosition,0,0)
scene.rootNode.addChildNode(originNode)
scene.rootNode.addChildNode(redNode)
scene.rootNode.addChildNode(blueNode)

3D Positional Audio – Move SCNAudioPlayer along Y and Z Axis

Using SceneKit, I can move audioNode from left to right on x axis, but I'm having problem moving on y and z axis. I'm wearing headphone, so I can hear the binaural (3d audio) effects. Also I'm running this on MacOS.
My testing code is below. Could someone let me know what I'm missing? I'd appreciate it!
import Cocoa
import SceneKit
class ViewController: NSViewController {
#IBOutlet weak var sceneView: SCNView!
override func viewDidLoad() {
super.viewDidLoad()
let path = Bundle.main.path(forResource: "Sounds/Test.mp3",
ofType: nil)
let url = URL(fileURLWithPath: path!)
let source = SCNAudioSource(url:url)!
source.loops = true
source.shouldStream = false
source.isPositional = true
source.load()
let player = SCNAudioPlayer(source: source)
let box = SCNBox(width: 100.0,
height: 100.0,
length: 100.0,
chamferRadius: 100.0)
let boxNode = SCNNode(geometry: box)
let audioNode = SCNNode()
boxNode.addChildNode(audioNode)
let scene = SCNScene()
scene.rootNode.addChildNode(boxNode)
sceneView.scene = scene
audioNode.addAudioPlayer(player)
let avm = player.audioNode as! AVAudioMixing
avm.volume = 1.0
let up = SCNAction.moveBy(x: 0, y: 100, z: 0, duration: 5)
let down = SCNAction.moveBy(x: 0, y: -100, z: 0, duration: 5)
let sequence = SCNAction.sequence([up, down])
let loop = SCNAction.repeatForever(sequence)
boxNode.runAction(loop)
// Do any additional setup after loading the view.
}
}
Updated.
You're casting the player.audioNode to AVAudioMixing protocol:
let avm = player.audioNode as! AVAudioMixing
But instead of it, you have to cast it to a class. A code looks like this:
let avm = player.audioNode as? AVAudioEnvironmentNode
Any node that conforms to the AVAudioMixing protocol (for example, AVAudioPlayerNode) can act as a source in this environment. The environment has an implicit listener. By controlling the listener’s position and orientation, the application controls the way the user experiences the virtual world. This node also defines properties for distance attenuation and reverberation that help characterize the environment.
And take into account !
Only inputs with a mono channel connection format to the environment node are spatialized. If the input is stereo, the audio is passed through without being spatialized. Inputs with connection formats of more than two channels aren't supported.
And, of course, you need to implement AVAudio3DMixing protocol.
Here's a working code:
import SceneKit
import AVFoundation
class ViewController: NSViewController, AVAudio3DMixing {
// YOU NEED MONO AUDIO !!!
var renderingAlgorithm = AVAudio3DMixingRenderingAlgorithm.sphericalHead
var rate: Float = 0.0
var reverbBlend: Float = 0.5
var obstruction: Float = -100.0
var occlusion: Float = -100.0
var position: AVAudio3DPoint = AVAudio3DPoint(x: 0, y: 0, z: -100)
override func viewDidLoad() {
super.viewDidLoad()
let scene = SCNScene()
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.camera?.zFar = 200
cameraNode.position = SCNVector3(x: 0, y: 0, z: 40)
scene.rootNode.addChildNode(cameraNode)
let sceneView = self.view as! SCNView
sceneView.scene = scene
sceneView.backgroundColor = NSColor.black
sceneView.autoenablesDefaultLighting = true
let path = Bundle.main.path(forResource: "Test_Mono", ofType: "mp3")
let url = URL(fileURLWithPath: path!)
let source = SCNAudioSource(url: url)!
source.loops = true
source.shouldStream = false // MUST BE FALSE
source.isPositional = true
source.load()
let player = SCNAudioPlayer(source: source)
let audioNode = SCNNode()
let box = SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0.2)
let boxNode = SCNNode(geometry: box)
boxNode.addChildNode(audioNode)
scene.rootNode.addChildNode(boxNode)
audioNode.addAudioPlayer(player)
let avm = player.audioNode as? AVAudioEnvironmentNode
avm?.reverbBlend = reverbBlend
avm?.renderingAlgorithm = renderingAlgorithm
avm?.occlusion = occlusion
avm?.obstruction = obstruction
let up = SCNAction.moveBy(x: 0, y: 0, z: 70, duration: 5)
let down = SCNAction.moveBy(x: 0, y: 0, z: -70 , duration: 5)
let sequence = SCNAction.sequence([up, down])
let loop = SCNAction.repeatForever(sequence)
boxNode.runAction(loop)
avm?.position = AVAudio3DPoint(
x: Float(boxNode.position.x),
y: Float(boxNode.position.y),
z: Float(boxNode.position.z))
}
}
After researching and experimenting for a hwile, I finally figured it out. There were two things that I needed to fix.
I had to change the default renderingAlgorithm for SCNAudioPlayer.AVAudioNode from equalPowerPanning to either HRTF or HRTFHQ. However, AVAudioNode does not have renderingAlgorithm property. However, I was able to cast SCNAudioPlayer.AVAudioNode as AVAudioPlayerNode, and AVAudioPlayerNode does have renderingAlgorithm property. Here's the relevant code.
if let apn = player.audioNode as? AVAudioPlayerNode {
apn.renderingAlgorithm = .HRTFHQ
}
I had to assign a node with SCNCamera to pointOfView for SCNView. Also I had to change the position of the camera node further away from the audioNode. Otherwise, I heard the drastic movement in the beginning. Here's the relevant code.
let cameraNode = SCNNode(geometry: SCNBox(width:1, height:1, length:1, chamferRadius: 0.1))
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: -10)
sceneView.pointOfView = cameraNode
My scene.rootNode is a box geometry with 100x100x100 dimension. Inside scene.rootNode, I have a boxNode with 50x5050 dimension. Then inside the boxNode, I have audioNode generating sound with 1x1x1 dimension as well as cameraNode with 1x1x1 dimension. AudioNode's start position is 0,0,0, and the position for the cameraNode is 0,0,-20.
Finally here's the entire working code.
import Cocoa
import AVFoundation
import SceneKit
class ViewController: NSViewController {
#IBOutlet weak var sceneView: SCNView!
override func viewDidLoad() {
super.viewDidLoad()
let path = Bundle.main.path(forResource: "Sounds/Test_mono.mp3", ofType: nil)
let url = URL(fileURLWithPath: path!)
let source = SCNAudioSource(url: url)!
source.loops = true
source.shouldStream = false
source.isPositional = true
source.load()
let player = SCNAudioPlayer(source: source)
if let apn = player.audioNode as? AVAudioPlayerNode {
apn.renderingAlgorithm = .HRTFHQ
}
let audioNode = SCNNode(geometry: SCNBox(width:1, height:1, length:1, chamferRadius: 0.1))
let cameraNode = SCNNode(geometry: SCNBox(width:1, height:1, length:1, chamferRadius: 0.1))
cameraNode.camera = SCNCamera()
let boxNode = SCNNode(geometry: SCNBox(width:50, height:50, length:50, chamferRadius: 1))
boxNode.addChildNode(audioNode)
audioNode.position = SCNVector3(x: 0, y: 0, z: 0)
boxNode.addChildNode(cameraNode)
cameraNode.position = SCNVector3(x: 0, y: 0, z: -10)
let scene = SCNScene()
scene.rootNode.geometry = SCNBox(width:100, height:100, length:100, chamferRadius: 0.1)
scene.rootNode.addChildNode(boxNode)
boxNode.position = SCNVector3(x: 0, y: 0, z: 0)
sceneView.scene = scene
sceneView.pointOfView = cameraNode
sceneView.audioListener = cameraNode
audioNode.addAudioPlayer(player)
let move = SCNAction.moveBy(x:1, y:0, z:0, duration: 1)
let sequence = SCNAction.sequence([move])
let loop = SCNAction.repeatForever(sequence)
audioNode.runAction(loop)
// Do any additional setup after loading the view.
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}

Swift Issue with SCNSceneRenderer ---Trouble getting protocol to work

I'm working to implement a Zig Zag game and following a set of instructions from an on-line course. I have a swift UIViewController, and I am trying now to use also SCNSceneRenderer (to do the game logic of keeping the game character on the zig-zag path). The code looks like this:
import UIKit
import QuartzCore
import SceneKit
class GameViewController: UIViewController, SCNSceneRenderer{
let scene = SCNScene()
let cameraNode = SCNNode()
var person = SCNNode()
let firstBox = SCNNode()
var goingLeft = Bool()
var tempBox = SCNNode()
var boxNumber = Int()
var prevBoxNumber = Int()
override func viewDidLoad() {
print("Yes -- view did load")
self.createScene()
}
func renderer(render: SCNSceneRenderer, updateAtTime time: TimeInterval){
let deleteBox = self.scene.rootNode.childNode(withName: "\(prevBoxNumber)", recursively: true )
if (deleteBox?.position.x)! > (person.position.x + 1) || (deleteBox?.position.z)! > (person.position.z + 1) {
prevBoxNumber += 1
deleteBox?.removeFromParentNode()
createBox()
}
}
func createBox(){
tempBox = SCNNode(geometry: firstBox.geometry)
let prevBox = scene.rootNode.childNode(withName: "\(boxNumber)", recursively: true )
boxNumber += 1
tempBox.name = "\(boxNumber)"
let randomNumber = arc4random() % 2
switch randomNumber {
case 0:
tempBox.position = SCNVector3Make((prevBox?.position.x)! - firstBox.scale.x, (prevBox?.position.y)!, (prevBox?.position.z)!)
break
case 1:
tempBox.position = SCNVector3Make((prevBox?.position.x)! , (prevBox?.position.y)!, (prevBox?.position.z)! - firstBox.scale.z)
break
default:
break
}
self.scene.rootNode.addChildNode(tempBox)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if goingLeft == false {
person.removeAllActions()
person.runAction(SCNAction.repeatForever(SCNAction.move(by: SCNVector3Make(-100, 0, 0), duration: 20)))
goingLeft = true
} else {
person.removeAllActions()
person.runAction(SCNAction.repeatForever(SCNAction.move(by: SCNVector3Make(0, 0, -100), duration: 20)))
goingLeft = false
}
print("boxNumber is \(boxNumber)")
}
func createScene(){
boxNumber = 0
prevBoxNumber = 0
self.view.backgroundColor = UIColor.orange
let sceneView = self.view as! SCNView
sceneView.delegate = self
sceneView.scene = scene
//Create Person
let personGeo = SCNSphere(radius: 0.2)
person = SCNNode (geometry : personGeo)
let personMat = SCNMaterial()
personMat.diffuse.contents = UIColor.red
personGeo.materials = [personMat]
person.position = SCNVector3Make(0, 1.1, 0)
scene.rootNode.addChildNode(person)
//Create Camera
cameraNode.camera = SCNCamera()
cameraNode.camera?.usesOrthographicProjection = true
cameraNode.camera?.orthographicScale = 3
cameraNode.position = SCNVector3Make(20,20,20)
cameraNode.eulerAngles = SCNVector3Make(-45,45,0)
//let constraint = SCNLookAtConstraint(target: firstBox)
let constraint = SCNLookAtConstraint(target: person)
constraint.isGimbalLockEnabled = true
self.cameraNode.constraints = [constraint]
scene.rootNode.addChildNode(cameraNode)
person.addChildNode(cameraNode)
//Create Box
let firstBoxGeo = SCNBox(width: 1.0, height: 1.5, length: 1.0, chamferRadius: 0)
firstBox.geometry = firstBoxGeo
let boxMaterial = SCNMaterial()
boxMaterial.diffuse.contents = UIColor(red: 0.2, green: 0.8, blue: 0.9, alpha: 1.0)
firstBoxGeo.materials = [boxMaterial]
firstBox.position = SCNVector3Make(0,0,0)
scene.rootNode.addChildNode(firstBox)
firstBox.name = "\(boxNumber)"
for _ in 0...6{
createBox()
}
//Create light
let light = SCNNode()
light.light = SCNLight()
light.light?.type = SCNLight.LightType.directional
light.eulerAngles = SCNVector3Make(-45, 45, 0)
scene.rootNode.addChildNode(light)
//Create light2
let light2 = SCNNode()
light2.light = SCNLight()
light2.light?.type = SCNLight.LightType.directional
light2.eulerAngles = SCNVector3Make(45, 45, 0)
scene.rootNode.addChildNode(light2)
}
}
When I added SCNSceneRenderer I get the following error:
"Type 'GameViewController' cannot conform to protocol 'SCNSceneRenderer' because it has requirements that cannot be satisfied"
Since my GameViewController isn't recognized as a SCNSceneRenderer, I also get an error at this line:
sceneView.delegate = self
the error is "Cannot assign value of type 'GameViewController' to type 'SCNSceneRendererDelegate'
I'm new to swift programming, but this seems like I am trying to implement an interface like in Java, I've been looking at the Swift documentation but don't see what I need to do to make my class be functional as an SCNSceneRenderer. I would appreciate help to solve this problem. Thanks!

Why is SceneKit's physicsWorld didBeginContact firing multiple times for a single collision?

I'm using SceneKit's physicsBody system to detect collisions between objects, and getting some very strange results. To illustrate, I've got a minimal example that produces two spheres with kinematic physicsBodies and moves them in straight lines so that they briefly overlap.
I would expect to see physicsWorld(:didBeginContact:) called exactly once, when the spheres first overlap, and physicsWorld(:didEndContact:) called once when they stop overlapping. Instead, I'm seeing each function called 25 times!
Here's the code to reproduce: In Xcode 8.0, create a brand new project using the "Game" template. Replace the contents of GameViewController.swift with this:
import UIKit
import SceneKit
class GameViewController: UIViewController, SCNSceneRendererDelegate, SCNPhysicsContactDelegate {
var scnScene: SCNScene!
var scnView: SCNView!
var cameraNode: SCNNode!
var nodeA: SCNNode!
var nodeB: SCNNode!
var countBeginnings: Int = 0
var countEndings: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
setupScene()
setupNodes()
}
func setupScene() {
// create a new SCNScene and feed it to the view
scnView = self.view as! SCNView
scnScene = SCNScene()
scnView.scene = scnScene
// assign self as SCNView delegate to get access to render loop
scnView.delegate = self
// assign self as contactDelegate to handle collisions
scnScene.physicsWorld.contactDelegate = self
// create the camera and position it at origin
cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3Zero
scnScene.rootNode.addChildNode(cameraNode)
// tell scnView to update every frame
scnView.isPlaying = true
}
func setupNodes() {
// create two spheres with physicsBodies, one inside the other
nodeA = SCNNode()
nodeA.name = "Node A"
nodeA.geometry = SCNSphere(radius: 1.0)
nodeA.geometry!.firstMaterial?.diffuse.contents = UIColor.yellow.withAlphaComponent(0.6)
// expected behavior
// nodeA.position = SCNVector3(x: 0.0, y: -0.8, z: -10.0)
// weird behavior
nodeA.position = SCNVector3(x: 0.0, y: -0.9, z: -10.0)
nodeA.physicsBody = SCNPhysicsBody(type: .kinematic, shape: SCNPhysicsShape(geometry: nodeA.geometry!, options: nil))
scnScene.rootNode.addChildNode(nodeA)
nodeB = SCNNode()
nodeB.name = "Node B"
nodeB.geometry = SCNSphere(radius: 0.5)
nodeB.geometry!.firstMaterial?.diffuse.contents = UIColor.red
nodeB.position = SCNVector3(x: -2.0, y: 0.0, z: -10.0)
nodeB.physicsBody = SCNPhysicsBody(type: .kinematic, shape: SCNPhysicsShape(geometry: nodeB.geometry!, options: nil))
scnScene.rootNode.addChildNode(nodeB)
// node A can collide with node B but not the other way around
nodeA.physicsBody!.categoryBitMask = 2
nodeB.physicsBody!.categoryBitMask = 1
nodeA.physicsBody!.contactTestBitMask = 1
}
func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
countBeginnings += 1
print("(" + String(countBeginnings) + ") " + contact.nodeA.name! + " began contact with " + contact.nodeB.name!)
}
func physicsWorld(_ world: SCNPhysicsWorld, didEnd contact: SCNPhysicsContact) {
countEndings += 1
print("(" + String(countEndings) + ") " + contact.nodeA.name! + " ended contact with " + contact.nodeB.name!)
}
var frameNumber = 0
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
nodeB.position.x += 0.01
nodeB.position.y -= 0.01
}
}
There's other weirdness going on too. If I change the initial position of one of the spheres just a little bit, moving the y position from -0.9 to -0.8:
nodeA.position = SCNVector3(x: 0.0, y: -0.8, z: -10.0)
Now I get the expected behavior, one call to begin and one call to end! A slightly different collision angle results in totally different behavior.
Could this be a SceneKit bug or is this actually the expected behavior?
The SCNRenderer runs the physics simulation at every frame, calling the renderer(_:didSimulatePhysicsAtTime:) method of its SCNSceneRendererDelegate.
During the course of the crossing of the two spheres, there will be several frames rendered and each time the physics simulation is run, the collision will be detected.
This is expected. It's up to you to process the collision and put the two spheres in a state where they don't collide anymore. For example, in a game, once a projectile hits a player, it disappears. The collision is processed and therefore does not fire anymore.
I used your code in the following way with XCode Version 8.0 beta 5 (8S193k) running as an OS X app. I see the following trace in the console. The begin and end methods are called exactly once !
(1) Node A began contact with Node B
(1) Node A ended contact with Node B
import SceneKit
import QuartzCore
class GameViewController: NSViewController, SCNSceneRendererDelegate, SCNPhysicsContactDelegate {
#IBOutlet weak var gameView: GameView!
// MARK: Properties
var cameraNode: SCNNode!
var nodeA: SCNNode!
var nodeB: SCNNode!
var countBeginnings: Int = 0
var countEndings: Int = 0
// MARK: Initialization
override func awakeFromNib(){
super.awakeFromNib()
// create a new scene
let scene = SCNScene(named: "art.scnassets/scene.scn")!
// create and add a camera to the scene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
// place the camera
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
// create and add a light to the scene
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light!.type = SCNLightTypeOmni
lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
scene.rootNode.addChildNode(lightNode)
// create and add an ambient light to the scene
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light!.type = SCNLightTypeAmbient
ambientLightNode.light!.color = NSColor.darkGray
scene.rootNode.addChildNode(ambientLightNode)
// set the scene to the view
self.gameView!.scene = scene
// allows the user to manipulate the camera
self.gameView!.allowsCameraControl = true
// show statistics such as fps and timing information
self.gameView!.showsStatistics = true
// configure the view
self.gameView!.backgroundColor = NSColor.black
self.gameView!.delegate = self
setupScene()
setupNodes()
}
func setupScene() {
// assign self as contactDelegate to handle collisions
self.gameView!.scene?.physicsWorld.contactDelegate = self
// create the camera and position it at origin
cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3Zero
self.gameView!.scene?.rootNode.addChildNode(cameraNode)
// tell scnView to update every frame
self.gameView.isPlaying = true
}
func setupNodes() {
// create two spheres with physicsBodies, one inside the other
nodeA = SCNNode()
nodeA.name = "Node A"
nodeA.geometry = SCNSphere(radius: 1.0)
nodeA.geometry!.firstMaterial?.diffuse.contents = NSColor.yellow.withAlphaComponent(0.6)
nodeA.position = SCNVector3(x: 0.0, y: -0.8, z: -10.0)
nodeA.physicsBody = SCNPhysicsBody(type: .kinematic, shape: SCNPhysicsShape(geometry: nodeA.geometry!, options: nil))
self.gameView!.scene?.rootNode.addChildNode(nodeA)
nodeB = SCNNode()
nodeB.name = "Node B"
nodeB.geometry = SCNSphere(radius: 0.5)
nodeB.geometry!.firstMaterial?.diffuse.contents = NSColor.red
nodeB.position = SCNVector3(x: -2.0, y: 0.0, z: -10.0)
nodeB.physicsBody = SCNPhysicsBody(type: .kinematic, shape: SCNPhysicsShape(geometry: nodeB.geometry!, options: nil))
self.gameView!.scene?.rootNode.addChildNode(nodeB)
// node A can collide with node B but not the other way around
nodeA.physicsBody!.categoryBitMask = 2
nodeB.physicsBody!.categoryBitMask = 1
nodeA.physicsBody!.contactTestBitMask = 1
}
// MARK: SCNPhysicsContactDelegate
func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
countBeginnings += 1
print("(" + String(countBeginnings) + ") " + contact.nodeA.name! + " began contact with " + contact.nodeB.name!)
}
func physicsWorld(_ world: SCNPhysicsWorld, didEnd contact: SCNPhysicsContact) {
countEndings += 1
print("(" + String(countEndings) + ") " + contact.nodeA.name! + " ended contact with " + contact.nodeB.name!)
}
// MARK: SCNSceneRendererDelegate
var frameNumber = 0
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
nodeB.position.x += 0.01
nodeB.position.y -= 0.01
}
}

SceneKit Swift 2.0 Collision Detections (SCNPhysicsContactDelegate)

I already build an application with collision detection using delegate SCNPhysicsContactDelegate. Right now I'm using Swift 2.0 and Xcode 7 and I try to detect collision with SCNPhysicsContactDelegate delegate but it doesn't work.
What I'm doing wrong? Below the code:
let CubeType = 1
let PlayerType = 2
class GameViewController: UIViewController, SCNSceneRendererDelegate,SCNPhysicsContactDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let scnView = self.view as! SCNView
scnView.scene = SCNScene()
scnView.scene?.physicsWorld.contactDelegate = self
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 5)
scnView.scene?.rootNode.addChildNode(cameraNode)
scnView.autoenablesDefaultLighting = true
scnView.allowsCameraControl = true
scnView.showsStatistics = true
scnView.backgroundColor = UIColor.blackColor()
let g = SCNBox(width: 0.5, height: 0.5, length: 0.5, chamferRadius: 0)
let n = SCNNode(geometry: g)
n.physicsBody = SCNPhysicsBody(type: .Kinematic, shape: SCNPhysicsShape(geometry: g, options: nil))
n.physicsBody?.categoryBitMask = CubeType
n.physicsBody?.collisionBitMask = PlayerType
n.position = SCNVector3Make(5, 0, 0)
scnView.scene?.rootNode.addChildNode(n)
let playerGeometry = SCNSphere(radius: 1)
let playerNode = SCNNode(geometry: playerGeometry)
playerNode.position = SCNVector3Zero
playerNode.physicsBody = SCNPhysicsBody(type: .Kinematic, shape: SCNPhysicsShape(geometry: playerGeometry, options:nil))
playerNode.physicsBody?.categoryBitMask = PlayerType
playerNode.physicsBody?.collisionBitMask = CubeType
scnView.scene?.rootNode.addChildNode(playerNode)
let moveAction = SCNAction.moveTo(n.position, duration: 3)
playerNode.runAction(moveAction)
}
func physicsWorld(world: SCNPhysicsWorld, didBeginContact contact: SCNPhysicsContact) {
print("-> didBeginContact")
}
func physicsWorld(world: SCNPhysicsWorld, didEndContact contact: SCNPhysicsContact) {
print("-> didEndContact")
}
func physicsWorld(world: SCNPhysicsWorld, didUpdateContact contact: SCNPhysicsContact) {
print("-> didupdatecontact")
}
The .contactTestBitMask property is missing for both nodes:
n.physicsBody?.contactTestBitMask = PlayerType
playerNode.physicsBody?.contactTestBitMask = CubeType