diff --git a/README.md b/README.md index 3e6d878..346ad23 100644 --- a/README.md +++ b/README.md @@ -66,24 +66,41 @@ let nexus = Nexus() then create entities by letting the `Nexus` generate them. ```swift -let myEntity = nexus.createEntity() +// an entity without components +let newEntity = nexus.createEntity() ``` -You can define `Components` like this +To define components conform your class to the `Component` protocol ```swift -class Movement: Component { - var position: (x: Double, y: Double) = (0.0, 1.0) - var velocity: Double = 0.1 +final class Position: Component { + var x: Int = 0 + var y: Int = 0 } ``` -and assign instances of them to an `Entity` with +and assign instances of it to an `Entity` with ```swift -let movement = Movement() -myEntity.assign(movement) +let position = Position(x: 1, y: 2) +entity.assign(position) ``` +You can be more efficient by assigning components while creating an entity. + +```swift +// an entity with two components assigned. +nexus.createEntity { + Position(x: 1, y: 2) + Color(.red) +} + +// bulk create entities with multiple components assigned. +nexus.createEntities(count: 100) { _ in + Position() + Color() +} + +``` ### 👪 Families This ECS uses a grouping approach for entities with the same component types to optimize cache locality and ease up access to them. @@ -186,7 +203,7 @@ class GameLogicSystem { ``` -### 👫 Relatives +### 👫 Relatives (deprecated) This ECS implementation provides an integrated way of creating a [directed acyclic graph (DAG)](https://en.wikipedia.org/wiki/Directed_acyclic_graph) hierarchy of entities by forming parent-child relationships. Entities can become children of a parent entity. In family terms they become **relatives**. Families provide iteration over these relationships. The entity hierarchy implementation does not use an additional component therefore keeping the hierarchy intact over different component-families. diff --git a/Sources/FirebladeECS/Entity.swift b/Sources/FirebladeECS/Entity.swift index 2b31389..89fcd6d 100644 --- a/Sources/FirebladeECS/Entity.swift +++ b/Sources/FirebladeECS/Entity.swift @@ -105,6 +105,7 @@ public struct Entity { /// Add an entity as child. /// - Parameter entity: The child entity. @discardableResult + @available(*, deprecated, message: "This will be removed in the next minor update.") public func addChild(_ entity: Entity) -> Bool { nexus.addChild(entity, to: self) } @@ -112,16 +113,19 @@ public struct Entity { /// Remove entity as child. /// - Parameter entity: The child entity. @discardableResult + @available(*, deprecated, message: "This will be removed in the next minor update.") public func removeChild(_ entity: Entity) -> Bool { nexus.removeChild(entity, from: self) } /// Removes all children from this entity. + @available(*, deprecated, message: "This will be removed in the next minor update.") public func removeAllChildren() { nexus.removeAllChildren(from: self) } /// Returns the number of children for this entity. + @available(*, deprecated, message: "This will be removed in the next minor update.") public var numChildren: Int { nexus.numChildren(for: self) } diff --git a/Sources/FirebladeECS/EntityIdentifier.swift b/Sources/FirebladeECS/EntityIdentifier.swift index 04ca439..03b8e79 100644 --- a/Sources/FirebladeECS/EntityIdentifier.swift +++ b/Sources/FirebladeECS/EntityIdentifier.swift @@ -8,14 +8,14 @@ public struct EntityIdentifier { static let invalid = EntityIdentifier(.max) - public typealias Id = Int + public typealias Idx = Int /// provides 4294967295 unique identifiers since it's constrained to UInt32 - invalid. - @usableFromInline let id: Id + @usableFromInline let id: Idx @usableFromInline init(_ uint32: UInt32) { - self.id = Id(uint32) + self.id = Idx(uint32) } } diff --git a/Sources/FirebladeECS/Family+Coding.swift b/Sources/FirebladeECS/Family+Coding.swift index 6dadf68..f6b6047 100644 --- a/Sources/FirebladeECS/Family+Coding.swift +++ b/Sources/FirebladeECS/Family+Coding.swift @@ -10,7 +10,7 @@ private struct FamilyMemberContainer where R: FamilyRequirementsManaging { } extension CodingUserInfoKey { - fileprivate static let nexusCodingStrategy = CodingUserInfoKey(rawValue: "nexusCodingStrategy")! + static let nexusCodingStrategy = CodingUserInfoKey(rawValue: "nexusCodingStrategy").unsafelyUnwrapped } // MARK: - encoding @@ -43,7 +43,7 @@ extension Family where R: FamilyEncoding { /// - Returns: The encoded data. public func encodeMembers(using encoder: inout Encoder) throws -> Encoder.Output where Encoder: TopLevelEncoder { encoder.userInfo[.nexusCodingStrategy] = nexus.codingStrategy - let components: [R.Components] = self.map { $0 } + let components = [R.Components](self) let container = FamilyMemberContainer(components: components) return try encoder.encode(container) } diff --git a/Sources/FirebladeECS/Family.swift b/Sources/FirebladeECS/Family.swift index 6310d3c..e154dc9 100644 --- a/Sources/FirebladeECS/Family.swift +++ b/Sources/FirebladeECS/Family.swift @@ -17,7 +17,7 @@ public struct Family where R: FamilyRequirementsManaging { nexus.onFamilyInit(traits: traits) } - @inlinable public var memberIds: UnorderedSparseSet { + @inlinable public var memberIds: UnorderedSparseSet { nexus.members(withFamilyTraits: traits) } @@ -58,7 +58,7 @@ extension Family: LazySequenceProtocol { } // MARK: - components iterator extension Family { public struct ComponentsIterator: IteratorProtocol { - @usableFromInline var memberIdsIterator: UnorderedSparseSet.ElementIterator + @usableFromInline var memberIdsIterator: UnorderedSparseSet.ElementIterator @usableFromInline unowned let nexus: Nexus public init(family: Family) { @@ -85,7 +85,7 @@ extension Family { } public struct EntityIterator: IteratorProtocol { - @usableFromInline var memberIdsIterator: UnorderedSparseSet.ElementIterator + @usableFromInline var memberIdsIterator: UnorderedSparseSet.ElementIterator @usableFromInline unowned let nexus: Nexus public init(family: Family) { @@ -111,7 +111,7 @@ extension Family { } public struct EntityComponentIterator: IteratorProtocol { - @usableFromInline var memberIdsIterator: UnorderedSparseSet.ElementIterator + @usableFromInline var memberIdsIterator: UnorderedSparseSet.ElementIterator @usableFromInline unowned let nexus: Nexus public init(family: Family) { diff --git a/Sources/FirebladeECS/Nexus+Component.swift b/Sources/FirebladeECS/Nexus+Component.swift index 11da234..4635a97 100644 --- a/Sources/FirebladeECS/Nexus+Component.swift +++ b/Sources/FirebladeECS/Nexus+Component.swift @@ -22,31 +22,9 @@ extension Nexus { } public final func assign(component: Component, to entity: Entity) { - let componentId: ComponentIdentifier = component.identifier let entityId: EntityIdentifier = entity.identifier - - // test if component is already assigned - guard !has(componentId: componentId, entityId: entityId) else { - delegate?.nexusNonFatalError("ComponentAdd collision: \(entityId) already has a component \(component)") - assertionFailure("ComponentAdd collision: \(entityId) already has a component \(component)") - return - } - - // add component instances to uniform component stores - if componentsByType[componentId] == nil { - componentsByType[componentId] = ManagedContiguousArray() - } - componentsByType[componentId]?.insert(component, at: entityId.id) - - // assigns the component id to the entity id - if componentIdsByEntity[entityId] == nil { - componentIdsByEntity[entityId] = Set() - } - componentIdsByEntity[entityId]?.insert(componentId) //, at: componentId.hashValue) - - update(familyMembership: entityId) - - delegate?.nexusEvent(ComponentAdded(component: componentId, toEntity: entity.identifier)) + assign(component: component, entityId: entityId) + delegate?.nexusEvent(ComponentAdded(component: component.identifier, toEntity: entity.identifier)) } public final func assign(component: C, to entity: Entity) where C: Component { diff --git a/Sources/FirebladeECS/Nexus+ComponentsBuilder.swift b/Sources/FirebladeECS/Nexus+ComponentsBuilder.swift new file mode 100644 index 0000000..2eb3929 --- /dev/null +++ b/Sources/FirebladeECS/Nexus+ComponentsBuilder.swift @@ -0,0 +1,91 @@ +// +// Nexus+ComponentsBuilder.swift +// +// +// Created by Christian Treffs on 30.07.20. +// + +@_functionBuilder +public enum ComponentsBuilderPreview { } +public typealias ComponentsBuilder = ComponentsBuilderPreview + +extension ComponentsBuilder { + public static func buildBlock(_ components: Component...) -> [Component] { + components + } + + public struct Context { + /// The index of the newly created entity. + /// + /// This is **NOT** equal to the entity identifier. + public let index: Int + } +} + +extension Nexus { + /// Create an entity assigning one component. + /// + /// Usage: + /// ``` + /// let newEntity = nexus.createEntity { + /// Position(x: 1, y: 2) + /// } + /// ``` + /// - Parameter builder: The component builder. + /// - Returns: The newly created entity with the provided component assigned. + @discardableResult + public func createEntity(@ComponentsBuilder using builder: () -> Component) -> Entity { + self.createEntity(with: builder()) + } + + /// Create an entity assigning multiple components. + /// + /// Usage: + /// ``` + /// let newEntity = nexus.createEntity { + /// Position(x: 1, y: 2) + /// Name(name: "Some name") + /// } + /// ``` + /// - Parameter builder: The component builder. + /// - Returns: The newly created entity with the provided components assigned. + @discardableResult + public func createEntity(@ComponentsBuilder using builder: () -> [Component]) -> Entity { + self.createEntity(with: builder()) + } + + /// Create multiple entities assigning one component each. + /// + /// Usage: + /// ``` + /// let newEntities = nexus.createEntities(count: 100) { ctx in + /// Velocity(a: Float(ctx.index)) + /// } + /// ``` + /// - Parameters: + /// - count: The count of entities to create. + /// - builder: The component builder providing context. + /// - Returns: The newly created entities with the provided component assigned. + @discardableResult + public func createEntities(count: Int, @ComponentsBuilder using builder: (ComponentsBuilder.Context) -> Component) -> [Entity] { + (0.. [Component]) -> [Entity] { + (0.. Entity { let newEntity = createEntity() - components.forEach { newEntity.assign($0) } + assign(components: components, to: newEntity.identifier) return newEntity } + @discardableResult + public func createEntity(with components: C) -> Entity where C: Collection, C.Element == Component { + let entity = self.createEntity() + assign(components: components, to: entity.identifier) + return entity + } + /// Number of entities in nexus. public var numEntities: Int { entityStorage.count diff --git a/Sources/FirebladeECS/Nexus+Family.swift b/Sources/FirebladeECS/Nexus+Family.swift index 95f3b30..f4357b2 100644 --- a/Sources/FirebladeECS/Nexus+Family.swift +++ b/Sources/FirebladeECS/Nexus+Family.swift @@ -18,8 +18,8 @@ extension Nexus { return traits.isMatch(components: componentIds) } - public func members(withFamilyTraits traits: FamilyTraitSet) -> UnorderedSparseSet { - familyMembersByTraits[traits] ?? UnorderedSparseSet() + public func members(withFamilyTraits traits: FamilyTraitSet) -> UnorderedSparseSet { + familyMembersByTraits[traits] ?? UnorderedSparseSet() } public func isMember(_ entity: Entity, in family: FamilyTraitSet) -> Bool { diff --git a/Sources/FirebladeECS/Nexus+FamilyUpdate.swift b/Sources/FirebladeECS/Nexus+FamilyUpdate.swift deleted file mode 100644 index a669077..0000000 --- a/Sources/FirebladeECS/Nexus+FamilyUpdate.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// Nexus+FamilyUpdate.swift -// FirebladeECS -// -// Created by Christian Treffs on 14.02.19. -// - -extension Nexus { - /// will be called on family init - final func onFamilyInit(traits: FamilyTraitSet) { - guard familyMembersByTraits[traits] == nil else { - return - } - - familyMembersByTraits[traits] = UnorderedSparseSet() - update(familyMembership: traits) - } - - final func update(familyMembership traits: FamilyTraitSet) { - // FIXME: iterating all entities is costly for many entities - var iter = entityStorage.makeIterator() - while let entityId = iter.next() { - update(membership: traits, for: entityId) - } - } - - final func update(familyMembership entityId: EntityIdentifier) { - // FIXME: iterating all families is costly for many families - var iter = familyMembersByTraits.keys.makeIterator() - while let traits = iter.next() { - update(membership: traits, for: entityId) - } - } - - final func update(membership traits: FamilyTraitSet, for entityId: EntityIdentifier) { - guard let componentIds = componentIdsByEntity[entityId] else { - // no components - so skip - return - } - - let isMember: Bool = self.isMember(entity: entityId, inFamilyWithTraits: traits) - if !exists(entity: entityId) && isMember { - remove(entityWithId: entityId, fromFamilyWithTraits: traits) - return - } - - let isMatch: Bool = traits.isMatch(components: componentIds) - - switch (isMatch, isMember) { - case (true, false): - add(entityWithId: entityId, toFamilyWithTraits: traits) - delegate?.nexusEvent(FamilyMemberAdded(member: entityId, toFamily: traits)) - - case (false, true): - remove(entityWithId: entityId, fromFamilyWithTraits: traits) - delegate?.nexusEvent(FamilyMemberRemoved(member: entityId, from: traits)) - - default: - break - } - } - - final func add(entityWithId entityId: EntityIdentifier, toFamilyWithTraits traits: FamilyTraitSet) { - familyMembersByTraits[traits]!.insert(entityId, at: entityId.id) - } - - final func remove(entityWithId entityId: EntityIdentifier, fromFamilyWithTraits traits: FamilyTraitSet) { - familyMembersByTraits[traits]!.remove(at: entityId.id) - } -} diff --git a/Sources/FirebladeECS/Nexus+Internal.swift b/Sources/FirebladeECS/Nexus+Internal.swift new file mode 100644 index 0000000..d0e570d --- /dev/null +++ b/Sources/FirebladeECS/Nexus+Internal.swift @@ -0,0 +1,136 @@ +// +// Nexus+Internal.swift +// FirebladeECS +// +// Created by Christian Treffs on 14.02.19. +// + +extension Nexus { + func assign(components: C, to entityId: EntityIdentifier) where C: Collection, C.Element == Component { + var iter = components.makeIterator() + while let component = iter.next() { + let componentId = component.identifier + // test if component is already assigned + guard !has(componentId: componentId, entityId: entityId) else { + delegate?.nexusNonFatalError("ComponentAdd collision: \(entityId) already has a component \(component)") + assertionFailure("ComponentAdd collision: \(entityId) already has a component \(component)") + return + } + + // add component instances to uniform component stores + insertComponentInstance(component, componentId, entityId) + + // assigns the component id to the entity id + assign(componentId, entityId) + } + + // Update entity membership + update(familyMembership: entityId) + } + + func assign(component: Component, entityId: EntityIdentifier) { + let componentId = component.identifier + + // test if component is already assigned + guard !has(componentId: componentId, entityId: entityId) else { + delegate?.nexusNonFatalError("ComponentAdd collision: \(entityId) already has a component \(component)") + assertionFailure("ComponentAdd collision: \(entityId) already has a component \(component)") + return + } + + // add component instances to uniform component stores + insertComponentInstance(component, componentId, entityId) + + // assigns the component id to the entity id + assign(componentId, entityId) + + // Update entity membership + update(familyMembership: entityId) + } + + func insertComponentInstance(_ component: Component, _ componentId: ComponentIdentifier, _ entityId: EntityIdentifier) { + if componentsByType[componentId] == nil { + componentsByType[componentId] = ManagedContiguousArray() + } + componentsByType[componentId]?.insert(component, at: entityId.id) + } + + 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) + } + } + + func update(familyMembership entityId: EntityIdentifier) { + // FIXME: iterating all families is costly for many families + // FIXME: this could be parallelized + var iter = familyMembersByTraits.keys.makeIterator() + while let traits = iter.next() { + update(membership: traits, for: entityId) + } + } + + /// will be called on family init + func onFamilyInit(traits: FamilyTraitSet) { + guard familyMembersByTraits[traits] == nil else { + return + } + + familyMembersByTraits[traits] = UnorderedSparseSet() + update(familyMembership: traits) + } + + func update(familyMembership traits: FamilyTraitSet) { + // FIXME: iterating all entities is costly for many entities + var iter = entityStorage.makeIterator() + while let entityId = iter.next() { + update(membership: traits, for: entityId) + } + } + + func update(membership traits: FamilyTraitSet, for entityId: EntityIdentifier) { + guard let componentIds = componentIdsByEntity[entityId] else { + // no components - so skip + return + } + + let isMember: Bool = self.isMember(entity: entityId, inFamilyWithTraits: traits) + if !exists(entity: entityId) && isMember { + remove(entityWithId: entityId, fromFamilyWithTraits: traits) + return + } + + let isMatch: Bool = traits.isMatch(components: componentIds) + + switch (isMatch, isMember) { + case (true, false): + add(entityWithId: entityId, toFamilyWithTraits: traits) + delegate?.nexusEvent(FamilyMemberAdded(member: entityId, toFamily: traits)) + + case (false, true): + remove(entityWithId: entityId, fromFamilyWithTraits: traits) + delegate?.nexusEvent(FamilyMemberRemoved(member: entityId, from: traits)) + + default: + break + } + } + + func add(entityWithId entityId: EntityIdentifier, toFamilyWithTraits traits: FamilyTraitSet) { + familyMembersByTraits[traits].unsafelyUnwrapped.insert(entityId, at: entityId.id) + } + + func remove(entityWithId entityId: EntityIdentifier, fromFamilyWithTraits traits: FamilyTraitSet) { + familyMembersByTraits[traits].unsafelyUnwrapped.remove(at: entityId.id) + } +} diff --git a/Sources/FirebladeECS/Nexus+SceneGraph.swift b/Sources/FirebladeECS/Nexus+SceneGraph.swift index d806b18..0e2f914 100644 --- a/Sources/FirebladeECS/Nexus+SceneGraph.swift +++ b/Sources/FirebladeECS/Nexus+SceneGraph.swift @@ -6,6 +6,7 @@ // extension Nexus { + @available(*, deprecated, message: "This will be removed in the next minor update.") public final func addChild(_ child: Entity, to parent: Entity) -> Bool { let inserted: Bool if childrenByParentEntity[parent.identifier] == nil { @@ -21,10 +22,12 @@ extension Nexus { return inserted } + @available(*, deprecated, message: "This will be removed in the next minor update.") public final func removeChild(_ child: Entity, from parent: Entity) -> Bool { removeChild(child.identifier, from: parent.identifier) } + @available(*, deprecated, message: "This will be removed in the next minor update.") @discardableResult public final func removeChild(_ child: EntityIdentifier, from parent: EntityIdentifier) -> Bool { let removed: Bool = childrenByParentEntity[parent]?.remove(child) != nil @@ -34,15 +37,18 @@ extension Nexus { return removed } + @available(*, deprecated, message: "This will be removed in the next minor update.") public final func removeAllChildren(from parent: Entity) { self.removeAllChildren(from: parent.identifier) } + @available(*, deprecated, message: "This will be removed in the next minor update.") public final func removeAllChildren(from parentId: EntityIdentifier) { childrenByParentEntity[parentId]?.forEach { removeChild($0, from: parentId) } return childrenByParentEntity[parentId] = nil } + @available(*, deprecated, message: "This will be removed in the next minor update.") public final func numChildren(for entity: Entity) -> Int { childrenByParentEntity[entity.identifier]?.count ?? 0 } diff --git a/Sources/FirebladeECS/Nexus.swift b/Sources/FirebladeECS/Nexus.swift index 306e4d4..d40da02 100644 --- a/Sources/FirebladeECS/Nexus.swift +++ b/Sources/FirebladeECS/Nexus.swift @@ -8,7 +8,7 @@ public final class Nexus { /// Main entity storage. /// Entities are tightly packed by EntityIdentifier. - @usableFromInline final var entityStorage: UnorderedSparseSet + @usableFromInline final var entityStorage: UnorderedSparseSet /// Entity ids that are currently not used. let entityIdGenerator: EntityIdentifierGenerator @@ -29,14 +29,14 @@ public final class Nexus { /// - Key: FamilyTraitSet aka component types that make up one distinct family. /// - Value: Tightly packed EntityIdentifiers that represent the association of an entity to the family. - @usableFromInline final var familyMembersByTraits: [FamilyTraitSet: UnorderedSparseSet] + @usableFromInline final var familyMembersByTraits: [FamilyTraitSet: UnorderedSparseSet] public final var codingStrategy: CodingStrategy public final weak var delegate: NexusEventDelegate? public convenience init() { - self.init(entityStorage: UnorderedSparseSet(), + self.init(entityStorage: UnorderedSparseSet(), componentsByType: [:], componentsByEntity: [:], entityIdGenerator: EntityIdentifierGenerator(), @@ -45,11 +45,11 @@ public final class Nexus { codingStrategy: DefaultCodingStrategy()) } - internal init(entityStorage: UnorderedSparseSet, + internal init(entityStorage: UnorderedSparseSet, componentsByType: [ComponentIdentifier: ManagedContiguousArray], componentsByEntity: [EntityIdentifier: Set], entityIdGenerator: EntityIdentifierGenerator, - familyMembersByTraits: [FamilyTraitSet: UnorderedSparseSet], + familyMembersByTraits: [FamilyTraitSet: UnorderedSparseSet], childrenByParentEntity: [EntityIdentifier: Set], codingStrategy: CodingStrategy) { self.entityStorage = entityStorage @@ -87,6 +87,6 @@ public struct DefaultCodingStrategy: CodingStrategy { public init() { } public func codingKey(for componentType: C.Type) -> DynamicCodingKey where C: Component { - DynamicCodingKey(stringValue: "\(C.self)")! + DynamicCodingKey(stringValue: "\(C.self)").unsafelyUnwrapped } } diff --git a/Sources/FirebladeECS/UnorderedSparseSet.swift b/Sources/FirebladeECS/UnorderedSparseSet.swift index 74b3d61..046f43c 100644 --- a/Sources/FirebladeECS/UnorderedSparseSet.swift +++ b/Sources/FirebladeECS/UnorderedSparseSet.swift @@ -13,38 +13,143 @@ /// /// See for a reference implementation. public struct UnorderedSparseSet { - /// An index into the dense store. - public typealias DenseIndex = Int + // swiftlint:disable nesting + @usableFromInline + final class Storage { + /// An index into the dense store. + @usableFromInline + typealias DenseIndex = Int - /// A sparse store holding indices into the dense mapped to key. - public typealias SparseStore = [Key: DenseIndex] + /// A sparse store holding indices into the dense mapped to key. + @usableFromInline + typealias SparseStore = [Key: DenseIndex] - /// A dense store holding all the entries. - public typealias DenseStore = ContiguousArray + /// A dense store holding all the entries. + @usableFromInline + typealias DenseStore = ContiguousArray - public struct Entry { - public let key: Key - public let element: Element + @usableFromInline + struct Entry { + @usableFromInline let key: Key + @usableFromInline let element: Element + + @usableFromInline + init(key: Key, element: Element) { + self.key = key + self.element = element + } + } + + @usableFromInline var dense: DenseStore + @usableFromInline var sparse: SparseStore + + @usableFromInline + init(sparse: SparseStore, dense: DenseStore) { + self.sparse = sparse + self.dense = dense + } + + @usableFromInline + convenience init() { + self.init(sparse: [:], dense: []) + } + + @usableFromInline var count: Int { dense.count } + @usableFromInline var isEmpty: Bool { dense.isEmpty } + + @inlinable var first: Element? { + dense.first?.element + } + + @inlinable + func findIndex(at key: Key) -> Int? { + guard let denseIndex = sparse[key], denseIndex < count else { + return nil + } + return denseIndex + } + + @inlinable + func findElement(at key: Key) -> Element? { + guard let denseIndex = findIndex(at: key) else { + return nil + } + let entry = self.dense[denseIndex] + guard entry.key == key else { + return nil + } + + return entry.element + } + + @inlinable + func insert(_ element: Element, at key: Key) -> Bool { + if let denseIndex = findIndex(at: key) { + dense[denseIndex] = Entry(key: key, element: element) + return false + } + + let nIndex = dense.count + dense.append(Entry(key: key, element: element)) + sparse.updateValue(nIndex, forKey: key) + return true + } + + @inlinable + func remove(at key: Key) -> Entry? { + guard let denseIndex = findIndex(at: key) else { + return nil + } + + let removed = swapRemove(at: denseIndex) + if !dense.isEmpty && denseIndex < dense.count { + let swappedElement = dense[denseIndex] + sparse[swappedElement.key] = denseIndex + } + sparse[key] = nil + return removed + } + + /// Removes an element from the set and returns it in O(1). + /// The removed element is replaced with the last element of the set. + /// + /// - Parameter denseIndex: the dense index + /// - Returns: the element entry + @inlinable + func swapRemove(at denseIndex: Int) -> Entry { + dense.swapAt(denseIndex, dense.count - 1) + return dense.removeLast() + } + + @inlinable + func removeAll(keepingCapacity: Bool = false) { + sparse.removeAll(keepingCapacity: keepingCapacity) + dense.removeAll(keepingCapacity: keepingCapacity) + } + + @inlinable + func makeIterator() -> IndexingIterator> { + dense.makeIterator() + } } - @usableFromInline var dense: DenseStore - @usableFromInline var sparse: SparseStore - public init() { - self.init(sparse: [:], dense: []) + self.init(storage: Storage()) } - init(sparse: SparseStore, dense: DenseStore) { - self.sparse = sparse - self.dense = dense + @usableFromInline + init(storage: Storage) { + self.storage = storage } - public var count: Int { dense.count } - public var isEmpty: Bool { dense.isEmpty } + @usableFromInline let storage: Storage + + public var count: Int { storage.count } + public var isEmpty: Bool { storage.isEmpty } @inlinable public func contains(_ key: Key) -> Bool { - find(at: key) != nil + storage.findIndex(at: key) != nil } /// Inset an element for a given key into the set in O(1). @@ -55,16 +160,8 @@ public struct UnorderedSparseSet { /// - key: the key /// - Returns: true if new, false if replaced. @discardableResult - public mutating func insert(_ element: Element, at key: Key) -> Bool { - if let (denseIndex, _) = find(at: key) { - dense[denseIndex] = Entry(key: key, element: element) - return false - } - - let nIndex = dense.count - dense.append(Entry(key: key, element: element)) - sparse[key] = nIndex - return true + public func insert(_ element: Element, at key: Key) -> Bool { + storage.insert(element, at: key) } /// Get the element for the given key in O(1). @@ -73,16 +170,12 @@ public struct UnorderedSparseSet { /// - Returns: the element or nil of key not found. @inlinable public func get(at key: Key) -> Element? { - guard let (_, element) = find(at: key) else { - return nil - } - - return element + storage.findElement(at: key) } @inlinable public func get(unsafeAt key: Key) -> Element { - find(at: key).unsafelyUnwrapped.1 + storage.findElement(at: key).unsafelyUnwrapped } /// Removes the element entry for given key in O(1). @@ -90,83 +183,45 @@ public struct UnorderedSparseSet { /// - Parameter key: the key /// - Returns: removed value or nil if key not found. @discardableResult - public mutating func remove(at key: Key) -> Entry? { - guard let (denseIndex, _) = find(at: key) else { - return nil - } - - let removed = swapRemove(at: denseIndex) - if !dense.isEmpty && denseIndex < dense.count { - let swappedElement = dense[denseIndex] - sparse[swappedElement.key] = denseIndex - } - sparse[key] = nil - return removed + public func remove(at key: Key) -> Element? { + storage.remove(at: key)?.element } @inlinable - public mutating func removeAll(keepingCapacity: Bool = false) { - sparse.removeAll(keepingCapacity: keepingCapacity) - dense.removeAll(keepingCapacity: keepingCapacity) - } - - /// Removes an element from the set and returns it in O(1). - /// The removed element is replaced with the last element of the set. - /// - /// - Parameter denseIndex: the dense index - /// - Returns: the element entry - private mutating func swapRemove(at denseIndex: Int) -> Entry { - dense.swapAt(denseIndex, dense.count - 1) - return dense.removeLast() - } - - @inlinable - public func find(at key: Key) -> (Int, Element)? { - guard let denseIndex = sparse[key], denseIndex < count else { - return nil - } - let entry = self.dense[denseIndex] - guard entry.key == key else { - return nil - } - - return (denseIndex, entry.element) + public func removeAll(keepingCapacity: Bool = false) { + storage.removeAll(keepingCapacity: keepingCapacity) } @inlinable public var first: Element? { - dense.first?.element - } - - @inlinable public var last: Element? { - dense.last?.element + storage.first } } extension UnorderedSparseSet where Key == Int { @inlinable - public subscript(position: DenseIndex) -> Element { + public subscript(key: Key) -> Element { get { - get(unsafeAt: position) + get(unsafeAt: key) } - set(newValue) { - insert(newValue, at: position) + nonmutating set(newValue) { + insert(newValue, at: key) } } } // MARK: - Sequence extension UnorderedSparseSet: Sequence { - public __consuming func makeIterator() -> ElementIterator { + public func makeIterator() -> ElementIterator { ElementIterator(self) } // MARK: - UnorderedSparseSetIterator public struct ElementIterator: IteratorProtocol { - public private(set) var iterator: IndexingIterator.Entry>> + var iterator: IndexingIterator> public init(_ sparseSet: UnorderedSparseSet) { - iterator = sparseSet.dense.makeIterator() + iterator = sparseSet.storage.makeIterator() } public mutating func next() -> Element? { @@ -176,13 +231,20 @@ extension UnorderedSparseSet: Sequence { } // MARK: - Equatable -extension UnorderedSparseSet.Entry: Equatable where Element: Equatable { } +extension UnorderedSparseSet.Storage.Entry: Equatable where Element: Equatable { } +extension UnorderedSparseSet.Storage: Equatable where Element: Equatable { + @usableFromInline + static func == (lhs: UnorderedSparseSet.Storage, rhs: UnorderedSparseSet.Storage) -> Bool { + lhs.dense == rhs.dense && lhs.sparse == rhs.sparse + } +} extension UnorderedSparseSet: Equatable where Element: Equatable { public static func == (lhs: UnorderedSparseSet, rhs: UnorderedSparseSet) -> Bool { - lhs.dense == rhs.dense && lhs.sparse == rhs.sparse + lhs.storage == rhs.storage } } // MARK: - Codable -extension UnorderedSparseSet.Entry: Codable where Element: Codable { } +extension UnorderedSparseSet.Storage.Entry: Codable where Element: Codable { } +extension UnorderedSparseSet.Storage: Codable where Element: Codable { } extension UnorderedSparseSet: Codable where Element: Codable { } diff --git a/Tests/FirebladeECSTests/EntityCreationTests.swift b/Tests/FirebladeECSTests/EntityCreationTests.swift new file mode 100644 index 0000000..2184ac7 --- /dev/null +++ b/Tests/FirebladeECSTests/EntityCreationTests.swift @@ -0,0 +1,80 @@ +// +// EntityCreationTests.swift +// +// +// Created by Christian Treffs on 30.07.20. +// + +import FirebladeECS +import XCTest + +final class EntityCreationTests: XCTestCase { + + func testCreateEntityOneComponent() throws { + let nexus = Nexus() + let entity = nexus.createEntity { + Position(x: 1, y: 2) + } + + XCTAssertEqual(entity[\Position.x], 1) + XCTAssertEqual(entity[\Position.y], 2) + + XCTAssertEqual(nexus.numEntities, 1) + XCTAssertEqual(nexus.numComponents, 1) + XCTAssertEqual(nexus.numFamilies, 0) + } + + func testCreateEntityMultipleComponents() throws { + let nexus = Nexus() + + let entity = nexus.createEntity { + Position(x: 1, y: 2) + Name(name: "Hello") + } + + XCTAssertEqual(entity[\Position.x], 1) + XCTAssertEqual(entity[\Position.y], 2) + + XCTAssertEqual(entity[\Name.name], "Hello") + + XCTAssertEqual(nexus.numEntities, 1) + XCTAssertEqual(nexus.numComponents, 2) + XCTAssertEqual(nexus.numFamilies, 0) + } + + func testBulkCreateEntitiesOneComponent() throws { + let nexus = Nexus() + + let entities = nexus.createEntities(count: 100) { ctx in + Velocity(a: Float(ctx.index)) + } + + XCTAssertEqual(entities[0][\Velocity.a], 0) + XCTAssertEqual(entities[99][\Velocity.a], 99) + + XCTAssertEqual(nexus.numEntities, 100) + XCTAssertEqual(nexus.numComponents, 100) + XCTAssertEqual(nexus.numFamilies, 0) + } + + func testBulkCreateEntitiesMultipleComponents() throws { + let nexus = Nexus() + + let entities = nexus.createEntities(count: 100) { ctx in + Position(x: ctx.index, y: ctx.index) + Name(name: "\(ctx.index)") + } + + XCTAssertEqual(entities[0][\Position.x], 0) + XCTAssertEqual(entities[0][\Position.y], 0) + XCTAssertEqual(entities[0][\Name.name], "0") + XCTAssertEqual(entities[99][\Position.x], 99) + XCTAssertEqual(entities[99][\Position.y], 99) + XCTAssertEqual(entities[99][\Name.name], "99") + + XCTAssertEqual(nexus.numEntities, 100) + XCTAssertEqual(nexus.numComponents, 200) + XCTAssertEqual(nexus.numFamilies, 0) + } + +} diff --git a/Tests/FirebladeECSTests/SceneGraphTests.swift b/Tests/FirebladeECSTests/SceneGraphTests.swift index 7e9021e..8a8c530 100644 --- a/Tests/FirebladeECSTests/SceneGraphTests.swift +++ b/Tests/FirebladeECSTests/SceneGraphTests.swift @@ -8,6 +8,7 @@ import XCTest import FirebladeECS +@available(*, deprecated, message: "This will be removed in the next minor update.") class SceneGraphTests: XCTestCase { var nexus: Nexus! diff --git a/Tests/FirebladeECSTests/SparseSetTests.swift b/Tests/FirebladeECSTests/SparseSetTests.swift index f02f4ec..ad0dde4 100644 --- a/Tests/FirebladeECSTests/SparseSetTests.swift +++ b/Tests/FirebladeECSTests/SparseSetTests.swift @@ -79,13 +79,13 @@ class SparseSetTests: XCTestCase { for idx in 0..() + let set = UnorderedSparseSet() let a = AClass() let b = AClass() set.insert(a, at: 0) set.insert(b, at: 1) - XCTAssertEqual(set.sparse.count, 2) - XCTAssertEqual(set.dense.count, 2) + XCTAssertEqual(set.storage.sparse.count, 2) + XCTAssertEqual(set.storage.dense.count, 2) XCTAssertEqual(set.count, 2) @@ -406,14 +406,14 @@ class SparseSetTests: XCTestCase { XCTAssertNotNil(set.remove(at: 1)) XCTAssertEqual(set.count, 1) - XCTAssertEqual(set.sparse.count, 1) - XCTAssertEqual(set.dense.count, 1) + XCTAssertEqual(set.storage.sparse.count, 1) + XCTAssertEqual(set.storage.dense.count, 1) XCTAssertNil(set.remove(at: 1)) XCTAssertEqual(set.count, 1) - XCTAssertEqual(set.sparse.count, 1) - XCTAssertEqual(set.dense.count, 1) + XCTAssertEqual(set.storage.sparse.count, 1) + XCTAssertEqual(set.storage.dense.count, 1) XCTAssertTrue(set.get(at: 0) === a) @@ -441,7 +441,7 @@ class SparseSetTests: XCTestCase { while let idx = indices.popFirst() { let entry = set.remove(at: idx)! - XCTAssertEqual(entry.key, idx) + XCTAssertEqual(entry.x, idx) recurseValueTest() XCTAssertEqual(set.count, indices.count) } @@ -471,7 +471,7 @@ class SparseSetTests: XCTestCase { } func testSparseSetReduce() { - var characters = UnorderedSparseSet() + let characters = UnorderedSparseSet() characters.insert("H", at: 4) characters.insert("e", at: 13) @@ -488,7 +488,7 @@ class SparseSetTests: XCTestCase { XCTAssertEqual(characters.count, 11) - let string: String = characters.dense.reduce("") { res, char in + let string: String = characters.storage.dense.reduce("") { res, char in res + "\(char.element)" } @@ -497,7 +497,7 @@ class SparseSetTests: XCTestCase { } func testSubscript() { - var characters = UnorderedSparseSet() + let characters = UnorderedSparseSet() characters[4] = "H" characters[13] = "e" @@ -528,32 +528,47 @@ class SparseSetTests: XCTestCase { } func testStartEndIndex() { - var set = UnorderedSparseSet() + let set = UnorderedSparseSet() set.insert("C", at: 33) set.insert("A", at: 11) set.insert("B", at: 22) - let mapped = set.dense.map { $0.element } + let mapped = set.storage.dense.map { $0.element } XCTAssertEqual(mapped, ["C", "A", "B"]) } func testAlternativeKey() { - var set = UnorderedSparseSet() + let set = UnorderedSparseSet() set.insert("A", at: "a") set.insert("C", at: "c") set.insert("B", at: "b") - let mapped = set.dense.map { $0.element } + let mapped = set.storage.dense.map { $0.element } XCTAssertEqual(mapped, ["A", "C", "B"]) - let keyValues = set.sparse.sorted(by: { $0.value < $1.value }).map { ($0.key, $0.value) } + let keyValues = set.storage.sparse.sorted(by: { $0.value < $1.value }).map { ($0.key, $0.value) } for (a, b) in zip(keyValues, [("a", 0), ("c", 1), ("b", 2)]) { XCTAssertEqual(a.0, b.0) XCTAssertEqual(a.1, b.1) } } + + func testEquality() { + + let setA = UnorderedSparseSet() + let setB = UnorderedSparseSet() + + setA.insert(3, at: "Hello") + setB.insert(3, at: "Hello") + + XCTAssertEqual(setA, setB) + + setB.insert(4, at: "World") + + XCTAssertNotEqual(setA, setB) + } } diff --git a/Tests/FirebladeECSTests/XCTestManifests.swift b/Tests/FirebladeECSTests/XCTestManifests.swift index 2a2b454..14f13cf 100644 --- a/Tests/FirebladeECSTests/XCTestManifests.swift +++ b/Tests/FirebladeECSTests/XCTestManifests.swift @@ -20,6 +20,18 @@ extension ComponentTests { ] } +extension EntityCreationTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__EntityCreationTests = [ + ("testBulkCreateEntitiesMultipleComponents", testBulkCreateEntitiesMultipleComponents), + ("testBulkCreateEntitiesOneComponent", testBulkCreateEntitiesOneComponent), + ("testCreateEntityMultipleComponents", testCreateEntityMultipleComponents), + ("testCreateEntityOneComponent", testCreateEntityOneComponent) + ] +} + extension EntityTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -134,6 +146,7 @@ extension SparseSetTests { // to regenerate. static let __allTests__SparseSetTests = [ ("testAlternativeKey", testAlternativeKey), + ("testEquality", testEquality), ("testSparseSetAdd", testSparseSetAdd), ("testSparseSetAddAndReplace", testSparseSetAddAndReplace), ("testSparseSetClear", testSparseSetClear), @@ -162,6 +175,7 @@ public func __allTests() -> [XCTestCaseEntry] { return [ testCase(ComponentIdentifierTests.__allTests__ComponentIdentifierTests), testCase(ComponentTests.__allTests__ComponentTests), + testCase(EntityCreationTests.__allTests__EntityCreationTests), testCase(EntityTests.__allTests__EntityTests), testCase(FamilyCodingTests.__allTests__FamilyCodingTests), testCase(FamilyTests.__allTests__FamilyTests),