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.
How can I overlay a SKView
over the GameScene
to make a pause menu?
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.
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
*
*/
}
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)