Swift 3 (SpriteKit): 游戏结束后重置GameScene

Swift 3 (SpriteKit): Reseting the GameScene after the game ends

我想 'reset' 和 'restart' GameScene 就像第一次调用 GameScene 一样。我已经研究了执行此操作的不同方法,但每次我收到警告,提示我正在尝试向已有父节点的父节点添加节点。但是,在我的代码中,我删除了所有现有节点,所以我真的很困惑如何重置 GameScene。这就是我现在的做法(当我想从头开始重新启动 GameScene 时调用此代码,它在 GameScene class 中调用):

let scene = GameScene(size: self.size)
scene.scaleMode = .aspectFill
let animation = SKTransition.fade(withDuration: 1.0) 
self.view?.presentScene(scene, transition: animation)
self.removeAllChildren()
self.removeAllActions()
self.scene?.removeFromParent()

1.Edited: 我意识到为什么我收到这个警告:"I'm trying to add a node to a parent which already has a parent" 是因为我有class 之外场景的所有变量和全局变量。然而,现在重新开始游戏时,游戏在左下角。为什么会这样,我该如何解决? - 固定

2.Edited: 现在一切正常,但现在我担心 deinit{} 不是即使每个节点都被删除并且 fps 不会随着时间的推移而下降,也会被调用。这是我在我的 GameViewController 中设置场景和我的 GameScene 中的内容(与场景相关的每个实例,所以基本上都是相关的):

import UIKit
import SpriteKit
import GameplayKit

var screenSize = CGSize()

class GameViewController: UIViewController {

override func viewDidLoad() {
    super.viewDidLoad()
    if let view = self.view as! SKView? {
        // Load the SKScene from 'GameScene.sks'
        if let scene = SKScene(fileNamed: "GameScene") {
            // Set the scale mode to scale to fit the window
            scene.scaleMode = .aspectFill
            screenSize = scene.size     
            // Present the scene
            view.presentScene(scene)
        }

        view.ignoresSiblingOrder = true

        view.showsFPS = true
        view.showsNodeCount = true
    }
}

那么我的GameScene基本上就是:

import SpriteKit
import GameplayKit

class GameScene: SKScene, SKPhysicsContactDelegate {
//Declare and initialise variables and enumerations here

deinit{print("GameScene deinited")}

override func didMove(to view: SKView) {
     //Setup scene and nodes
}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    //Do other things depending on when and where you touch

    //When I want to reset the GameScene 
    let newScene = GameScene(size: self.size)
    newScene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
    newScene.scaleMode = self.scaleMode
    let animation = SKTransition.fade(withDuration: 1.0)
    self.view?.presentScene(newScene, transition: animation)
}

任何答案将不胜感激:)

如何重置场景?

只要你想,你只需要再次呈现一个新的、相同的场景。所以,你做得很好。

可能是漏水问题?

另外,如果你的游戏没有泄漏,意味着没有强引用循环,你甚至不需要 self.removeAllChildren()self.removeAllActions()...当然,如果你明确想要要在过渡动画开始之前停止动作,使用此方法很有意义。关键是,当场景解除分配时,所有依赖于它的对象也应该/将解除分配。

不过,如果您从一开始就不知道自己在做什么以及如何防止泄漏,例如。你在 block 中使用了 strong self,它是动作序列的一部分,永远重复,那么你肯定有泄漏,并且 self.removeAllActions() 可能会有所帮助(在许多情况下,但这不是最终的解决方案)。我建议阅读有关捕获列表和 ARC 的一般信息,因为了解这些情况下所有这些是如何工作的可能很有用。

场景是根节点

在场景本身上调用 removeFromParent() 没有任何效果。 Scene 是根节点,因此无法在您当前的上下文中将其删除。如果您检查场景的父级 属性,您会注意到它是零。当然,可以将一个场景添加到另一个场景,但在这种情况下,作为子节点添加的场景将充当普通节点。

最后,新场景如何呈现?很简单,像这样:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {


    let newScene = GameScene(size: self.size)
    newScene.scaleMode = self.scaleMode
    let animation = SKTransition.fade(withDuration: 1.0)
    self.view?.presentScene(newScene, transition: animation)


}

如果某些东西对你不起作用,很可能是你有泄漏(意味着你的场景没有被释放)。要检查这一点,请在 GameScene 的某处重写 deinit 方法,如下所示:

deinit{print("GameScene deinited")} 

进一步向你解释一下......应该发生的是你应该呈现一个新场景,应该发生一个转换,一个旧场景应该被释放,你应该看到一个具有初始状态的新场景.

此外,重写 deinit 只会告诉您场景是否已正确解除分配。但它不会告诉你为什么。由您在代码中找到保留场景的内容。

我能想到的主要有两种方法可以做到这一点。我这样做的主要方式是,如果游戏结束,(由于角色健康下降到零,或者他们与导致回合结束的物体发生碰撞,或者时间到了或其他什么),当这种情况发生时我喜欢过渡到一个新场景,该场景是他们分数的摘要屏幕,他们取得了多远等。

我通过在主 GamePlay 场景中使用一个 bool 变量来做到这一点。

    var gameOver: Bool = false

然后在触发游戏结束的代码中设置该变量 = true。

在更新函数中检查是否 gameOver == true 并转换到 GameOverScene。

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

    // Initialize _lastUpdateTime if it has not already been
    if (self.lastUpdateTime == 0) {
        self.lastUpdateTime = currentTime
    }

    // Calculate time since last update
    let dt = currentTime - self.lastUpdateTime

    // Update entities
    for entity in self.entities {
        entity.update(deltaTime: dt)
    }

    self.lastUpdateTime = currentTime

    if gameOver == true {
        print("Game Over!")

        let nextScene = GameOverScene(size: self.scene!.size)
        nextScene.scaleMode = self.scaleMode
        nextScene.backgroundColor = UIColor.black
        self.view?.presentScene(nextScene, transition: SKTransition.fade(with: UIColor.black, duration: 1.5))
    }
}

更新函数将在每一帧渲染时检查游戏是否结束,如果发现结束,它将执行您需要的任何操作,然后呈现下一个场景。

然后在 GameOverScene 上我放了一个按钮说 "Retry" 当他们点击它时它再次触发 GamePlayScene,运行 视图 DidLoad 函数并从头开始设置 GamePlayScene应该。

这是我如何处理的示例。调用场景转换有几种不同的方法。如果效果不佳,您可以试一试。

    if node.name == "retryButton" {

            if let scene = GameScene(fileNamed:"GameScene") {
                // Configure the view.
                let skView = self.view! as SKView

                /* Sprite Kit applies additional optimizations to improve rendering performance */
                skView.ignoresSiblingOrder = true

                /* Set the scale mode to scale to fit the window */
                scene.scaleMode = .aspectFill
                scene.size = skView.bounds.size

                skView.presentScene(scene, transition: SKTransition.fade(withDuration: 2.0))
            }
        }

这是我处理游戏过渡的首选方法。

另一种方法是创建一个函数来重置所有在播放过程中发生变化的变量。然后您可以使用上面更新函数中的相同代码,但您可以在场景上创建一个标签而不是过渡。如果用户单击标签,它将触发该功能以重置所有已更改的变量、重置玩家位置、停止所有操作、重置玩家健康等。取决于您在游戏过程中更改了多少东西过渡到新场景以提供摘要然后通过按钮重新加载 GamePlayScene 可能会更实用(正如我发现的那样)。然后一切都将加载,就像用户第一次进入主 GamePlayScene 时一样。