With a couple of other developers we have been busy with migrating our SpriteKit
games and frameworks to Swift 6.
There is one issue we are unable to resolve, and this involves the interaction between SpriteKit
and GameplayKit
.
There is a very small demo repo created that clearly demonstrates the issue. It can be found here:
https://github.com/AchrafKassioui/GameplayKitExplorer/blob/main/GameplayKitExplorer/Basic.swift
The relevant code also pasted here:
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")
}
}
As SKNode
is designed to run on the MainActor
, the BasicRenderComponent
is attributed with MainActor
as well. This is needed as this GKComponent
is dedicated to encapsulate the node that is rendered to the scene.
There is also a BasicAnimationComponent
, this GKComponent
is responsible for animating the rendered node.
Obviously, this is just an example, but when using GameplayKit
in combination with SpriteKit
it is very common that a GKComponent
instance manipulates an SKNode
referenced from another GKComponent
instance, often done via open func update(deltaTime seconds: TimeInterval)
or as in this example, inside didAddToEntity
.
Now, the problem is that in the above example (but the same goes for update(deltaTime seconds: TimeInterval)
the method didAddToEntity
is not isolated to the MainActor
, as GKComponent
is not either.
This leads to the error Call to main actor-isolated instance method 'run' in a synchronous nonisolated context
, as indeed the compiler can not infer that didAddToEntity
is isolated to the MainActor
.
Marking BasicAnimationComponent
as @MainActor
does not help, as this isolation is not propogated back to the superclass inherited methods.
In fact, we tried a plethora of other options, but none resolved this issue.
How should we proceed with this? As of now, this is really holding us back migrating to Swift 6. Hope someone is able to help out here!
You can:
@MainActor
Sendable
(or @unchecked Sendable
where necessary)MainActor.assumeIsolated {...}
in any of the callbacks that manipulate SpriteKit nodes, like didAddToEntity()
and update(delta:)
For example:
@MainActor // <-- ADDED THIS
class BasicAnimationComponent: GKComponent, Sendable { // <-- ADDED Sendable (or @unchecked Sendable where necessary)
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")
}
}
Of course, make sure you call your component updates from the main actor, but that's usually the case when working with SpriteKit.