使用 SpriteView(isPaused:) 从 SwiftUI 暂停 SpriteKit 场景,而不是每次都重新初始化它?

Pause a SpriteKit scene from SwiftUI using SpriteView(isPaused:), without reinitializing it every time?

我正在尝试在 SpriteView 初始值设定项中使用 isPaused: 参数来在 SwiftUI 中的状态 属性 发生变化时暂停 SKScene

但是当我在初始化程序中使用状态变量作为 isPaused: 的参数时:

SpriteView(scene: scene, isPaused: showingLevelChooser)

而不只是:

SpriteView(scene: scene)

每次变量改变时都会重新创建场景,这不是我想要的。我只想暂停游戏。

我很困惑 isPaused: 参数应该如何工作。 SpriteView documentation.

中没有太多信息

据我了解,发生这种情况是因为 SwiftUI 重新创建了依赖于状态的视图。但如果是这种情况,你怎么能从 SwiftUI 暂停 SpriteKit 场景,而不是每次都重新初始化它?

我在这里创建了一个示例 Xcode 13 项目:https://github.com/clns/SpriteView-isPaused

SKScene 正在屏幕上显示“已用时间”,以秒为单位。每次 SwiftUI sheet 被呈现并且状态变量 showingLevelChooser 改变时,定时器从 0(零)开始,因为场景被重新初始化。

所有相关代码都在ContentView.swift.

class GameScene: SKScene {
    private let label = SKLabelNode(text: "Time Elapsed:\n0")
    private var lastUpdateTime : TimeInterval = 0
    
    override func didMove(to view: SKView) {
        addChild(label)
    }
    
    override func update(_ currentTime: TimeInterval) {
        if (self.lastUpdateTime == 0) {
            self.lastUpdateTime = currentTime
        }
        
        let seconds = Int(currentTime - lastUpdateTime)
        label.text = "Time Elapsed:\n\(seconds)"
        label.numberOfLines = 2
    }
}

struct ContentView: View {
    @State private var showingLevelChooser = false
    
    var scene: SKScene {
        let scene = GameScene()
        scene.size = CGSize(width: 300, height: 400)
        scene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
        scene.scaleMode = .fill
        return scene
    }
    
    var body: some View {
        ZStack {
            SpriteView(scene: scene, isPaused: showingLevelChooser)
                .ignoresSafeArea()
            VStack {
                Button("Level Chooser") {
                    showingLevelChooser.toggle()
                }
                Spacer()
            }
        }
        .sheet(isPresented: $showingLevelChooser) {
            VStack {
                Button("Cancel") {
                    showingLevelChooser.toggle()
                }
                Text("Level Chooser")
            }
        }
    }
}

这里有两个问题需要注意。首先是您想要引用您的 SKScene 并保留下来。使其成为 View 的计算 属性 将不起作用(如您所见),因为 View 是可传递的,每次重新加载时,您都会得到一个新的 SKScene.

对此有多种可接受的解决方案,但我选择将 SKScene 封装在一个 ObservableObject 中,您可以通过 StateObject 持有对它的引用。您甚至可以尝试将 SKScene 设为 ObservableObject 本身(此处未显示)。

其次,您的 determine/showing 屏幕上“暂停”的逻辑很难准确地看到,因为它总是只显示经过的时间 -- 不涉及不计算的逻辑暂停期间的时间。我将其替换为一个显示更新次数的简单计数器。这样,你就可以清楚地看出场景确实已经暂停(并且没有更新)。


class SceneStore : ObservableObject {
    var scene = GameScene()
    
    init() {
        scene.size = CGSize(width: 300, height: 400)
        scene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
        scene.scaleMode = .fill
    }
}

class GameScene: SKScene {
    private let label = SKLabelNode(text: "Updates: 0")
    private var updates = 0
    
    override func didMove(to view: SKView) {
        addChild(label)
    }
    
    override func update(_ currentTime: TimeInterval) {
        updates += 1
        label.text = "Updates: \(updates)"
        label.numberOfLines = 2
    }
}

struct ContentView: View {
    @State private var showingLevelChooser = false
    @StateObject private var sceneStore = SceneStore()
    
    var body: some View {
        ZStack {
            SpriteView(scene: sceneStore.scene, isPaused: showingLevelChooser)
                .ignoresSafeArea()
            VStack {
                Button("Level Chooser") {
                    showingLevelChooser.toggle()
                }
                Spacer()
            }
        }
        .sheet(isPresented: $showingLevelChooser) {
            VStack {
                Button("Cancel") {
                    showingLevelChooser.toggle()
                }
                Text("Level Chooser")
            }
        }
    }
}