5

I'm having some troubles with SCNode hit detection. I need to detect which object was touched in the scene having a SCNNode, I have implemented this piece of code but it seems crashing when I'm touching the object but working good when I'm touching the rest of the sceneView.

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first as! UITouch
        if(touch.view == self.sceneView){
            print("touch working")
            let viewTouchLocation:CGPoint = touch.location(in: sceneView)
            guard let result = sceneView.hitTest(viewTouchLocation, options: nil).first else {
                return
            }
            if (bottleNode?.contains(result.node))! { //bottleNode is declared as  SCNNode? and it's crashing here
                print("match")
            }

        }
    }
Pietro Messineo
  • 777
  • 8
  • 28

4 Answers4

6

There are multiple problems here, and the existing answers are addressing only some of them.

  1. It's unclear from the code you've posted whether bottleNode can be nil when this method runs. Calling a method through the optional (the ? in bottle?.contains) when its value is nil would fail silently — causing the entire expression result to wrap in an Optional whose value is nil — but you've got parens and a force unwrap around the whole expression, so the nil-unwrap would crash.

  2. contains(_:) is not a method on SCNNode. It's unclear what type your bottleNode could be that you could even write this method call without getting compiler errors... but if bottleNode actually is an SCNNode and you've done some type-erasing/Any-casting goop to allow the call to compile, the call would fail at runtime due to the non-existent method.

If your goal with the bottleNode.contains line is to determine whether the hit test result is either bottleNode itself or a child node thereof, I'd recommend defining and using an extension method like this:

extension SCNNode {
    func hasAncestor(_ node: SCNNode) -> Bool {
        if self === node {
            return true // this is the node you're looking for
        }
        if self.parent == nil {
            return false // target node can't be a parent/ancestor if we have no parent
        } 
        if self.parent === node {
            return true // target node is this node's direct parent
        }
        // otherwise recurse to check parent's parent and so on
        return self.parent.hasAncestor(node)
    }
}

// in your touchesBegan method...
if let bottleNode = bottleNode, result.node.hasAncestor(bottleNode) { 
    print("match")
}

If instead your goal is to determine whether result.node lies within or overlaps the bounding box of bottleNode (regardless of node hierarchy), answering that question is a little more complicated. A simple position within boundingSphere check is pretty easy, or if you're looking for containment/overlap, an octree might help.

ondermerol
  • 524
  • 1
  • 11
  • 29
rickster
  • 124,678
  • 26
  • 272
  • 326
  • 1
    1. Indeed not mentioned in the question, but if you really want to assume it can be nil before a hittest is performed specifically for that node, it would make sense to actually perform that check before doing the hittest. – Xartec Sep 27 '17 at 19:21
  • P2. Aside from the invalid contains method there is no reason to assume the bottleNode has childnodes but if that is the case, is there any added value to using a custom extension to traverse parents of the result node rather than simply checking if the bottlenode childnodes contains the result node? (I.e. enumerateHierarchy on the bottle node would avoid checking all parents of the results node). I’m sure after copying and pasting your code it worked out but that would be due to the self==node portion of the extension so I don’t think this should have been the accepted answer. – Xartec Sep 27 '17 at 19:44
  • @Xartec Good points. Not knowing the node hierarchy in question, I opted for a solution that's algorithmically efficient in general. Walking up the parent hierarchy is bounded by the height of the node tree, but searching the target node's child subtree is bounded by the full breadth of that subtree. For any point in an arbitrarily large tree, worst-case subtree size is potentially much greater than height back to the root. But it's worth thinking about the node hierarchy you expect to have in your app — if it's pretty flat, a simple pointer equality test against the test node may suffice. – rickster Sep 27 '17 at 22:27
4

It could be that, bottleNode is nil. What line is causing the crash? Check if bottleNode exists before the comparison:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first as! UITouch
        if(touch.view == self.sceneView){
            print("touch working")
            let viewTouchLocation:CGPoint = touch.location(in: sceneView)
            guard let result = sceneView.hitTest(viewTouchLocation, options: nil).first else {
                return
            }
            if let bottleNode = bottleNode, bottleNode == result.node { //bottleNode is declared as  SCNNode? and it's crashing here
                print("match")
            }

        }
    }
Nana Ghartey
  • 7,901
  • 1
  • 24
  • 26
1

You are currently testing if your SCNNode “contains” the node from the hittest result while you should instead simply compare them, i.e. if (bottlenode == result.node)...

Xartec
  • 2,369
  • 11
  • 22
1

Have you actually created a bottleNode? If bottleNode is nil, the expression bottleNode?.contains(result.node) will also be nil, so when you force unwrap it, it will throw an error.

If you know that bottleNode will always be defined, it shouldn't be an optional. If you don't know that, you need to check that it exists before using it. This should do what you want:

if let bottleNode = bottleNode, bottleNode.contains(result.node) {

Be advised that if bottleNode doesn't exist, this test will fail silently.

Robert
  • 6,660
  • 5
  • 39
  • 62