rrbox / ecs-swift

Entity Component System for swift
MIT License
3 stars 0 forks source link

Bundle の実装 #50

Open rrbox opened 7 months ago

rrbox commented 7 months ago

作れそうだったら作ってみたいです。以下のようなイメージですが、はたして実現可能なのか..?

例: グラフィック用データの定義

struct Transform: Bundle {
    @component var position: Graphic.Position
    @component var zPosition: Graphic.ZPosition
}

struct Sprite: Bundle {
    @bundle var transform: Transform
    @component var size: Graphic.Size
    @component var texture: Graphic.Texture
}

最新の検討案の欄

struct Transform: Bundle {
    var body: some BuilderElement {
        bundleBuilder {
            component(Pos())
            component(ZPos())
        }
    }
}
struct Sprite: Bundle {
    var body: some BuilderElement {
        bundleBuilder {
            component(Size())
            component(Texture())
            bundle(Transform())
        }
    }
}
rrbox commented 7 months ago

プロパティラッパーの仕様では、多分無理では?

rrbox commented 7 months ago

result builder を使う方法がいいかもしれません。

struct Transform: Bundle {
    var builder: some BundleElements {
        // いい感じにつくれるようにする
        BundleBuilder {
            Position()
            ZPosition()
        }
    }
}
rrbox commented 7 months ago

こんな感じの API をまず思いつきました。

struct Transform: Bundle {
    var body: some BuilderElement {
        bundleBuilder {
            component(Pos())
            component(ZPos())
        }
    }
}
struct Sprite: Bundle {
    var body: some BuilderElement {
        bundleBuilder {
            component(Size())
            component(Texture())
            bundle(Transform())
        }
    }
}

Bundlebody プロパティ内部には、Component の二分木が隠蔽されています。この二分木をスキャンすることで、順番に、そして静的ディスパッチによって各コンポーネントにアクセスできます。 欠点として、Builder で配置可能な要素数に制限があることです。要素数を増やしたい場合は、ライブラリ側の実装数を増やす必要があります。


この API の内部についての補足

二分木ノード

まず、二分木のノードの定義です。 Bundle protocol の body プロパティは以下の End, Link のいずれかになる想定です。

protocol BuilderElement {

}

struct End<C: Component>: BuilderElement {
    let initialValue: C
}

struct Link<Previous: BuilderElement, Behind: BuilderElement>: BuilderElement {
    let previous: Previous
    let behind: Behind
}

bundle builder

続いて bundleBuilder 関数の実装です。以下の実装に別れています。

まずは result builder です。 何かしら受け取って、Link へ統合しています。

@resultBuilder
struct BundleBuilder {
    static func buildBlock<T>(_ value: T) -> T {
        value
    }

    static func buildBlock<P0, P1>(_ p0: P0, _ p1: P1) -> Link<P0, P1> {
        Link(previous: p0, behind: p1)
    }

    static func buildBlock<P0, P1, P2>(_ p0: P0, _ p1: P1, _ p2: P2) -> Link<P0, Link<P1, P2>> {
        Link(previous: p0, behind: Link(previous: p1, behind: p2))
    }

    static func buildBlock<P0, P1, P2, P3>(_ p0: P0, _ p1: P1, _ p2: P2, _ p3: P3) -> Link<Link<P0, P1>, Link<P2, P3>> {
        Link(previous: Link(previous: p0, behind: p1), behind: Link(previous: p2, behind: p3))
    }

    static func buildBlock<P0, P1, P2, P3, P4>(_ p0: P0, _ p1: P1, _ p2: P2, _ p3: P3, _ p4: P4) -> Link<Link<P0, Link<P1, P2>>, Link<P3, P4>> {
        Link(previous: Link(previous: p0, behind: Link(previous: p1, behind: p2)), behind: Link(previous: p3, behind: p4))
    }
}

次に bundleBuilder 関数の実装です。 ここで要素に BuilderElement の制約をかけます。

func bundleBuilder<T: BuilderElement>(@BundleBuilder _ statement: () -> T) -> T {
    statement()
}

Bundle protocol

Bundle protocol を定義します。 この protocol を実装すると、EndLink といった BuilderElement を定義する関数が完成します。

protocol Bundle {
    associatedtype Body: BuilderElement
    var body: Body { get }
}

このままでは使いづらい(EndLink を直接書きたくない)ので、最後に Component や Bundle を bundleBuilder の要素へ変換する関数を作ります。

func component<T>(_ c: T) -> End<T> {
    End(initialValue: c)
}

func bundle<T: Bundle>(_ b: T) -> T.Body {
    b.body
}

おまけ bundle builder の結果

たとえば、上記の Spritebody はこんなデータになっています。

Link<End<Size>, Link<End<Texture>, Link<End<Pos>, End<ZPos>>>>(
    previous: End<Size>(
        initialValue: Size()
    ),
    behind: Link<End<Texture>, Link<End<Pos>, End<ZPos>>>(
        previous: End<Texture>(
            initialValue: Texture()
        ),
        behind: Link<End<Pos>, End<ZPos>>(
            previous: End<Pos>(
                initialValue: Pos()
            ),
            behind: End<ZPos>(
                initialValue: ZPos()
            )
        )
    )
)
rrbox commented 2 months ago

Swift macros を使った API が実装できないか検討してください。

rrbox commented 2 months ago

Swift macros を使った API が実装できないか検討してください。

イメージですが、型アノテーション付きで全プロパティを定義し、型の記述部分をコード文字列として受け取れるかもしれません。

受け取った型に関するコード文字を使って bundle を組み立てるメソッドを作れるかも。

rrbox commented 2 months ago

Macro かけました。

実装

struct BundleMacro: MemberMacro {
    static func expansion(
        of node: AttributeSyntax,
        providingMembersOf declaration: some DeclGroupSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        let membersAddition = declaration.memberBlock.members
            .compactMap { $0.decl.as(VariableDeclSyntax.self) }
            .compactMap { i in
                i.bindings.first?.pattern.as(IdentifierPatternSyntax.self)?.identifier.text
            }
            .reduce(into: "") { partialResult, identifier in
                partialResult.append("record.addComponent(self.\(identifier))\n")
            }
            .dropLast()

        return [
            """
            public func addComponent(forEntity record: EntityRecord) {
                \(raw: membersAddition)
            }
            """
        ]
    }
}

Macro

@attached(member, names: named(addComponent))
public macro Bundle() = #externalMacro(module: "MacroSampleMacros", type: "BundleMacro")

使い方

public protocol Component {

}

public class EntityRecord {
    public func addComponent<C: Component>(_ component: C) {

    }
}

@Bundle
struct MyBuldle {
    let id: Int
    let name: String
    let position = Position(x: 0, y: 0)
}

func sample() {
    let b = MyBuldle(id: 0, name: "")
    b.addComponent(forEntity: EntityRecord())
}

展開されているコード


@Bundle
struct MyBuldle {
    let id: Int
    let name: String
    let position = Position(x: 0, y: 0)

    // 展開コード
    // ```
    public func addComponent(forEntity record: EntityRecord) {
        record.addComponent(self.id)
        record.addComponent(self.name)
        record.addComponent(self.position)
    }
    // ```
}