まずECSって何?
Wikiを読んだりググったりしてね
参考: エンティティ・コンポーネント・システム(Wikipedia)
3行で
オブジェクトをクラスで表すんじゃなくて、
Entityって呼ばれるIDにComponentって呼んでる任意のデータを紐付けて
Systemって呼ばれる関数でConponent毎に処理する。
何が嬉しいの?
- IDにコンポーネントを紐付けてオブジェクトを表すので簡単にいろんなオブジェクトを作れる。
- コンポーネント毎に処理するので、CPUキャッシュが有効に使えて処理が早くなる(実装による)
- ゲームみたいな多様なデータをもつオブジェクトを表現する場合、静的なクラスを設計しなくていいので楽
実際どんな感じ?
まずはエンティティやコンポートネントの置き場所となるWorldオブジェクトを作成する。
let world = World()
次にEntityを作成する。
let entity = world.newEntity
Entityにコンポーネントを紐づける
# 位置情報(Position)と名前(Name)を紐づける
entity.with(Position(x: 1, y: 2)).with(Name("Bob"))
Entityからコンポーネントを取得する場合、get関数に型を指定する。
entity.get(Position) == Position(x: 1, y: 2) # True
Entityから一度に複数のコンポーネントを取得できるようにする。
let (pos, name) = entity.getAll(Position, Name)
Entityが指定コンポーネントを持っているかチェックする。
entity.hasAll(Position, Name) # True
Systemを実装するために指定の全てのコンポーネントを処理する。
# 値のみ取得
for pos in world.compornentsOf(Position):
# pになんかする
# Entityも取得する
for entity, pos in world.compornentsOf(Position):
# p, eになんかする
使い終わったEntityを削除する。
entity.delete
データ構造
こんな感じになっております。
type
EntityId = uint64
Entity* = ref object
id: EntityId
world: World
AbstructComponent = ref object of RootObj
index: Table[Entity, int]
freeIndex: seq[int]
Component*[T] = ref object of AbstructComponent
components: seq[T]
World* = ref object
lastEntityId: EntityId
components: Table[string, AbstructComponent]
Entity
EntityId自体はただのuint64。
newEntityされるたびにインクリメントしていく。
余程のことがないとオーバーフローはしないと思うので、
使い回しはしない感じでいく。
ただ、Entityを起点として色々操作したいので、
自分が属するWorldの参照を持たせとく。
Component
これはちょっとヒネってる。
管理情報だけをもつAbstructComponentを継承して、
実際のデータリストをもつCompornentをジェネリックで作る。
AbstructComponentのindexはEntityと
紐づいているデータのComponent配列のインデックス情報のテーブル。
freeIndexはComponent配列の空いたところを使い回すようの空きインデックス管理。
単純にEntityとComponentのテーブルにすると、
System関数で回すときにCPUキャッシュの恩恵を受けられそうにないので、
Componentを配列で保持するためにこんな感じになっております。
World
EntityIDの管理とコンポーネントのテーブルを持ってる。
コンポーネントは文字列とAbstructComponentのテーブル。
キーの文字列は型名。使う時はAbstructComponentをキャストして使う。
どうやってんの?
Entity作成
これは単純
proc newEntity*(self: World): Entity =
self.lastEntityId += 1
Entity(id: self.lastEntityId, world: self)
Component紐付け
これは面倒
with関数自体はジェネリック関数
proc with*[T](self: Entity, component: T): Entity {.inline, discardable.} =
self.world.assign(self, component)
self
Worldのassign関数に丸投げ。
assignは対象のコンポーネントをすでに持ってれば作成。
そのあと、キャストされたComponentを取得して引数のentityとcomponentを追加する。
proc assign*[T](self: World, entity: Entity, component: T) =
if self.has(T) == false:
self.newComponent(T)
self.componentsOf(T).assign(entity, component)
そのコンポーネントを持っているかどうかは以下の関数でチェック
proc has*(self: World, T: typedesc): bool {.inline.} =
self.components.hasKey(T.type.name)
型自体をtypedescで受け取り、type.nameで型名を文字列に変換し、
componentsテーブルにあるかhasKeyでチェックする。
型名から文字列を取得するname関数はtypetraitsをimportする必要がある。
newComponentは単に指定の型のComponentをテーブルに追加するだけ。
proc newComponent*(self: World, T: typedesc) {.inline.} =
self.components[T.type.name] = Component[T]()
componentsOfはcomponentsテーブルから指定された型の文字列をキーに
AbstructComponentを持ってきてそれを指定された型にcastして返す。
proc componentsOf*(self: World, T: typedesc): Component[T] {.inline.} =
cast[Component[T]](self.components[T.type.name])
Componentのassignはこんな感じ。
proc assign*[T](self: Component[T], entity: Entity, component: T) =
if self.index.hasKey(entity):
self.components[self.index[entity]] = component
return
if self.freeIndex.len > 0:
let index = self.freeIndex.pop
self.index[entity] = index
self.components[index] = component
return
self.index[entity] = self.components.len
self.components.add(component)
既に割り当てられてたら置換え。
空きindexがあればそこに格納。
そうでなければ配列を拡張して追加。
Componentの取得
Entity経由での取得はWorldに丸投げ。
型はtypedescで指定する。
proc get*(self: Entity, T: typedesc): T {.inline.} =
self.world.get(self, T)
WorldからComponentリストを取得して、
Componentから指定したentityのコンポーネントを取得する。
proc get*(self: World, entity: Entity, T: typedesc): T {.inline.} =
self.componentsOf(T).get(entity)
Componentから指定されたEntityのデータ取得
単にentityをキーにindexテーブルからインデックスを取得して、
components配列からデータを取り出してくるだけ。
proc get*[T](self: Component[T], entity: Entity): T {.inline.} =
return self.components[self.index[entity]]
ちなみにEntityはオブジェクトなので、ハッシュのキーにする場合は
hash関数の定義が必要。
import hashes
proc hash*(self: Entity): Hash {.inline.} =
self.id.hash
複数取得はマクロを使って実装。
やってることはgetを並べてるだけ。
macro getAll*(self: Entity, types: varargs[typed]): untyped =
var body = ""
for t in types:
if len(body) > 0:
body.add(", ")
body.add(fmt"{repr(self)}.get({repr(t)})")
parseStmt(fmt"({body})")
Componentを持っているかチェック
worldが持っててかつEntityが持ってれば真を返す感じ。
proc has*(self: Entity, T: typedesc): bool {.inline.} =
self.world.has(T) and self.world.componentsOf(T).has(self)
Componentのチェックは単純にhasKeyで。
proc has(self: AbstructComponent, entity: Entity): bool {.inline.} =
self.index.hasKey(entity)
複数の型を一気にチェックできた方が便利なので
これもマクロで実装。hasを並べてるだけ。
macro hasAll*(self: Entity, types: varargs[typed]): untyped =
var body = ""
for t in types:
if len(body) > 0:
body.add(" and ")
body.add(fmt"{repr(self)}.has({repr(t)})")
parseStmt(body)
Component全処理
Componentにitmesとpairsイテレーターを実装すれば
Component、Entity、Componentのペアを
forループでぶん回せる。
iterator items*[T](self: Component[T]): T =
for i in self.index.values:
yield self.components[i]
iterator pairs*[T](self: Component[T]): tuple[key: Entity, val: T] =
for e, i in self.index.pairs:
yield (e, self.components[i])
Entityの削除
worldから対象のEntityを削除して
念のためidに無効なidを設定する。
let InvalidEntityId: EntityId = 0
proc delete*(self: Entity) =
self.world.deleteEntity(self)
self.id = InvalidEntityId
Entityが有効かチェックする関数も定義しとくと良いかもしんない。
proc isValid*(self: Entity): bool {.inline.} =
self.id != InvalidEntityId
worldからの削除は全Componentから対象Entityのデータを削除する。
proc deleteEntity(self: World, entity: Entity) =
for c in self.components.values:
c.remove(entity)
Componentからの削除はEntityが持っていれば、
データ自体の削除処理はしないで対象EntityのインデックスをfreeIndexに移動するだけ。
proc remove(self: AbstructComponent, entity: Entity) =
if self.has(entity):
self.freeIndex.add(self.index[entity])
self.index.del(entity)
最後に
ECSを知って興味が出てNimで実装してみただけなので、
実際使ったら色々問題あるかもだし、ベンチマークとかも取ってないので遅いかもだけど、
あくまでご参考までに。
以上〜。