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

5

我想要“重置”和“重新开始”GameScene,使其像第一次调用GameScene一样。我已经尝试了不同的方法来实现这个目标,但每次都会收到警告说我正在尝试向已经有父级的节点添加一个节点。然而,在我的代码中,我删除了所有现有的节点,所以我真的很困惑如何重置GameScene。下面是我现在的做法(当我想要从头开始重启GameScene并且它在GameScene类内部被调用时):

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.编辑:我意识到为什么会出现警告:“我正在尝试将节点添加到已经有父节点的父节点”,原因是我把场景的所有变量都放在类外面作为全局变量。然而,现在当游戏重新开始时,游戏在左下角。为什么会这样,我该如何修复? - 已修复

2.编辑:现在一切都正常了,但我的担心是即使每个节点都被删除且fps不会随时间降低,deinit{}也没有被调用。以下是我在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)
}

任何答案都将不胜感激 :)

关于“尝试将节点添加到已经有父节点的父节点”这个问题,我想说你可能搞砸了 :) 如果旧场景没有被释放,并且你正在全局存储节点并尝试将它们添加到当前场景中,那么就会出现这种情况。不过,这只是一个猜测,也许你的旧场景已经被正确释放,而你真正想做的是将一个节点添加到另一个父节点上。因此,需要更多信息。当你遇到这个错误时,在哪个时刻你正在将一个节点添加到场景中? - Whirlwind
游戏场景文件中的所有变量(包括获取此错误的节点)是否都定义在类外面,这样做可以吗?这可能是问题所在吗? - J.Treutlein
这个方法是有效的,但是当游戏重新开始时,游戏会出现在屏幕左下角。我该如何解决这个问题? - J.Treutlein
2个回答

13

如何重置场景?

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

可能存在的泄漏问题?

如果您的游戏中没有泄漏,也就是没有强引用循环,您甚至不需要self.removeAllChildren()self.removeAllActions()… 当然,如果您明确希望在转换动画开始之前停止动作,则使用这种方法是有意义的。关键是当场景释放时,所有依赖于它的对象也应该/将被释放。

但是,如果您一开始不知道自己在做什么,也不知道如何防止泄漏,例如您正在使用块内的强self作为动作序列的一部分,而该序列永远重复,那么您肯定会发生泄漏,self.removeAllActions()可能有所帮助(在许多情况下,但这并不是终极解决方案)。我建议阅读有关捕获列表和ARC的文章,因为它对于了解所有这些情况的工作原理可能很有用。

场景是根节点

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

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

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只会告诉您场景是否正确地被释放,但不会告诉您原因。需要您找出在您的代码中保留场景的内容。


1
@J.Treutlein 这段代码是基于你的问题的代码编写的。这意味着场景不是从sks文件创建的,这意味着它的anchorPoint设置为0.0, 0.0。所以我不知道你真正想要什么,但如果你想让你的节点默认添加到屏幕中间,可以将场景的anchor更改为0.5, 0.5或从sks文件创建场景。顺便说一下,我认为我已经在你的一个问题中解释过这个了 :) - Whirlwind
FPS不像其他泄漏问题那样随着场景重置的次数逐渐下降。 - J.Treutlein
我已经编辑了我的帖子,并附上了我的游戏相关代码。 - J.Treutlein
@J.Treutlein 当你创建一个空的SpriteKit项目时,GameViewController中有一段代码可以从sks文件创建场景。无论如何,这并不能解决泄漏问题。 - Whirlwind
你看了我的编辑了吗?有什么问题吗? - J.Treutlein
显示剩余7条评论

0

我认为有两种主要的方法可以实现这个。我通常采用的主要方式是,如果游戏结束了(由于角色健康值降至零,或者它们与导致回合结束的对象碰撞,或时间到了等),当发生这种情况时,我会转换到一个新的场景,显示他们的得分、进展情况等。

我通过在主GamePlay场景中设置一个bool变量来实现这一点,如下所示。

    var gameOver: Bool = false

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

在update函数中检查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中,我放置了一个按钮,上面写着“重试”,当他们点击它时,它会再次启动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))
            }
        }

这是我处理游戏结束转换的首选方法。

另一种方法是创建一个函数来重置所有在游戏过程中发生变化的变量。然后,您可以使用上面 Update 函数中的相同代码,但是不会过渡到新场景,而是在场景上创建一个标签。如果用户单击该标签,它将触发该函数以重置已更改的所有变量、玩家位置、停止所有动作、重置玩家生命值等内容。根据您在游戏过程中有多少个变化的内容,过渡到新场景并通过按钮重新加载 GamePlayScene 可能更实用(正如我所发现的那样)。然后,所有内容都将与用户第一次进入该主要的 GamePlayScene 时加载的内容完全相同。


我现在的问题是,游戏场景重置后,游戏位置在左上角。当他们点击重试按钮时,就会发生这种情况。您能否编辑您的帖子,将单击重试按钮时使用的代码添加到其中,以确保我没有出现任何有趣的问题? - J.Treutlein
@J.Treutlein 我刚刚编辑了一下,加入了一个我实现重试按钮的例子。你也可以使用相同的转换代码,就像在更新语句中一样,并更改场景的名称。 - crpDave
非常感谢您的帮助,但是Whirlwind的答案实际上解决了问题。无论如何,这对于其他遇到同样问题的人也很有用。 - J.Treutlein

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接