Closed rrbox closed 5 months ago
Entity の管理方法を検討するべきだと思います(Entity mapper)。 Sparse set の場合はインクリメント方式が相性がいいかもしれません。
Filter についても考察すべきです
v0.1 時点の query の辞書部分を sparse set に置き換えることができるかと思います。Entity に sparse index に変換するルールも同時に作る必要がありそう。
Sparse set の最小実装を考えました。Swift playgrounds では正常な動作が確認されています。
構造体で実装されており、entity を key にした Dictionary と完全に置き換えることができます。運用の際は、Query などで使用されている dictionary と置き換える形となります。
Entity の slot をインクリメントする仕組みは別途用意する必要があります。Entities
という構造体を作り、そこに実装しましょう。World に Entities
のインスタンスを配置し、entity を生成するたびにインクリメントしていきます。
Entity を生成する際は、ただインクリメントするだけでなく、再利用することも考慮にいれましょう。下記の SparseSet
は pop
で空いた slot を再利用できるようにはしていますが、Entity の生成については責任をもたないです。
struct Entity: Equatable {
let slot: Int
let generation: Int
}
struct SparseSet<T> {
typealias DenseIndex = Int
var sparse: [DenseIndex?]
var dense: [Entity]
var data: [T]
func value(forEntity entity: Entity) -> T? {
guard let i = self.sparse[entity.slot] else { return nil }
guard self.dense[i] == entity else { return nil }
return self.data[i]
}
mutating func update(forEntity entity: Entity, _ execute: (inout T) -> ()) {
guard let i = self.sparse[entity.slot] else { return }
guard self.dense[i].generation == entity.generation else { return }
execute(&self.data[i])
}
mutating func update(_ execute: (inout T) -> ()) {
for i in self.data.indices {
execute(&self.data[i])
}
}
mutating func allocate() {
self.sparse.append(nil)
}
mutating func insert(_ value: T, withEntity entity: Entity) {
let denseIndex = self.dense.count
self.sparse[entity.slot] = denseIndex
self.dense.append(entity)
self.data.append(value)
}
mutating func pop(entity: Entity) {
assert(entity.generation == self.dense[self.sparse[entity.slot]!].generation)
let denseIndexLast = self.dense.count-1
let removeIndex = self.sparse[entity.slot]!
self.sparse[self.dense[denseIndexLast].slot] = removeIndex
self.dense.swapAt(removeIndex, denseIndexLast)
self.data.swapAt(removeIndex, denseIndexLast)
self.data.removeLast()
self.dense.removeLast()
}
}
ついでに、デバッグ用にどうぞ。
#if DEBUG
extension SparseSet: CustomStringConvertible {
var description: String {
let s = self.sparse.map { denseIndex in
if let i = denseIndex { return "\(i) " }
else { return "- " }
}
.reduce(into: "[ ") {
$0 += $1
} + "]"
return "SparseSet<\(T.self)> {\n\tentities: \(self.dense.map { "(\($0.slot) \($0.generation))" })\n\tsparseMap: \(s)\n\tdata: \(self.data)\n}"
}
}
#endif
pop すると正しく検索できなくなるようです。ただしいコードに書き直します... ↑修正しました。
Sparse set を用いた場合、Entity は sparse index を内部にもつ構造体になります。sparse index はインクリメントで生成できます。問題は、一度削除された slot を再利用する場合のアルゴリズムを考える必要があることです。現状の考えは、entity を despawn するたびに削除した entity の slot を何かしらの方法で保持しておくことです。初めに単方向連結リストを使用した方法を考えたいです。
単方向連結リストでなくとも、スタックで実装できそうです。
ちなみに、Array は RandomAccessCollection に準拠しているので、辞書よりも効率の良い部分がいくつかありそうです。
辞書よりもイテレーションに特化しているかもです。パフォーマンス比較してみたいところです。 また entity の実装を UUID ではなく、加算される数値を利用できるようにもし、衝突を完全に回避します。
下のは試しに考えた見たやつです。