Refactored entity storage to sparse set

This commit is contained in:
Christian Treffs 2017-11-20 22:55:05 +01:00
parent 22a9abb882
commit 9a30453e5e
9 changed files with 39 additions and 104 deletions

View File

@ -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

View File

@ -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) {

View File

@ -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)
}

View File

@ -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

View File

@ -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
}

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)
}