diff --git a/Sources/FirebladeECS/Entity.swift b/Sources/FirebladeECS/Entity.swift index 0bda847..59f11d6 100644 --- a/Sources/FirebladeECS/Entity.swift +++ b/Sources/FirebladeECS/Entity.swift @@ -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(_ component: C) -> Entity where C: Component { - nexus.add(component: component, to: identifier) + public final func assign(_ component: C) -> Entity where C: Component { + nexus.assign(component: component, to: identifier) return self } @discardableResult public static func += (lhs: Entity, rhs: C) -> Entity where C: Component { - return lhs.add(rhs) + return lhs.assign(rhs) } @discardableResult public static func << (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) } } diff --git a/Sources/FirebladeECS/EntityIdentifier.swift b/Sources/FirebladeECS/EntityIdentifier.swift index dd290e3..64ca9b9 100644 --- a/Sources/FirebladeECS/EntityIdentifier.swift +++ b/Sources/FirebladeECS/EntityIdentifier.swift @@ -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 diff --git a/Sources/FirebladeECS/Nexus+Component.swift b/Sources/FirebladeECS/Nexus+Component.swift index 85d461e..5f641f3 100644 --- a/Sources/FirebladeECS/Nexus+Component.swift +++ b/Sources/FirebladeECS/Nexus+Component.swift @@ -25,7 +25,7 @@ extension Nexus { } } - public func add(component: C, to entityId: EntityIdentifier) where C: Component { + public func assign(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 } diff --git a/Sources/FirebladeECS/Nexus+Entity.swift b/Sources/FirebladeECS/Nexus+Entity.swift index 818b4b5..ea5529f 100644 --- a/Sources/FirebladeECS/Nexus+Entity.swift +++ b/Sources/FirebladeECS/Nexus+Entity.swift @@ -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 diff --git a/Sources/FirebladeECS/Nexus.swift b/Sources/FirebladeECS/Nexus.swift index 097afc1..c7d84c9 100644 --- a/Sources/FirebladeECS/Nexus.swift +++ b/Sources/FirebladeECS/Nexus.swift @@ -30,11 +30,14 @@ public class Nexus { var componentIdsByEntityIdx: [EntityIndex: ComponentIdentifiers] + var freeEntities: ContiguousArray + public init() { entities = Entities() componentsByType = [:] componentIndexByEntityComponentHash = [:] componentIdsByEntityIdx = [:] + freeEntities = ContiguousArray() } @@ -46,6 +49,11 @@ extension Nexus { Log.debug(event) // TODO: implement } + + func report(_ message: String) { + Log.warn(message) + // TODO: implement + } } /* diff --git a/Tests/FirebladeECSTests/NexusTests.swift b/Tests/FirebladeECSTests/NexusTests.swift index 2b850d9..7ae123f 100644 --- a/Tests/FirebladeECSTests/NexusTests.swift +++ b/Tests/FirebladeECSTests/NexusTests.swift @@ -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) } + }