Error: hit-testing with siblings and the userInteractionEnabled property in the Sprite Kit

Error - the hit test does not work as intended when the brothers and sisters overlap:

There are 2 overlapping nodes in the scene that have the same parent element (i.e. siblings)

The topmost node has userInteractionEnabled = NO , and the other node has userInteractionEnabled = YES .

If the overlap is affected, after the highest node is checked with an error and does not work (because userInteractionEnabled = NO ), instead of the lower node following after testing, it is skipped, and the parent of 2 siblings is checked for impact.

What should happen is that the next sibling (lower node) is being tested with an error, and does not fall into the parent hit-test tag.

According to the Sprite Kit documentation:

“In the scene, when the Sprite Kit processes touch or mouse events, it looks through the scene to find the nearest node that wants to receive the event. If this node does not want the event, the Sprite Kit checks the next nearest node and therefore on. The order of processing hit tests "This is essentially the reverse order of drawing. For a node that should be considered during impact testing, its userInteractionEnabled property must be set to YES. The default value is NO for any node except the node scene."


This is a mistake when the brothers and sisters from node face their parents - this should be the next brother, not his parent. In addition, if the node has userInteractionEnabled = NO , then of course it should be transparent with respect to impact testing - but here it is not the way it leads to a change in behavior when the node test misses.

I searched on the Internet but cannot find anyone and messages or messages about this error. So should I report this?


And then the reason why I posted it here is because I would like to suggest a “fix” for this error (that is, a proposal to implement some code somewhere so that SpriteKit works in the “intended” manner for impact testing)


To reproduce the error:

Use the "Hello World" template provided when you launch the new Game project in Xcode (it has a "Hello World" and adds rocket sprites when you click).

Optional: [I also deleted the image of the raster sprite from the project, since the rectangle with X , which occurs when the image is not found, is easier to work with for debugging, visually]

Add SKSpriteNode to the scene using userInteractionEnabled = YES (now I will call it node A).

Run the code.

You will notice that when you press node A, no rocket sprites are generated. (expected behavior, since the bit test should stop after it succeeds - it stops when it succeeds in node A.)

However, if you create several rockets that are next to node A, and then click on the place where node A and the rocket overlap, then you can create another rocket on top of node A - but this should not be possible. This means that after the test failure fails on the topmost node (rocket with userInteractionEnabled = NO by default), instead of testing node A next, it instead checks the rocket's parent, namely Scene.


Note. I am using Xcode 7.3.1, Swift, iOS. I have not tested yet whether this error is universal.


Additional information: I did some additional debugging (a little complication for replication above) and determined that the hit test is then sent to parents and, therefore, not necessarily to the stage.

+7
swift touch sprite-kit sknode hittest
source share
3 answers

I suspect that the error or documentation is incorrect. In any case, this is a workaround that may be what you are looking for.

It looks like you would like to interact with a node, which may be

  • shaded by one or more nodes whose userInteractionEnabled property userInteractionEnabled set to false
  • node background child
  • deep in the node tree

nodesAtPoint is a good starting point. It returns an array of nodes that intersects the transition point. Add this to the touchesBegan scene and filter out nodes that don't have userInteractionEnabled set to true on

 let nodes = nodesAtPoint(location).filter { $0.userInteractionEnabled } 

At this point, you can sort the array of nodes using zPosition and node -tree depth. You can use the following extension to define these properties for 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 using

 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 verify the code above, define a subclass of SKSpriteNode that allows user interaction

 class Sprite:SKSpriteNode { var offset:CGPoint? // Save the node 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 code above can be used to drag and drop sprites under other sprites (using userInteractionEnabled = true ). Note that even if sprites are children of a blue background sprite that spans the entire scene, the touchesBegan scene is called when the user drags the sprite.

enter image description here

+3
source share

The discrepancy seems to be due to the fact that the SKView template SKView has the ignoresSiblingOrder property set to true in the viewDidLoad implementation on the GameViewController . By default, this property is false .

From the docs:

A Boolean value that indicates whether parent-child relationships and sibling relationships affect the rendering order of nodes in a scene. The default value is false , which means that when several nodes have the same z position, these nodes are sorted and displayed in a deterministic order. Parents face their children, and brothers and sisters appear in array order. If this property is set to true , the position of the nodes in the tree is ignored when determining the rendering order. The order of rendering nodes in the same z-position is arbitrary and can change each time a new frame is rendered. When parental and parental order are ignored, SpriteKit applies additional optimizations to improve rendering performance. If you need nodes that will be displayed in a specific and deterministic order, you must set the z-position of these nodes.

So, in your case, you can just delete this line to get the usual behavior. As @Fujia noted in the comments, this should only affect the processing order, not the testing.

Like UIKit, the direct descendants of SpriteKit UIResponder presumably implement their touch processing methods to forward events along the responder chain. Thus, this inconsistency can be caused by overridden implementations of these methods on SKNode . If you are reasonably certain that the problem is related to these inherited methods, you can work around this problem by overriding them with your own event forwarding logic. In this case, it would be nice to submit a bug report to the test project.

+2
source share

You can solve this problem by overwriting your mouseDown scene (or equivalent touch events) as shown below. Basically you check the nodes at a point and find the one that has the highest zPosition and userInteractionEnabled. This works as a reserve for a situation where you do not have such a node as the highest position to start 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) } 
+2
source share

All Articles