sprite-kitgameplay-kitswift6

Call to main actor-isolated instance method 'run' in a synchronous non-isolated context


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!


Solution

  • You can:

    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.