1

I'm making pretty heavy use of SKCropNode in my game both for stylistic uses and also in one case where I've built my own SpriteKit version of UIScrollView. I've noticed that when I get a touch event or when a gesture recognizer fires at a certain point, SKScene.nodeAtPoint(...) is still hitting nodes that are hidden at the touch point from the crop node.

How do I prevent SKScene.nodeAtPoint(...) from hitting cropped content, and instead return the next visible node beneath it?

Brent Traut
  • 5,614
  • 6
  • 29
  • 54

2 Answers2

3

You can't. Everything is based on the frame of the content, and with crop nodes, it is huge. The only thing you can do is check the point of touch, use nodesAtPoint to get all nodes, then enumerate one by one, checking whether or not the touch point is touching a visible pixel by either checking the pixel data, or having a CGPath outlining of your sprite and checking if you are inside that.

Knight0fDragon
  • 16,609
  • 2
  • 23
  • 44
  • See here: http://stackoverflow.com/questions/31950525/how-do-i-make-a-skspritenode-that-does-not-respond-to-touch-if-its-pixels-are-t I would have marked duplicate, but the answer was not accepted or upvoted. – Knight0fDragon Sep 04 '16 at 12:37
  • Thanks again, Knight. Crop nodes seem EXTREMELY limited (and even somewhat buggy). A huge cause of frustration. – Brent Traut Sep 04 '16 at 17:06
  • It is not buggy, you are just not using it correctly – Knight0fDragon Sep 04 '16 at 17:44
  • Agreed that this case isn't a bug (but is still a very annoying limitation). But [this](http://stackoverflow.com/questions/39320085/skcropnode-fails-when-i-add-extra-sknode-children-in-hierarchy) seems buggy. – Brent Traut Sep 04 '16 at 18:18
  • @BrentTraut SpriteKit suffers from (my guess) budget, team and inspiration constraint. With SKWarpNode and SKCropNode I don't think they even realise what they're close to achieving, nor do they have any insight of how designers and creatives conceive and perceive these potential capabilities. Not the first time in history engineers have overlooked creativity... And there are bugs, every release, that mystify in both their obviousness and their lack of transparency on when they'll be fixed, or if they're even recognised. Bugs and limitations, and abysmal documentation. A tragic case. – Confused Nov 05 '16 at 16:18
1

I managed to work my way around this one, but it isn't pretty and it doesn't work in every scenario.

The idea is, when a touch is recorded, I iterate over all nodes at that point. If any of those nodes are (or are children of) SKCropNodes, I check the frame of the mask on the crop node. If the touch lies outside the mask, we know the node is not visible at that point and we can assume the touch didn't hit it.

The issue with this method is that I don't do any evaluation of transparency within the crop mask - I just assume it's a rectangle. If the mask is an abnormal shape, I may give false positives on node touches.

extension SKScene {
    // Get the node at a given point, but ignore those that are hidden due to SKCropNodes.
    func getVisibleNodeAtPoint(point: CGPoint) -> SKNode? {
        var testedNodes = Set<SKNode>()

        // Iterate over all the nodes hit by this click.
        for touchedNode in self.nodesAtPoint(point) {
            // If we've already checked this node, skip it. This happens because nodesAtPoint
            // returns both leaf and ancestor nodes, and we test the ancestor nodes while
            // testing leaf nodes.
            if testedNodes.contains(touchedNode) {
                continue
            }

            var stillVisible = true

            // Walk the ancestry chain of the target node starting with the touched
            // node itself.
            var currentNode: SKNode = touchedNode
            while true {
                testedNodes.insert(currentNode)

                if let currentCropNode = currentNode as? SKCropNode {
                    if let currentCropNodeMask = currentCropNode.maskNode {
                        let pointNormalizedToCurrentNode = self.convertPoint(point, toNode: currentCropNode)
                        let currentCropNodeMaskFrame = currentCropNodeMask.frame

                        // Check if the touch is inside the crop node's mask. If not, we
                        // know we've touched a hidden point.
                        if
                            pointNormalizedToCurrentNode.x < 0 || pointNormalizedToCurrentNode.x > currentCropNodeMaskFrame.size.width ||
                                pointNormalizedToCurrentNode.y < 0 || pointNormalizedToCurrentNode.y > currentCropNodeMaskFrame.size.height
                        {
                            stillVisible = false
                            break
                        }
                    }
                }

                // Move to next parent.
                if let parent = currentNode.parent {
                    currentNode = parent
                } else {
                    break
                }
            }

            if !stillVisible {
                continue
            }

            // We made it through the ancestry nodes. This node must be visible.
            return touchedNode
        }

        // No visible nodes found at this point.
        return nil
    }
}
Brent Traut
  • 5,614
  • 6
  • 29
  • 54
  • That stacking of tested nodes does not need to happen, I am pretty sure the list given is zorder from top to bottom. Also you are still registering touches when hitting the transparent parts of your mask, it just wont fire without it. i have a question, does content inside your crop node change? – Knight0fDragon Sep 04 '16 at 18:27
  • I mentioned at the top that I'm registering touches when hitting transparent parts. I don't know how to work around this without some really nasty code. The content inside the crop node does change, yes. – Brent Traut Sep 04 '16 at 18:32
  • What about outside the crop node – Knight0fDragon Sep 04 '16 at 18:34
  • While the crop node is on scene – Knight0fDragon Sep 04 '16 at 18:34
  • Part of what I'm using the crop node for is for scroll containers. I have multiple of these on the screen at the same time, so I guess the content both inside and outside each can change. – Brent Traut Sep 05 '16 at 04:22