Detecting touch events on SKSpriteNodes - ios

so there are some similar questions on here, but they don't really answer the questions/problem I am having.
The following explanation of my project may or may not help, I am adding it here just in case...
I am trying to build my first iOS Game, and I have built a scene using the scene editor. The scene contains a SKNode named "World". There is then a backgroundNode and a foregroundNode that are children of the world node. Everything in the scene right now, all SKSpriteNodes, are children of the backgroundNode.
In my GameScene.swift file I have variables attached to the background and foreground nodes so that I can add children to these nodes as the game progresses.
Hopefully this is clear so far...
Now I have added 5 other *.sks files to my project. These file contain scenes that I have made that will be added as children of the foreground node through code in my GameScene.swift file. The SKSpriteNodes in these scene files are placed in the foreground, but their z-position is less than the z-position of one of the background child nodes. This is because I want to have a box appear behind a light beam (the light beam is apart of the background and the box is added to the foreground). Here is a picture in case I caused any confusion
My problem is this...
I want to tap on the screen using gesture recognizers so that when I tap on the box I can then do some stuff. Trouble is that since the light beam has a greater z-position (cause of the effect I want), every time I use the atPoint(_ p: CGPoint) -> SKNode method to determine what node I tapped on I get returned the light beam node and not the box node.
How do I tap on just the box? I have tried changing the isUserInteractionEnabled property for the lights to false already, and I have tried using touchesBegan as shown in many of the other responses to similar question. I have also tried reading the swift developer documents provided by Apple, but I can't figure this out.
The code for my gesture Recognizers is below:
//variables for gesture recognition
let tapGesture = UITapGestureRecognizer()
let swipeUpGesture = UISwipeGestureRecognizer()
//set up the tap gesture recognizer
tapGesture.addTarget(self, action: #selector(GameScene.tappedBox(_:)))
self.view!.addGestureRecognizer(tapGesture)
tapGesture.numberOfTapsRequired = 1
tapGesture.numberOfTouchesRequired = 1
//handles the tap event on the screen
#objc func tappedBox(_ recognizer: UITapGestureRecognizer) {
//gets the location of the touch
let touchLocation = recognizer.location(in: recognizer.view)
if TESTING_TAP {
print("The touched location is: \(touchLocation)")
}
//enumerates through the scenes children to see if the box I am trying to tap on can be detected (It can be detected using this just don't know how to actually detect it by tapping on the box)
enumerateChildNodes(withName: "//Box1", using: { node, _ in
print("We have found a single box node")
})
//tells me what node is returned at the tapped location
let touchedNode = atPoint(touchLocation)
print("The node touched was: \(String(describing: touchedNode.name))")
//chooses which animation to run based on the game and player states
if gameState == .waitingForTap && playerState == .idle{
startRunning() //starts the running animation
unPauseAnimation()
}else if gameState == .waitingForTap && playerState == .running {
standStill() //makes the player stand still
unPauseAnimation()
}
}
Hopefully this is enough for you guys... If you need some more code from me, or need me to clarify anything please let me know

Please read notes in the code. You can get what you want easily in spriteKit. Hope you get the answer.
#objc func tappedBox(_ recognizer: UITapGestureRecognizer) {
let touchLocation = recognizer.location(in: recognizer.view)
// Before you check the position, you need to convert the location from view to Scene. That's the right location.
let point = (recognizer.view as! SKView).convert(touchLocation, to: self)
print ( getNodesatPoint(point, withName: "whatever Node name" ) )
}
// 2 : This function gives you all nodes with the name you assign. If you node has a unique name, you got it.
/// You can change name to other properties and find out.
private func getNodesatPoint(_ point : CGPoint , withName name: String) -> [SKNode] {
return self.nodes(at: point).filter{ $0.name == name}
}

You want to use nodesAtPoint to do a hit test and get all of your nodes that is associated with the point. Then filter the list to find the box you are looking for.
If you have a lot of layers going on, you also may want to just add an invisible layer on top of your graphics that handles nodes being touched
Your other option is to turn off isUserInteractionEnabled on everything except the boxes and then override the touch events for your boxes, but that would mean you can't use gestures like you are doing now.

Related

Detecting when a SKNode is tapped on Apple Watch

I'm writing an app for Apple Watch using SpriteKit, so I don't have access to functions like touchesBegan and I have to use a WKTapGestureRecognizer to detect taps, no big deal, but I have issues detecting taps on a node.
In my InterfaceController I have:
#IBAction func handleTap(tapGestureRecognizer: WKTapGestureRecognizer){
scene?.didTap(tapGesture: tapGestureRecognizer)
}
And in my Scene file I have
func didTap(tapGesture:WKTapGestureRecognizer) {
let position = tapGesture.locationInObject()
let hitNodes = self.nodes(at: position)
if hitNodes.contains(labelNode) {
labelNode.text = "tapped!"
}
Problem is the Tap Gesture Recognizer gives me the absolute coordinates of the touch point (for example 11.0, 5,0) while my node is positioned relatively to the center of the screen (so its position is -0.99,-11.29 even though is at the center of the screen) therefore the tap is hitting the node not when actually tapping it, but when I tap on the top left of the screen. I searched everywhere and it looks like this is the way to do it yet I don't find people having the same issues. The node has been added via the editor. What am I doing wrong?
So you have the right idea. You are getting this wrong because hitNodes is an array of SKNodes. Those are newly created. So when you use hitNodes.contains the addresses of the labelNode and the address of the newly created SKNode that is being compared would be completely different. Therefore it would never be tapped.
Here's what I would do. This would be in my Scene File. Your InterfaceController class is correct.
func didTap(tapGesture:WKTapGestureRecognizer) {
let position = tapGesture.locationInObject()
if labelNode.contains(position) {
labelNode.text = "tapped!"
}
}
OR another way would be this. I like this way because you only have one function which would be in the WKInterfaceControlller And you would need no functions in your Scene File.
#IBAction func tapOnScreenAct(_ sender: WKGestureRecognizer) {
if scene.labelNode.contains(sender.locationInObject()) {
scene.labelNode.text = "tapped!"
}
}
Either way, both should work. Let me know if you have any more questions or clarifications.

ARKit API by example

I'm trying to wrap my head around Apple's ARKit API and I have pushed their example ARKitExample project up to GitHub.
In this demo/sample project, you move your phone camera around your environment, and it appears to automatically detect flat surfaces and place a set of "focus squares" over where your camera is centered over that surface. If you then press a "+" UI button and select from one of several objects (lamp, cups, vase, etc.) and it will render that virtual object in place of the focus squares. You can see all of this in action right here which is probably better than me trying to explain it!
I'm trying to find the place in the code where the virtual object is actually invoked and rendered onscreen. This would be just after it is selected, which I think takes place here:
#IBAction func chooseObject(_ button: UIButton) {
// Abort if we are about to load another object to avoid concurrent modifications of the scene.
if isLoadingObject { return }
textManager.cancelScheduledMessage(forType: .contentPlacement)
performSegue(withIdentifier: SegueIdentifier.showObjects.rawValue, sender: button)
}
But essentially, the user selects a virtual object and then it gets rendered wherever the focus square are currently located at -- I'm looking for where this happens, any ideas?
It adds the virtualObject instance (which is a subclass of SCNNode) as a child of the SCNScene's root node:
func virtualObjectSelectionViewController(_: VirtualObjectSelectionViewController, didSelectObjectAt index: Int) {
guard let cameraTransform = session.currentFrame?.camera.transform else {
return
}
let definition = VirtualObjectManager.availableObjects[index]
let object = VirtualObject(definition: definition)
let position = focusSquare?.lastPosition ?? float3(0)
virtualObjectManager.loadVirtualObject(object, to: position, cameraTransform: cameraTransform)
if object.parent == nil {
serialQueue.async {
self.sceneView.scene.rootNode.addChildNode(object)
}
}
}

How to touch and hold a node in Swift?

I am currently working on a project in which I need the user to press on a node to make an action occur. So currently I am working with some code that will allow this to happen but only if the user touches the screen.
I want it to work when the user touches and holds a specific node. Any help is appreciated! My code is in a picture below.
Thank You!
Since you already know how to do the gesture the rest is easy:
All we are going to do is take the point from the view, convert it to scene coordinates, and grab 1/all node(s) from the scene.
#IBAction func TELE(_ gestureRecognizer : UILongPressGestureRecognizer) {
if gestureRecognizer.state == .began{
var touchPoint = gestureRecognizer.location(in: view)
var touchPointInScene = view.scene.convertPoint(fromView:touchPoint)
//use atPoint for the deepest node, node(:at) for all nodes)
var node = view.scene.atPoint(touchPointInScene)
var nodes = view.scene.nodes(at:touchPointInScene)
}
}

Swift and spritekit, performance

I'm having some issues getting my game working in Swift/SpriteKit, and can't find answers elsewhere so I was hoping I'd get help here.
I have a screen filled with sksprite nodes, around 1800 of them, and when my finger moves over them they disappear.
I originally had this working using physics, by dragging a hidden node around under my finger, however I've just loaded it on my phone and the performance isn't up to scratch.
I then redesigned it to use 'containsPoint'.
Now the performance is better when at rest (60FPS), however whenever I touch the screen is hangs, and CPU goes up.
This is because it's having to loop through the location of each tile on every move, which is over 1800 calculations per move, and compare it to my current touch.
Please can someone suggest a more elegant solution? I've tried using a dictionary however it complains that CGPOINT isn't hashable.
I'll paste the relevant code below, many thanks!
func addTiles() {
let rowsReq=56, colsReq=32
for i in 0..< rowsReq*colsReq {
...
let tileNode:SKSpriteNode = SKSpriteNode()
tilePosArray.append(tileNode.position)
...
}
}
func moveMarker(location: CGPoint){
markerLayer.position=location
for i in 0..<tilePosArray.count {
if (markerLayer.containsPoint(tilePosArray[i])) {
tileTouched(i)
}
}
}
func tileTouched(tileIdInt: Int) {
tileArray[tileIdInt].alpha=0
checkComplete()
}
You could use:
self.nodeAtPoint(point: CGPoint)
or
self.nodesAtPoint(point: CGPoint)
with the touch location to find the node(s) that are being touched, and then the alpha can be changed for the node(s)

Scenekit detecting User tapped object

I recently started using scenekit for scenekit in iOS 8. I am facing difficulty in detecting whether the user has tapped or pressed on the object. Is there any way to do that?
See the documentation for the hitTest method. Call that from wherever you're handling touch events to get a list of 3D scene objects/locations "under" a 2D screen point.
An easy way to get sample code that shows the hitTest in action is to create a sample app using the Game template in XCode6. Create a new project, select the "Game" template.
The hitTest code should be there in the implementation of:
- (void) handleTap:(UIGestureRecognizer*)gestureRecognize
Add a tap gesture to the object and check whether it is a SCNNode()
#objc func tapGestureRec(sender: UIPanGestureRecognizer? = nil){
let location: CGPoint = (sender?.location(in: self.view))!
let hits = self.sceneKitView.hitTest(location, options: nil)
if let tappedNode : SCNNode = hits.first?.node {
...
}
}

Resources