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.
Related
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.
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)
}
}
}
This is for a 2D game:
I have certain quality of life SKactions repeating forever, the two big ones for me are coins rotating/bobbing up and down and water flowing.
According to Apple's documentation, SKactions are instanced. So as long as I have the action subclassed then its only running "once" regardless of how many sprites its being used on. For example, as long as I have all my coins getting their actions from the same "Coin" class, then the memory footprint being used by the coin's action is the same regardless if I have 1 or 20 coins.
All that being said, it seems like such a waste to have these actions going when they aren't even in view of the camera/player.
Is there a way to have repeating forever actions deactivate when they aren't in view of the camera? I know that defeats the purpose of "forever" but as far as I can tell its either choosing some some sort of static duration or choosing forever.
Any advice is greatly appreciated.
Is better to add node only when is visible, but if you have few nodes, you can use
func containedNodeSet() -> Set
Example
class Enemy: SKSprite {
func startAnimationForever() {
//do animation if is not already running
}
func stopAnimation() {//stop animation}
}
in your scene, suppose your cam i myCam:
//add all enemies in this var or search in the scene all enemy
var allEnemies = Set<Enemy>()
func allVisibleEnemy() -> Set<Enemy>() {
let allVisibleEnemy = myCam.containedNodeSet().enumerated().flatMap{node in
if let enemy = node as? Enemy {
return enemy
}
}
return Set(arrayLiteral: allVisibleEnemy)
}
func allInvisibleEnemy() -> Set<Enemy>() {
let allVisibleEnemy = allVisibleEnemy()
return allEnemies.substract(allVisibleEnemy)
}
override func update() {
//all your stuff
let allVisibleEnemy = getVisibleEnemy()
let allInvisibleEnemy = allInvisibleEnemy()
allVisibleEnemy.forEach{enemy in
enemy.startAnimationForever()
}
allInvisibleEnemy.forEach{enemy in
enemy.stopAnimation()
}
}
You can optimizate it if necessary
I've not the compiler, fell free to edit.
If for any reason you have nodes that are not on the screen that does need to be on the scene for anything, then you should be taking it off the scene to help improve performance. This would stop actions on the nodes from running, and stop your physics world from having to check if anything physics related needs to placed on it.
Now there are many ways to go about doing this, but a very basic principal would be to establish some kind of map that lays out your nodes (This could be an SKScene that you have not attached to the main scene) Then use the map(scene) to keep track of all of your nodes. Take your camera and find all the nodes on the map (scene) that is in the view of the camera, and move those nodes over to the main scene.
I am developing a game using Sprite and the game works perfectly fine. I am trying to add in a feature that allows you to change things on that GameView. But the buttons for the feature are on a different ViewController. How would I be able to have the code be implemented into a different view controller?
This is the code i want the button to implement
for touch: AnyObject in touches {
// Get the location of the touch in this scene
let location = touch.location(in: self)
// Check if the location of the touch is within the button's bounds
if button.contains(location) {
Ghost.texture = SKTexture(imageNamed:"Ghost2")
}
}
And here is the button
#IBAction func button(_ sender: AnyObject) {
}
But the button is on ViewController1 and the object that i want to change is on ViewController2 (GemScene)
Take a look to this question:
Passing Data between View Controllers (One of the answers shows Swift 3 syntax.)
Anyway, what I personally have done in these cases is to use NotificationCenter, which provides loosely coupled messages even between different ViewControllers. You can take a look here: NSNotifications in Swift 3
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)