Add entity reuse concept;
Add tests for entity creation/deletion & component creation/deletion
This commit is contained in:
parent
4e0522aa49
commit
bf62fde5db
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue