错误:在Sprite Kit中,使用兄弟节点进行点击测试和userInteractionEnabled属性的问题

9
错误 - 当兄弟节点重叠时,命中测试无法按预期工作:

场景中有2个重叠的节点,它们拥有相同的父节点(即兄弟节点)

最上方的节点为 userInteractionEnabled = NO ,而另一个节点为 userInteractionEnabled = YES

如果重叠处被触摸,则在顶部节点被命中测试并失败(因为userInteractionEnabled = NO)后,应该命中测试下一个兄弟节点(即底部节点),而不是跳到这两个兄弟节点的父节点。

此问题应该导致命中测试下一个兄弟节点(即底部节点),而不是跳到父节点。

根据Sprite Kit文档:

"在场景中,当Sprite Kit处理触摸或鼠标事件时,它会遍历场景以查找要接受事件的最近节点。如果该节点不想接受事件,则Sprite Kit会检查下一个最近的节点,依此类推。命中测试处理的顺序基本上与绘制顺序相反。 要考虑在命中测试期间,必须将其userInteractionEnabled属性设置为YES。对于任何节点,除了场景节点之外,默认值为NO。"

这是一个错误,因为节点的兄弟节点比它们的父节点先呈现,所以在测试时应该测试下一个兄弟节点,而不是其父节点。此外,如果一个节点具有 userInteractionEnabled = NO,那么在命中测试方面它应该是“透明”的,但这里不是这样,因为它会导致在测试中跳过一个节点。
我搜索了网络,但找不到任何人也报告或发布了这个错误。那么我应该报告这个错误吗?
然后我在这里发布这篇文章的原因是因为我想寻求对此错误的修复建议(即,在某些代码实现中提供一些建议,使SpriteKit在命中测试方面按照“预期”方式工作)。
要复制这个错误:
使用Xcode中启动新的“Game”项目时提供的“Hello World”模板(它有“Hello World”,并在单击时添加火箭精灵)。
可选:[我还从项目中删除了火箭精灵图像,因为当未找到图像时出现带有X的矩形更易于进行调试]
向场景添加一个SKSpriteNode,设置userInteractionEnabled = YES (从现在开始我将称其为节点A)。
运行代码。
您会注意到当您点击节点A时,不会产生任何火箭精灵。(由于命中测试应在成功后停止 - 当它成功地停在节点A上时,它也停止了。)
但是,如果您产生几个与节点A相邻的火箭,然后单击节点A和火箭重叠的地方,则可能在节点A上方再生成一个火箭 - 但这不应该是可能的。这意味着,在命中测试在最上面的节点(默认情况下具有userInteractionEnabled = NO的火箭)失败后,它不是测试下一个节点A,而是测试火箭的父节点,即Scene。
注意: 我使用的是Xcode 7.3.1、Swift、iOS——我还没有测试这个错误是否是通用的。
额外的细节: 我做了一些额外的调试(略微复杂化了上面的复制),并确定命中测试随后发送给了父级,而不一定是场景。

我需要您的帮助来解决这个问题 - 如果节点重叠,所有种类的错误都会因此而发生,所以在解决这个问题之前,我不敢开始我的新项目。肯定还有其他人在制作项目时也遇到了这个问题,因为它非常基本?非常感谢您的帮助! - Shuri2060
我不确定是否应该将其视为错误......这实际上取决于游戏上下文中哪种方式更可取。我可以很容易地想象一个游戏,如果一个精灵阻挡另一个精灵,那么理所当然的是不会调用下面的那个,而是检查场景通用动作。 - OwlOCR
@GOR 不行 - 这一定是个bug。这种行为与文档所说的相反,这意味着它是无意的。此外,这没有逻辑意义,因为 userInteractionEnabled 属性旨在表示与命中测试相关的透明度,而由于这种行为而失败了。 - Shuri2060
3个回答

3
我怀疑这可能是一个bug,或者文档有误。无论哪种情况,下面是一种解决方法,可能正是你所需要的。
听起来你想要与一个节点进行交互,该节点可能会被以下因素遮挡:
1. 一个或多个节点的`userInteractionEnabled`属性设置为`false` 2. 是一个“背景”节点的子节点 3. 位于节点树的深处
`nodesAtPoint`是一个很好的起点。它返回与点击点相交的节点数组。将其添加到场景的`touchesBegan`中,并通过过滤没有设置`userInteractionEnabled`为`true`的节点来筛选节点。
let nodes = nodesAtPoint(location).filter {
    $0.userInteractionEnabled
}

此时,您可以通过zPosition和节点树深度对节点数组进行排序。您可以使用以下扩展程序确定节点的这些属性:

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)
    }
}

使用排序算法对数组进行排序

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}

为了测试上述代码,请定义一个允许用户交互的SKSpriteNode子类。
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
    }
}

并且在 SKScene 子类中添加以下触摸处理程序

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
    }
}

以下视频展示了如何使用上述代码拖放位于其他精灵下方的精灵(具有 userInteractionEnabled = true)。请注意,即使这些精灵是完全覆盖整个场景的蓝色背景精灵的子级,当用户拖动精灵时,场景的 touchesBegan 也会被调用。

enter image description here


2
不同之处似乎是因为模板的SKView实例在GameViewControllerviewDidLoad实现中将其ignoresSiblingOrder属性设置为true。默认情况下,此属性为false
从文档中可以看到:
一个布尔值,指示父子和同级关系是否影响场景中节点的渲染顺序。默认值为false,这意味着当多个节点共享相同的z位置时,这些节点按照确定性顺序排序并呈现。父节点在其子节点之前呈现,兄弟节点按照数组顺序呈现。当将此属性设置为true时,忽略节点在树中的位置来确定呈现顺序。相同z位置的节点的呈现顺序是任意的,并且每次呈现新帧时都可能会更改。当忽略兄弟姐妹和父母顺序时,SpriteKit应用额外的优化以提高呈现性能。如果您需要以特定且确定的顺序呈现节点,则必须设置这些节点的z-position。
因此,在您的情况下,您应该能够简单地删除此行以获得通常的行为。如评论中@Fujia所述,这只会影响呈现顺序,而不会影响命中测试。
像UIKit一样,SpriteKit的UIResponder的直接后代可能实现其触摸处理方法,以便将事件向下转发到响应者链。因此,这种不一致性可能是由于在SKNode上覆盖的这些方法的实现造成的。如果您相当确定问题出在这些继承的方法上,那么您可以通过使用自己的事件转发逻辑来解决该问题。如果是这种情况,还可以使用您的测试项目提交错误报告。

3
ignoresSiblingOrder 只影响渲染行为,而不影响命中测试。 - Fujia
我同意@Fujia的观点 - 我测试了ignoresSiblingOrder的开启与关闭,但并没有任何区别。即使这种情况发生了,兄弟节点的顺序被交换了,上述事件也不会发生 - hit-test不应该跳过任何一个兄弟节点。 - Shuri2060
在这种情况下,我建议仔细检查您的子节点对UIResponder方法的实现。如果这些看起来正确,那么这可能确实是一个SpriteKit的bug。 - user3739999
是的 - 代码中没有比我在重现错误时所说的更多的内容。然而,根据你所说的,也许可以直接覆盖UIResponder实现中的代码来解决这个问题? - Shuri2060
当然可以。虽然这只是一个解决方法,但你可以重写它们来自己进行排序。这基本上就是@Epsilon答案提供的方法。 - user3739999
显示剩余2条评论

2

您可以通过以下方式解决此问题,覆盖场景的mouseDown(或等效的触摸事件)。基本上,您检查该点处的节点并找到具有最高zPosition和userInteractionEnabled的节点。这适用于当您没有这样的节点作为起始位置时的后备情况。

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)
}

谢谢。我猜测nodes数组按照应该被检查的顺序排序(从最底部的场景父节点到最顶部的节点)?然而,这肯定不太可行:当有节点在某个点上时,点击事件并不能穿透所有节点,因此场景的mouseDown不会被调用。 - Shuri2060
肯定可以。如果在该位置有一个具有userInteractionEnabled的节点,则会调用该节点的mouseDown方法。如果该位置上最顶层的节点是userInteractionEnabled=false的内容,则会调用场景,正如您所注意到的,因此会调用此函数。如果该位置没有节点,则也会调用场景。它有效,请试一试。至于节点顺序问题,这就是为什么在循环中比较zPositions的原因。 - OwlOCR
  1. 当z位置相同时,节点顺序应该按照渲染顺序来比较——除非nodesAtPoint(...)提供了正确的顺序。
  2. 我正在考虑的问题是,这个“补丁”只有在场景的mouseDown被调用时才会被调用。如果我添加一个覆盖整个场景的“毯子”节点,并调整代码使所有内容都添加到这个“毯子”上而不是场景上,则上述问题仍然会发生。如果最上层的节点不是其父节点的话,场景的mouseDown不一定会被调用。
- Shuri2060

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接