与其他几个开发人员一起,我们一直忙于将我们的
SpriteKit
游戏和框架迁移到 Swift 6。
有一个问题我们无法解决,这涉及到
SpriteKit
和GameplayKit
之间的相互作用。
创建了一个非常小的演示存储库,可以清楚地演示该问题。可以在这里找到:
https://github.com/AchrafKassioui/GameplayKitExplorer/blob/main/GameplayKitExplorer/Basic.swift
相关代码也贴在这里:
import SwiftUI
import SpriteKit
struct BasicView: View {
var body: some View {
SpriteView(scene: BasicScene())
.ignoresSafeArea()
}
}
#Preview {
BasicView()
}
class BasicScene: SKScene {
override func didMove(to view: SKView) {
size = view.bounds.size
anchorPoint = CGPoint(x: 0.5, y: 0.5)
backgroundColor = .gray
view.isMultipleTouchEnabled = true
let entity = BasicEntity(color: .systemYellow, size: CGSize(width: 100, height: 100))
if let renderComponent = entity.component(ofType: BasicRenderComponent.self) {
addChild(renderComponent.sprite)
}
}
}
@MainActor
class BasicEntity: GKEntity {
init(color: SKColor, size: CGSize) {
super.init()
let renderComponent = BasicRenderComponent(color: color, size: size)
addComponent(renderComponent)
let animationComponent = BasicAnimationComponent()
addComponent(animationComponent)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
@MainActor
class BasicRenderComponent: GKComponent {
let sprite: SKSpriteNode
init(color: SKColor, size: CGSize) {
self.sprite = SKSpriteNode(texture: nil, color: color, size: size)
super.init()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class BasicAnimationComponent: GKComponent {
let action1 = SKAction.scale(to: 1.3, duration: 0.07)
let action2 = SKAction.scale(to: 1, duration: 0.15)
override init() {
super.init()
}
override func didAddToEntity() {
if let renderComponent = entity?.component(ofType: BasicRenderComponent.self) {
renderComponent.sprite.run(SKAction.repeatForever(SKAction.sequence([action1, action2])))
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
由于
SKNode
被设计为在 MainActor
上运行,因此 BasicRenderComponent
也归属于 MainActor
。这是必需的,因为这个 GKComponent
专用于封装渲染到场景的节点。
还有一个
BasicAnimationComponent
,这个GKComponent
负责对渲染节点进行动画处理。
显然,这只是一个示例,但是当将
GameplayKit
与 SpriteKit
结合使用时,GKComponent
实例操作从另一个 SKNode
实例引用的 GKComponent
是很常见的,通常通过 open func update(deltaTime seconds: TimeInterval)
完成或者如本例所示,在 didAddToEntity
内。
现在,问题在于,在上面的示例中(但对于
update(deltaTime seconds: TimeInterval)
也是如此,方法 didAddToEntity
并未与 MainActor
隔离,因为 GKComponent
也不是孤立的。
这会导致错误
Call to main actor-isolated instance method 'run' in a synchronous nonisolated context
,因为编译器确实无法推断 didAddToEntity
与 MainActor
是隔离的。
将
BasicAnimationComponent
标记为 @MainActor
没有帮助,因为这种隔离不会传播回超类继承的方法。
事实上,我们尝试了很多其他选项,但都没有解决这个问题。
我们应该如何进行呢?到目前为止,这确实阻碍了我们迁移到 Swift 6。希望有人能够在这里提供帮助!
您可以将 GameplayKit 组件标记为
@MainActor
并使用 MainActor.assumeIsolated {...}
是操作 SpriteKit 节点的任何回调,例如 didAddToEntity()
和 update(delta:)
。
例如:
@MainActor // <-- ADDED THIS
class BasicAnimationComponent: GKComponent {
let action1 = SKAction.scale(to: 1.3, duration: 0.07)
let action2 = SKAction.scale(to: 1, duration: 0.15)
override init() {
super.init()
}
override func didAddToEntity() {
MainActor.assumeIsolated { // <-- ADDED THIS
if let renderComponent = entity?.component(ofType: BasicRenderComponent.self) {
renderComponent.sprite.run(
SKAction.repeatForever(SKAction.sequence([action1, action2]))
)
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
当然,请确保从主要参与者调用组件更新,但在使用 SpriteKit 时通常会出现这种情况。