diff --git a/README.md b/README.md index 7ab009f..9af39d3 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,11 @@ # Fireblade ECS (Entity-Component System) -[![github CI](https://github.com/fireblade-engine/ecs/workflows/CI/badge.svg)](https://github.com/fireblade-engine/ecs/actions?query=workflow%3ACI) [![license](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE) -[![swift version](https://img.shields.io/badge/swift-5.1+-brightgreen.svg)](https://swift.org) -[![platforms](https://img.shields.io/badge/platforms-%20macOS%20|%20iOS%20|%20tvOS%20|%20watchOS-brightgreen.svg)](#) -[![platforms](https://img.shields.io/badge/platforms-linux-brightgreen.svg)](#) -[![platforms](https://img.shields.io/badge/platforms-WebAssembly-brightgreen.svg)](https://github.com/swiftwasm/swift#swiftwasm) +[![github CI](https://github.com/fireblade-engine/ecs/workflows/CI/badge.svg)](https://github.com/fireblade-engine/ecs/actions?query=workflow%3ACI) [![codecov](https://codecov.io/gh/fireblade-engine/ecs/branch/master/graph/badge.svg)](https://codecov.io/gh/fireblade-engine/ecs) -[![documentation](https://github.com/fireblade-engine/ecs/workflows/Documentation/badge.svg)](https://github.com/fireblade-engine/ecs/wiki) +[![documentation](https://github.com/fireblade-engine/ecs/workflows/Documentation/badge.svg)](https://github.com/fireblade-engine/ecs/wiki) +[![spi-swift-versions](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Ffireblade-engine%2Fecs%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/fireblade-engine/ecs) +[![spi-swift-platforms](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Ffireblade-engine%2Fecs%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/fireblade-engine/ecs) +[![platform-webassembly](https://img.shields.io/badge/Platform-WebAssembly-blue.svg)](https://github.com/swiftwasm/swift#swiftwasm) This is a **dependency free**, **lightweight**, **fast** and **easy to use** [Entity-Component System](https://en.wikipedia.org/wiki/Entity_component_system) implementation in Swift. It is developed and maintained as part of the [Fireblade Game Engine project](https://github.com/fireblade-engine). @@ -36,7 +35,7 @@ import PackageDescription let package = Package( name: "YourPackageName", dependencies: [ - .package(url: "https://github.com/fireblade-engine/ecs.git", from: "0.15.4") + .package(url: "https://github.com/fireblade-engine/ecs.git", from: "0.16.0") ], targets: [ .target( diff --git a/Sources/FirebladeECS/ComponentIdentifier.swift b/Sources/FirebladeECS/ComponentIdentifier.swift index c1c4bde..72fc9e5 100644 --- a/Sources/FirebladeECS/ComponentIdentifier.swift +++ b/Sources/FirebladeECS/ComponentIdentifier.swift @@ -7,22 +7,18 @@ /// Identifies a component by it's meta type public struct ComponentIdentifier { - @usableFromInline - typealias Hash = Int - @usableFromInline - typealias StableId = UInt64 - - @usableFromInline let hash: Hash + public typealias Identifier = Int + public let id: Identifier } extension ComponentIdentifier { @usableFromInline init(_ componentType: C.Type) where C: Component { - self.hash = Self.makeRuntimeHash(componentType) + self.id = Self.makeRuntimeHash(componentType) } /// object identifier hash (only stable during runtime) - arbitrary hash is ok. - internal static func makeRuntimeHash(_ componentType: C.Type) -> Hash where C: Component { + internal static func makeRuntimeHash(_ componentType: C.Type) -> Identifier where C: Component { ObjectIdentifier(componentType).hashValue } } diff --git a/Sources/FirebladeECS/EntityIdentifierGenerator.swift b/Sources/FirebladeECS/EntityIdentifierGenerator.swift index 8a58673..1c9a035 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,55 +32,71 @@ 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: [UInt32] + @usableFromInline var stack: [EntityIdentifier.Identifier] @usableFromInline var count: Int { stack.count } @usableFromInline - init(inUse entityIds: [EntityIdentifier]) { - stack = entityIds.reversed().map { UInt32($0.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 func nextId() -> EntityIdentifier { - if stack.count == 1 { - defer { stack[0] += 1 } - return EntityIdentifier(stack[0]) - } else { + guard stack.count == 1 else { return EntityIdentifier(stack.removeLast()) } + defer { stack[0] += 1 } + return EntityIdentifier(stack[0]) } @usableFromInline func markUnused(entityId: EntityIdentifier) { - stack.append(UInt32(entityId.id)) + stack.append(entityId.id) } } @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/Sources/FirebladeECS/Family.swift b/Sources/FirebladeECS/Family.swift index ca0804a..c97289e 100644 --- a/Sources/FirebladeECS/Family.swift +++ b/Sources/FirebladeECS/Family.swift @@ -114,7 +114,7 @@ extension Family { guard let entityId = memberIdsIterator.next() else { return nil } - return nexus.get(unsafeEntity: entityId) + return Entity(nexus: nexus, id: entityId) } } } diff --git a/Sources/FirebladeECS/Generated/Family.generated.swift b/Sources/FirebladeECS/Generated/Family.generated.swift index a5ae33c..a128a3b 100644 --- a/Sources/FirebladeECS/Generated/Family.generated.swift +++ b/Sources/FirebladeECS/Generated/Family.generated.swift @@ -28,7 +28,7 @@ public struct Requires1: FamilyRequirementsManaging where Comp1: Componen } public static func entityAndComponents(nexus: Nexus, entityId: EntityIdentifier) -> (Entity, Comp1) { - let entity: Entity = nexus.get(unsafeEntity: entityId) + let entity = Entity(nexus: nexus, id: entityId) let comp1: Comp1 = nexus.get(unsafeComponentFor: entityId) return (entity, comp1) } @@ -122,7 +122,7 @@ public struct Requires2: FamilyRequirementsManaging where Comp1: C } public static func entityAndComponents(nexus: Nexus, entityId: EntityIdentifier) -> (Entity, Comp1, Comp2) { - let entity: Entity = nexus.get(unsafeEntity: entityId) + let entity = Entity(nexus: nexus, id: entityId) let comp1: Comp1 = nexus.get(unsafeComponentFor: entityId) let comp2: Comp2 = nexus.get(unsafeComponentFor: entityId) return (entity, comp1, comp2) @@ -222,7 +222,7 @@ public struct Requires3: FamilyRequirementsManaging where C } public static func entityAndComponents(nexus: Nexus, entityId: EntityIdentifier) -> (Entity, Comp1, Comp2, Comp3) { - let entity: Entity = nexus.get(unsafeEntity: entityId) + let entity = Entity(nexus: nexus, id: entityId) let comp1: Comp1 = nexus.get(unsafeComponentFor: entityId) let comp2: Comp2 = nexus.get(unsafeComponentFor: entityId) let comp3: Comp3 = nexus.get(unsafeComponentFor: entityId) @@ -328,7 +328,7 @@ public struct Requires4: FamilyRequirementsManaging } public static func entityAndComponents(nexus: Nexus, entityId: EntityIdentifier) -> (Entity, Comp1, Comp2, Comp3, Comp4) { - let entity: Entity = nexus.get(unsafeEntity: entityId) + let entity = Entity(nexus: nexus, id: entityId) let comp1: Comp1 = nexus.get(unsafeComponentFor: entityId) let comp2: Comp2 = nexus.get(unsafeComponentFor: entityId) let comp3: Comp3 = nexus.get(unsafeComponentFor: entityId) @@ -440,7 +440,7 @@ public struct Requires5: FamilyRequirementsMa } public static func entityAndComponents(nexus: Nexus, entityId: EntityIdentifier) -> (Entity, Comp1, Comp2, Comp3, Comp4, Comp5) { - let entity: Entity = nexus.get(unsafeEntity: entityId) + let entity = Entity(nexus: nexus, id: entityId) let comp1: Comp1 = nexus.get(unsafeComponentFor: entityId) let comp2: Comp2 = nexus.get(unsafeComponentFor: entityId) let comp3: Comp3 = nexus.get(unsafeComponentFor: entityId) @@ -558,7 +558,7 @@ public struct Requires6: FamilyRequire } public static func entityAndComponents(nexus: Nexus, entityId: EntityIdentifier) -> (Entity, Comp1, Comp2, Comp3, Comp4, Comp5, Comp6) { - let entity: Entity = nexus.get(unsafeEntity: entityId) + let entity = Entity(nexus: nexus, id: entityId) let comp1: Comp1 = nexus.get(unsafeComponentFor: entityId) let comp2: Comp2 = nexus.get(unsafeComponentFor: entityId) let comp3: Comp3 = nexus.get(unsafeComponentFor: entityId) @@ -682,7 +682,7 @@ public struct Requires7: Family } public static func entityAndComponents(nexus: Nexus, entityId: EntityIdentifier) -> (Entity, Comp1, Comp2, Comp3, Comp4, Comp5, Comp6, Comp7) { - let entity: Entity = nexus.get(unsafeEntity: entityId) + let entity = Entity(nexus: nexus, id: entityId) let comp1: Comp1 = nexus.get(unsafeComponentFor: entityId) let comp2: Comp2 = nexus.get(unsafeComponentFor: entityId) let comp3: Comp3 = nexus.get(unsafeComponentFor: entityId) @@ -812,7 +812,7 @@ public struct Requires8: } public static func entityAndComponents(nexus: Nexus, entityId: EntityIdentifier) -> (Entity, Comp1, Comp2, Comp3, Comp4, Comp5, Comp6, Comp7, Comp8) { - let entity: Entity = nexus.get(unsafeEntity: entityId) + let entity = Entity(nexus: nexus, id: entityId) let comp1: Comp1 = nexus.get(unsafeComponentFor: entityId) let comp2: Comp2 = nexus.get(unsafeComponentFor: entityId) let comp3: Comp3 = nexus.get(unsafeComponentFor: entityId) diff --git a/Sources/FirebladeECS/Hashing.swift b/Sources/FirebladeECS/Hashing.swift index f9d4f2e..d319029 100644 --- a/Sources/FirebladeECS/Hashing.swift +++ b/Sources/FirebladeECS/Hashing.swift @@ -45,15 +45,6 @@ public func hash(combine seed: Int, _ value: Int) -> Int { return Int(bitPattern: uSeed) } -/// Calculates the combined hash value of the elements. This implementation is based on boost::hash_range. -/// Is sensitive to the order of the elements. -/// - Parameter hashValues: sequence of hash values to combine. -/// - Returns: combined hash value. -public func hash(combine hashValues: S) -> Int where S.Element == Int { - /// http://www.boost.org/doc/libs/1_65_1/doc/html/hash/reference.html#boost.hash_range_idp517643120 - hashValues.reduce(0) { hash(combine: $0, $1) } -} - /// Calculates the combined hash value of the elements. This implementation is based on boost::hash_range. /// Is sensitive to the order of the elements. /// - Parameter hashValues: sequence of hash values to combine. diff --git a/Sources/FirebladeECS/Nexus+Entity.swift b/Sources/FirebladeECS/Nexus+Entity.swift index a8279e8..772da3e 100644 --- a/Sources/FirebladeECS/Nexus+Entity.swift +++ b/Sources/FirebladeECS/Nexus+Entity.swift @@ -9,7 +9,7 @@ extension Nexus { @discardableResult public func createEntity() -> Entity { let entityId: EntityIdentifier = entityIdGenerator.nextId() - entityStorage.insert(entityId, at: entityId.id) + componentIdsByEntity[entityId] = [] delegate?.nexusEvent(EntityCreated(entityId: entityId)) return Entity(nexus: self, id: entityId) } @@ -30,22 +30,15 @@ extension Nexus { /// Number of entities in nexus. public var numEntities: Int { - entityStorage.count + componentIdsByEntity.keys.count } public func exists(entity entityId: EntityIdentifier) -> Bool { - entityStorage.contains(entityId.id) + componentIdsByEntity.keys.contains(entityId) } - public func get(entity entityId: EntityIdentifier) -> Entity? { - guard let id = entityStorage.get(at: entityId.id) else { - return nil - } - return Entity(nexus: self, id: id) - } - - public func get(unsafeEntity entityId: EntityIdentifier) -> Entity { - Entity(nexus: self, id: entityStorage.get(unsafeAt: entityId.id)) + public func entity(from entityId: EntityIdentifier) -> Entity { + Entity(nexus: self, id: entityId) } @discardableResult @@ -55,7 +48,7 @@ extension Nexus { @discardableResult public func destroy(entityId: EntityIdentifier) -> Bool { - guard entityStorage.remove(at: entityId.id) != nil else { + guard componentIdsByEntity.keys.contains(entityId) else { delegate?.nexusNonFatalError("EntityRemove failure: no entity \(entityId) to remove") return false } @@ -64,6 +57,10 @@ extension Nexus { update(familyMembership: entityId) } + if let index = componentIdsByEntity.index(forKey: entityId) { + componentIdsByEntity.remove(at: index) + } + entityIdGenerator.markUnused(entityId: entityId) delegate?.nexusEvent(EntityDestroyed(entityId: entityId)) diff --git a/Sources/FirebladeECS/Nexus+Internal.swift b/Sources/FirebladeECS/Nexus+Internal.swift index 43bd5d2..ff6d7a5 100644 --- a/Sources/FirebladeECS/Nexus+Internal.swift +++ b/Sources/FirebladeECS/Nexus+Internal.swift @@ -56,19 +56,7 @@ extension Nexus { } func assign(_ componentId: ComponentIdentifier, _ entityId: EntityIdentifier) { - if componentIdsByEntity[entityId] == nil { - componentIdsByEntity[entityId] = Set(arrayLiteral: componentId) - } else { - componentIdsByEntity[entityId]?.insert(componentId) - } - } - - func assign(_ componentIds: Set, _ entityId: EntityIdentifier) { - if componentIdsByEntity[entityId] == nil { - componentIdsByEntity[entityId] = componentIds - } else { - componentIdsByEntity[entityId]?.formUnion(componentIds) - } + componentIdsByEntity[entityId]!.insert(componentId) } func update(familyMembership entityId: EntityIdentifier) { @@ -92,7 +80,7 @@ extension Nexus { func update(familyMembership traits: FamilyTraitSet) { // FIXME: iterating all entities is costly for many entities - var iter = entityStorage.makeIterator() + var iter = componentIdsByEntity.keys.makeIterator() while let entityId = iter.next() { update(membership: traits, for: entityId) } diff --git a/Sources/FirebladeECS/Nexus.swift b/Sources/FirebladeECS/Nexus.swift index 948a7a5..8f7c7f3 100644 --- a/Sources/FirebladeECS/Nexus.swift +++ b/Sources/FirebladeECS/Nexus.swift @@ -6,10 +6,6 @@ // public final class Nexus { - /// Main entity storage. - /// Entities are tightly packed by EntityIdentifier. - @usableFromInline final var entityStorage: UnorderedSparseSet - /// - Key: ComponentIdentifier aka component type. /// - Value: Array of component instances of same type (uniform). /// New component instances are appended. @@ -39,21 +35,18 @@ public final class Nexus { public final weak var delegate: NexusEventDelegate? public convenience init() { - self.init(entityStorage: UnorderedSparseSet(), - componentsByType: [:], + self.init(componentsByType: [:], componentsByEntity: [:], entityIdGenerator: DefaultEntityIdGenerator(), familyMembersByTraits: [:], codingStrategy: DefaultCodingStrategy()) } - internal init(entityStorage: UnorderedSparseSet, - componentsByType: [ComponentIdentifier: ManagedContiguousArray], + internal init(componentsByType: [ComponentIdentifier: ManagedContiguousArray], componentsByEntity: [EntityIdentifier: Set], entityIdGenerator: EntityIdentifierGenerator, familyMembersByTraits: [FamilyTraitSet: UnorderedSparseSet], codingStrategy: CodingStrategy) { - self.entityStorage = entityStorage self.componentsByType = componentsByType self.componentIdsByEntity = componentsByEntity self.familyMembersByTraits = familyMembersByTraits @@ -66,8 +59,6 @@ public final class Nexus { } public final func clear() { - entityStorage.forEach { destroy(entityId: $0) } - entityStorage.removeAll() componentsByType.removeAll() componentIdsByEntity.removeAll() familyMembersByTraits.removeAll() diff --git a/Sources/FirebladeECS/Single.swift b/Sources/FirebladeECS/Single.swift index 88aeb55..399d1ef 100644 --- a/Sources/FirebladeECS/Single.swift +++ b/Sources/FirebladeECS/Single.swift @@ -32,7 +32,7 @@ extension Single where A: SingleComponent { } public var entity: Entity { - nexus.get(entity: entityId).unsafelyUnwrapped + Entity(nexus: self.nexus, id: entityId) } } diff --git a/Sources/FirebladeECS/Stencils/Family.stencil b/Sources/FirebladeECS/Stencils/Family.stencil index 252f388..548a093 100644 --- a/Sources/FirebladeECS/Stencils/Family.stencil +++ b/Sources/FirebladeECS/Stencils/Family.stencil @@ -52,7 +52,7 @@ public struct Requires{{ idx }}<{{ CompParams }}>: FamilyRequirementsManaging wh } public static func entityAndComponents(nexus: Nexus, entityId: EntityIdentifier) -> (Entity, {{ CompParams }}) { - let entity: Entity = nexus.get(unsafeEntity: entityId) + let entity: Entity = Entity(nexus: nexus, id: entityId) {% for comp in components %} let {{ comp|lowercase }}: {{ comp }} = nexus.get(unsafeComponentFor: entityId) {% endfor %} diff --git a/Sources/FirebladeECS/UnorderedSparseSet.swift b/Sources/FirebladeECS/UnorderedSparseSet.swift index 046f43c..bc9904c 100644 --- a/Sources/FirebladeECS/UnorderedSparseSet.swift +++ b/Sources/FirebladeECS/UnorderedSparseSet.swift @@ -75,10 +75,7 @@ public struct UnorderedSparseSet { return nil } let entry = self.dense[denseIndex] - guard entry.key == key else { - return nil - } - + assert(entry.key == key, "entry.key and findIndex(at: key) must be equal!") return entry.element } diff --git a/Tests/FirebladeECSTests/EntityIdGenTests.swift b/Tests/FirebladeECSTests/EntityIdGenTests.swift new file mode 100644 index 0000000..d804f49 --- /dev/null +++ b/Tests/FirebladeECSTests/EntityIdGenTests.swift @@ -0,0 +1,82 @@ +// +// 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 testGeneratorWithDefaultEmptyCollection() { + gen = DefaultEntityIdGenerator(startProviding: []) + XCTAssertEqual(gen.nextId(), 0) + XCTAssertEqual(gen.nextId(), 1) + } + + 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),