iosswiftsprite-kitskview

Swift 3: Making a Pause Menu in SpriteKit by overlaying a SKView?


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
     *
     */
}

Solution

  • 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)