2

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?

Pablo
  • 1,302
  • 1
  • 16
  • 35
  • 1
    This is a major decision point and pain point for working with SpriteKit. Don't feel bad that you're feeling inefficient or otherwise confused about how to do this, everyone that ever uses SpriteKit goes through this phase. My "solutions" are FAR worse than yours. – Confused Jun 07 '17 at 20:19
  • You shouldn't install individual buttons as SKSpriteNode on your home scene. Just create a big home screen as a background picture and place blank nodes to cover buttons. – El Tomato Jun 07 '17 at 22:18
  • 1
    @ElTomato I'm not sure what you are saying, but there is nothing wrong with using nodes as buttons on a scene. – Fluidity Jun 08 '17 at 06:38
  • @ElTomato I think you are a bit wrong here, but maybe you can point out the benefits of what you have stated ? I can see a few drawbacks though. Eg, when you want to change the size of a button, you have to edit the background image + to resize and re-position the blank button. So, two actions vs one in the case of `SKSpriteNode` button... Also, lets say that you want to rotate buttons or animate them...How would you do that ? – Whirlwind Jun 08 '17 at 08:26
  • @Pablo Take a look at this : https://stackoverflow.com/a/23978854/3402095 – Whirlwind Jun 08 '17 at 08:51
  • @Pablo This is how you can create a button using delegation pattern : https://stackoverflow.com/a/36524132/3402095 – Whirlwind Jun 08 '17 at 09:44

2 Answers2

7

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!)")
}
Ron Myschuk
  • 6,011
  • 2
  • 20
  • 32
1

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
    }
  }
}
Fluidity
  • 3,985
  • 1
  • 13
  • 34
  • Hi Fluidity, Can you please update your answer with the usage of Reference class? Say that you have your toggler and another node that rotates forever. How would you toggle the rotation (through node's `paused` property) using your class ? – Whirlwind Jun 08 '17 at 09:45
  • @Whirlwind sure. Basically the point here is to use objects since they are reference types, whereas Bool (or structs) are value type. By wrapping a value-type in a reference type you can directly alter someone else's value type from anywhere ... without a reference to the containing class. It's a pointer. Let me write a gist here. (PS, I can't believe WW is asking me for code :P) – Fluidity Jun 08 '17 at 09:48
  • I am aware of a whole idea, but I somehow failed to reproduce good result. Still it was a brief try, and asking you to write a specific example rather than debugging was more easier :) – Whirlwind Jun 08 '17 at 09:51
  • @Whirlwind my bad I should have included an example. I'm adding a gamescene snippet now – Fluidity Jun 08 '17 at 10:03
  • @Whirlwind ok it's up. Here is another "button" class that uses Reference, this time it's a RefDouble: https://gist.github.com/fluidityt/f6593608adf6b20e2c1017788e93f514 It adjusts a double with an arrow on left or right to increase / decrease, also, UPVOTE MY ANSWER! I need 2k :P – Fluidity Jun 08 '17 at 10:06
  • Okay that was a missing part. I didn't really look into your class completely, and I was expecting something else. More like that you were using inout parameter so that simple toggling will do everything, rather than having a need to do `spinnyNode.isPaused = true/false`. – Whirlwind Jun 08 '17 at 10:15
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/146152/discussion-between-whirlwind-and-fluidity). – Whirlwind Jun 08 '17 at 10:17
  • Also what you used here in order to implement "blind communication" (button shouldn't know about scene's implementation details, because ideally it is generic element) between button and a scene's `shouldSpin` property is usually done using delegation & protocols. Button talk back to the scene using protocols...And that is called delegation. – Whirlwind Jun 08 '17 at 10:42
  • @Whirlwind haha, you say that, but that "blindness" is really just a warm fuzzy feeling of encapsulation that doesn't actually mean anything or do anything (except add another class into the mix). This is a basic C style pointer, and in Swift 4 it will be nice to have much better support for it. Half the time I'm about to delete all methods and make a big file called 'Functions' because this OOP stuff gets pretty tiring trying to play the "what goes where" game when really it should all be straightforward code. The only reason I don't do this is because autocomplete is nice. – Fluidity Jun 08 '17 at 11:11
  • @Whirlwind if you have an example of what you are saying though I'd look at it. I'm familiar with delegation and protocols but I don't really see the point in it here. – Fluidity Jun 08 '17 at 11:13
  • Here is the example https://stackoverflow.com/a/36524132/3402095... This way, button doesn't know anything about scene's implementation, but rather just says to the scene: "hey, I am done, here is some info you can process, and decide what is needed to be done". That is the whole point, but it is a big topic, and I guess not suitable for comments (we can activate Stackoverflow chat though). – Whirlwind Jun 08 '17 at 11:15
  • @Whirlwind ok I will head to chat if you have a minute – Fluidity Jun 08 '17 at 11:18