Add entity reuse concept;

Add tests for entity creation/deletion & component creation/deletion
This commit is contained in:
Christian Treffs 2017-10-15 10:59:53 +02:00
parent 4e0522aa49
commit bf62fde5db
6 changed files with 213 additions and 37 deletions

View File

@ -7,7 +7,7 @@
public final class Entity: UniqueEntityIdentifiable {
public let identifier: EntityIdentifier
internal(set) public var identifier: EntityIdentifier = EntityIdentifier.invalid
public var name: String?
fileprivate let nexus: Nexus
@ -20,6 +20,20 @@ public final class Entity: UniqueEntityIdentifiable {
}
// MARK: - Invalidate
extension Entity {
public var isValid: Bool {
return nexus.isValid(entity: self)
}
func invalidate() {
assert(nexus.isValid(entity: identifier), "Invalid entity \(self) is being invalidated.")
identifier = EntityIdentifier.invalid
name = nil
}
}
// MARK: - Equatable
public func ==(lhs: Entity, rhs: Entity) -> Bool {
return lhs.identifier == rhs.identifier
@ -53,19 +67,19 @@ public extension Entity {
public extension Entity {
@discardableResult
public final func add<C>(_ component: C) -> Entity where C: Component {
nexus.add(component: component, to: identifier)
public final func assign<C>(_ component: C) -> Entity where C: Component {
nexus.assign(component: component, to: identifier)
return self
}
@discardableResult
public static func += <C>(lhs: Entity, rhs: C) -> Entity where C: Component {
return lhs.add(rhs)
return lhs.assign(rhs)
}
@discardableResult
public static func << <C>(lhs: Entity, rhs: C) -> Entity where C: Component {
return lhs.add(rhs)
return lhs.assign(rhs)
}
}
@ -118,8 +132,7 @@ public extension Entity {
// MARK: - destroy/deinit entity
extension Entity {
final func destroy() {
clear()
//TODO: notifyDestoryed()
nexus.destroy(entity: self)
}
}

View File

@ -9,6 +9,11 @@
public typealias EntityIdentifier = UInt64 // provides 18446744073709551615 unique identifiers
public typealias EntityIndex = Int
public typealias EntityReuseCount = UInt32
public extension EntityIdentifier {
static let invalid: EntityIdentifier = EntityIdentifier.max
}
public extension EntityIdentifier {
public var index: EntityIndex { return EntityIndex(self & 0xffffffff) } // shifts entity identifier by UInt32.max

View File

@ -25,7 +25,7 @@ extension Nexus {
}
}
public func add<C>(component: C, to entityId: EntityIdentifier) where C: Component {
public func assign<C>(component: C, to entityId: EntityIdentifier) where C: Component {
let componentId = C.identifier
let entityIdx = entityId.index
let hash: EntityComponentHash = componentId.hashValue(using: entityIdx)
@ -76,18 +76,26 @@ extension Nexus {
let hash: EntityComponentHash = componentId.hashValue(using: entityId.index)
guard let componentIdx: ComponentIndex = componentIndexByEntityComponentHash.removeValue(forKey: hash) else {
assert(false, "ComponentRemove failure: entity \(entityId) has no component \(componentId)")
report("ComponentRemove failure: entity \(entityId) has no component \(componentId)")
return false
}
guard componentsByType[componentId]?.remove(at: componentIdx) != nil else {
assert(false, "ComponentRemove failure: no component instance for \(componentId) with the given index \(componentIdx)")
report("ComponentRemove failure: no component instance for \(componentId) with the given index \(componentIdx)")
return false
}
// FIXME: this is expensive
if let removeIndex: Int = get(components: entityId)?.index(where: { $0 == componentId }) {
componentIdsByEntityIdx[entityId.index]?.remove(at: removeIndex)
guard let removeIndex: Int = get(components: entityId)?.index(where: { $0 == componentId }) else {
assert(false, "ComponentRemove failure: no component found to be removed")
report("ComponentRemove failure: no component found to be removed")
return false
}
guard componentIdsByEntityIdx[entityId.index]?.remove(at: removeIndex) != nil else {
assert(false, "ComponentRemove failure: nothing was removed")
report("ComponentRemove failure: nothing was removed")
return false
}

View File

@ -7,18 +7,49 @@
extension Nexus {
fileprivate func nextEntityIdx() -> EntityIndex {
guard let nextReused: EntityIdentifier = freeEntities.popLast() else {
return entities.count
}
return nextReused.index
}
public func create(entity name: String? = nil) -> Entity {
let newEntityIndex: EntityIndex = entities.count // TODO: use free entity index
let newEntityIndex: EntityIndex = nextEntityIdx()
let newEntityIdentifier: EntityIdentifier = newEntityIndex.identifier
let newEntity = Entity(nexus: self, id: newEntityIdentifier, name: name)
entities.insert(newEntity, at: newEntityIndex)
notify(EntityCreated(entityId: newEntityIdentifier))
return newEntity
if entities.count > newEntityIndex {
let reusedEntity: Entity = entities[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)
entities.insert(newEntity, at: newEntityIndex)
notify(EntityCreated(entityId: newEntityIdentifier))
return newEntity
}
}
/// Number of entities in nexus.
public var count: Int {
return entities.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 < entities.count
}
public func has(entity entityId: EntityIdentifier) -> Bool {
return entities.count > entityId.index // TODO: reuse free index
return isValid(entity: entityId) // TODO: reuse free index
}
public func get(entity entityId: EntityIdentifier) -> Entity? {
@ -27,7 +58,8 @@ extension Nexus {
}
@discardableResult
public func destroy(entity entityId: EntityIdentifier) -> Bool {
public func destroy(entity: Entity) -> Bool {
let entityId: EntityIdentifier = entity.identifier
guard has(entity: entityId) else {
assert(false, "EntityRemove failure: no entity \(entityId) to remove")
return false
@ -35,8 +67,13 @@ extension Nexus {
clear(componentes: entityId)
// FIXME: this is wrong since it removes the entity that should be reused
entities.remove(at: entityId.index)
entity.invalidate()
// replace with "new" invalid entity to keep capacity of array
let invalidEntity = Entity(nexus: self, id: EntityIdentifier.invalid)
entities[entityId.index] = invalidEntity
freeEntities.append(entityId)
notify(EntityDestroyed(entityId: entityId))
return true

View File

@ -30,11 +30,14 @@ public class Nexus {
var componentIdsByEntityIdx: [EntityIndex: ComponentIdentifiers]
var freeEntities: ContiguousArray<EntityIdentifier>
public init() {
entities = Entities()
componentsByType = [:]
componentIndexByEntityComponentHash = [:]
componentIdsByEntityIdx = [:]
freeEntities = ContiguousArray<EntityIdentifier>()
}
@ -46,6 +49,11 @@ extension Nexus {
Log.debug(event)
// TODO: implement
}
func report(_ message: String) {
Log.warn(message)
// TODO: implement
}
}
/*

View File

@ -10,35 +10,140 @@ import XCTest
class NexusTests: XCTestCase {
let nexus = Nexus()
func testNexus() {
let e0 = nexus.create(entity: "E0")
XCTAssert(e0.identifier == 0)
XCTAssert(e0.identifier.index == 0)
override func setUp() {
super.setUp()
}
override func tearDown() {
super.tearDown()
}
func testCreateEntity() {
let nexus: Nexus = Nexus()
XCTAssert(nexus.count == 0)
let e0 = nexus.create()
XCTAssert(nexus.isValid(entity: e0))
XCTAssert(nexus.count == 1)
let e1 = nexus.create(entity: "Named e1")
XCTAssert(nexus.isValid(entity: e1))
XCTAssert(nexus.count == 2)
XCTAssert(e0.name == nil)
XCTAssert(e1.name == "Named e1")
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.count == 0)
XCTAssert(nexus.freeEntities.count == 0)
let e0 = nexus.create(entity: "e0")
XCTAssert(nexus.isValid(entity: e0))
XCTAssert(nexus.count == 1)
let e1 = nexus.create(entity: "e1")
XCTAssert(nexus.isValid(entity: e1))
XCTAssert(nexus.count == 2)
e0.destroy()
XCTAssert(!nexus.isValid(entity: e0))
XCTAssert(nexus.isValid(entity: e1))
XCTAssert(nexus.count == 1)
XCTAssert(nexus.freeEntities.count == 1)
let e2 = nexus.create(entity: "e2")
XCTAssert(!e0.isValid)
XCTAssert(e1.isValid)
XCTAssert(e2.isValid)
XCTAssert(nexus.count == 2)
XCTAssert(nexus.freeEntities.count == 0)
XCTAssert(!(e0 == e2))
XCTAssert(!(e0 === e2))
}
func testComponentCreation() {
let nexus: Nexus = Nexus()
XCTAssert(nexus.count == 0)
XCTAssert(nexus.freeEntities.isEmpty)
XCTAssert(nexus.componentsByType.isEmpty)
let e0: Entity = nexus.create(entity: "e0")
let p0 = Position(x: 1, y: 2)
let n0 = Name(name: "FirstName")
e0.add(p0)
e0.add(n0)
e0.assign(p0)
let rE1 = nexus.get(entity: e0.identifier)
XCTAssert(e0.isValid)
XCTAssert(e0.hasComponents)
XCTAssert(e0.numComponents == 1)
let rN0: Name = rE1!.component(Name.self)
let rP0: Position = rE1!.component(Position.self)
XCTAssert(rN0.name == "FirstName")
let rP0: Position = e0.component(Position.self)
XCTAssert(rP0.x == 1)
XCTAssert(rP0.y == 2)
}
let count = nexus.count(components: rE1!.identifier)
func testComponentDeletion() {
let nexus = Nexus()
let identifier: EntityIdentifier = nexus.create(entity: "e0").identifier
XCTAssert(count == 2)
let e0 = nexus.get(entity: identifier)!
XCTAssert(e0.numComponents == 0)
e0.remove(Position.self)
XCTAssert(e0.numComponents == 0)
let n0 = Name(name: "myName")
let p0 = Position(x: 99, y: 111)
e0.assign(n0)
XCTAssert(e0.numComponents == 1)
XCTAssert(e0.hasComponents)
e0.remove(Name.self)
XCTAssert(e0.numComponents == 0)
XCTAssert(!e0.hasComponents)
e0.assign(p0)
XCTAssert(e0.numComponents == 1)
XCTAssert(e0.hasComponents)
e0.remove(p0)
XCTAssert(e0.numComponents == 0)
XCTAssert(!e0.hasComponents)
e0.assign(n0)
e0.assign(p0)
XCTAssert(e0.numComponents == 2)
let (name, position) = e0.components(Name.self, Position.self)
XCTAssert(name.name == "myName")
XCTAssert(position.x == 99)
XCTAssert(position.y == 111)
e0.destroy()
XCTAssert(e0.numComponents == 0)
XCTAssert(!e0.isValid)
}
}