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
Related
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
}
}
I am making a SpriteKit game that has a button node that is at the highest zPosition. I am overriding the touchesBegan function to add functionality to the button. It works because that button is at the highest position in the z dimension. The .zPosition is set to the highest amount compared to any other nodes. I need to have another button in the scene but it needs to have a lower .zPosition because it needs to be covered by another node at one point. When I have a .zPosition that is lower than another node, the touchesBegan method does not trigger. It works if I have a higher .zPosition but that makes the other button node not work. How can I make the touchesBegan method work regardless of what the .zPosition is? Please ask me for more clarification. Thanks!
Once you get the point in touchesBegan did you try to test if the button node contains the point :
func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) -> Bool {
if touches.count == 1 {
if let touch = touches.first {
let location = touch.location(in: self)
if myButtonNode.contains(location) {
...
You want to use the nodes(at:) method. This is an array of all nodes at a particular point, the point being where you touch.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches {
let location = t.location(in: self)
let touchedNodes = self.nodes(at: location) // Array of all nodes at the point
for node in touchedNodes { // Iterate through all nodes in the touchedNode array
if node.name == "your button node" {
... // Add your button code here
}
}
}
}
I'm trying to implement the "3d" audio Apple has added to SpriteKit, but the sounds really don't seem to be positioned at all, even with headphones on. Here's my relevant code.
Play Sound function:
func playTapSound(node: SKNode) {
let tapSound = SKAudioNode(fileNamed: "glassNote")
tapSound.positional = true
tapSound.autoplayLooped = false
node.addChild(tapSound)
tapSound.runAction(SKAction.play())
}
Game Scene:
let listenerNode = SKNode()
...
override func didMoveToView(view: SKView) {
listenerNode.position = view.center
listener = listenerNode
}
playTapSound gets called if the user taps a game piece node.
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touchedNode = scene.nodeAtPoint(sceneTouchPoint)
playTapSound(touchedNode)
}
I've checked the positions of all the nodes including the listener node and they're all correct. Listener node is in the center of the screen and the game pieces are basically a checkerboard layout.
When I tap a piece, I really can't tell there's any sort of positioning on the audio. Is it just extremely subtle? Can't find much info out there about it except Apple saying it's part of SpriteKit..
Turns out my mistake was not adding my listenerNode to the scene before setting it to the listener.
From the docs on listener:
If a non-nil value is specified, it must be a node in the scene.
Game Scene updated and verified:
let listenerNode = SKNode()
...
override func didMoveToView(view: SKView) {
listenerNode.position = view.center
addChild(listenerNode)
listener = listenerNode
}
I've two classes in separate files.
In the "main" file, where my scene is, I'm calling this function:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */
for touch in touches {
character().spawnCharacter()
}
}
So, in my second file, character.swift I've this code:
import SpriteKit
public func spawnCharacter() -> SKSpriteNode{
let charNode = SKSpriteNode(imageNamed: "Hero")
charNode.name = "Hero"
charNode.postion = CGPointZero
charNode.zPosition = 3
addChild(charNode)
print("Node added")
return charNode
}
So, the print line is printed, but the node is never added.
I've been over my code a couple of times, but I can't crack it.
Any clues?
I am not entirely sure how your main file is organised, but I think the problem is here:
for touch in touches {
character().spawnCharacter() // <- SKSpriteNode is created but not added
}
You create sprite-node in spawnCharacter() but you do not use the returned object.
Potential fix would be:
for touch in touches {
let character = character().spawnCharacter()
scene.addChild(character)
}
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.