diff --git a/Sources/FirebladeECS/EntityIdentifierGenerator.swift b/Sources/FirebladeECS/EntityIdentifierGenerator.swift index 46fb50d..d4d9d3d 100644 --- a/Sources/FirebladeECS/EntityIdentifierGenerator.swift +++ b/Sources/FirebladeECS/EntityIdentifierGenerator.swift @@ -5,18 +5,23 @@ // Created by Christian Treffs on 26.06.20. // -/// An entity identifier generator provides new entity -/// identifiers on entity creation. -/// It also allows entity ids to be marked for re-use. -/// Entity identifiers must be unique. +/// **Entity Identifier Generator** +/// +/// An entity identifier generator provides new entity identifiers on entity creation. +/// It also allows entity ids to be marked as unused (to be re-usable). +/// +/// You should strive to keep entity ids tightly packed around `EntityIdentifier.Identifier.min` since it has an influence on the underlying memory layout. public protocol EntityIdentifierGenerator { - /// Initialize the generator with entity ids already in use. - /// - Parameter entityIds: The entity ids already in use. Default should be an empty array. - init(inUse entityIds: [EntityIdentifier]) + /// Initialize the generator providing entity ids to begin with when creating new entities. + /// + /// Entity ids provided should be passed to `nextId()` in last out order up until the collection is empty. + /// The default is an empty collection. + /// - Parameter initialEntityIds: The entity ids to start providing up until the collection is empty (in last out order). + init(startProviding initialEntityIds: EntityIds) where EntityIds: BidirectionalCollection, EntityIds.Element == EntityIdentifier /// Provides the next unused entity identifier. /// - /// The provided entity identifier is at least unique during runtime. + /// The provided entity identifier must be unique during runtime. func nextId() -> EntityIdentifier /// Marks the given entity identifier as free and ready for re-use. @@ -27,17 +32,35 @@ public protocol EntityIdentifierGenerator { } /// A default entity identifier generator implementation. +public typealias DefaultEntityIdGenerator = LinearIncrementingEntityIdGenerator + +/// **Linear incrementing entity id generator** /// -/// Provides entity ids starting at `0` incrementing until `UInt32.max`. -public struct DefaultEntityIdGenerator: EntityIdentifierGenerator { +/// This entity id generator creates linearly incrementing entity ids +/// unless an entity is marked as unused then the marked id is returned next in a FIFO order. +/// +/// Furthermore it respects order of entity ids on initialization, meaning the provided ids on initialization will be provided in order +/// until all are in use. After that the free entities start at the lowest available id increasing linearly skipping already in-use entity ids. +public struct LinearIncrementingEntityIdGenerator: EntityIdentifierGenerator { @usableFromInline final class Storage { @usableFromInline var stack: [EntityIdentifier.Identifier] @usableFromInline var count: Int { stack.count } @usableFromInline - init(inUse entityIds: [EntityIdentifier]) { - stack = entityIds.reversed().map(\.id) + init(startProviding initialEntityIds: EntityIds) where EntityIds: BidirectionalCollection, EntityIds.Element == EntityIdentifier { + let initialInUse: [EntityIdentifier.Identifier] = initialEntityIds.map { $0.id } + let maxInUseValue = initialInUse.max() ?? 0 + let inUseSet = Set(initialInUse) // a set of all eIds in use + let allSet = Set(0...maxInUseValue) // all eIds from 0 to including maxInUseValue + let freeSet = allSet.subtracting(inUseSet) // all "holes" / unused / free eIds + let initialFree = Array(freeSet).sorted().reversed() // order them to provide them linear increasing after all initially used are provided. + stack = initialFree + initialInUse + } + + @usableFromInline + init() { + stack = [0] } @usableFromInline @@ -57,25 +80,24 @@ public struct DefaultEntityIdGenerator: EntityIdentifierGenerator { } @usableFromInline let storage: Storage - @usableFromInline var count: Int { storage.count } + @inlinable + public init(startProviding initialEntityIds: EntityIds) where EntityIds: BidirectionalCollection, EntityIds.Element == EntityIdentifier { + self.storage = Storage(startProviding: initialEntityIds) + } + @inlinable public init() { - self.init(inUse: [EntityIdentifier(0)]) + self.storage = Storage() } - @inlinable - public init(inUse entityIds: [EntityIdentifier]) { - self.storage = Storage(inUse: entityIds) - } - - @inlinable + @inline(__always) public func nextId() -> EntityIdentifier { storage.nextId() } - @inlinable + @inline(__always) public func markUnused(entityId: EntityIdentifier) { storage.markUnused(entityId: entityId) } diff --git a/Tests/FirebladeECSTests/EntityIdGenTests.swift b/Tests/FirebladeECSTests/EntityIdGenTests.swift new file mode 100644 index 0000000..bab14de --- /dev/null +++ b/Tests/FirebladeECSTests/EntityIdGenTests.swift @@ -0,0 +1,76 @@ +// +// EntityIdGenTests.swift +// +// +// Created by Christian Treffs on 21.08.20. +// + +import FirebladeECS +import XCTest + +final class EntityIdGenTests: XCTestCase { + var gen: EntityIdentifierGenerator! + + override func setUp() { + super.setUp() + gen = DefaultEntityIdGenerator() + } + + func testGeneratorDefaultInit() { + XCTAssertEqual(gen.nextId(), 0) + } + + func testLinearIncrement() { + for i in 0..<1_000_000 { + XCTAssertEqual(gen.nextId(), EntityIdentifier(EntityIdentifier.Identifier(i))) + } + } + + func testGenerateWithInitialIds() { + let initialIds: [EntityIdentifier] = [2, 4, 11, 3, 0, 304] + gen = DefaultEntityIdGenerator(startProviding: initialIds) + + let generatedIds: [EntityIdentifier] = (0.. [XCTestCaseEntry] { testCase(ComponentIdentifierTests.__allTests__ComponentIdentifierTests), testCase(ComponentTests.__allTests__ComponentTests), testCase(EntityCreationTests.__allTests__EntityCreationTests), + testCase(EntityIdGenTests.__allTests__EntityIdGenTests), testCase(EntityTests.__allTests__EntityTests), testCase(Family1Tests.__allTests__Family1Tests), testCase(Family2Tests.__allTests__Family2Tests),