Вопрос или проблема
С несколькими другими разработчиками мы были заняты миграцией наших игр и фреймворков 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 требует, чтобы любой вызывающий метод имел соответствующий уровень изоляции для успешного выполнения.
Решение проблемы
Для решения этой проблемы есть несколько подходов:
-
Изменить структуру классов: Вы можете обернуть код, который вызывает
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
. -
Использование
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.