Search code examples
swiftsprite-kitskspritenodeskcameranode

Swift 3.0: SKSpriteNode as Button does not work when given a non-SKView parent?


There are already a few questions out there regarding how to make a button in SpriteKit such as Swift Spritekit Adding Button Programaticly and Setting up buttons in SKScene.

Either way, the solution is to make an SKSpriteNode with a texture for the button and then in the function touchesEnded, test to see if the touch falls within the button-SKSpriteNode.

Interestingly, however, is that if one were to make the button a child to, say a SKCameraNode, or another SKSpriteNode, then this method no longer works.

My question is why? and how to overcome this dilemma.

UPDATE:

In regards to the post below here are two alternate version of simple GameScene.swift file. The main difference is that in the first case, sprite2 is not a child of the camera, whereas in the version 2 it is.

Note: In the gifs, notice how on version 1, clicking the purple sprite (sprite2) causes it to print "You tapped the purple sprite.", whereas it says "blue sprite" in version 2. Thus the issue is made clear:

Tapping the child of another SKNode registers as tapping the uppermost parent node, rather than as the node actually tapped!

New question: How to correct this.

Addendum: in version 2 sprite2 has changed positions a bit due to becoming a child of the camera - as it is an M.W.E. and it doesn't impact this demonstration, I chose not to correct it.

Version 1

class GameScene: SKScene {
    var cam: SKCameraNode!

    var sprite = SKSpriteNode(imageNamed: "sprite")
    var sprite2 = SKSpriteNode(imageNamed: "sprite2")


    override func didMove(to view: SKView) {
        backgroundColor = SKColor.white

        sprite.position = CGPoint(x: size.width/4,y: size.height/2)
        sprite2.position = CGPoint(x: size.width/4 * 3,y: size.height/2)

        sprite.anchorPoint = CGPoint(x:0.5, y:0.5)

        // Set up the camera
        cam = SKCameraNode()
        self.camera = cam
        cam.setScale(3.0)

        cam.position = CGPoint(x: size.width/4, y: 0)
        sprite.addChild(cam)



        addChild(sprite); addChild(sprite2)


    }


    func touchDown(atPoint pos : CGPoint) {

    }

    func touchMoved(toPoint pos : CGPoint) {

    }

    func touchUp(atPoint pos : CGPoint) {

    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

        for t in touches { self.touchDown(atPoint: t.location(in: self)) }
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        for t in touches { self.touchMoved(toPoint: t.location(in: self)) }
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first
        let touchLocation = touch!.location(in: self)

        if sprite.contains(touchLocation) {
            print("You tapped the blue sprite")
        }

        if sprite2.contains(touchLocation) {
            print("You tapped the purple sprite")
        }

    }

    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
        for t in touches { self.touchUp(atPoint: t.location(in: self)) }
    }


    override func update(_ currentTime: TimeInterval) {
        // Called before each frame is rendered
    }
}

enter image description here

Version 2

class GameScene: SKScene {
    var cam: SKCameraNode!

    var sprite = SKSpriteNode(imageNamed: "sprite")
    var sprite2 = SKSpriteNode(imageNamed: "sprite2")


    override func didMove(to view: SKView) {
        backgroundColor = SKColor.white

        // Scale Sprites
//        sprite.setScale(0.3)
        sprite2.setScale(0.3)

        sprite.position = CGPoint(x: size.width/4,y: size.height/2)
//        sprite2.position = CGPoint(x: size.width/4 * 3,y: size.height/2)

        sprite.anchorPoint = CGPoint(x:0.5, y:0.5)

        // Set up the camera
        cam = SKCameraNode()
        self.camera = cam
        cam.setScale(3.0)

        cam.position = CGPoint(x: size.width/4, y: 0)
        sprite.addChild(cam)



        addChild(sprite); //addChild(sprite2)
        cam.addChild(sprite2)


    }


    func touchDown(atPoint pos : CGPoint) {

    }

    func touchMoved(toPoint pos : CGPoint) {

    }

    func touchUp(atPoint pos : CGPoint) {

    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

        for t in touches { self.touchDown(atPoint: t.location(in: self)) }
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        for t in touches { self.touchMoved(toPoint: t.location(in: self)) }
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first
        let touchLocation = touch!.location(in: self)

        if sprite.contains(touchLocation) {
            print("You tapped the blue sprite")
        }

        if sprite2.contains(touchLocation) {
            print("You tapped the purple sprite")
        }

    }

    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
        for t in touches { self.touchUp(atPoint: t.location(in: self)) }
    }


    override func update(_ currentTime: TimeInterval) {
        // Called before each frame is rendered
    }
}

enter image description here


Solution

  • I don't think what you're saying is true. You can detect a touch from a child node. To demonstrate, I just ran a little test code within one of my SpriteKit projects where I detected touch on my camera node

    var cameraNode = SKCameraNode()
    

    Then in didMove(to:):

    addChild(cameraNode)
    camera = cameraNode
    camera?.position = CGPoint(x: size.width/2, y: size.height/2)
    

    Detect touch on cameraNode using touchesEnded:

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
    
        let location = touch.location(in: cameraNode)
    
        print("LocationX: \(location.x), LocationY: \(location.y)")
    }
    

    Here was my print out:

    LocationX: -13.9129028320312, LocationY: 134.493041992188
    

    To further explain, if you added a button as a child of your cameraNode, you would then do something like this:

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
    
        let location = touch.location(in: cameraNode)
    
        // Check if tap was within button frame (assuming your buttonNode is named button and a child of cameraNode)
        if button.frame.contains(location) {
            // Tap was within the frame of the button
            // Do whatever is necessary
        }
    }
    

    Further Edit -

    Your problem stems from the node in which you are requesting the touch location. As I mentioned in my answer you need to extrapolate the touch location based on the node within which you're check the sprite's frame. In your edit you are detecting touch only on self, which will then give you coordinates relative to your scene. If you want to detect touch on a subview like the camera node, you need to request the touch location within the camera node. Here is what I'm talking about based on the code you added:

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
    
        // This will give you touch location from the camera node 'cam' NOT 'self'
        let cameraLocation = touch.location(in: cam)
    
        // This will give you touch location from the scene itself
        let sceneLocation = touch.location(in: self)
    
        if sprite.contains(sceneLocation) {
            print("You tapped the blue sprite")
        }
    
        if sprite2.contains(cameraLocation) {
            print("You tapped the purple sprite")
        }
    
    }
    

    I just tried this and it worked fine for me.