SpriteView 不会在状态更改时暂停场景

SpriteView doesn't pause scene on state change

问题

状态 属性 的 SpriteView doesn't pause the SKScene 似乎传递给了 SpriteView(scene:isPaused:) 初始值设定项。

示例项目

我在 iOS 15 模拟器上创建了一个 sample Xcode 13 project on GitHub 运行,其中 ContentView@State var paused 属性 被发送到一个 child SpriteView(scene: scene, isPaused: paused)。状态 属性 由“Paused:”按钮更改。 属性 更改并且按钮上的文本更新,但 SpriteView 永远不会暂停场景。

看起来 SpriteView 没有获取更新,也没有暂停其中的底层 SKView 和 SKScene。

所有相关代码在ContentView.swift:

import SwiftUI
import SpriteKit

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

struct ContentView: View {
    @State private var paused = false
    
    @StateObject private var scene: GameScene = {
        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 {
        if #available(iOS 15.0, *) {
            print(Self._printChanges())
        }
        return ZStack {
            SpriteView(scene: scene, isPaused: paused).ignoresSafeArea()
            VStack {
                Text("Updates from SKScene: \(scene.updates)").padding().foregroundColor(.white)
                Button("Paused: \(paused)" as String) {
                    paused.toggle()
                }.padding()
                Spacer()
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

重启问题

我还创建了一个与第一个类似的 second GitHub project,但它还有一个显示相同问题的“重新启动”按钮。

重新启动”按钮应在按下时重新创建 SKScene。 SKScene 在 SceneStoreContentViewText 视图中重新创建(新时间分配给场景的 name 属性),但是SpriteView 不会改变场景。

好像是SpriteView在内存中保留了初始场景,并没有放手去替换新场景。这可以在控制台中看到,方法是点击“重新启动”按钮两次并查找类似“-- Scene 7:49:44 PM deinit --”的内容。

场景中 @Published var updates = 0 属性 的更新也停止了(在屏幕顶部),因为创建的新场景没有添加到视图中,所以 SKScene.didMove(to view:) 方法从未被调用。

这个的相关代码在ContentView.swift:

import SwiftUI
import SpriteKit

class GameScene: SKScene, ObservableObject {
    @Published var updates = 0
    private let label = SKLabelNode(text: "Updates in SKScene:\n0")
    
    override func didMove(to view: SKView) {
        addChild(label)
        label.numberOfLines = 4
        label.position = CGPoint(x: 0, y: -100)
    }
    
    override func update(_ currentTime: TimeInterval) {
        updates += 1
        label.text = "Updates in SKScene:\n\(updates)\nScene created at:\n\(name!)"
    }
    
    deinit {
        print("-- Scene \(name!) deinit --")
    }
}

class SceneStore : ObservableObject {
    @Published var currentScene: GameScene
    
    init() {
        currentScene = SceneStore.createScene()
    }
    
    func restartLevel() {
        currentScene = SceneStore.createScene()
    }
    
    // MARK: - Class Functions
    
    static private func createScene() -> GameScene {
        let scene = GameScene()
        scene.size = CGSize(width: 300, height: 400)
        scene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
        scene.scaleMode = .fill
        scene.name = Date().formatted(date: .omitted, time: .standard)
        return scene
    }
}

struct ContentView: View {
    @EnvironmentObject private var sceneStore: SceneStore
    @EnvironmentObject private var scene: GameScene
    @State private var paused = false
    
    var body: some View {
        if #available(iOS 15.0, *) {
            print(Self._printChanges())
        }
        return ZStack {
            SpriteView(scene: scene, isPaused: paused).ignoresSafeArea()
            VStack {
                Text("Updates from SKScene: \(scene.updates)").padding().foregroundColor(.white)
                Text("Scene created at: \(scene.name!)" as String).foregroundColor(.white)
                Button("Restart") {
                    sceneStore.restartLevel()
                }.padding()
                Button("Paused: \(paused)" as String) {
                    paused.toggle()
                }
                Spacer()
            }
        }
    }
}

问题/解决方法?

我错过了什么吗?或者这是一个错误?如果是这样,是否有任何解决方法?

你可以查看Pausing a sprite kit scene

基本上每次点击暂停按钮都要暂停场景

    @State private var paused = false {
        didSet {
            self.scene.isPaused = paused
        }
    }

这将暂停您的更新

你说得对,当传递给 SpriteView 时,isPaused 似乎不会影响暂停状态。

为了解决这个问题,我使用了:

.onChange(of: paused) { newValue in
  sceneStore.currentScene.isPaused = newValue
}

你的第二个问题是一个部分 SpriteView 问题和一个部分 ObservableObject 问题。

需要知道的重要一点是,嵌套的 ObservableObject 不会 传播它们的状态,除非您手动调用 objectWillChange.send()。因此,我使用 Combine 跟踪子对象上的 @Published updates 变量,并在有更新时调用 objectWillChange

此外,当有新场景时,会再次调用 setupCombine,这不仅会传播更新,还会提醒 View 场景已更改。

最后,因为场景不会以其他方式重新加载,所以我使用的 id 在创建新的 GameScene 时会发生变化——这会强制 SwiftUI 创建一个新的 SpirteView.

import SwiftUI
import Combine

@main
struct SpriteView_not_updatingApp: App {
    @StateObject private var sceneStore = SceneStore()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(sceneStore)
        }
    }
}

class GameScene: SKScene, ObservableObject {
    @Published var updates = 0
    var id = UUID()
    private let label = SKLabelNode(text: "Updates in SKScene:\n0")
    
    override func didMove(to view: SKView) {
        addChild(label)
        label.numberOfLines = 4
        label.position = CGPoint(x: 0, y: -100)
    }
    
    override func update(_ currentTime: TimeInterval) {
        updates += 1
        label.text = "Updates in SKScene:\n\(updates)\nScene created at:\n\(name!)"
    }
    
    deinit {
        print("-- Scene \(name!) deinit --")
    }
}

class SceneStore : ObservableObject {
    var currentScene: GameScene
    
    var cancellable : AnyCancellable?
    
    init() {
        currentScene = SceneStore.createScene()
        setupCombine()
    }
    
    func restartLevel() {
        currentScene = SceneStore.createScene()
        setupCombine()
    }
    
    func setupCombine() {
        cancellable = currentScene.$updates.sink { _ in
            self.objectWillChange.send()
        }
    }
    
    // MARK: - Class Functions
    
    static private func createScene() -> GameScene {
        let scene = GameScene()
        scene.size = CGSize(width: 300, height: 400)
        scene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
        scene.scaleMode = .fill
        scene.name = Date().formatted(date: .omitted, time: .standard)
        return scene
    }
}

struct ContentView: View {
    @EnvironmentObject private var sceneStore: SceneStore
    @State private var paused = false
    
    var body: some View {
        if #available(iOS 15.0, *) {
            print(Self._printChanges())
        }
        return ZStack {
            SpriteView(scene: sceneStore.currentScene, isPaused: paused)
                .id(sceneStore.currentScene.id) //<-- Here
                .ignoresSafeArea()
                .onChange(of: paused) { newValue in
                    sceneStore.currentScene.isPaused = newValue //<-- Here
                }
            VStack {
                Text("Updates from SKScene: \(sceneStore.currentScene.updates)").padding().foregroundColor(.white)
                Text("Scene created at: \(sceneStore.currentScene.name!)" as String).foregroundColor(.white)
                Button("Restart") {
                    sceneStore.restartLevel()
                }
                .padding()
                Button("Paused: \(paused)" as String) {
                    paused.toggle()
                }
                Spacer()
            }
        }
    }
}