4

Context

While there are some games that choose to forgo the pause menu - putatively because of short game play duration such as Don't Grind - I personally think pausing a game is a critical function and would like to learn how to implement it in Swift 3 for SpriteKit.

I have seen attempts to do this using a UIAlertController, which works, but I - perhaps falsely - believe that the better alternative would be to overlay a SKView on top of the current SKView.

I have looked at Apple's DemoBots to see if I could figure out how they pause the game. However, after downloading and running this on my device, it caused an error, so I am not inclined to follow suit. However, if someone could thoroughly explain the plethora of files such as "LevelScene+Pause", "SceneManager", "SceneOperation", etc and how they work together, that would also be cool.

Question

How can I overlay a SKView over the GameScene to make a pause menu?

Minimum Working Example

The M.W.E., StackOverflow SpriteKit with Menu, is a barebones "game" to contextualize answers. Please answer the question in relation to the M.W.E.

Update

Below is a modified version from the M.W.E. of the file "GameScene". It takes into account adding a main node for elements to be paused and another node for the pause menu.

While the pause menu works, the background still works even though gameNode.isPaused = true. (Try tapping the leftmost blue sprite).

//

//  GameScene.swift
//  StackOverflow
//
//  Created by Sumner on 1/17/17.
//  Copyright © 2017 Sumner. All rights reserved.
//

import SpriteKit
import GameplayKit

class GameScene: SKScene {
    var cam: SKCameraNode!
    
    
    
    var sprite = SKSpriteNode(imageNamed: "sprite")
    var sprite2 = SKSpriteNode(imageNamed: "sprite2")
    
    let pauseLabel = SKLabelNode(text: "Pause!")
    
    
    /*
     *
     * START: NEW CODE
     *
     */
    let gameNode = SKNode()
    var pauseMenuSprite: SKShapeNode!
    let pauseMenuTitleLabel = SKLabelNode(text: "Pause Menu")
    let pauseMenuContinueLabel = SKLabelNode(text: "Resume game?")
    let pauseMenuToMainMenuLabel = SKLabelNode(text: "Main Menu?")
    /*
     *
     * END: NEW CODE
     *
     */
    
    
    var timeStart: Date!
    
    init(size: CGSize, difficulty: String) {
        super.init(size: size)
        gameDifficulty = difficulty
        timeStart = Date()
        /*
         *
         * START: NEW CODE
         *
         */
        pauseMenuSprite = SKShapeNode(rectOf: size)
        /*
         *
         * END: NEW CODE
         *
         */
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func didMove(to view: SKView) {
        backgroundColor = SKColor.white
        
        print("Game starting with \(gameDifficulty) difficulty")
        
        
        
        
        // 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)
        
        /*
         *
         * START: NEW CODE
         *
         */
        gameNode.addChild(sprite)
        gameNode.addChild(sprite2)
        addChild(gameNode)
        /*
         *
         * END: NEW CODE
         *
         */
       
        if gameDifficulty == "hard" {
            let sprite3 = SKSpriteNode(imageNamed: "sprite")
            sprite3.setScale(0.3)
            sprite3.position = CGPoint(x: size.width/4 * 2,y: size.height/2)
            addChild(sprite3)
        }
        
        
        
        pauseLabel.fontColor = SKColor.black
        pauseLabel.position = CGPoint(x: size.width/4 * 2,y: size.height/4)
        addChild(pauseLabel)
        
    }
    
    
    
    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)
        
        let pausedTouchLocation = touch?.location(in: pauseMenuSprite)
        
        if sprite.contains(touchLocation) {
            print("You tapped the blue sprite")
            /*
            let alert = UIAlertController(title: "Alert", message: "Message", preferredStyle: UIAlertControllerStyle.alert)
            let action = UIAlertAction(title: "Ok", style: .default) { action in
                // Handle when button is clicked
                let reveal = SKTransition.doorsOpenVertical(withDuration: 0.5)
                let menuScene = MenuScene(size: self.size)
                self.view?.presentScene(menuScene, transition: reveal)

                
                
            }
            alert.addAction(action)
            if let vc = self.scene?.view?.window?.rootViewController {
                vc.present(alert, animated: true, completion: nil)
            }
            */
            
        }
        
        if sprite2.contains(touchLocation) {
            print("You tapped the purple sprite")
            
            let now = Date()
            let howLong = now.timeIntervalSinceReferenceDate - timeStart.timeIntervalSinceReferenceDate
            
            let reveal = SKTransition.doorsOpenVertical(withDuration: 0.5)
            let scoreScene = ScoreScene(size: self.size, score: howLong)
            self.view?.presentScene(scoreScene, transition: reveal)
        }
        
        
        /*
         *
         * START: NEW CODE
         *
         */
        if pauseMenuContinueLabel.contains(pausedTouchLocation!) {
            pauseMenuSprite.removeFromParent()
            pauseMenuSprite.removeAllChildren()
            
            gameNode.isPaused = true
        }

        
        if pauseMenuToMainMenuLabel.contains(pausedTouchLocation!) {
            let reveal = SKTransition.doorsOpenVertical(withDuration: 0.5)
            let menuScene = MenuScene(size: self.size)
            self.view?.presentScene(menuScene, transition: reveal)
        }

        
        if pauseLabel.contains(touchLocation) {
            print("pause")
            setParametersForPauseMenu(size: size)
            addChild(pauseMenuSprite)
            
            gameNode.isPaused = true
            
        }
        
        /*
         *
         * END: NEW CODE
         *
         */
        
    }
    
    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
    }
    
    /*
     *
     * START: NEW CODE
     *
     */
    func setParametersForPauseMenu(size: CGSize) {
        pauseMenuSprite.fillColor = SKColor.white
        pauseMenuSprite.alpha = 0.85
        pauseMenuSprite.position = CGPoint(x: size.width / 2, y: size.height / 2)
        pauseMenuSprite.zPosition = 100
        
        pauseMenuTitleLabel.fontColor = SKColor.black
        pauseMenuContinueLabel.fontColor = SKColor.black
        pauseMenuToMainMenuLabel.fontColor = SKColor.black
        
        
        pauseMenuTitleLabel.position = CGPoint(x: 0 ,y: size.height / 2 - pauseMenuSprite.frame.size.height / 6 )
        pauseMenuContinueLabel.position = CGPoint(x: 0 ,y: size.height / 2 - pauseMenuSprite.frame.size.height / 6 * 4 )
        pauseMenuToMainMenuLabel.position = CGPoint(x: 0 ,y:  size.height / 2 - pauseMenuSprite.frame.size.height / 6 * 5)
        
        pauseMenuSprite.addChild(pauseMenuTitleLabel)
        pauseMenuSprite.addChild(pauseMenuContinueLabel)
        pauseMenuSprite.addChild(pauseMenuToMainMenuLabel)

    }
    /*
     *
     * END: NEW CODE
     *
     */
}
Community
  • 1
  • 1
SumNeuron
  • 4,850
  • 5
  • 39
  • 107
  • Generally pausing of a game in SpriteKit could be simple and one way is to pause the container node of your game elements l. You should have a world node, and buttons / menu node that are not the part of the world. That way, you can pause the world node at any time using those buttons. – Whirlwind Jan 25 '17 at 08:18
  • I agree with Whirlwind, if you create a new scene then you remove your game scene instead of pause it: by this way you should save every single state of your game to reproduce it when you recall the gameScene after the pause scene (that's very unconfortable..) – Alessandro Ornano Jan 25 '17 at 08:28
  • @AlessandroOrnano @Whirlwind this question isn't about how to pause. In my actual game, I have a world node, and a camera node that scroll and I can pause via the methods you described. However, this question is how to present a *pause menu* e.g. a view on top of (not instead of) the `GameScene` – SumNeuron Jan 25 '17 at 08:31
  • @SumNeuron Take always present Sprite-kit is not UIKit, you will meet tons of problems when you decide to handle multiple SKViews, not to mention the use of memory. Take a look to [this](http://stackoverflow.com/a/35409586/1894067) answer – Alessandro Ornano Jan 25 '17 at 08:52
  • @SumNeuron You are obviously trying to add an UIKit element over the current view. But that is not the way to go in SpriteKit and I doubt you will find something like that in DemoBots you linked above. Things in SpriteKit are like this: a scene is presented by the SKView. Pause menu should be part of a scene, not of an SKView. Pause menu should be a subclass of some kind of a node, not of a UIView. – Whirlwind Jan 25 '17 at 09:06
  • @AlessandroOrnano I appreciate your consistent good answers and advice. However, I do not find that answer relevant. The M.W.E. that I use for this and my other questions have exactly 1 `ViewController` and several `Scenes` (as that question suggests). All I want is to have a Pause Menu display when the game is paused. I can use the `UIAlertController`, but I rather not. So then how can I make the pause menu? Add a subview? – SumNeuron Jan 25 '17 at 09:08
  • @Whirlwind I can not answer whether or not DemoBots does this. I do not understand the code. However it appears that `LevelScene` has a way to present another view on top (from looking at the file `LevelScene+Pause`). How do you recommend I make a menu appear when the game is paused? Add a large white rectangle sprite with child sprites that act as buttons?? – SumNeuron Jan 25 '17 at 09:10
  • @AlessandroOrnano also no, I haven't yet commit the new PauseScene to the M.W.E. (hence me pasting it in the question). I am not sure if SKView is the right way to go.... – SumNeuron Jan 25 '17 at 09:11
  • Oh thank you, that's not an answer, it's only a comment and some advices from my point of view: you decide. In this case, I agree with Whirlwind: you should think to refactor this pause menu. – Alessandro Ornano Jan 25 '17 at 09:11
  • The point is that, to make your life more simple, you could use only a ViewController, than focus all your energies to the scenes and nodes (and believe me, it is already so tough!) because for example, one of the first important thing to check is if a scene is deallocated correctly when you call another scene, for example.. – Alessandro Ornano Jan 25 '17 at 09:21
  • 1
    I try to help you with more informations: don't fix your ideas to the "subviews", think to the spritekit framework , you have a scene and nodes, you can present a new scene, add new nodes, handle your scenes and nodes to make all you need to your game. – Alessandro Ornano Jan 25 '17 at 09:30
  • @AlessandroOrnano good point. I do not know the SpriteKit Framework well enough to make that kind of decision on how best to do this. So I am eager to see your solution :) – SumNeuron Jan 25 '17 at 09:31
  • @SumNeuron I can't look at the code at the moment, but about "appears that LevelScene has a way to present another view on top" part, the *view* is probably a node. – Whirlwind Jan 25 '17 at 09:32
  • @Whirlwind please check the update :) – SumNeuron Jan 26 '17 at 07:06
  • @AlessandroOrnano please check the update – SumNeuron Jan 26 '17 at 07:06
  • @SumNeuron That is because scene's touchesBegan work no matter if you pause the `gameNode` or a scene. It will not fire only if you pause the view. Still, you don't really need to pause the complete view, or a scene. You can set a boolean instance variable on a scene that tracks is your game paused. And that's it. – Whirlwind Jan 26 '17 at 23:03

1 Answers1

3

I struggled with the problem of pausing the game within the game scene for a while.

As several others have suggested in the comments, building a "pause scene" to transition into when the game is paused and then out of is an effective solution. This approach avoids problems you might run into with timers firing within the game scene while the game is paused or animation skips when waking up.

To implement a pause scene, I use a custom subclass of UIViewController to handle scene transitions.

Within my CustomViewController:

var sceneForGame: MyGameScene? //scene to handle gameplay
var paused: PauseScene? //scene to appear when paused

...

// presentPauseScene() and unpauseGame() handle the transition from game to pause and back

  func presentPauseScene() {
    //transition the outgoing scene
    let transitionFadeLength = 0.30
    let transitionFadeColor = UIColor.white
    let pauseTransition = SKTransition.fade(with: transitionFadeColor, duration: transitionFadeLength)
    pauseTransition.pausesOutgoingScene = true

    let currentSKView = view as! SKView
    currentSKView.presentScene(paused!, transition: pauseTransition)
  }

  func unpauseGame() {
    let transitionFadeLength = 0.30
    let transitionFadeColor = UIColor.white
    let unpauseTransition = SKTransition.fade(with: transitionFadeColor, duration: transitionFadeLength)
    unpauseTransition.pausesIncomingScene = false

    let currentSKView = view as! SKView
    currentSKView.presentScene(sceneForGame!, transition: unpauseTransition)
  }

Within MyGameScene class (subclass of SKScene):

var parentViewController: CustomViewController?  // ref to the managing view controller 

...

   // invoke this func when you want to pause
  func setScenePause() {
    parentViewController?.presentPauseScene()
    self.isPaused = true
  }

...

// you may need a snippet like this in your game scene's didMove(toView: ) to wake up when you come back to the game
    else if self.isPaused {
      self.isPaused = false
    }

This is my PauseScene implementation. This version will unpause when the user taps anywhere in the pause scene, except for an endGameButton, which terminates the current game:

struct PauseNames {
  static let endGameButton = "ENDGAME"
  static let pausedButton = "PAUSE"
}

class PauseScene: SKScene {

  var center : CGPoint?
  var pauseButton: SKSpriteNode?
  var endGameButton: SKSpriteNode?
  var parentViewController: CustomViewController?

  override func didMove(to view: SKView) {
    setUpScene()
  }

  func setUpScene() {
    self.backgroundColor = SKColor.white
    self.center = CGPoint(x: self.size.width / 2, y: self.size.height / 2)
    self.isUserInteractionEnabled = false

    setUpSceneNodes()
    showPauseEndButtons()

  } // end setup scene

  func setUpSceneNodes() {
    let buttonScale: CGFloat = 0.5
    let smallButtonScale: CGFloat = 0.25

    let pauseOffset = //some CGPoint
    let endGameOffset = //some CGPoint
    pauseButton = SKSpriteNode(imageNamed: PauseNames.pausedButton)
    pauseButton?.name = PauseNames.pausedButton
    pauseButton?.anchorPoint = CGPoint(x: 0.5, y: 0.5)
    pauseButton?.position = self.center! + pauseOffset
    pauseButton?.alpha = 0
    pauseButton?.setScale(buttonScale)

    endGameButton = SKSpriteNode(imageNamed: PauseNames.endGameButton)
    endGameButton?.name = PauseNames.pausedButton
    endGameButton?.anchorPoint = CGPoint(x: 0.5, y: 0.5)
    endGameButton?.position = self.center! + endGameOffset
    endGameButton?.alpha = 0
    endGameButton?.setScale(smallButtonScale)
  }

  func showPauseEndButtons() {
    let buttonFadeInTime = 0.25
    let pauseDelay = 1.0

    self.addChild(pauseButton!)
    self.addChild(endGameButton!)

    pauseButton?.run(SKAction.fadeIn(withDuration: buttonFadeInTime))
    endGameButton?.run(SKAction.fadeIn(withDuration: buttonFadeInTime))
    self.run(SKAction.sequence([
      SKAction.wait(forDuration: pauseDelay),
      SKAction.run{ self.isUserInteractionEnabled = true }]))
  }

  func endGamePressed() {
    // add confrim logic
    parentViewController?.endGame()
  }

  func unpausePress() {
    parentViewController?.unpauseGame()
  }

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

      if endGameButton!.contains(touchLocation) {
        endGamePressed()
        return
      }
      else {
        unpausePress()
      }

    } // end for each touch
  } // end touchesBegan

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

} //end class PauseScene

(The pauseButton is really more of a banner to inform the user of the pause state in this version)

caffreyd
  • 1,151
  • 1
  • 17
  • 25