Finding touched SKNode when a child of SCNScene overlaySKScene - ios

I have an SCNScene for a 3D game I am building. I have positioned a HUD over this scene using SCNScene.overlaySKScene.
In order to receive taps on my 3D scene, I use a gesture recogniser and hitTest and this works fine.
I am trying to determine the SKSpriteNodes that I have added to the HUD SKScene on taps now. This does not work with the gesture recogniser.
Having looked at the WWDC 2014 Banana sample, I don't think I am doing anything all that different in my use of the alternative touchesBegan method (below).
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
println("touchesBegan")
let scnView = self.view as SCNView
let skScene = scnView.overlaySKScene
let touch: AnyObject? = touches.anyObject()
let p = touch?.locationInNode(skScene)
let touchedNode = skScene.nodeAtPoint(p!)
println("touchedNode \(touchedNode.name)")
super.touchesBegan(touches, withEvent: event)
}
The 2nd println is finding a nil result.
I create by HUD:
scnView.overlaySKScene = SKScene(fileNamed: "HUDScene.sks")
scnView.overlaySKScene.name = "HUD"
scnView.overlaySKScene.delegate = self
I then add a root SKNode to this scene
var hudRoot = SKNode()
And add SKSpriteNodes with userInteractionEnabled=true to this hudRoot.
touchesBegan is being called, but nil is returned when touching the SKSpriteNodes. Interestingly, if I touch outside of the SKSpriteNodes and NOT on any 3D nodes, then it returns the hudRoot node even though that is defaulted to userInteractionEnabled=false.
Now that I have checked my implementation with Apple's own Bananas example, I am without any remaining ideas.

Try changing:
let p = touch?.locationInNode(skScene)
let touchedNode = skScene.nodeAtPoint(p!)
To:
let p = touch?.locationInNode(hudRoot)
let touchedNode = hudRoot.nodeAtPoint(p!)
The reason I suggest this is because the nodes you're trying to interact with are children of hudRoot and therefore their positions are in hudRoots coordinate system. The touch location you're currently getting is in the skScene's coordinate system.
If the above doesn't solve your problem try logging the touch location to the console and comparing what you're getting with what you're expecting.
Hopefully this helps or at least gives you a hint on what your problem may be.

Related

SpriteKit not detecting physics bodies when using alpha mask but does when using bounding circle

I am pretty new to SpriteKit so I may be missing something quite obvious.
I am attempting to create an interactive map of the US. I have loaded PNG images for each state and placed a couple of them into the main SKScene using the scene editor.
My goal is wanting to detect when each state is tapped by the user. I initially wrote some code that would find the nodes within a touch location however, this meant that the user could tap the frame and it would be counted as a valid tap. I only want to register touches that happen within the state texture and not the frame. Reading online it was suggested to use SKPhysicsBody to register when a tap takes place. So I changed my code to the following.
class GameScene: SKScene {
override func didMove(to view: SKView) {}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location: CGPoint = self.convertPoint(fromView: touch.location(in: self.view))
let body = self.physicsWorld.body(at: location)
if let state = body?.node, let name = state.name {
state.run(SKAction.run({
var sprite = self.childNode(withName: name) as! SKSpriteNode
sprite.color = UIColor.random()
sprite.colorBlendFactor = 1
}))
}
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
}
}
Now, if I choose the Bounding circle body type everything works as expected (shown above in the screenshot). When I click within the boudning circle it runs the SKAction otherwise it does nothing. However, when I change the body type to Alpha mask (the body type I want) it suddenly stops detecting the state. In fact, it returns the SKPhysicsBody for the MainScene entity.
Any advice on how to fix this would be greatly appreciated.
Thanks.
i can reproduce this behavior (bug?) when using the scene editor. however it goes away if you skip the sks file and initialize your sprites in code. (i acknowledge that setting locations for 50 states is more tedious this way.)
class GameScene: SKScene {
let ok = SKSpriteNode(imageNamed: "ok")
let nm = SKSpriteNode(imageNamed: "nm")
let tx = SKSpriteNode(imageNamed: "tx")
override func didMove(to view: SKView) {
tx.name = "Texas"
nm.name = "New Mexico"
ok.name = "Oklahoma"
//set up physics bodies w/ alpha mask
tx.physicsBody = SKPhysicsBody(texture: tx.texture!, size: tx.texture!.size())
tx.physicsBody?.affectedByGravity = false
nm.physicsBody = SKPhysicsBody(texture: nm.texture!, size: nm.texture!.size())
nm.physicsBody?.affectedByGravity = false
ok.physicsBody = SKPhysicsBody(texture: ok.texture!, size: ok.texture!.size())
ok.physicsBody?.affectedByGravity = false
//then position your sprites and add them as children
}
}

SKSpriteNode does not let touches pass through when isUserInteractionEnabled false

I'm attempting to create an overlay in SpriteKit which by using an SKSpriteNode. However, I want the touches to pass through the overlay, so I set isUserInteractionEnabled to false. However, when I do this, the SKSpriteNode still seems to absorb all the touches and does not allow the underlying nodes to do anything.
Here's an example of what I'm talking about:
class
TouchableSprite: SKShapeNode {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("Touch began")
}
}
class GameScene: SKScene {
override func sceneDidLoad() {
// Creat touchable
let touchable = TouchableSprite(circleOfRadius: 100)
touchable.zPosition = -1
touchable.fillColor = SKColor.red
touchable.isUserInteractionEnabled = true
addChild(touchable)
// Create overlay
let overlayNode = SKSpriteNode(imageNamed: "Fade")
overlayNode.zPosition = 1
overlayNode.isUserInteractionEnabled = false
addChild(overlayNode)
}
}
I've tried subclassing SKSpriteNode and manually delegating the touch events to the appropriate nodes, but that results in a lot of weird behavior.
Any help would be much appreciated. Thanks!
Reading the actual sources you find:
/**
Controls whether or not the node receives touch events
*/
open var isUserInteractionEnabled: Bool
but this refers to internal node methods of touches.
By default isUserInteractionEnabled is false then the touch on a child like your overlay SKSpriteNode is, by default, a simple touch handled to the main (or parent) class (the object is here, exist but if you don't implement any action, you simply touch it)
To go through your overlay with the touch you could implement a code like this below to your GameScene.Remember also to not using -1 as zPosition because means it's below your scene.
P.S. :I've added a name to your sprites to recall it to touchesBegan but you can create global variables to your GameScene:
override func sceneDidLoad() {
// Creat touchable
let touchable = TouchableSprite(circleOfRadius: 100)
touchable.zPosition = 0
touchable.fillColor = SKColor.red
touchable.isUserInteractionEnabled = true
touchable.name = "touchable"
addChild(touchable)
// Create overlay
let overlayNode = SKSpriteNode(imageNamed: "fade")
overlayNode.zPosition = 1
overlayNode.name = "overlayNode"
overlayNode.isUserInteractionEnabled = false
addChild(overlayNode)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("GameScene Touch began")
for t in touches {
let touchLocation = t.location(in: self)
if let overlay = self.childNode(withName: "//overlayNode") as? SKSpriteNode {
if overlay.contains(touchLocation) {
print("overlay touched")
if let touchable = self.childNode(withName: "//touchable") as? TouchableSprite {
if touchable.contains(touchLocation) {
touchable.touchesBegan(touches, with: event)
}
}
}
}
}
}
}
You need to be careful with zPosition. It is possible to go behind the scene, making things complicated.
What is worse is the order of touch events. I remember reading that it is based on the node tree, not the zPosition. Disable the scene touch and you should be seeing results.
This is what it says now:
The Hit-Testing Order Is the Reverse of Drawing Order In a scene, when
SpriteKit processes touch or mouse events, it walks the scene to find
the closest node that wants to accept the event. If that node doesn’t
want the event, SpriteKit checks the next closest node, and so on. The
order in which hit-testing is processed is essentially the reverse of
drawing order.
For a node to be considered during hit-testing, its
userInteractionEnabled property must be set to YES. The default value
is NO for any node except a scene node. A node that wants to receive
events needs to implement the appropriate responder methods from its
parent class (UIResponder on iOS and NSResponder on OS X). This is one
of the few places where you must implement platform-specific code in
SpriteKit.
But this may not have always been the case, so you will need to check between 8 , 9, and 10 for consistency

does SKSpriteNode recognize gesture?

i build a wall and the bricks are not moving but i need to have one of them calling some info. this code below is inside a function may be should have this brick global
let brk = SKSpriteNode(imageNamed: imgName)
if (imgName == "brickinfo"){
brk.name = "brickinfo"
}
brk.position = CGPoint(x:CGRectGetMinX(self.frame)+brkx, y: CGRectGetMinY(self.frame)+brky)
if (imgName == "brickinfo"){
let singleTap = UITapGestureRecognizer(target: self, action: Selector("ShowInfo"))
singleTap.numberOfTapsRequired = 1
brk.userInteractionEnabled = true
//brk.addGestureRecognizer(ShowInfo) //do not exist
}
brk.xScale = 0.2
brk.yScale = 0.2
addChild(brk)
i am using the same code for other moving images which mean they have SKAction so when i click on them, touches began is fired. they both using the same code, add a name, addchild().
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */
for touch in touches {
let location = touch.locationInNode(self)
//675
//30
print(self.nodeAtPoint(location).name)
if let theName = self.nodeAtPoint(location).name {
but the name of "brickinfo" is always coming back nil.
Can this brick image fired touchesbegan?
Thank you
ok i was not looking for gesture, i thought i needed to add gesture to trigger the image, but i only needed to have this image responding with its name. After 3-4 days looking at many different ways to do it, i found the solution and it is pretty easy, the zposition was not set up for this specific brick. Now of course this bring its own problem. Sorry for the title of the question. Does xcode comes with a zposition layer view in 3D?
Thank you

moving sprite based on drag length/direction

In my game I have a node on the bottom of the screen that id like to move along the x axis using touch. I'd like for my node to move left or right depending on the direction of the drag, and to also move the same distance as the drag. So if the user drags from left to right (CGPoint(x: 200, y: 500)toCGPoint(x:300, y: 500)) the node would move 100 to the right. This is what I've tried to do, but it didn't work. If anyone has a way to fix this I'd really appreciate it
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
let touch = touches.first as! UITouch
let touchLocation = touch.locationInNode(self)
firstTouch = touchLocation
override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
let touch = touches.first as! UITouch
let touchLocation = touch.locationInNode(self)
secondTouch = touchLocation
if gameStarted {
let change = secondTouch.x - firstTouch.x
let move = SKAction.moveToX(greenGuy.position.x + change, duration: 0.1)
greenGuy.runAction(move)
}
}
Update your touchesMoved with the following code:
let touch = touches.first as! UITouch
let touchLocation = touch.locationInNode(self)
secondTouch = touchLocation
if gameStarted {
let change = secondTouch.x - firstTouch.x
//Update greenGuys position
greenGuy.position = CGPoint(x: greenGuy.position.x + change, y:greenGuy.position.y)
//Update the firstTouch
firstTouch = secondTouch
}
The reason not using an SKAction as I commented before is because we don't know how much time will pass between two calls of touchesMoved method, so we don't know exactly what time to input in SKAction duration.
You have a pretty good start. First, change:
let move = SKAction.moveToX(greenGuy.position.x + change, duration: 0.1)
to:
let move = SKAction.moveByX(greenGuy.position.x + changeInX, duration: moveDuration)
If you want 2-dimensional movement, use the SKAction moveByX:ChangeInX y:ChangeInY duration:moveDuration instead. Now, you have a few more variables that are based on the duration/distance of the swipe. The duration that you will choose for moveDuration will be up to you, and it will be the product of some coefficient and the swipe distance.
To get the swipe distance:
Id advise you to ditch the touches method and use a UIGestureRecognizer. The one you need is UIPanGestureRecognizer.
Here's a useful link detailing its use: UISwipeGestureRecognizer Swipe length .
Essentially it has different states that get set when the user starts or ends a swipe/drag motion. Then, you could take the locationInView at those moments and calculate the distance between them :D
Hope I helped. I know touchesMoved is also a way to do it, but Ive had problems with it in the past (unnecessary lagging and uncertainty) and gesture recognizers are much more intuitive and easier to use.

Differentiating touches based on first or second

I'm trying to implement 2 distinct touch based behaviours. The first behaviour's logic is like this:
override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
let touch = touches.first as! UITouch
let touchLocation = touch.locationInNode(self)
moveViewTo(touchLocation) //moves my view to touchLocation
}
Now, for my second behaviour, I want to rotate the view based on where I have my finger on the screen. For this, I tried this:
override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
let touch = touches.first as! UITouch
let touchLocation = touch.locationInNode(self)
rotateViewTo(touchLocation) //rotates my view to face touchLocation
}
Now, I want these two distinct behaviours to work concurrently. In particular, I want the first touch to change the position of the view, and the second touch to rotate the view.
Rotating is possible only if there are two touches in the view, and changes direction based on the second touch.
Is there any way to differentiate which of the touches are first and which are second? I can't find a way to differentiate from the touches: Set<NSObject> because by nature Set is an unordered collection.
I believe your only option is to store your first touch as a var. My swift isn't strong enough to write out the code for you but here are the steps.
Loop through your touches instead of just grabbing the first one.
Check if there is a firstTouch (your var for tracking first touch)
If there isn't a firstTouch assign the first touch and do your position logic
If there is a first touch check to see if each touch is the firstTouch. If it isn't then do your rotation logic.
On touches ended and canceled loop through each touch. If it is the firstTouch set your var to nil.
Sorry I can't write all the code out for you, but hopefully that gets you going in the right direction.

Resources