Search code examples
swiftscenekit

How to check if a node is within a certain distance of node and when it left that area?


Here is what I'm trying to do: In games, when approaching an NPC, players will be given an indicator to interact with the NPC. The indicator shows up when the player is within a certain distance of the npc. It also goes away when the player moves away from the NPC.

Here is what I tried: I had thought that it would be as easy as using the physics world methods of didBegin/didEnd contact and a transparent cylinder around the NPC as a contact trigger. This unfortunately didn't work because didBegin/didEnd methods are called every frame and not when contact is made (this is how I thought it worked).

I also tried to use PHYKit from GitHub but It didn't seem compatible to what I was trying to do.

I've thought about giving the NPC a Magnetic field and checking if player is within the scope of that field but it doesn't look like there is way to check for that (maybe I missed something).

I thought I could also use hitTestWithSegment but didn't understand how I can apply it to what I'm trying to do.

There also doesn't seem to be anything online to help with this (I've checked for the last three days so if there is anything I'm willing to see what it's about).

The Question: How can I check if a node is within a certain distance of another node and when it left that area?


Solution

  • Updated Answer:

    Here is a better answer and overall more effective than my old answer. It takes advantage of SCNActions with physicsWorld methods and when they are called.

    When the first contact between nodes happens, the physicsWorld methods are called in this order:

    1. beginsContact
    2. updateContact
    3. endContact

    And as the node proceeds to pass through the other node it goes in this order every other moment:

    1. beginsContact
    2. endContact
    3. updateContact

    Once the node is fully inside the other node and moves about inside the node, only this method is called.

    • updateContact

    Note: In my example below, when referring to nodes, I'm talking about the player node and the invisible chatRadius as the other node for an NPC.

    Since the beginsContact method is only called when both nodes have their edges touching, I have it set a boolean value equal true.

    Note: The bool value is what I use to show interaction indicator for the player and is interchangeable for whatever you what to use.

    var interaction: Bool = false          
    
    func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
        let node = (contact.nodeA.name == "player") ? contact.nodeB : contact.nodeA
        if node.physicsBody!.categoryBitMask == control.CategoryInteraction && interaction {
            interaction = true
        }
    }
    

    The endContact method is almost always called after the didBegin contact method, so I have it wait for a while before setting the boolean value back to false.

    Note: I make the action wait for the updateContact method to be called.

    func physicsWorld(_ world: SCNPhysicsWorld, didEnd contact: SCNPhysicsContact) {
        let node = (contact.nodeA.name == "player") ? contact.nodeB : contact.nodeA
        
        if node.physicsBody!.categoryBitMask == control.CategoryInteraction && ViewManager.shared.interaction {
            let wait = SCNAction.wait(duration: 0.1)
            let leave = SCNAction.run { _ in
                self.interaction = false
            }
            let sequence = SCNAction.sequence([wait, leave])
            player.runAction(sequence, forKey: "endInteraction")
        }
    }
    

    The update method is always called when the node moves around the edges or inside the other node. So I have it counter the endContact method by removing the action that would have set the bool value to false.

    func physicsWorld(_ world: SCNPhysicsWorld, didUpdate contact: SCNPhysicsContact) {
        if player.action(forKey: "endInteraction") != nil {
            player.removeAction(forKey: "endInteraction")
        }
    }
    

    The endContact method is also called when the two nodes stop touching (who would have guessed) and, in my testing, is always the last method to be called.

    Old Answer

    It's not at all a perfect answer and it still has room for improvement, but for now, this is how I did it.

    For the first code section, I am checking to see if the player has collided with the chat radius (or is within a certain distance of the NPC). If the player is inside the radius, then show the indicator to interact with the NPC and add 1 to the count.

    var count = 0
    var previousCount = 0
    
    func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
        
        let node = (contact.nodeA.name == "Player") ? contact.nodeB : contact.nodeA
        
        if node.physicsBody!.categoryBitMask == CategoryInteraction {
            
            // Show chat indicator here.
    
            count += 1
            if count > 100 {
                count = 0
            }
        }
    }
    

    For the second code section, I'm checking to see if the player has left the chat radius by making the previousCount equal to count. If the count is equal to previousCount, then the player has left the chat radius so hide the interaction indicator.

    func physicsWorld(_ world: SCNPhysicsWorld, didEnd contact: SCNPhysicsContact) {
        if contact.nodeA.name != "chatRadius" && contact.nodeB.name != "chatRadius" { return }
        
        if previousCount != count {
            previousCount = count
        } else {
            
            // Hide chat indicator here.
    
            count = 0
            previousCount = 0
        }
    }