使用 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")
}
}
}
}
我正在尝试在 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")
}
}
}
}