Swift 3: создание меню паузы в SpriteKit путем наложения SKView?

Контекст

Хотя есть некоторые игры, которые предпочитают отказываться от меню паузы - предположительно из-за короткой продолжительности игры, например Не заморачивайтесь. Лично я считаю, что приостановка игры является важной функцией, и хотел бы узнать, как реализовать ее в Swift 3 для SpriteKit.

Я видел попытки сделать это с помощью UIAlertController, и это работает, но я — возможно, ошибочно — считаю, что лучшей альтернативой было бы наложение SKView поверх текущего SKView.

Я просмотрел DemoBots от Apple, чтобы узнать, могли понять, как они ставят игру на паузу. Однако после загрузки и запуска на моем устройстве это вызвало ошибку, поэтому я не склонен следовать этому примеру. Однако, если бы кто-то мог подробно объяснить множество файлов, таких как LevelScene+Pause, SceneManager, SceneOperation и т. д., и то, как они работают вместе, это тоже было бы круто.

Вопрос

Как я могу наложить SKView на GameScene, чтобы сделать меню паузы?

Минимальный рабочий пример

M.W.E., StackOverflow SpriteKit с меню, представляет собой простую игру для контекстуализации ответов. Пожалуйста, ответьте на вопрос, касающийся M.W.E.

Обновлять

Ниже представлена ​​модифицированная версия M.W.E. файла GameScene. При этом учитывается добавление основного узла для элементов, которые нужно приостановить, и другого узла для меню паузы.

Пока работает меню паузы, фон все равно работает, хотя gameNode.isPaused = true. (Попробуйте коснуться самого левого синего спрайта).

//

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

person SumNeuron    schedule 25.01.2017    source источник
comment
Как правило, приостановка игры в SpriteKit может быть простой, и один из способов — приостановить узел-контейнер ваших игровых элементов l. У вас должен быть узел мира и узел кнопок/меню, которые не являются частью мира. Таким образом, вы можете приостановить мировой узел в любое время, используя эти кнопки.   -  person Whirlwind    schedule 25.01.2017
comment
Я согласен с Whirlwind, если вы создаете новую сцену, вы удаляете свою игровую сцену вместо того, чтобы приостанавливать ее: таким образом, вы должны сохранять каждое состояние вашей игры, чтобы воспроизвести его, когда вы вызываете gameScene после сцены паузы (это очень неудобно). ..)   -  person Alessandro Ornano    schedule 25.01.2017
comment
@AlessandroOrnano @Whirlwind, этот вопрос не о том, как сделать паузу. В моей реальной игре у меня есть узел мира и узел камеры, которые прокручиваются, и я могу приостановить их с помощью описанных вами методов. Однако этот вопрос заключается в том, как представить меню паузы, например. вид поверх (а не вместо) GameScene   -  person SumNeuron    schedule 25.01.2017
comment
@SumNeuron Возьмите всегда присутствующий Sprite-kit, это не UIKit, вы столкнетесь с кучей проблем, когда решите обрабатывать несколько SKView, не говоря уже об использовании памяти. Взгляните на этот ответ   -  person Alessandro Ornano    schedule 25.01.2017
comment
@SumNeuron Очевидно, вы пытаетесь добавить элемент UIKit поверх текущего представления. Но в SpriteKit это не так, и я сомневаюсь, что вы найдете что-то подобное в DemoBots, на которые вы ссылались выше. В SpriteKit все так: сцена представлена ​​SKView. Меню паузы должно быть частью сцены, а не SKView. Меню паузы должно быть подклассом какого-то узла, а не UIView.   -  person Whirlwind    schedule 25.01.2017
comment
@AlessandroOrnano Я ценю ваши постоянные хорошие ответы и советы. Однако я не считаю этот ответ актуальным. M.W.E. которые я использую для этого и других моих вопросов, имеют ровно 1 ViewController и несколько Scenes (как предполагает этот вопрос). Все, что я хочу, это иметь отображение меню паузы, когда игра приостановлена. Я могу использовать UIAlertController, но лучше не буду. Тогда как мне сделать меню паузы? Добавить подвид?   -  person SumNeuron    schedule 25.01.2017
comment
@Whirlwind Я не могу ответить, делают ли это DemoBots. Я не понимаю код. Однако оказывается, что LevelScene имеет способ представить другой вид поверх (из просмотра файла LevelScene+Pause). Как вы порекомендуете мне сделать так, чтобы меню появлялось, когда игра поставлена ​​на паузу? Добавить большой белый прямоугольный спрайт с дочерними спрайтами, которые действуют как кнопки??   -  person SumNeuron    schedule 25.01.2017
comment
@AlessandroOrnano тоже нет, я еще не передал новую PauseScene в MWE. (поэтому я вставляю это в вопрос). Я не уверен, что SKView - правильный путь....   -  person SumNeuron    schedule 25.01.2017
comment
О, спасибо, это не ответ, это всего лишь комментарий и несколько советов с моей точки зрения: вам решать. В этом случае я согласен с Whirlwind: вам следует подумать о рефакторинге этого меню паузы.   -  person Alessandro Ornano    schedule 25.01.2017
comment
Дело в том, что для упрощения своей жизни можно было бы использовать только ViewController, чем сосредоточить всю свою энергию на сценах и нодах (и поверьте, это уже так тяжело!), потому что, например, один из первых важных нужно проверить, правильно ли освобождается сцена, например, когда вы вызываете другую сцену..   -  person Alessandro Ornano    schedule 25.01.2017
comment
Я пытаюсь помочь вам с дополнительной информацией: не фиксируйте свои идеи в подпредставлениях, думайте о структуре spritekit, у вас есть сцена и узлы, вы можете представить новую сцену, добавить новые узлы, обрабатывать свои сцены и узлы, чтобы сделать все, что вам нужно для вашей игры.   -  person Alessandro Ornano    schedule 25.01.2017
comment
@AlessandroOrnano хорошая мысль. Я недостаточно хорошо знаю SpriteKit Framework, чтобы принимать решения о том, как лучше всего это сделать. Поэтому я с нетерпением жду вашего решения :)   -  person SumNeuron    schedule 25.01.2017
comment
@SumNeuron В данный момент я не могу смотреть на код, но похоже, что в LevelScene есть способ представить другое представление в верхней части, представление, вероятно, является узлом.   -  person Whirlwind    schedule 25.01.2017
comment
@Whirlwind, пожалуйста, проверьте обновление :)   -  person SumNeuron    schedule 26.01.2017
comment
@AlessandroOrnano, пожалуйста, проверьте обновление   -  person SumNeuron    schedule 26.01.2017
comment
@SumNeuron Это потому, что касания сцены начинают работать независимо от того, приостанавливаете ли вы gameNode или сцену. Он не сработает, только если вы приостановите просмотр. Тем не менее, вам не нужно приостанавливать весь просмотр или сцену. Вы можете установить логическую переменную экземпляра для сцены, которая отслеживает, приостановлена ​​ли ваша игра. И это все.   -  person Whirlwind    schedule 27.01.2017


Ответы (1)


Я некоторое время боролся с проблемой приостановки игры в игровой сцене.

Как некоторые другие предложили в комментариях, создание «сцены паузы», в которую можно переходить, когда игра поставлена ​​на паузу, а затем из нее, является эффективным решением. Этот подход позволяет избежать проблем, с которыми вы можете столкнуться, когда таймеры срабатывают в игровой сцене, когда игра приостановлена, или анимация пропускается при пробуждении.

Чтобы реализовать сцену паузы, я использую пользовательский подкласс UIViewController для обработки переходов между сценами.

В моем 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)
  }

В классе MyGameScene (подкласс 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
    }

Это моя реализация PauseScene. В этой версии пауза будет возобновлена, когда пользователь нажмет в любом месте сцены паузы, за исключением endGameButton, которая завершает текущую игру:

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

(На самом деле pauseButton больше похож на баннер, информирующий пользователя о состоянии паузы в этой версии)

person caffreyd    schedule 20.03.2017