Adding two buttons on SKScene: no action for the lower button

I am creating an SKScene with two buttons.



import UIKit
    import SpriteKit


    class IntroScene: SKScene {
      override func didMove(to view: SKView) {
        // BUTTON PLAY GAME
        let btnPlay = BtnPlay(normalTexture: defaultBtnTexture,
                              selectedTexture: selectedBtnTexture,
                              disabledTexture: nil)
        btnPlay.size = CGSize(width: 250, height: 75)
        btnPlay.position = CGPoint(x: frame.midX,y: frame.height/3*1)
        btnPlay.name = "btnPlay"
        btnPlay.setButtonAction(target: self, triggerEvent: .TouchUpInside, action: #selector(IntroScene.goToGameScene))
        print("BUTTON PLAY FRAME 1 ",btnPlay.frame)
        addChild(btnPlay)


        // BUTTON 'FIND PLAYERS'
        let btnFindPlayers = BtnFindPlayers(normalTexture: defaultBtnTexture,
                                            selectedTexture: selectedBtnTexture,
                                            disabledTexture: nil)
       
        btnFindPlayers.size = CGSize(width: 250, height: 75)
        btnFindPlayers.position = CGPoint(x: self.frame.midX,y: self.frame.height/3*2)
        btnFindPlayers.name = "btnFindPlayers"
        btnFindPlayers.setButtonAction(target: self, triggerEvent: .TouchUpInside, action: #selector(IntroScene.goToConnectVC))
        print("BUTTON FIND FRAME 1 ",btnFindPlayers.frame)
        addChild(btnFindPlayers)
      }
   
      @objc func goToGameScene() {
        print("Go to Game Scene")
      }
   
      @objc func goToConnectVC() {
        print("Go to Connect VC")
      }
    }



This is the custom class for one of the buttons. The other class uses similar methods:



import SpriteKit


    class BtnPlay: SKSpriteNode {
      enum BtnPlayType: Int {
        case TouchUpInside = 1,TouchDown, TouchUp
      }
   
      var disabledTexture: SKTexture?
      var defaultTexture: SKTexture
      var selectedTexture: SKTexture
      var label: SKLabelNode
   
      var isEnabled: Bool = true {
        didSet {
            if disabledTexture != nil {
                texture = isEnabled ? defaultTexture : disabledTexture
            }
        }
      }
   
      var isSelected: Bool = false {
        didSet {
            texture = isSelected ? selectedTexture : defaultTexture
        }
      }
   
    required init(coder: NSCoder) {
        fatalError("NSCoding not supported")
    }
   
    init(normalTexture defaultTexture: SKTexture!, selectedTexture:SKTexture!, disabledTexture: SKTexture?) {
        self.defaultTexture = defaultTexture
        self.selectedTexture = selectedTexture
        self.disabledTexture = disabledTexture
        self.label = SKLabelNode(fontNamed: "Helvetica");
        self.label.text = "PLAY"
       
        super.init(texture: defaultTexture, color: UIColor.white, size: defaultTexture.size())
        isUserInteractionEnabled = true
       
        self.label.verticalAlignmentMode = SKLabelVerticalAlignmentMode.center;
        self.label.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.center;
        addChild(self.label)
       
        let bugFixLayerNode = SKSpriteNode(texture: nil, color: UIColor.clear, size: defaultTexture.size())
        bugFixLayerNode.position = self.position
        addChild(bugFixLayerNode)
    }
   
    var actionTouchUpInside: Selector?
    var actionTouchUp: Selector?
    var actionTouchDown: Selector?
    weak var targetTouchUpInside: AnyObject?
    weak var targetTouchUp: AnyObject?
    weak var targetTouchDown: AnyObject?
   
    func setButtonAction(target: AnyObject, triggerEvent event:BtnPlayType, action:Selector) {
        switch (event) {
        case .TouchUpInside:
            targetTouchUpInside = target
            actionTouchUpInside = action
        case .TouchDown:
            targetTouchDown = target
            actionTouchDown = action
        case .TouchUp:
            targetTouchUp = target
            actionTouchUp = action
        }
    }
   
    func setButtonLabel(title: NSString, font: String, fontSize: CGFloat) {
        self.label.text = title as String
        self.label.fontSize = fontSize
        self.label.fontName = font
    }
   
    override func touchesBegan(_ touches: Set, with event: UIEvent?) {
        if !isEnabled { return }
       
        isSelected = true
       
        if (targetTouchDown != nil && targetTouchDown!.responds(to: actionTouchDown)) {
            UIApplication.shared.sendAction(actionTouchDown!, to: targetTouchDown, from: self, for: nil)
        }
    }


    override func touchesMoved(_ touches: Set, with event: UIEvent?) {
       
        if !isEnabled { return }
       
        let touch: AnyObject! = touches.first
        let touchLocation = touch.location(in: parent!)
       
        if (frame.contains(touchLocation)) {
            isSelected = true
        } else {
            isSelected = false
        }
       
    }
   
    override func touchesEnded(_ touches: Set, with event: UIEvent?) {
        if !isEnabled { return }
       
        isSelected = false
       
        if (targetTouchUpInside != nil && targetTouchUpInside!.responds(to: actionTouchUpInside!)) {
            let touch: AnyObject! = touches.first
            let touchLocation = touch.location(in: parent!)
           
            if (frame.contains(touchLocation)) {
                UIApplication.shared.sendAction(actionTouchUpInside!, to: targetTouchUpInside, from: self, for: nil)
            }


            print("BUTTON PLAY FRAME ",frame)
            print("BUTTON PLAY TOUCHES ",touchLocation)
        }
       
        if (targetTouchUp != nil && targetTouchUp!.responds(to: actionTouchUp!)) {
            UIApplication.shared.sendAction(actionTouchUp!, to: targetTouchUp, from: self, for: nil)
        }
      }
    }


The scene and the buttons are created just fine. Button "Find Players" is created on top. The problem is that when I tap on the lower button ("Play"), nothing happens. The culprit is tapping on the screen. Whenever I tap somewhere on the screen, except for the lowest part of it, the upper button gets selected. So, if I tap on the lower button, I still see on the console:



BUTTON FIND FRAME  (387.0, 474.5, 250.0, 75.0)
    BUTTON FIND TOUCHES  (409.139404296875, 563.622131347656)


Looks like the frame of the upper button is much greater than the button itself and superimposes the lower button not letting me tap on it.


Have no idea why this is happening.


I would greatly appreciate if someone can help.

Accepted Reply

You should carefully read the section titled "The Hit-Testing Order Is the Reverse of Drawing Order" in this document:


https://developer.apple.com/documentation/spritekit/sknode


Basically, a node that handles touches needs to reject the touch if it's not inside the node's bounds (or whatever "sensitive" area it wants). Your code assumes that the touch won't arrive if it's not inside the node, but that's not true for Sprite Kit event handling.


It may be simpler to NOT enable interaction on the buttons, but leave it enabled for the scene. Then, use the technique described in the last paragraph of the document section mentioned above to find the correct button directly.

Replies

You should carefully read the section titled "The Hit-Testing Order Is the Reverse of Drawing Order" in this document:


https://developer.apple.com/documentation/spritekit/sknode


Basically, a node that handles touches needs to reject the touch if it's not inside the node's bounds (or whatever "sensitive" area it wants). Your code assumes that the touch won't arrive if it's not inside the node, but that's not true for Sprite Kit event handling.


It may be simpler to NOT enable interaction on the buttons, but leave it enabled for the scene. Then, use the technique described in the last paragraph of the document section mentioned above to find the correct button directly.

Thank you, QuinceyMorris.


I did not quite get your first comment, so I implemented what you suggested in your last sentence in the IntroScene class as follows:


override func touchesBegan(_ touches: Set, with event: UIEvent?)
{
        let touch = touches.first
        let positionInScene = touch!.location(in: self)
        let allNodes = nodes(at: positionInScene)
        for node in allNodes
        {
            let nodePositionConverted = self.convert(node.position, from: node)
            let nodeFrameConverted = CGRect(origin: CGPoint(x:nodePositionConverted.x-node.frame.maxX,y:nodePositionConverted.y-node.frame.maxY),size:node.frame.size)
            if nodeFrameConverted.contains(positionInScene)
            {
                if(node == btnPlay)
                {
                    btnPlay.isSelected = true
                }
                else if(node == btnFindPlayers)
                {
                    btnFindPlayers.isSelected = true
                }
            }
        }
    }


Cheers.