Использование GameplayKit с Swift 6: Вызов экземплярного метода ‘run’ главного актора из синхронного неизолированного контекста

Вопрос или проблема

С несколькими другими разработчиками мы были заняты миграцией наших игр и фреймворков 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 и SpriteKit в Swift 6

При миграции ваших игр и фреймворков на Swift 6 с использованием SpriteKit и GameplayKit, вы столкнулись с проблемой, связанной с контекстами изоляции актеров. Давайте подробно рассмотрим, как можно решить эту проблему, возникшую из-за ошибки: "Call to main actor-isolated instance method ‘run’ in a synchronous nonisolated context".

Понимание проблемы

Ваша реализация использует классы GKEntity и GKComponent, где класс BasicAnimationComponent должен вызывать метод run для элемента SKSpriteNode. Проблема в том, что метод didAddToEntity, вызываемый для GKComponent, не имеет изоляции для MainActor, что приводит к несоответствию при попытке выполнения кода, изолированного для MainActor.

Ошибка возникает, когда вы пытаетесь вызвать метод run, который изолирован для MainActor, из метода, который не имеет этого изолирования. Изоляция методов в Swift 6 требует, чтобы любой вызывающий метод имел соответствующий уровень изоляции для успешного выполнения.

Решение проблемы

Для решения этой проблемы есть несколько подходов:

  1. Изменить структуру классов: Вы можете обернуть код, который вызывает run, в асинхронный метод и использовать Task { }, чтобы наглядно указать, что данный код должен выполняться на главном потоке.

    Пример кода:

    class BasicAnimationComponent: GKComponent {
       let action1 = SKAction.scale(to: 1.3, duration: 0.07)
       let action2 = SKAction.scale(to: 1, duration: 0.15)
    
       override func didAddToEntity() {
           Task { @MainActor in
               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")
       }
    }

    В этом коде мы создали Task, который указывает, что выполнение должно произойти в контексте изоляции MainActor.

  2. Использование MainActor.run: Если вы не хотите оборачивать свой код каждый раз в Task, до версии Swift 6.1 можно использовать MainActor.run для безопасного выполнения методов, требующих MainActor.

    Пример кода:

    override func didAddToEntity() {
       MainActor.run {
           if let renderComponent = entity?.component(ofType: BasicRenderComponent.self) {
               renderComponent.sprite.run(SKAction.repeatForever(SKAction.sequence([action1, action2])))
           }
       }
    }

Заключение

Проблема, с которой вы столкнулись, совсем не редка при работе с новым управлением потоками в Swift 6. Правильное использование MainActor и асинхронных задач поможет вам обойти ограничения и правильно интегрировать компоненты GameplayKit и SpriteKit.

Пока ваша база кода продолжает развиваться, важно внимательно следить за контекстам изоляции актеров и корректно управлять вызовами методов, связанными с UI и игровыми объектами. Надеюсь, эти стратегии помогут вам успешно осуществить миграцию и продолжить разработку ваших проектов на Swift 6.

Оцените материал
Добавить комментарий

Капча загружается...