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

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?

I still think your physics answer works. Yeah it worked differently than I thought it did too, but you have to play around with it a bit and check it both ways:
func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact)
{
if(data.gameState == .run)
{
guard let nodeNameA = contact.nodeA.name else { return }
guard let nodeNameB = contact.nodeB.name else { return }
if(nodeNameA.prefix(5) == "Explo" && nodeNameB.prefix(5) == "Missi")
{
gameControl.enemyMissileHit(vIndex: Int(nodeNameB.suffix(4))!)
}
if(nodeNameA.prefix(5) == "Missi" && nodeNameB.prefix(5) == "Explo")
{
gameControl.enemyMissileHit(vIndex: Int(nodeNameA.suffix(4))!)
}
if(nodeNameA.prefix(5) == "Explo" && nodeNameB.prefix(5) == "Hive ")
{
gameControl.enemyHiveHit(vIndex: Int(nodeNameB.suffix(4))!)
}
if(nodeNameA.prefix(5) == "Hive " && nodeNameB.prefix(5) == "Explo")
{
gameControl.enemyHiveHit(vIndex: Int(nodeNameA.suffix(4))!)
}
}
}
Or - on a timer, check the node distance periodically:
func distance3D(vector1: SCNVector3, vector2: SCNVector3) -> Float
{
let x: Float = (vector1.x - vector2.x) * (vector1.x - vector2.x)
let y: Float = (vector1.y - vector2.y) * (vector1.y - vector2.y)
let z: Float = (vector1.z - vector2.z) * (vector1.z - vector2.z)
let temp = x + y + z
return Float(sqrtf(Float(temp)))
}

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:
beginsContact
updateContact
endContact
And as the node proceeds to pass through the other node it goes in this order every other moment:
beginsContact
endContact
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
}
}

Related

remove nodes from scene after use ARSCNView

I am making an app using ARKit to measure between two points. The goal is to be able to measure length, store that value, then measure width and store.
The problem I am having is disposing of the nodes after I get the measurement.
Steps so far:
1) added a button with a restartFunction. this worked to reset the measurements but did not remove the spheres from scene, and also made getting the next measurement clunky.
2) set a limit on > 2 nodes. This functionaly works best. But again the spheres just stay floating in the scene.
Here is a screen shot of the best result I have had.
#objc func handleTap(sender: UITapGestureRecognizer) {
let tapLocation = sender.location(in: sceneView)
let hitTestResults = sceneView.hitTest(tapLocation, types: .featurePoint)
if let result = hitTestResults.first {
let position = SCNVector3.positionFrom(matrix: result.worldTransform)
let sphere = SphereNode(position: position)
sceneView.scene.rootNode.addChildNode(sphere)
let tail = nodes.last
nodes.append(sphere)
if tail != nil {
let distance = tail!.position.distance(to: sphere.position)
infoLabel.text = String(format: "Size: %.2f inches", distance)
if nodes.count > 2 {
nodes.removeAll()
}
} else {
nodes.append(sphere)
}
}
}
I am new to Swift (coding in general) and most of my code has come from piecing together tutorials.
I think the issue here is that your are not actually removing the SCNNodes you have added to hierarchy.
Although you are removing the nodes from what I assume is an array of SCNNodes by calling: nodes.removeAll(), you first need to actually remove them from the scene hierarchy.
So what you need to do is call the following function on any node you wish to remove:
removeFromParentNode()
Which simply:
Removes the node from its parent’s array of child nodes.
As such you would do something like this which first removes the nodes from the hierarchy, and then removes them from the array:
for nodeAdded in nodesArray{
nodeAdded.removeFromParentNode()
}
nodesArray.removeAll()
So based on the code provided you could do the following:
if nodes.count > 2 {
for nodeAdded in nodes{
nodeAdded.removeFromParentNode()
}
nodes.removeAll()
}
For future reference, if you want to remove all SCNNodes from you hierarchy you can also call:
self.augmentedRealityView.scene.rootNode.enumerateChildNodes { (existingNode, _) in
existingNode.removeFromParentNode()
}
Whereby self.augmentedRealityView refers to the variable:
var augmentedRealityView: ARSCNView!
Here is a very basic working example based on (and modified from) the code you have provided:
/// Places A Marker Node At The Desired Tap Point
///
/// - Parameter sender: UITapGestureRecognizer
#objc func handleTap(_ sender: UITapGestureRecognizer) {
//1. Get The Current Tap Location
let currentTapLocation = sender.location(in: sceneView)
//2. Check We Have Hit A Feature Point
guard let hitTestResult = self.augmentedRealityView.hitTest(currentTapLocation, types: .featurePoint).first else { return }
//3. Get The World Position From The HitTest Result
let worldPosition = positionFromMatrix(hitTestResult.worldTransform)
//4. Create A Marker Node
createSphereNodeAt(worldPosition)
//5. If We Have Two Nodes Then Measure The Distance
if let distance = distanceBetweenNodes(){
print("Distance == \(distance)")
}
}
/// Creates A Marker Node
///
/// - Parameter position: SCNVector3
func createSphereNodeAt(_ position: SCNVector3){
//1. If We Have More Than 2 Nodes Remove Them All From The Array & Hierachy
if nodes.count >= 2{
nodes.forEach { (nodeToRemove) in
nodeToRemove.removeFromParentNode()
}
nodes.removeAll()
}
//2. Create A Marker Node With An SCNSphereGeometry & Add It To The Scene
let markerNode = SCNNode()
let markerGeometry = SCNSphere(radius: 0.01)
markerGeometry.firstMaterial?.diffuse.contents = UIColor.cyan
markerNode.geometry = markerGeometry
markerNode.position = position
sceneView.scene.rootNode.addChildNode(markerNode)
//3. Add It To The Nodes Array
nodes.append(markerNode)
}
/// Converts A matrix_float4x4 To An SCNVector3
///
/// - Parameter matrix: matrix_float4x4
/// - Returns: SCNVector3
func positionFromMatrix(_ matrix: matrix_float4x4) -> SCNVector3{
return SCNVector3(matrix.columns.3.x, matrix.columns.3.y, matrix.columns.3.z)
}
/// Calculates The Distance Between 2 Nodes
///
/// - Returns: Float?
func distanceBetweenNodes() -> Float? {
guard let firstNode = nodes.first, let endNode = nodes.last else { return nil }
let startPoint = GLKVector3Make(firstNode.position.x, firstNode.position.y, firstNode.position.z)
let endPoint = GLKVector3Make(endNode.position.x, endNode.position.y, endNode.position.z)
let distance = GLKVector3Distance(startPoint, endPoint)
return distance
}
For an example of a measuringApp which might help your development you can look here: ARKit Measuring Example
Hope it helps...
This looks like a logic issue. You're assigning nodes.last to tail just before checking if tail is not nil. So it will never be != nil so you'll never execute the nodes.append(sphere) in the else.
I agree with #dfd. Set a breakpoint to make sure the code is being executed before continuing.

iOS Sprites Contact Detection

I'm working on a platform game where the character can climb ladders. Since the total length of each ladder varies, I have several ladders stacked on top of each other in SceneEditor. Upon entering a ladder, the contact delegate works fine and my code allows the character to move up the ladder. The problem I'm having is that as soon as the character moves off the first ladder segment, the didEnd method fires even though the character has entered the next ladder segment. I've gotten around it by given the ladder segments different category masks. Is this the only way to do it?
try overlapping the ladders by 1 rung, and then set a count whenever didBegin contact is fired increase the value by 1 whenever didEnd is called decrease the value by 1. at the end of the didEnd func check if onladderCount == 0. if it is, trigger whatever code is supposed to fire when the player is not on a ladder.
this assumes that you have a ladder class, and will have to put a property in the ladder class for onLadder to ensure that the += 1 isn't called multiple times.
var onladderCount: Int = 0
func didBegin(_ contact: SKPhysicsContact) {
let contactAName = contact.bodyA.node?.name
let contactBName = contact.bodyB.node?.name
if (contactAName == "ladder") || (contactBName == "ladder") {
let ladder: Ladder? = (contact.bodyA.categoryBitMask == PhysicsCategory. ladder ? (contact.bodyA.node as? Ladder) : (contact.bodyB.node as? Ladder))
if !ladder.onLadder {
ladder.onLadder = true
onladderCount += 1
}
}
}
func didEnd(_ contact: SKPhysicsContact) {
let contactAName = contact.bodyA.node?.name
let contactBName = contact.bodyB.node?.name
if (contactAName == "ladder") || (contactBName == "ladder") {
let ladder: Ladder? = (contact.bodyA.categoryBitMask == PhysicsCategory. ladder ? (contact.bodyA.node as? Ladder) : (contact.bodyB.node as? Ladder))
if ladder.onLadder {
ladder.onLadder = false
onladderCount -= 1
}
}
if onladderCount == 0 {
//trigger whatever code happens when they leave a ladder
}
}

SpriteKit Issue with Double Jump only working some of the time

Code:
func jumpAction(_ JumpPos: CGPoint, _ clickPos: CGPoint) {
if jumpCount < 2 {
var disty = (clickPos.y - JumpPos.y)
var distx = clickPos.x
self.physicsBody?.velocity = CGVector(dx: 0, dy: 0)
self.physicsBody?.applyImpulse(CGVector(dx: distx, dy: disty))
if onGround == false {
jumpCount += 1
}
}
}//
func didBegin(_ other: SKSpriteNode, _ contact: SKPhysicsContact) {
if contact.contactPoint.y < self.frame.minY + 10 {
jumpCount = 1
onGround = true
}
if other.physicsBody?.categoryBitMask == Mask.SpinPlat {
jumpCount = 0
}
}
func didEnd(_ contact: SKPhysicsContact) {
onGround = false
}
I have 3 functions above. The JumpAction is called when I press on the Jump Button on the screen. The didBegin and didEnd functions are called when contact with the top of a platform occurs with the player.
My issue:
So if player is on platform and I jump, the jump action is called before the didEnd function. This is why I have the jumpCount set to 1 when the player contacts the platform. This works fine...
The real issue is when my player lands on the platform, and he bounces slightly off (due to restitution) and then the jump button is pressed.
When this occurs, the onGround flag is flipped to false before the jump action is called and my player can only single jump.
I'm trying to find a simple way to get around this issue without having to implement a bunch of code, haven't found a way yet.
So this is why I am here. I want to be able to double jump even with that slight little bounce I explained earlier.
Thank you in advance!!!
Additional Info:
I use the onGround flag so that if the player is on the ground, he can slide along the ground if they press the jump button in the right location...
Have you tried simply delaying setting the "on ground" flag by a fraction of a second?
let waitAction = SKAction.wait(forDuration: 0.5)
scene.runAction(waitAction, completion: {
self.onGround = false
})
Obviously you could experiment with the exact duration to make sure it gives the "bounce" just enough time to end.

Creating sound effects for bouncing ball in SpriteKit

My Swift game uses SpriteKit and SKPhysics to animate a small red ball that frequently collides with other physicsBodys over the course of the game. I want to create an natural sounding bounce sound effect whenever the ball collides. I currently have a rather disorderly method for doing this, and there's a significant lag noticeable while employing it that disappears when I disable sounds:
var bounceSoundPlayers = [AVAudioPlayer!]()
func buildBounceSound() {
let path = Bundle.main.path(forResource: "Bounce.mp3", ofType:nil)!
let url = URL(fileURLWithPath: path)
for _ in 0...2 {
do {
let sound = try AVAudioPlayer(contentsOf: url)
sound.prepareToPlay()
bounceSoundPlayers.append(sound)
} catch {
fatalError("couldn't load music file")
}
}
}
This creates three AVAudioPlayers with sound "Bounce.mp3" and runs the prepareToPlay() method on them. Then it stores them in an array called bounceSoundPlayers so I can use them later. Then I have a didBeginContact method that is called whenever anything collides with anything:
func didBegin(_ contact: SKPhysicsContact) {
if contact.collisionImpulse > 0.45 && defaults.bool(forKey: "SFX") {
if ((contact.bodyA.node!.name == "ball" && contact.bodyB.node!.name == "permeable platform") ||
(contact.bodyB.node!.name == "ball" && contact.bodyA.node!.name == "permeable platform") ||
(contact.bodyA.node!.name == "ball" && contact.bodyB.node!.name == "ring") ||
(contact.bodyB.node!.name == "ball" && contact.bodyA.node!.name == "ring"))
&&
ball.collidingWithPermeable {
playBounceSound(contact.collisionImpulse/8<=1 ? contact.collisionImpulse/8 : 1)
}
if (contact.bodyA.node!.name == "ball" && contact.bodyB.node!.name == "impermeable platform") ||
(contact.bodyB.node!.name == "ball" && contact.bodyA.node!.name == "impermeable platform") ||
(contact.bodyA.node!.name == "ball" && contact.bodyB.node!.name == "wall") ||
(contact.bodyB.node!.name == "ball" && contact.bodyA.node!.name == "wall") {
playBounceSound(contact.collisionImpulse/5<=1 ? contact.collisionImpulse/5 : 1)
}
}
}
Basically what this method does is it checks each collision, and if the ball collides with the right kind of platform or wall, it calls the method playBounceSound(volume: CGFloat) with a volume proportional to the force of the collision. So if the ball collides with a platform lightly, the volume is quieter than if it's a really strong collision. Here's the playBounceSound method:
var bouncePlayerIndex = Int(0)
func playBounceSound(_ volume: CGFloat) {
let previousIndex = bouncePlayerIndex - 1 >= 0 ? bouncePlayerIndex - 1 : 2
if bounceSoundPlayer[previousIndex].isPlaying {
if bounceSoundPlayer[previousIndex].currentTime > 0.1 {
bounceSoundPlayer[bouncePlayerIndex].volume = Float(volume)
bounceSoundPlayer[bouncePlayerIndex].play()
}
}
else {
bounceSoundPlayer[bouncePlayerIndex].volume = Float(volume)
bounceSoundPlayer[bouncePlayerIndex].play()
}
bouncePlayerIndex += 1
if bouncePlayerIndex > 2 {bouncePlayerIndex = 0}
}
Basically what this method does is it cycles through the three AVAudioPlayers we created earlier by using an index integer. Every time it's called, it tells the bounceSoundPlayer at bouncePlayerIndex to play() at the given volume. Then it increments bouncePlayerIndex so that next time it's called, it uses a different AVAudioPlayer. If bouncePlayerIndex > 2, it goes back to 0. Using three AVAudioPlayers allows me to play multiple bounces simultaneous — i.e. a bounce sound can begin before the last one ended, such as when the ball lands on a platform and bounces high, then lower and lower and more frequently, until it stops bouncing.
I'm sure there's a better way of doing this than with an array of AVAudioPlayers. Please help.
Swift 3
This is the best way I have found to play sounds. They can overlap and you can play as many as you want at the same time. Fast and simple.
1.) Make sure you import the framework
import AVFoundation
2.) Create the function
// my sound file is named shatter.mp3
func shatterSound() {
if let soundURL = Bundle.main.url(forResource: "shatter", withExtension: "mp3") {
var mySound: SystemSoundID = 0
AudioServicesCreateSystemSoundID(soundURL as CFURL, &mySound)
AudioServicesPlaySystemSound(mySound);
}
}
3.) Call the function where you need it with
shatterSound()

How to end a game when hitting an object from below?

Hey so I am making this project in which the player has to jump platforms all the way to the top. Some monsters spawn randomly throughout the game. So the idea is to lose the game when you hit them from below, but can go on if you jump on them. I already did the part in which the player jumps on it and you destroy the monster but I am still stuck on that part to lose the game when you hit it from below. Any ideas on how I can manage to do this? For this project I followed Ray Wenderlich's tutorial on How To Make a Game Like Mega Jump.
So on my GameScene, I have the didBeginContact method:
func didBeginContact(contact: SKPhysicsContact) {
var updateHUD = false
let whichNode = (contact.bodyA.node != player) ? contact.bodyA.node : contact.bodyB.node
let other = whichNode as GameObjectNode
updateHUD = other.collisionWithPlayer(player)
if updateHUD {
lblStars.text = String(format: "X %d", GameState.sharedInstance.stars)
lblScore.text = String(format: "%d", GameState.sharedInstance.score)
}
}
Which then calls the method from the GameObjectNode Scene.
class MonsterNode: GameObjectNode {
var monsterType: MonsterType!
override func collisionWithPlayer(player: SKNode) -> Bool {
if player.physicsBody?.velocity.dy < 0 {
player.physicsBody?.velocity = CGVector(dx: player.physicsBody!.velocity.dx, dy: 450.0)
if monsterType == .Normal {
self.removeFromParent()
}
}
When the player jumps on top of the monster, the monster is removed from the parent. I was trying to set that if the player's velocity is greater than 0 when colliding with the monster, then the player is removed from parent. Then when I go back to my GameScene, I could declare something in my update method so that when the player is removed from the parent call the endGame() method.
override func update(currentTime: NSTimeInterval) {
if gameOver {
return
}
if Int(player.position.y) > endLevelY {
endGame()
}
if Int(player.position.y) < maxPlayerY - 500 {
endGame()
}
}
Of course I wasn't able to make that work, and I still can't. So if anyone could help me out on how I can manage to do this, or probably some tutorial which could guide me into doing this I would really appreciate it! Thank you in advance.
First you use the didBeginContact method to establish if a contact between player and monster has been made. Next you compare the y positions of the player and monster. If the monster's y position is greater than than the player's... BANG, player dies.
The code sample assumes you have multiple monsters, each with a unique name, and have them all stored in an array.
- (void)didBeginContact:(SKPhysicsContact *)contact {
uint32_t collision = (contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask);
if (collision == (CategoryMonster | CategoryPlayer)) {
for(SKSpriteNode *object in monsterArray) {
if(([object.name isEqualToString:contact.bodyB.node.name]) || ([object.name isEqualToString:contact.bodyA.node.name])) {
if(object.position.y > player.position.y) {
// moster is above player
}
}
}
}
Couple of notes... If you have more than one monster active at any one time, you will need to have a unique name for each one. Reason being that you will need to know which monster contacted the player and that can only happen if you can differentiate between monsters. Hence the unique name for each one.
The if check for the y position is a simple one and only needs +1 y to be fulfilled. If it is possible for your player to make side contact with a monster and not die, you can change the if condition to be something like if(object.position.y > player.position.y+50) to make sure that the contact was actually from the bottom.
(I am not all too proficient in Swift yet so code sample is in Obj-C.)