适用于SpriteKit和SceneKit的HitTest

6

我有一个看起来非常基础的ViewController,其中包含一个SCNView,并且有一个overlaySKScene

我的问题是,如果首先在覆盖场景中的SpriteKit节点中检测到了点击事件,我不想在底层的self.scene(如下例中的gameScene)中检测到该点击事件。

通过以下代码,即使点击事件发生在SKOverlay场景节点actionButtonNode上,两个场景也会报告发生了点击事件。

ViewController

import UIKit
import SceneKit

class ViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()

    sceneView = self.view as? SCNView
    gameScene = GameScene()

    let view = sceneView
    view!.scene = gameScene
    view!.delegate = gameScene as? SCNSceneRendererDelegate
    view!.overlaySKScene = OverlayScene(size: self.view.frame.size)

    let tapGesture = UITapGestureRecognizer(target: self, action: #selector(ViewController.handleTap(_:)))
    tapGesture.cancelsTouchesInView = false
    view!.addGestureRecognizer(tapGesture)
  }

  @objc func handleTap(_ sender:UITapGestureRecognizer) {
    let projectedOrigin = self.sceneView!.projectPoint(SCNVector3Zero)
    let taplocation = sender.location(in: self.view!)
    let opts = [ SCNHitTestOption.searchMode:1, SCNHitTestOption.ignoreHiddenNodes:0 ]
    let hitList = self.sceneView!.hitTest(taplocation, options: opts)
    if hitList.count > 0 {
      for hit in hitList {
        print("hit:", hit)
      }
    }
  }
}

覆盖场景

import UIKit
import SpriteKit

class OverlayScene: SKScene {
  var actionButtonNode: SKSpriteNode!

  override init(size: CGSize) {
    super.init(size: size)

    self.backgroundColor = UIColor.clear

    self.actionButtonNode = SKSpriteNode()
    self.actionButtonNode.size = CGSize(width: size.width / 2, height: 60)
    self.actionButtonNode.position = CGPoint(x: size.width / 2, y: 80)
    self.actionButtonNode.color = UIColor.blue
    self.addChild(self.actionButtonNode)
  }

  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
  }

  override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    let touch = touches.first
    let location = touch?.location(in: self)
    if self.actionButtonNode.contains(location!) {
      print("actionButtonNode touch")
    }
  }
}
2个回答

1
我没有一份精美的代码解决方案,但有两个可能的解决方式:
a. 通过在主视图控制器中处理所有触摸事件来避免这种情况。我使用SceneKit和SpriteKit叠加层进行处理,将逻辑放在叠加场景外,并仅用于预期用途(图形叠加层,而不是常规交互式SpriteKit场景)。您可以测试是否在Scenekit Viewcontroller中单击了操作按钮或其他任何按钮。如果没有按下按钮,则3D场景被击中。
b. 在主Scenekit视图控制器中添加一个bool变量,isHandledInOverlay并在SpriteKit场景中设置它,如果触摸了按钮。在主Scenekit视图控制器中检查bool变量,在handleTap中返回并且如果值为true则不做任何事情。
根据评论编辑: 我的意思只是移动这段代码:
if self.actionButtonNode.contains(location!) {
  print("actionButtonNode touch")
}

将handleTap函数添加到ViewController中。您需要创建一个变量来保存SKOverlayScene,该变量又将actionButtonNode(和任何其他按钮)作为属性,因此在handleTap中,您最终会得到类似以下的内容:

if self.theOverlay.actionButtonNode.contains(taplocation) {
  print("actionButtonNode touch")
} else if self.theOverlay.action2ButtonNode.contains(taplocation) {
  print("action2ButtonNode touch")
} else if self.theOverlay.action3ButtonNode.contains(taplocation) {
  print("action3ButtonNode touch")
} else {
    //no buttons hit in 2D overlay, let's do 3D hittest:
    let opts = [ SCNHitTestOption.searchMode:1, SCNHitTestOption.ignoreHiddenNodes:0 ]
    let hitList = self.sceneView!.hitTest(taplocation, options: opts)
    if hitList.count > 0 {
        for hit in hitList {
           print("hit:", hit)
        }
    }
}

首先,您需要将2D触摸位置与按钮的2D坐标进行比较,以查看是否已被点击,如果没有,则使用2D触摸位置使用命中测试来查看是否已点击3D场景中的对象。
编辑:刚刚查看了自己(obj c)的代码,并注意到我在主视图控制器中翻转Y坐标以获取SKOverlay中的坐标。
CGPoint tappedSKLoc = CGPointMake(location.x, self.menuOverlayView.view.frame.size.height-location.y);

您可能需要在按钮节点的contains函数中使用提供的位置来执行此操作。

感谢Xartec一直帮助我解决Swift的问题。关于你提到的第一点,我尝试过但并没有成功,因为似乎当我迭代命中测试时,它们的工作方式不同(这是真的吗?),只有场景元素才能被检测到。 - JP Silvashy
是的,您仍然需要手动检查水龙头是否在按钮内,就像您在SpriteKit场景中一样,但是这次在主视图控制器中进行(并且仅在未触摸SpriteKit按钮的情况下才进行scenekit hittest之前)。 - Xartec
Xartec,如果你有一个能够说明你意思的代码示例,如果它有效,我可以授予你赏金。 - JP Silvashy
更新了我的答案并添加了一些代码。我建议保持悬赏开启,看看是否会有更好/其他的答案。 - Xartec
1
添加了另一个必要的部分,翻转Y坐标,否则在SCNView中的触摸和spriteit覆盖之间无法对齐。 - Xartec

1

我犹豫是否要发布,因为我不是专家,并且如果我正确理解了Xartec,我“认为”我以类似的方式解决了它。我也需要在场景中使用屏幕边缘平移 - 花了一段时间才弄清楚。

我创建了handleTap和handlePan(具有识别器状态的begin、changed和end)。

我调用CheckMenuTap来循环遍历所有活动的SpriteKit按钮,如果其中一个按钮被点击,则返回true。

如果不是true,则处理hitTest并查看是否获取到节点。这是我发现的仅避免在不需要它们的情况下同时在SpriteKit和SceneKit上获得命中的唯一方法 - 希望这可以帮助您。

@IBAction func handleTap(recognizer:UITapGestureRecognizer) { let location:CGPoint = recognizer.location(in:scnView)

    if(windowController.checkMenuTap(vLocation: location) == true)
    {
        return
    }

    let hitResults: [SCNHitTestResult]  = scnView.hitTest(location, options: hitTestOptions)
    if(data.gameStateManager.gameState == .run)
    {
        for vHit in hitResults
        {
            //print("Hit: \(vHit.node.name)")
            if(vHit.node.name?.prefix(5) == "Panel")
            {
                if(data.gamePaused == true) { return }
                windowController.gameControl.selectPanel(vPanel: vHit.node.name!)
                return
            }
        }
    }
}

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