如何将ECS与SwiftUI结合使用? (实体组件系统)

时间:2020-07-21 17:26:15

标签: swift swiftui entity-component-system

也许您可以在我那黑暗而无知的道路上照亮一些光线? 我的目的地是一个SwiftUI应用,我可以在其中将组件注入视图中,并使系统在这些组件上运行一些逻辑。基础上的弱点使我重新开始滑坡。让我向您展示我的尝试!

protocol DataComponent: Component {
    associatedtype DataType
    var data: DataType { get set }
    init()
    init(data: DataType)
}

struct NameComponent: DataComponent { var data = "default" }

这是基本数据组件。我的实体组件系统使用的是EntitasKit

enum Entities {
    case source
    ...
}

protocol EntityServicing {
    var context: Context { get }
    func entity(_ entity: Entities) -> Entity
}

final class EntityService: EntityServicing {
    let context = Context()
    func entity(_ entity: Entities) -> Entity {
        switch entity {
        case .source:
            guard let first = context.entities.first else {
                return context.createEntity()
            }
            return first
        ...
        }
    }
}

这是应用程序范围的EntityService,它保存实体及其组件的上下文。它是使用Resolver注入的。

现在,这是SwiftUI中的视图。

struct ContentView: View {
    @EntityState(.source, "Dale Cooper") var name: NameComponent
    var body: some View {
        Button(action: { name = NameComponent(data: "Dougie Jones") }) {
            Text(name.data).padding()
        }
    }
}

EntityState就是它的发音;一个属性包装器,它捕获实体组件的状态并使它对SwiftUI可见。

@propertyWrapper
struct EntityState<C>: DynamicProperty where C: DataComponent {

    @State private var state = C()
    @Injected private var entityService: EntityServicing
    private let entityID: Entities
    private var entity: Entity { entityService.entity(entityID) }
    
    init(_ id: Entities, _ data: C.DataType? = nil) {
        self.entityID = id
        if let data = data {
            entity.set(C(data: data))
        } else {
            guard entity.has(C.cid) else {
                entity.set(state)
                return
            }
        }
        _state = State(wrappedValue: entity.get()!)
    }

    public var wrappedValue: C {
        get { entity.get()! }
        nonmutating set { state = newValue }
    }
        
    mutating func update() {
        entity.set(state)
    }
}

SwiftUI注意到dynamic属性中的状态更改。单击该按钮可重新呈现视图并更新实体上的组件。

当我添加一个更改组件的系统时,这种基础开始崩溃。

class NameSystem: ReactiveSystem {
    
    @Injected var entityService: EntityServicing
    lazy var collector: Collector = Collector(
        group: entityService.context.group(
            Matcher(
                all: [NameComponent.cid]
            )
        ),
        type: .added
    )
    let name = "Name System Added Here!"

    func execute(entities: Set<Entity>) {
        for e in entities {
            if let data = e.get(NameComponent.self)?.data {
                e.set(NameComponent(data: "Special Agent " + data))
            }
        }
    }
}

问题是SwiftUI状态从不通知,但是我希望SwiftUI知道组件在系统更新时发生的更改。

我尝试了许多不同的方法:

  • 组件/实体/上下文使用@Published作为ObservableObject。我的理解是,这是将数据模型连接到SwiftUI的好方法。我已经拥有@Published组件,组件和实体中的数据,但是让我停下来的问题是让EntityState重新呈现视图。由于已注入上下文,因此无法使用其他属性包装器,例如@ObservedObject。
  • 绑定。我尝试观察EntityState中的组件对象,并将组件绑定传递给实体,但是我无法在同一字典中存储对不同DataComponent的绑定。我什至不知道对绑定的更改是否会回到EntityState重新呈现视图。对@States的绑定也有相同的限制。无论如何,我让自己陷入了困境。
  • 发布者和订阅者。将EntityState转换为订阅者,以便它可以接收值要求它是可变的。为了使EntityState可变,它必须成为一个类,但这会中断SwiftUI的重新渲染。发布者可以是某种主题,也可以是组件的自定义发布者。我只是无法解决如何接收值并将其分配给状态,因此SwiftUI重新呈现。整个合并区域对我来说有点模糊。可能有一种聪明的方法来做我不知道的惊人事情。

如何从系统向状态分配组件,以便SwiftUI重新呈现?

我希望您发现这个有趣的问题要解决,并且它使您像我对解决方案一样好奇。至少我真的很兴奋。

祝大家有美好的一天!

0 个答案:

没有答案