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 在 SceneStore
和 ContentView
的 Text
视图中重新创建(新时间分配给场景的 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()
}
}
}
}
问题
状态 属性 的 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 在 SceneStore
和 ContentView
的 Text
视图中重新创建(新时间分配给场景的 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()
}
}
}
}