Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ECS に対応しやすいバインディングシステム #114

Open
rrbox opened this issue Jul 14, 2023 · 5 comments
Open

ECS に対応しやすいバインディングシステム #114

rrbox opened this issue Jul 14, 2023 · 5 comments
Labels
type: Discussion 機能や利用方法などの考察をメモします type: Enhancement New feature or request

Comments

@rrbox
Copy link
Owner

rrbox commented Jul 14, 2023

現在、UI とモデルとのやり取りをゲーム向けに最適化することが要求されています。また、ECS (Entity Component System) をゲームの主要な設計(構造)にすることも考えられています。そこで、UI とモデルとのやり取りを ECS で行えるような仕組みを作ってみようと考えています。

この issue では ECS に対応可能な API の設計を目標にします。

ECS は基本的に「データ指向」であるため、同じようにデータ指向の API にすることで ECS に対応しやすくなるはずです。

@rrbox rrbox added type: Enhancement New feature or request type: Discussion 機能や利用方法などの考察をメモします labels Jul 14, 2023
@rrbox
Copy link
Owner Author

rrbox commented Jul 14, 2023

Model to Widget (Output)

Button

  • isPushed: Bool: true の時にボタンが押下されたアクションを開始する。

@rrbox
Copy link
Owner Author

rrbox commented Jul 14, 2023

Widget to Model (Input)

Node の状態を評価して Model に反映させる関数を用意することが最適だと思われます。

@rrbox
Copy link
Owner Author

rrbox commented Jul 22, 2023

SpriteKit 側からの SKNode の更新(物理演算による座標, 回転など)はすべて無視しても問題ありません。なぜなら、このライブラリが UI 専用だからです。

@rrbox
Copy link
Owner Author

rrbox commented Jul 26, 2023

データや UI モデルの更新を update で行うべきか? という提案もあります。

@rrbox rrbox changed the title ECS を利用したバインディングシステム ECS に対応しやすいバインディングシステム Jul 29, 2023
@rrbox
Copy link
Owner Author

rrbox commented Nov 2, 2023

Property wrapper の活用についても考えましょう。

Widget protocol に準拠した構造体のインスタンスを格納した変数から SKNode をコントロールするコードを考えてみました。

まずは構造体のプロパティの変更を SKNode に伝達するための Attribute です。

import Combine

class Location<Wrapped> {
    let publisher: CurrentValueSubject<Wrapped, Never>
    
    init(value: Wrapped) {
        self.publisher = CurrentValueSubject<Wrapped, Never>(value)
    }
    
    public func value<T>(keyPath: KeyPath<Wrapped, T>) -> T {
        self.publisher.value[keyPath: keyPath]
    }
}

@propertyWrapper
@dynamicMemberLookup
public struct WidgetBinding<Wrapped> {
    let publisher: AnyPublisher<Wrapped, Never>
    
    public var wrappedValue: Wrapped {
        var result: Wrapped?
        publisher.sink { result = $0 }
        return result!
    }
    
    init<P: Publisher>(publisher: P) where P.Output == Wrapped, P.Failure == Never {
        self.publisher = publisher.eraseToAnyPublisher()
    }
    
    public subscript<Subject>(dynamicMember keyPath: KeyPath<Wrapped, Subject>) -> WidgetBinding<Subject> {
        WidgetBinding<Subject>(publisher: self.publisher.map { $0[keyPath: keyPath] })
    }
}

@propertyWrapper public struct WidgetState<Wrapped> {
    let location: Location<Wrapped>
    
    public var projectedValue: WidgetBinding<Wrapped> {
        WidgetBinding(publisher: self.location.publisher)
    }
    
    public var wrappedValue: Wrapped {
        get {
            self.location.publisher.value
        }
        set(v) {
            self.location.publisher.send(v)
        }
    }
    
    public init(wrappedValue: Wrapped) {
        self.location = Location(value: wrappedValue)
    }
}

extension Location {
    func optional() -> Location<Wrapped?> {
        Location<Wrapped?>(value: self.publisher.value)
    }
}

extension WidgetBinding {
    func optional() -> WidgetBinding<Wrapped?> {
        WidgetBinding<Wrapped?>(publisher: self.publisher.map { Optional($0) })
    }
}

extension WidgetState {
    func optional() -> WidgetState<Wrapped?> {
        WidgetState<Wrapped?>(wrappedValue: self.location.publisher.value)
    }
}

使用例です

public struct Text: Widget, CreateModels {
    @WidgetBinding var text: String?
    
    public var body: Never {
        fatalError()
    }
    
    public func createModels() -> (SKLabelNode, AnyCancellable) {
        let result = SKLabelNode(text: self.text)
        let subscriber = self._text
            .publisher
            .assign(to: \.text, on: result)
        return (result, subscriber)
    }
    
    public init(_ text: String) {
        self._text = .init(publisher: Just(text).map { Optional($0) })
    }
    
    public init(_ text: WidgetBinding<String>) {
        self._text = text.optional()
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: Discussion 機能や利用方法などの考察をメモします type: Enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

1 participant