hidden nodes don't show up with childnodewithname - swift

I'm building a list with a checkbox function, where the check symbol is a child of the empty check box. In touchesBegan:, the checkbox should go from hidden to unhidden, and back, with each touch of the checkbox. The code looks like this:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if let touch = touches.first {
location = touch.locationInNode(self)
for node in self.nodesAtPoint(location) {
if node.childNodeWithName("checkSymbol")?.hidden == true {
node.childNodeWithName("checkSymbol")?.hidden = false
} else if childNodeWithName("checkSymbol")?.hidden == false {
node.childNodeWithName("checkSymbol")?.hidden = true
}
}
}
}
The problem is the childNode (the checkSymbol node) is not being picked up by the .nodesAtPoint() method when it's hidden. That's the reason I've tried to workaround using the .childNodeWithName() approach above, but the box remains checked for subsequent touches. Has anyone a suggestion for how to fix this?

If you wish for hidden nodes to be picked up by the nodesAtPoint you can always change your implementation of hidden.
You could alternatively use node.zPosition = -1, presuming you have a background node with zPosition 0 to hide behind.

Related

Touch duration in SpriteKit

I am currently working with SpriteKit and I want to run a code block in the update-loop as long as the user is touching a certain SpriteNode. I tried to achieve this by using a Boolean, that gets set to true, when the touchesBegan() method recognises a touch on this node and gets set to false, when the touchesEnded() method recognises a touch ending on this node. However, when the user touches the node and then moves his finger outside of the boundaries, the touchesEnded() method does not recognise this.
Is there a simple way to check if a touch, that began in this node, but then moved outside of it, still exists? Or can I check in general if a UITouch instance still exists?
It's not clear what behavior you want, but in general you probably want to use touch identity to track what's happening.
For example, if you're handling touches in the scene containing the node, and if the desire is simply to have the action start when the node is touched and stop when that touch ends, then something like:
// Whatever touch initiated the action
var activeTouch: UITouch?
// Checked by update loop
var doingSomething = false
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
// Ignore new touches if one is already active
guard activeTouch == nil else { return }
let location = touch.location(in: self)
let touchedNodes = self.nodes(at: location)
for node in touchedNodes {
if <some test for the interesting node> {
activeTouch = touch
doingSomething = true
}
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
if touch == activeTouch {
// Finished
activeTouch = nil
doingSomething = false
}
}
}
If you also want the action to stop if the user moves their finger off the node and restart if they move back on, then also override touchesMoved, e.g.:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
if touch == activeTouch {
let location = touch.location(in: self)
let touchedNodes = self.nodes(at: location)
// Assume they moved off the node
doingSomething = false
for node in touchedNodes {
if <some test for the interesting node> {
// Nope, still touching it
doingSomething = true
}
}
}
}
}
(You should also handle touchesCancelled in some appropriate way, maybe stopping the action and clearing activeTouch as in touchesEnded)
Perhaps you have some other behavior in mind for the case when there are multiple touches active. You might need to keep track of all the active touches and their status of being on or off the node, and then set doingSomething = true if any active touch is on the node. Or maybe you want touchesMoved to discard an active touch as soon as it moves off the node, so moving that touch back on again won't reactivate.
The main point is that keeping track of touch identity gives you a lot of flexibility, but you have to decide on how you want the game to react.

touch transparent area of spritenode

I have a few button with amorphous look. (the Rect of the buttons are intersecting) First I evaluated the BeganTouch in the GameScene. Then I get several touches.
Since I have in my buttons still child nodes, they have swallowed the touchs. Ok, I have made with the help of here a subclass of the SpriteNodes and processed the touch inside the subclass. Now I have the problem that the first button does not pass the touch to the underlying sprites.
But now I would like top ignore the transparent areas of the buttons, how can I do that?
I have read that one can work with physicsbody, I have tried, gravitiy = 0 but since I move and scale the buttons via actions there were violent effects.
Why can I check for the alpha in the touch location and pass the touch to the next sprite.
by the way: how can I get a reference to the view? to get the global loc.
let loc = touch.location(in: view)
with the global touch location I could check all sprites under this point for the alpha!
you can try passing the touch to it's parent (presumably the scene) from your subclass.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first as UITouch! {
//handle whatever code you want your subclass to do
...
//pass the touch event to the parent
super.touchesBegan(touches, with: event)
}
}
and then in your scene, cycle through the your buttons (in this example I have the buttons in a button array)
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first as UITouch! {
let touchLocation = touch.location(in: self)
for button in buttons {
if button.contains(touchLocation) {
//handle all other buttons
...
}
}
}
}
although this seems a little redundant to do the touches in two different locations. #Knight0fDragon is correct, it seems odd to have a button have transparent areas.
ok, it's simple, all buttons are from the same subclass. this subclass delegates to an method in the GameScene. here I can check with
allNodes = nodes(at: globalLocation)
now I can check for the name of the node, calculate the point of the pixel inside each node and get the alpha value.
thanxs all

SpriteKit - Why SKNode's are not being touch detected

I have reviewed countless references to try to understand why my scene is not behaving the way i expected it to, such as this.
Here is my very simple SKScene (2 child nodes):
The scene has a SpriteNode (which covers the entire scene as a background image). This has a zPosition = 0.
The scene has a 2nd node (SKNode) which itself has another child (up to 2 levels). This has a zPosiiton - 2.
ALL nodes have .userInteractionEnabled = false
Issue:
When i click anywhere all i see is that the 1st child (SpriteNode) is touched. The 2nd child (SKNode) is never touch-detected.
Note that the z-ordering of the Nodes are being rendered as I expect them. It is the touch-detection that doesnt appear to be working.
Snippet of my touchesBegan method:
for touch in touches {
let touchLocation = touch.locationInNode(self)
let sceneTouchPoint = self.convertPointToView(touchLocation)
let touchedNode = self.nodeAtPoint(sceneTouchPoint)
if (touchedNode.name != nil) {
print("Touched = \(touchedNode.name! as String)")
}
}
I had a similar issue (background in z: 999 + spawning "ducks" nodes in z: <999) that I solved with the following code in Swift 4:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch:UITouch = touches.first!
let positionInScene = touch.location(in: self)
let touchedNodes = self.nodes(at: positionInScene)
for touch in touchedNodes {
let touchName = touch.name
if (touchName != nil && touchName!.hasPrefix("pato_")) {
touch.removeFromParent()
}
}
}
I had several layers of nodes because I used a mask over my game with buttons to make selections and move forward. I had issues with the buttons not working until I made a "startState:Bool = true" and updated this to false when the start screen was clicked through. I then had each of my buttons on that start page to have && startState==true for there clicks to be taken. It may be that your clicks are being recorded - but its not the node you think you are using. I would put print("NodeXXX") on each entry in touches and give it a unique name so you can see where the touches are actually happening.
Hope that helps.
Best regards,
mpe

Bug: hit-testing with sibling nodes and the userInteractionEnabled property in Sprite Kit

Bug — hit-testing doesn't work as intended when siblings overlap:
There are 2 overlapping nodes in a scene which have the same parent (ie. siblings)
The topmost node has userInteractionEnabled = NO whilst the other node has userInteractionEnabled = YES.
If the overlap is touched, after the topmost node is hit-tested and fails (because userInteractionEnabled = NO), instead of the bottom node being the next to be hit-tested, it is skipped and the parent of the 2 siblings is hit-tested.
What should happen is that the next sibling (the bottom node) is hit-tested rather than the hit-test jumping to the parent.
According to the Sprite Kit documentation:
"In a scene, when Sprite Kit 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, Sprite Kit 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."
This is a bug as siblings of a node are rendered before their parents — a sibling should be the next to be tested, and not its parent. In addition, if a node has userInteractionEnabled = NO, then surely it should be 'transparent' with regards to hit-testing — but here it is not as it results in a change of behaviour as a node is skipped over in the test.
I have searched online, but can't find anyone also reporting or posting about this bug. So should I report this?
And then the reason why I've posted this here is because I would like a suggestion for a 'fix' of this bug (ie. a suggestion for an implementation of some code somewhere so that SpriteKit works in the 'intended' manner for hit-testing)
To replicate the bug:
Use the "Hello World" template provided when you start a new "Game" project in Xcode (it has "Hello World" and adds rocket sprites when you click).
Optional: [I also deleted the rocket sprite image from the project as the rectangle with the X which occurs when the image isn't found is easier to work with for debugging, visually]
Add a SKSpriteNode to the scene with userInteractionEnabled = YES (I'll refer to it as Node A from now on).
Run the code.
You'll notice that when you click on Node A, no rocket sprites are spawned. (expected behaviour since the hit-test should stop after it is successful - it stops as it succeeds on Node A.)
However, if you spawn a few rockets which are next to Node A, and then click on a place where Node A and a rocket overlaps, it is then possible to spawn another rocket on top of Node A — but this shouldn't be possible. This means that after the hit-test fails on the topmost node (the rocket which has userInteractionEnabled = NO by default), instead of testing Node A next, it tests the parent of the rocket instead which is the Scene.
Note: I am using Xcode 7.3.1, Swift, iOS — I haven't tested to see if this bug is universal, yet.
Extra detail: I did some additional debugging (slight complication to the replication above) and determined that the hit-test is sent to the parent afterwards and therefore not necessarily to the scene.
I suspect it's either a bug or the documentation is incorrect. Either way, here's a workaround that may be what you're looking for.
It sounds like you would like to interact with a node that may be
obscured by one or more nodes that have userInteractionEnabled property set to false
a child of a "background" node
deep in the node tree
nodesAtPoint is a good starting point. It returns an array of nodes that intersects the tap point. Add this to the scene's touchesBegan and filter the nodes that don't have userInteractionEnabled set to true by
let nodes = nodesAtPoint(location).filter {
$0.userInteractionEnabled
}
At this point, you can sort the array of nodes by zPosition and node-tree depth. You can use the following extension to determine these properties for a node:
extension SKNode {
var depth:(level:Int,z:CGFloat) {
var node = parent
var level = 0
var zLevel:CGFloat = zPosition
while node != nil {
zLevel += node!.zPosition
node = node!.parent
level += 1
}
return (level, zLevel)
}
}
and sort the array with
let nodes = nodesAtPoint(location)
.filter {$0.userInteractionEnabled}
.sort {$0.depth.z == $1.depth.z ? $0.depth.level > $1.depth.level : $0.depth.z > $1.depth.z}
To test the above code, define a SKSpriteNode subclass that allows user interaction
class Sprite:SKSpriteNode {
var offset:CGPoint?
// Save the node's relative location
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if let touch = touches.first {
let location = touch.locationInNode(self)
offset = location
}
}
// Allow the user to drag the node to a new location
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
if let touch = touches.first, parentNode = parent, relativePosition = offset {
let location = touch.locationInNode(parentNode)
position = CGPointMake(location.x-relativePosition.x, location.y-relativePosition.y)
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
offset = nil
}
}
and add the following touch handlers to the SKScene subclass
var selectedNode:SKNode?
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if let touch = touches.first {
let location = touch.locationInNode(self)
// Sort and filter nodes that intersect with location
let nodes = nodesAtPoint(location)
.filter {$0.userInteractionEnabled}
.sort {$0.depth.z == $1.depth.z ? $0.depth.level > $1.depth.level : $0.depth.z > $1.depth.z}
// Forward the touch events to the appropriate node
if let first = nodes.first {
first.touchesBegan(touches, withEvent: event)
selectedNode = first
}
}
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
if let node = selectedNode {
node.touchesMoved(touches, withEvent: event)
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
if let node = selectedNode {
node.touchesEnded(touches, withEvent: event)
selectedNode = nil
}
}
The following movie shows how the above code can be used to drag/drop sprites that are under other sprites (with userInteractionEnabled = true). Note that even though the sprites are children of the blue background sprite that covers the entire scene, the scene's touchesBegan is called when a user drags a sprite.
You can workaround the issue by overwriting your scene mouseDown (or equivalent touch events) as below. Basically you check the nodes at the point and find the one that has the highest zPosition and userInteractionEnabled. This works as fallback for the situation when you don't have such a node as the highest position to begin with.
override func mouseDown(theEvent: NSEvent) {
/* Called when a mouse click occurs */
let nodes = nodesAtPoint(theEvent.locationInNode(self))
var actionNode : SKNode? = nil
var highestZPosition = CGFloat(-1000)
for n in nodes
{
if n.zPosition > highestZPosition && n.userInteractionEnabled
{
highestZPosition = n.zPosition
actionNode = n
}
}
actionNode?.mouseDown(theEvent)
}
The discrepancy appears to occur because the template's SKView instance has its ignoresSiblingOrder property set to true in the implementation of viewDidLoad on GameViewController. This property is false by default.
From the docs:
A Boolean value that indicates whether parent-child and sibling relationships affect the rendering order of nodes in the scene.
The default value is false, which means that when multiple nodes share the same z position, those nodes are sorted and rendered in a deterministic order. Parents are rendered before their children, and siblings are rendered in array order. When this property is set to true, the position of the nodes in the tree is ignored when determining the rendering order. The rendering order of nodes at the same z-position is arbitrary and may change every time a new frame is rendered. When sibling and parent order is ignored, SpriteKit applies additional optimizations to improve rendering performance. If you need nodes to be rendered in a specific and deterministic order, you must set the z-position of those nodes.
So in your case you should be able to simply delete this line to get the usual behavior. As noted by #Fujia in the comments, this should only affect rendering order, not hit testing.
Like UIKit, SpriteKit's direct descendants of UIResponder presumably implement its touch-handling methods in order to forward events down the responder chain. So this inconsistency may be caused by the overridden implementations of these methods on SKNode. If you're reasonably sure the issue lies with these inherited methods, you could work around the issue by overriding them with your own event-forwarding logic. If that's the case, it'd also be nice to file a bug report with your test project.

How to make SKSpriteNode with fixed position (after adding physics)?

I have several programmatically made SKSpriteNode's. Some of them I want to move around, some I want to be static (have a fixed position). When adding physics to the nodes (need that to be able to do collision detection, right?) and set physicsBodyXXXX.dynamic = false they stay in the same position when moving other object over them. That's fine!
But, I'm still able to grab the node I want to be statically positioned, and move them around. How can I mask out the node I don't want to move in touches function? Or is there another solution?
Tried to find a property like static which made the node's position fixed, but can't find it...
Here's my code for auto generating nodes (in override func didMoveToView(view: SKView):
for Character in englishWord{
// Make letters:
let letterToMove = SKSpriteNode(imageNamed: "\(Character)")
//then setting size and position
var physicsBodyLetterToMove = SKPhysicsBody(rectangleOfSize: letterToMove.size)
physicsBodyLetterToMove.affectedByGravity = false
physicsBodyLetterToMove.allowsRotation = false
physicsBodyLetterToMove.dynamic = false
letterToMove.physicsBody = physicsBodyLetterToMove
self.addChild(letterToMove)
// Make empty boxes for the letters:
let letterRecBox = SKSpriteNode(imageNamed: "EmptyBox")
//then setting size and position
var physicsBodyLetterRecBox = SKPhysicsBody(rectangleOfSize: letterRecBox.size)
physicsBodyLetterToMove.affectedByGravity = false
physicsBodyLetterRecBox.dynamic = false
letterRecBox.physicsBody = physicsBodyLetterRecBox
self.addChild(letterRecBox)
}
So the touches func's:
var selected: [UITouch: SKNode] = [:]
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
/* Called when a touch begins */
selected = [:]
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
selected[touch as UITouch] = nodeAtPoint(location)
}
}
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
for (touch, node) in selected{
if !contains([self], node){
let action = SKAction.moveTo(location, duration: 0.1)
node.runAction(SKAction.repeatAction(action, count: 1))
}
}
}
}
Any idea?
Setting dynamic to false will make the node unaffected by physics. SKActions and touch events are not considered physics so they will still affect your nodes that are not dynamic.
You could do something like:
YourSpriteNode.name = #"staticNode"; //Right after you create it
Then alter your touch method:
for (touch, node) in selected{
if !contains([self], node){
if(![node.name isEqualToString:#"staticNode"])
{
let action = SKAction.moveTo(location, duration: 0.1)
node.runAction(SKAction.repeatAction(action, count: 1))
}
}
}
The newly introduced if statement will prevent any node named "staticNode" from getting moved due to your SKActions. Other nodes will move as expected.
Thanx, that worked.
Only, I had to write it like this:
letterRecBox.name = "staticNode"
...
if !(node.name == "staticNode"){
...then do something...
}