Refactored entity storage to sparse set
This commit is contained in:
parent
22a9abb882
commit
9a30453e5e
|
|
@ -17,19 +17,6 @@ public final class Entity: UniqueEntityIdentifiable {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - Invalidate
|
||||
extension Entity {
|
||||
|
||||
public var isValid: Bool {
|
||||
return nexus.isValid(entity: self)
|
||||
}
|
||||
|
||||
func invalidate() {
|
||||
identifier = EntityIdentifier.invalid
|
||||
name = nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Equatable
|
||||
public func == (lhs: Entity, rhs: Entity) -> Bool {
|
||||
return lhs.identifier == rhs.identifier
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ public protocol UniformStorage: class {
|
|||
func add(_ element: Element, at index: Index)
|
||||
func has(_ index: Index) -> Bool
|
||||
func get(at index: Index) -> Element?
|
||||
func remove(at index: Index)
|
||||
@discardableResult
|
||||
func remove(at index: Index) -> Bool
|
||||
func clear(keepingCapacity: Bool)
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +57,8 @@ public class ManagedContiguousArray: UniformStorage {
|
|||
return store[index]
|
||||
}
|
||||
|
||||
public func remove(at index: Index) {
|
||||
@discardableResult
|
||||
public func remove(at index: Index) -> Bool {
|
||||
if store[index] != nil {
|
||||
size -= 1
|
||||
}
|
||||
|
|
@ -64,6 +66,7 @@ public class ManagedContiguousArray: UniformStorage {
|
|||
if size == 0 {
|
||||
clear()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public func clear(keepingCapacity: Bool = false) {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ extension Nexus {
|
|||
public func assign(component: Component, to entity: Entity) {
|
||||
let componentId = component.identifier
|
||||
let entityIdx = entity.identifier.index
|
||||
let entityId: EntityIdentifier = entity.identifier
|
||||
|
||||
/// test if component is already assigned
|
||||
guard !has(componentId: componentId, entityIdx: entityIdx) else {
|
||||
// FIXME: this is still open to debate
|
||||
|
|
@ -53,7 +55,6 @@ extension Nexus {
|
|||
componentIdsByEntity[entityIdx]?.add(componentId, at: componentId.hashValue)
|
||||
|
||||
// FIXME: iterating all families is costly for many families
|
||||
let entityId: EntityIdentifier = entity.identifier
|
||||
for (_, family) in familiesByTraitHash {
|
||||
update(membership: family, for: entityId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,6 @@
|
|||
|
||||
extension Nexus {
|
||||
|
||||
public var entities: [Entity] {
|
||||
// FIXME: we do not want this kind of access to the underlying entity store
|
||||
return entityStorage.filter { isValid(entity: $0.identifier) }
|
||||
}
|
||||
|
||||
private func nextEntityIdx() -> EntityIndex {
|
||||
guard let nextReused: EntityIdentifier = freeEntities.popLast() else {
|
||||
return entityStorage.count
|
||||
|
|
@ -22,49 +17,32 @@ extension Nexus {
|
|||
public func create(entity name: String? = nil) -> Entity {
|
||||
let newEntityIndex: EntityIndex = nextEntityIdx()
|
||||
let newEntityIdentifier: EntityIdentifier = newEntityIndex.identifier
|
||||
if entityStorage.count > newEntityIndex {
|
||||
let reusedEntity: Entity = entityStorage[newEntityIndex]
|
||||
assert(reusedEntity.identifier == EntityIdentifier.invalid, "Stil valid entity \(reusedEntity)")
|
||||
reusedEntity.identifier = newEntityIdentifier
|
||||
reusedEntity.name = name
|
||||
notify(EntityCreated(entityId: newEntityIdentifier))
|
||||
return reusedEntity
|
||||
} else {
|
||||
let newEntity = Entity(nexus: self, id: newEntityIdentifier, name: name)
|
||||
entityStorage.insert(newEntity, at: newEntityIndex)
|
||||
notify(EntityCreated(entityId: newEntityIdentifier))
|
||||
return newEntity
|
||||
}
|
||||
|
||||
let newEntity = Entity(nexus: self, id: newEntityIdentifier, name: name)
|
||||
entityStorage.add(newEntity, at: newEntityIndex)
|
||||
notify(EntityCreated(entityId: newEntityIdentifier))
|
||||
return newEntity
|
||||
}
|
||||
|
||||
/// Number of entities in nexus.
|
||||
public var numEntities: Int {
|
||||
return entityStorage.count - freeEntities.count
|
||||
}
|
||||
|
||||
func isValid(entity: Entity) -> Bool {
|
||||
return isValid(entity: entity.identifier)
|
||||
}
|
||||
|
||||
func isValid(entity entitiyId: EntityIdentifier) -> Bool {
|
||||
return entitiyId != EntityIdentifier.invalid &&
|
||||
entitiyId.index >= 0 &&
|
||||
entitiyId.index < entityStorage.count
|
||||
return entityStorage.count
|
||||
}
|
||||
|
||||
public func has(entity entityId: EntityIdentifier) -> Bool {
|
||||
return isValid(entity: entityId)
|
||||
return entityStorage.has(entityId.index)
|
||||
}
|
||||
|
||||
public func get(entity entityId: EntityIdentifier) -> Entity {
|
||||
return entityStorage[entityId.index]
|
||||
public func get(entity entityId: EntityIdentifier) -> Entity? {
|
||||
return entityStorage.get(at: entityId.index)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func destroy(entity: Entity) -> Bool {
|
||||
let entityId: EntityIdentifier = entity.identifier
|
||||
// FIXME: we can make this cheaper by eliminating the need to ask if entity is present
|
||||
guard has(entity: entityId) else {
|
||||
let entityIdx: EntityIndex = entityId.index
|
||||
|
||||
guard entityStorage.remove(at: entityIdx) else {
|
||||
report("EntityRemove failure: no entity \(entityId) to remove")
|
||||
return false
|
||||
}
|
||||
|
|
@ -72,12 +50,6 @@ extension Nexus {
|
|||
let cleared: Bool = clear(componentes: entityId)
|
||||
assert(cleared, "Could not clear all components form entity \(entityId)")
|
||||
|
||||
entity.invalidate()
|
||||
|
||||
// replace with "new" invalid entity to keep capacity of array
|
||||
let invalidEntity = Entity(nexus: self, id: EntityIdentifier.invalid)
|
||||
entityStorage[entityId.index] = invalidEntity
|
||||
|
||||
freeEntities.append(entityId)
|
||||
|
||||
// FIXME: iterating all families is costly for many families
|
||||
|
|
|
|||
|
|
@ -66,7 +66,6 @@ extension Nexus {
|
|||
/// will be called on family init defer
|
||||
func onFamilyInit(family: Family) {
|
||||
// FIXME: this is costly for many entities
|
||||
// FIXME: we iterate invalid entities here
|
||||
for entity: Entity in entityStorage {
|
||||
update(membership: family, for: entity.identifier)
|
||||
}
|
||||
|
|
@ -87,7 +86,7 @@ extension Nexus {
|
|||
}
|
||||
|
||||
let is_Member: Bool = isMember(entityId, in: family)
|
||||
if !isValid(entity: entityId) && is_Member {
|
||||
if !has(entity: entityId) && is_Member {
|
||||
remove(from: traitHash, entityId: entityId, entityIdx: entityIdx)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ public typealias UniformComponents = ContiguousComponentArray
|
|||
public typealias UniformEntityIdentifiers = SparseEntityIdentifierSet
|
||||
public typealias ComponentIdentifiers = ContiguousArray<ComponentIdentifier>
|
||||
public typealias ComponentSet = Set<ComponentIdentifier>
|
||||
public typealias Entities = ContiguousArray<Entity>
|
||||
public typealias Entities = SparseEntitySet
|
||||
public typealias EntityIdSet = Set<EntityIdentifier>
|
||||
public typealias FamilyTraitSetHash = Int
|
||||
public typealias TraitEntityIdHash = Int
|
||||
|
|
@ -31,7 +31,6 @@ public class Nexus {
|
|||
|
||||
/// - Index: index value matching entity identifier shifted to Int
|
||||
/// - Value: each element is a entity instance
|
||||
// FIXME: sparse set my be valuable
|
||||
var entityStorage: Entities
|
||||
|
||||
/// - Key: component type identifier
|
||||
|
|
@ -40,7 +39,6 @@ public class Nexus {
|
|||
|
||||
/// - Key: entity id as index
|
||||
/// - Value: each element is a component identifier associated with this entity
|
||||
// FIXME: this may be refactored to a uniform sparse set
|
||||
var componentIdsByEntity: [EntityIndex: SparseComponentIdentifierSet]
|
||||
|
||||
/// - Values: entity ids that are currently not used
|
||||
|
|
@ -61,11 +59,12 @@ public class Nexus {
|
|||
}
|
||||
|
||||
deinit {
|
||||
for e in entities {
|
||||
|
||||
for e: Entity in entityStorage {
|
||||
destroy(entity: e)
|
||||
}
|
||||
|
||||
entityStorage.removeAll()
|
||||
entityStorage.clear()
|
||||
freeEntities.removeAll()
|
||||
|
||||
assert(entityStorage.isEmpty)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ public class SparseSet<Element>: UniformStorage, Sequence {
|
|||
}
|
||||
|
||||
public var count: Int { return size }
|
||||
var isEmpty: Bool { return size == 0 }
|
||||
var capacitySparse: Int { return sparse.capacity }
|
||||
var capacityDense: Int { return dense.capacity }
|
||||
|
||||
|
|
@ -52,18 +53,19 @@ public class SparseSet<Element>: UniformStorage, Sequence {
|
|||
return dense[sIdx]?.value
|
||||
}
|
||||
|
||||
public func remove(at index: Index) {
|
||||
@discardableResult
|
||||
public func remove(at index: Index) -> Bool {
|
||||
guard has(index) else {
|
||||
return
|
||||
return false
|
||||
}
|
||||
guard let removeIdx: DenseIndex = sparse[index] else {
|
||||
return
|
||||
return false
|
||||
}
|
||||
let lastIdx: DenseIndex = count - 1
|
||||
dense.swapAt(removeIdx, lastIdx)
|
||||
sparse[index] = nil
|
||||
guard let swapped: Pair = dense[removeIdx] else {
|
||||
return
|
||||
return false
|
||||
}
|
||||
sparse[swapped.key] = removeIdx
|
||||
dense.removeLast()
|
||||
|
|
@ -71,6 +73,7 @@ public class SparseSet<Element>: UniformStorage, Sequence {
|
|||
if size == 0 {
|
||||
clear(keepingCapacity: false)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public func clear(keepingCapacity: Bool = false) {
|
||||
|
|
@ -105,6 +108,10 @@ public class SparseSet<Element>: UniformStorage, Sequence {
|
|||
|
||||
// MARK: - specialized sparse sets
|
||||
|
||||
public class SparseEntitySet: SparseSet<Entity> {
|
||||
public typealias Index = EntityIndex
|
||||
}
|
||||
|
||||
public class SparseEntityIdentifierSet: SparseSet<EntityIdentifier> {
|
||||
public typealias Index = EntityIndex
|
||||
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ class FamilyTests: XCTestCase {
|
|||
|
||||
var countA: Int = 0
|
||||
familyA.iterate(components: Position.self) { (entityId, _) in
|
||||
let e = nexus.get(entity: entityId)
|
||||
let e = nexus.get(entity: entityId)!
|
||||
e.assign(Velocity(a: 3.14))
|
||||
e.remove(Position.self)
|
||||
countA += 1
|
||||
|
|
@ -153,7 +153,7 @@ class FamilyTests: XCTestCase {
|
|||
|
||||
var countB: Int = 0
|
||||
familyB.iterate(components: Velocity.self) { eId, velocity in
|
||||
let e = nexus.get(entity: eId)
|
||||
let e = nexus.get(entity: eId)!
|
||||
e.assign(Position(x: 1, y: 2))
|
||||
e.remove(velocity!)
|
||||
countB += 1
|
||||
|
|
|
|||
|
|
@ -24,51 +24,20 @@ class NexusTests: XCTestCase {
|
|||
|
||||
let e0 = nexus.create()
|
||||
XCTAssert(e0.identifier.index == 0)
|
||||
XCTAssert(e0.isValid)
|
||||
XCTAssert(nexus.numEntities == 1)
|
||||
|
||||
let e1 = nexus.create(entity: "Named e1")
|
||||
XCTAssert(e1.identifier.index == 1)
|
||||
XCTAssert(e1.isValid)
|
||||
XCTAssert(nexus.numEntities == 2)
|
||||
|
||||
XCTAssert(e0.name == nil)
|
||||
XCTAssert(e1.name == "Named e1")
|
||||
|
||||
let rE0 = nexus.get(entity: e0.identifier)
|
||||
let rE0 = nexus.get(entity: e0.identifier)!
|
||||
XCTAssert(rE0.name == e0.name)
|
||||
XCTAssert(rE0.identifier == e0.identifier)
|
||||
}
|
||||
|
||||
func testDestroyAndReuseEntity() {
|
||||
let nexus: Nexus = Nexus()
|
||||
XCTAssert(nexus.numEntities == 0)
|
||||
|
||||
let e0 = nexus.create(entity: "e0")
|
||||
XCTAssert(e0.isValid)
|
||||
XCTAssert(nexus.numEntities == 1)
|
||||
|
||||
let e1 = nexus.create(entity: "e1")
|
||||
XCTAssert(e1.isValid)
|
||||
XCTAssert(nexus.numEntities == 2)
|
||||
|
||||
e0.destroy()
|
||||
|
||||
XCTAssert(!e0.isValid)
|
||||
XCTAssert(e1.isValid)
|
||||
XCTAssert(nexus.numEntities == 1)
|
||||
|
||||
let e2 = nexus.create(entity: "e2")
|
||||
XCTAssert(!e0.isValid)
|
||||
XCTAssert(e1.isValid)
|
||||
XCTAssert(e2.isValid)
|
||||
|
||||
XCTAssert(nexus.numEntities == 2)
|
||||
|
||||
XCTAssert(!(e0 == e2))
|
||||
XCTAssert(!(e0 === e2))
|
||||
}
|
||||
|
||||
func testComponentCreation() {
|
||||
let nexus: Nexus = Nexus()
|
||||
XCTAssert(nexus.numEntities == 0)
|
||||
|
|
@ -80,7 +49,6 @@ class NexusTests: XCTestCase {
|
|||
e0.assign(p0)
|
||||
e0.assign(p0)
|
||||
|
||||
XCTAssert(e0.isValid)
|
||||
XCTAssert(e0.hasComponents)
|
||||
XCTAssert(e0.numComponents == 1)
|
||||
|
||||
|
|
@ -93,7 +61,7 @@ class NexusTests: XCTestCase {
|
|||
let nexus = Nexus()
|
||||
let identifier: EntityIdentifier = nexus.create(entity: "e0").identifier
|
||||
|
||||
let e0 = nexus.get(entity: identifier)
|
||||
let e0 = nexus.get(entity: identifier)!
|
||||
|
||||
XCTAssert(e0.numComponents == 0)
|
||||
e0.remove(Position.self)
|
||||
|
|
@ -134,7 +102,6 @@ class NexusTests: XCTestCase {
|
|||
e0.destroy()
|
||||
|
||||
XCTAssert(e0.numComponents == 0)
|
||||
XCTAssert(!e0.isValid)
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue