- Swift
- 2Dゲーム開発
- macOS
- iOS
Entity Component System for swift
License: MIT License
辞書よりもイテレーションに特化しているかもです。パフォーマンス比較してみたいところです。
また entity の実装を UUID ではなく、加算される数値を利用できるようにもし、衝突を完全に回避します。
下のは試しに考えた見たやつです。
import Foundation
class World {
let buffer = WorldBuffer()
}
struct SparseSet<T: Component> {
var indices: [Entity] = []
var data: [T] = []
}
class SparseSetRef<T: Component>: BufferElement {
var sparseSet: SparseSet<T>
init(sparseSet: SparseSet<T>) {
self.sparseSet = sparseSet
super.init()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class SparseSetBuffer {
let buffer: Buffer
init(buffer: Buffer) {
self.buffer = buffer
}
func addComponentStrage<T: Component>(typeOf type: T.Type) {
self.buffer.addComponent(SparseSetRef(sparseSet: SparseSet<T>()))
}
func sparseSetRef<T: Component>(typeOf type: T.Type) -> SparseSetRef<T>? {
self.buffer.component(ofType: SparseSetRef<T>.self)
}
}
extension WorldBuffer {
var sparseSetBuffer: SparseSetBuffer { SparseSetBuffer(buffer: self) }
}
final class Query<C: Component>: SystemParameter {
let ref: SparseSetRef<C>
init(ref: SparseSetRef<C>) {
self.ref = ref
}
static func register(to worldBuffer: WorldBuffer) {
guard worldBuffer.sparseSetBuffer.sparseSetRef(typeOf: C.self) == nil else { return }
worldBuffer.sparseSetBuffer.addComponentStrage(typeOf: C.self)
}
static func getParameter(from worldBuffer: WorldBuffer) -> Query<C>? {
Query<C>(ref: worldBuffer.sparseSetBuffer.sparseSetRef(typeOf: C.self)!)
}
}
final class Query2<C0: Component, C1: Component>: SystemParameter {
let ref: (SparseSetRef<C0>, SparseSetRef<C1>)
init(ref: (SparseSetRef<C0>, SparseSetRef<C1>)) {
self.ref = ref
}
static func register(to worldBuffer: WorldBuffer) {
guard worldBuffer.sparseSetBuffer.sparseSetRef(typeOf: C0.self) == nil else { return }
guard worldBuffer.sparseSetBuffer.sparseSetRef(typeOf: C1.self) == nil else { return }
worldBuffer.sparseSetBuffer.addComponentStrage(typeOf: C0.self)
worldBuffer.sparseSetBuffer.addComponentStrage(typeOf: C1.self)
}
static func getParameter(from worldBuffer: WorldBuffer) -> Query2<C0, C1>? {
Query2<C0, C1>(ref: (worldBuffer.sparseSetBuffer.sparseSetRef(typeOf: C0.self)!,
worldBuffer.sparseSetBuffer.sparseSetRef(typeOf: C1.self)!))
}
}
現在のこれら二つのコマンドは、world にある archetype に変更を与えるのみで、既存の Chunk に対して影響を与えないようになっています。Spawn, Despawn と同様に、World にメソッドを用意し、その中で Chunk への反映を実装し、World のメソッドを Commands から実行するような作りに書き換えてください。
パフォーマンスの向上を考える上で、処理の速度比較をするのは必須です。
Entity が spawn / despawn されたタイミングで実行される処理をカスタマイズできるようにしたいです。Entity と関連づけられた Node を削除したりする時に活用できると思います。
AnyMap<Buffer>
-> AnyMap<WorldStorage>
BufferElement
-> WorldStorageElement
BufferRef
-> WorldStorageRef
Package.swift に iOS のバージョン指定がありません。追加しましょう。
リポジトリを public にしてコードを公開するのであれば、wiki も用意しておきたいところです。
なにを書こうかしら
to do:
Component
意外の型を指定できるようにするEntity
を Query で受け取れるようにするこの提案を実装すると、Query が Entity を受け取るかどうかをオプションにすることができます。
v0.1.0 での Query でコンポーネントを受け取る方法はこのようになっています:
func system(query: Query<SomeComponent>) {
query.update { entity, someComponent in
}
}
この場合は、Entity を常に受け取るようになっており、受け取りたくない場合は _
に置き換える処置となっています。
Entity を Component に準拠させた場合は以下のように変更することができます。
func system(query: Query2<Entity, SomeComponent>) {
query.update { entity, someComponent in
}
}
この場合は、Entity を受け取りたくない時は query の型を Query<SomeComponent>
にすることで解決できます。
func system(query: Query<SomeComponent>) {
query.update { someComponent in
}
}
考えられる方法を列挙してください
AnyMap<Mode>
Schedule
States
Buffer
Buffer
(旧) -> AnyMap<WorldStorage>
EntityRecord
-> AnyMap<EntityRecord>
WorldBuffer
-> WorldStorageRef
BufferElement
-> WorldStorageElement
Event を WorldStorage (旧 WorldBuffer) から取り出す必要があります。Event 発信の方法を見直してください。
正しくは
buildWillDespawnResponder
position, zRotation については、SpriteKit 側の物理演算の結果を component に反映させる必要があるので、双方向バインディングにします(didSimulatePhisics)。
その他のデータは ECS → SpriteKit の単方向のデータバインディングのみで十分となるはずです。
各プロパティを別々に管理するためには、各プロパティのデータを別のコンポーネントで管理することになると思われます。そこで #50 を利用して Graphic 用バンドルを予め用意しておくと便利かと思います。
ECS の最低限の機能を持たせた v0.0.1 をリリースします。
Update, Event の実行を並列処理で行う提案です。
RPG のようにゲーム全体として状態が切り替わるような場合に States の実装が不可欠です。まずは仕様から考えましょう。
World は update の中でどの処理をどのような順番で実行するべきなのかを考えます
現在実装されている処理は以下の通りです
Chunk には、entity が追加/削除されたり component が追加/削除された時に、そのデータを反映する機能があります。
Filtered query も chunk の一種ですが、どうやら上記の関数が実装されていないようです。
まずは不具合が出ていないか確認してください。
不具合があったら、修正してくださいださい。
public にするにあたり、作ったほうがいいかもです
#70 で entity 生成直後か entity 検索によるかで entity commands の種類を変える方法を採用しました。
しかし、#55 で entity への graphic の割り当ての際に entity commands によって実装方法が異なることや、それぞれに同じメソッドを作成しても、entity commands 共通のメソッドとして動的に実行することが難しくなるなど、問題が生じているようです。
最良は、同じ entity command (entity record の変更)を別々の entity commands に push するだけで、実行方法が分けられることです。
Entity commands に用意されている set graphic メソッドは、親のない SKNode を SKScene に配置するようになっています。
すでに配置されている、ECS 管理外の SKNode を entity に紐づける方法を用意したいです。
以下のような仕様にします。
簡単な Documentation の整備を v0.1.0 公開前に行いました。
新しい System の実装です。
#18 Schedule の登場により、これまでの Set up systems, Update systems, Event systems が 1 つに統合され、Systems
というモジュールで実装されます。
以下の Component を追加します。
Entity 間で親子関係が作られた際に、Child と Parent がそれぞれ子と親に追加されます。Child は親の Entity が含まれています。Parent はこの Entity の配列が含まれます。
特定の State が activated なときだけ Event に反応するシステムを作りたい。
sparse set の導入により、entity 生成方法が変わります。それまでは引数を取らないイニシャライザで簡単に生成できましたが、今後は world の entities から空きスロットに対応する entity を生成しなければなりません。
この変化に伴い、これまでの commands の entity spawn 処理および、entity commands の作成がうまく機能しなくなります。とくに、entity spawn 処理は world へのアクセスを要求するため、遅延実行されることになります。従って、続く entity commands は entity のデータをプロパティに格納することができないことになります。
そこで entity commands の再定義と spawn, despawn の処理内容の調整を行うべきとしました。
何書こうかしら
ecs-swift
?SearchedEntityCommands
をその場で生成する仕様になったので、オプショナル型である必要がありません。Swift 5.9 では以下の機能が通過されたようです。
今後 swift の更新が ECS に大きな影響を与える可能性が大きいので、今のうちに妄想を膨らませておきます。
entity record や entity (id) は定数なので、public にしてもいいかもしれません。
commands は公開しなくてもいいです。
正 : System registry
システムが実行されるタイミングを Schedule
という構造体で制御する提案です。
この機能により、それまで UpdateSystem
, EventSystem
, SetUpSystem
として別々に実装していたシステム群を1種類にまとめることができます。
デメリットは、start up で使用できないパラメータ(Query)があること、event reader が他のスケジュールでは利用できないことが挙げられます(不正なパラメータの使用)。以前はコンパイル時に「不正なパラメータの使用」が検出できましたが、schedule を利用すると実行時のみでしか検出できないようです。
画面タップイベントを ECS に送る plug in です。
MacOS 向けではマウスのクリックイベントを取り扱う予定です。
Filtered query の更新処理は、プロパティ query
を1度介して実行する必要があります。
func system(query: Filtered<Query<Use>, With<NotUse>>) {
query.query.update { _, use in
// update `use`
}
}
しかし Filtered
という名称ではあるものの、実際は Query
の一種として扱うことが多いかと思います(例えば、パラメータ名をquery
, -Query
にするなど)。
そこで filtered query についても query と同様に直接 update でコンポーネントを更新するようにした方が扱いやすいのでは、と提案します。
func system(query: Filtered<Query<Use>, With<NotUse>>) {
query.update { _, use in
// update `use`
}
}
ecs-swift はわかりやすいですが、差別化しづらいように思います。
World には以下の処理がデフォルトで実装されます。
なぜ
の順番・構成なのか?
今後 Graphic3D が登場するかもしれません。
クラスで実装された実体は、Binding を使わなくても Component ととして使用することが可能です。ただし、クラスは参照型なので、Component の実体以外の参照からコントロールすることが可能となってしまう欠点があります(複数の参照から操作可能な実体は不具合の原因になりやすいですよねー)。複数の参照を防ぐ方法として、 binding を使用して構造体とクラスを相互反映させる方法が考えられます。ただしこの方法は実装が複雑になりやすく(特にデータバインディング部分)、実装が過剰になりがちです。
上記のような問題を解決するために、以下のように方針を決めました。
作れそうだったら作ってみたいです。以下のようなイメージですが、はたして実現可能なのか..?
例: グラフィック用データの定義
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())
}
}
}
v0.2 現在、システム実行中におこる何かしらの不具合やエラーは、握りつぶす or クラッシュのいずれかとなっています。
これはモデルの変更操作の根幹を成す Commands の実行方式に起因しています。
Commands のように遅延して実行される処理についても、どのシステムで実行されたタスクが失敗したのかを分かりやすくプログラマに伝える方法を考えたいです。
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.