Pablo
Pablo

Reputation: 1332

Creation of buttons for SpriteKit

I am creating the main menu for a sprite kit application I am building. Throughout my entire project, I have used SKScenes to hold my levels and the actual gameplay. However, now I need a main menu, which holds buttons like "Play," "Levels," "Shop," etc... However, I don't feel really comfortable the way I am adding buttons now, which is like this:

let currentButton = SKSpriteNode(imageNamed: button)  // Create the SKSpriteNode that holds the button

self.addChild(currentButton) // Add that SKSpriteNode to the SKScene

And I check for the touch of the button like this:

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

    for node in self.nodes(at: touchLocation) {
        guard let nodeName = node.name else {
            continue
        }
        if nodeName == ButtonLabel.Play.rawValue {
            DispatchQueue.main.asyncAfter(deadline: .now()) {
                let transition = SKTransition.reveal(with: .left, duration: 1)
                self.view?.presentScene(self.initialLevel, transition: transition)
                self.initialLevel.loadStartingLevel()
            }
            return
        }
        if nodeName == ButtonLabel.Levels.rawValue {
            slideOut()
        }

    }
}

However, I don't know if this is considered efficient. I was thinking of using UIButtons instead, but for that would I have to use an UIView?

Or can I add UIButtons to an SKView (I don't really get the difference between an SKView, SKScene, and UIView) What is recommended for menus?

Upvotes: 2

Views: 1629

Answers (2)

Ron Myschuk
Ron Myschuk

Reputation: 6061

I totally agree with @Whirlwind here, create a separate class for your button that handles the work for you. I do not think the advice from @ElTomato is the right advice. If you create one image with buttons included you have no flexibility on placement, size, look and button state for those buttons.

Here is a very simple button class that is a subclass of SKSpriteNode. It uses delegation to send information back to the parent (such as which button has been pushed), and gives you a simple state change (gets smaller when you click it, back to normal size when released)

import Foundation
import SpriteKit

protocol ButtonDelegate: class {
    func buttonClicked(sender: Button)
}

class Button: SKSpriteNode {

    //weak so that you don't create a strong circular reference with the parent
    weak var delegate: ButtonDelegate!

    override init(texture: SKTexture?, color: SKColor, size: CGSize) {

        super.init(texture: texture, color: color, size: size)

        setup()
    }

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

        setup()
    }

    func setup() {
        isUserInteractionEnabled = true
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        setScale(0.9)
        self.delegate.buttonClicked(sender: self)
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        setScale(1.0)
    }
}

This button can be instantiated 2 ways. You can create an instance of it in the Scene editor, or create an instance in code.

creating the button in Scene editor

class MenuScene: SKScene, ButtonDelegate {

    private var button = Button()

    override func didMove(to view: SKView) {

        if let button = self.childNode(withName: "button") as? Button {
            self.button = button
            button.delegate = self
        }

        let button2 = Button(texture: nil, color: .magenta, size: CGSize(width: 200, height: 100))
        button2.name = "button2"
        button2.position = CGPoint(x: 0, y: 300)
        button2.delegate = self
        addChild(button2)
    }
}

func buttonClicked(sender: Button) {
    print("you clicked the button named \(sender.name!)")
}

You have to remember to make the scene conform to the delegate

class MenuScene: SKScene, ButtonDelegate

func buttonClicked(sender: Button) {
    print("you clicked the button named \(sender.name!)")
}

Upvotes: 7

Fluidity
Fluidity

Reputation: 3995

For simple scenes what you are doing is fine, and actually preferred because you can use the .SKS file.

However, if you have a complex scene what I like to do is subclass a Sprite and then override that node's touchesBegan.

Here is a node that I use in all of my projects... It is a simple "on off" button. I use a "pointer" to a Boolean via the custom Reference class I made, so that way this node doesn't need to be concerned with your other scenes, nodes, etc--it simply changes the value of the Bool for the other bits of code to do with what they want:

public final class Reference<T> { var value: T; init(_ value: T) { self.value = value } }

// MARK: - Toggler:
public final class Toggler: SKLabelNode {

  private var refBool: Reference<Bool>
  var value: Bool { return refBool.value }

  var labelName: String
  /*
   var offText = ""
   var onText = ""
   */

  func toggleOn() {
    refBool.value = true
    text = labelName + ": on"
  }

  func toggleOff() {
    refBool.value = false
    text = labelName + ": off"
  }

  /*init(offText: String, onText: String, refBool: Reference<Bool>) {
   ref = refBool
   super.init(fontNamed: "Chalkduster")
   if refBool.value { toggleOn() } else { toggleOff() }
   isUserInteractionEnabled = true
   }
   */

  init(labelName: String, refBool: Reference<Bool>) {
    self.refBool = refBool
    self.labelName = labelName
    super.init(fontNamed: "Chalkduster")
    isUserInteractionEnabled = true

    self.refBool = refBool
    self.labelName = labelName
    if refBool.value { toggleOn() } else { toggleOff() }
  }

  public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    if refBool.value { toggleOff() } else { toggleOn() }
  }

  public required init?(coder aDecoder: NSCoder) { fatalError("") }
  override init() {
    self.refBool = Reference<Bool>(false)
    self.labelName = "ERROR"
    super.init()
  }
};

This is a more elaborate button than say something that just runs a bit of code when you click it.

The important thing here is that if you go this route, then you need to make sure to set the node's .isUserInteractionEnabled to true or it will not receive touch input.

Another suggestion along the lines of what you are doing, is to separate the logic from the action:

// Outside of touches func:
func touchPlay() {
  // Play code
}

func touchExit() {
  // Exit code
}

// In touches began:
guard let name = node.name else { return }

switch name {
  case "play": touchPlay()
  case "exit": touchExit()
  default:()
}

PS:

Here is a very basic example of how to use Toggler:

class Scene: SKScene {

  let spinnyNode = SKSpriteNode(color: .blue, size: CGSize(width: 50, height: 50))

  // This is the reference type instance that will be stored inside of our Toggler instance:
  var shouldSpin = Reference<Bool>(true)

  override func didMove(to view: SKView) {
    addChild(spinnyNode)
    spinnyNode.run(.repeatForever(.rotate(byAngle: 3, duration: 1)))

    let toggleSpin = Toggler(labelName: "Toggle Spin", refBool: shouldSpin)
    addChild(toggleSpin)
    toggleSpin.position.y += 100
  }

  override func update(_ currentTime: TimeInterval) {
    if shouldSpin.value == true {
      spinnyNode.isPaused = false
    } else if shouldSpin.value == false {
      spinnyNode.isPaused = true
    }
  }
}

Upvotes: 1

Related Questions