From a33281b1fa58f8f53323300f028c096a4ed482e3 Mon Sep 17 00:00:00 2001 From: Christian Treffs Date: Sat, 28 Oct 2017 13:55:02 +0200 Subject: [PATCH] Fix family assign/remove problem --- .../ContiguousComponentArray.swift | 62 +++++++++++++++++++ Sources/FirebladeECS/Entity.swift | 2 +- Sources/FirebladeECS/Family.swift | 2 +- Sources/FirebladeECS/Nexus+Component.swift | 48 ++++---------- Sources/FirebladeECS/Nexus+Family.swift | 24 ++----- Sources/FirebladeECS/Nexus.swift | 16 +---- Tests/FirebladeECSTests/FamilyTests.swift | 33 +++++++++- 7 files changed, 115 insertions(+), 72 deletions(-) create mode 100644 Sources/FirebladeECS/ContiguousComponentArray.swift diff --git a/Sources/FirebladeECS/ContiguousComponentArray.swift b/Sources/FirebladeECS/ContiguousComponentArray.swift new file mode 100644 index 0000000..fd8a659 --- /dev/null +++ b/Sources/FirebladeECS/ContiguousComponentArray.swift @@ -0,0 +1,62 @@ +// +// ContiguousComponentArray.swift +// FirebladeECS +// +// Created by Christian Treffs on 28.10.17. +// + +private let pow2: [Int] = [ 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576, 2097152, 4194304, 8388608, 16777216, 33554432, 67108864, 134217728, 268435456, 536870912, 1073741824, 2147483648, 4294967296 +] + +private func nearestToPow2(_ value: Int) -> Int { + let exp = (value.bitWidth-value.leadingZeroBitCount) + return pow2[exp] +} + +public class ContiguousComponentArray { + public typealias Element = Component + + private var _store: ContiguousArray + + public init(minEntityCount minCount: Int) { + let count = nearestToPow2(minCount) + _store = ContiguousArray(repeating: nil, count: count) + } + + public func insert(_ element: Element, at entityIdx: EntityIndex) { + + if needsToGrow(entityIdx) { + grow(to: entityIdx) + } + _store[entityIdx] = element + } + + public func has(_ entityIdx: EntityIndex) -> Bool { + if _store.count <= entityIdx { return false } + return _store[entityIdx] != nil + } + + public func get(at entityIdx: EntityIndex) -> Element? { + return _store[entityIdx] + } + + public func remove(at entityIdx: EntityIndex) { + return _store[entityIdx] = nil + } + + fileprivate func needsToGrow(_ entityId: EntityIndex) -> Bool { + return entityId > _store.count - 1 + } + + fileprivate func grow(to minIndex: Int) { + if minIndex >= _store.count { + let newCapacity: Int = nearestToPow2(minIndex) + let count: Int = newCapacity-_store.count + let nilElements: ContiguousArray = ContiguousArray.init(repeating: nil, count: count) + + _store.reserveCapacity(newCapacity) + _store.append(contentsOf: nilElements) + } + } + +} diff --git a/Sources/FirebladeECS/Entity.swift b/Sources/FirebladeECS/Entity.swift index e38f67e..39543fc 100644 --- a/Sources/FirebladeECS/Entity.swift +++ b/Sources/FirebladeECS/Entity.swift @@ -54,7 +54,7 @@ public extension Entity { } public final func has(_ uct: ComponentIdentifier) -> Bool { - return nexus.has(component: uct, entity: identifier) + return nexus.has(componentId: uct, entityIdx: identifier.index) } public final var hasComponents: Bool { diff --git a/Sources/FirebladeECS/Family.swift b/Sources/FirebladeECS/Family.swift index a56dabf..411246e 100644 --- a/Sources/FirebladeECS/Family.swift +++ b/Sources/FirebladeECS/Family.swift @@ -43,7 +43,7 @@ extension Family { /*public var members: LazyMapCollection>, Entity> { return nexus.members(of: self) }*/ - internal var memberIds: EntityIds { + internal var memberIds: EntityIdSet { return nexus.members(of: self) } } diff --git a/Sources/FirebladeECS/Nexus+Component.swift b/Sources/FirebladeECS/Nexus+Component.swift index 8c245a9..e5d8580 100644 --- a/Sources/FirebladeECS/Nexus+Component.swift +++ b/Sources/FirebladeECS/Nexus+Component.swift @@ -7,13 +7,9 @@ extension Nexus { - public func has(component: ComponentIdentifier, entity: EntityIdentifier) -> Bool { - let hash: EntityComponentHash = component.hashValue(using: entity.index) - return has(hash) - } - - fileprivate func has(_ hash: EntityComponentHash) -> Bool { - return componentIndexByEntityComponentHash[hash] != nil + public func has(componentId: ComponentIdentifier, entityIdx: EntityIndex) -> Bool { + guard let uniforms = componentsByType[componentId] else { return false } + return uniforms.has(entityIdx) } public func count(components entityId: EntityIdentifier) -> Int { @@ -30,18 +26,16 @@ extension Nexus { let entityIdx = entity.identifier.index let hash: EntityComponentHash = componentId.hashValue(using: entityIdx) /// test if component is already assigned - guard !has(hash) else { + guard !has(componentId: componentId, entityIdx: entityIdx) else { report("ComponentAdd collision: \(entityIdx) already has a component \(component)") // TODO: replace component?! copy properties?! return } - var newComponentIndex: ComponentIndex = ComponentIndex.invalid if componentsByType[componentId] != nil { - newComponentIndex = componentsByType[componentId]!.count // TODO: get next free index - componentsByType[componentId]!.append(component) // Amortized O(1) + componentsByType[componentId]!.insert(component, at: entityIdx) } else { - newComponentIndex = 0 - componentsByType[componentId] = UniformComponents(arrayLiteral: component) + componentsByType[componentId] = UniformComponents(minEntityCount: entities.count) + componentsByType[componentId]!.insert(component, at: entityIdx) } // assigns the component id to the entity id @@ -54,9 +48,6 @@ extension Nexus { componentIdsByEntityLookup[hash] = 0 } - // assign entity / component to index - componentIndexByEntityComponentHash[hash] = newComponentIndex - // FIXME: this is costly for many families let entityId: EntityIdentifier = entity.identifier for (_, family) in familiyByTraitHash { @@ -71,22 +62,18 @@ extension Nexus { } public func get(component componentId: ComponentIdentifier, for entityId: EntityIdentifier) -> Component? { - let hash: EntityComponentHash = componentId.hashValue(using: entityId.index) - guard let componentIdx: ComponentIndex = componentIndexByEntityComponentHash[hash] else { return nil } guard let uniformComponents: UniformComponents = componentsByType[componentId] else { return nil } - return uniformComponents[componentIdx] + return uniformComponents.get(at: entityId.index) } public func get(for entityId: EntityIdentifier) -> C? where C: Component { let componentId: ComponentIdentifier = C.identifier - let hash: EntityComponentHash = componentId.hashValue(using: entityId) - return get(componentId: componentId, hash: hash) + return get(componentId: componentId, entityIdx: entityId.index) } - fileprivate func get(componentId: ComponentIdentifier, hash: EntityComponentHash) -> C? where C: Component { - guard let componentIdx: ComponentIndex = componentIndexByEntityComponentHash[hash] else { return nil } + fileprivate func get(componentId: ComponentIdentifier, entityIdx: EntityIndex) -> C? where C: Component { guard let uniformComponents: UniformComponents = componentsByType[componentId] else { return nil } - return uniformComponents[componentIdx] as? C + return uniformComponents.get(at: entityIdx) as? C } public func get(components entityId: EntityIdentifier) -> ComponentIdentifiers? { @@ -98,20 +85,10 @@ extension Nexus { let hash: EntityComponentHash = componentId.hashValue(using: entityId.index) // MARK: delete component instance - guard let componentIdx: ComponentIndex = componentIndexByEntityComponentHash.removeValue(forKey: hash) else { - 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 - } + componentsByType[componentId]?.remove(at: entityId.index) // MARK: unassign component guard let removeIndex: ComponentIdsByEntityIndex = componentIdsByEntityLookup.removeValue(forKey: hash) else { - assert(false, "ComponentRemove failure: no component found to be removed") report("ComponentRemove failure: no component found to be removed") return false } @@ -127,6 +104,7 @@ extension Nexus { // FIXME: may be expensive but is cheap for small entities for (index, compId) in remainingComponents.enumerated() { let cHash: EntityComponentHash = compId.hashValue(using: entityId.index) + assert(cHash != hash) componentIdsByEntityLookup[cHash] = index } } diff --git a/Sources/FirebladeECS/Nexus+Family.swift b/Sources/FirebladeECS/Nexus+Family.swift index 455e87b..988fa42 100644 --- a/Sources/FirebladeECS/Nexus+Family.swift +++ b/Sources/FirebladeECS/Nexus+Family.swift @@ -39,7 +39,7 @@ extension Nexus { return family.traits.isMatch(components: componentSet) } - public func members(of family: Family) -> EntityIds { + public func members(of family: Family) -> EntityIdSet { let traitHash: FamilyTraitSetHash = family.traits.hashValue return familyMembersByTraitHash[traitHash] ?? [] // FIXME: fail? } @@ -99,22 +99,13 @@ extension Nexus { } } - // TODO: move - fileprivate func calculateTrash(traitHash: FamilyTraitSetHash, entityId: EntityIdentifier) -> TraitEntityIdHash { - return hash(combine: traitHash, entityId.index) - } - fileprivate func add(to family: Family, entityId: EntityIdentifier) { let traitHash: FamilyTraitSetHash = family.traits.hashValue - let trash: TraitEntityIdHash = calculateTrash(traitHash: traitHash, entityId: entityId) if familyMembersByTraitHash[traitHash] != nil { - let newIndex: Int = familyMembersByTraitHash[traitHash]!.count - trashMap[trash] = newIndex - familyMembersByTraitHash[traitHash]!.append(entityId) + familyMembersByTraitHash[traitHash]?.insert(entityId) } else { - familyMembersByTraitHash[traitHash] = EntityIds(arrayLiteral: entityId) - trashMap[trash] = 0 + familyMembersByTraitHash[traitHash] = EntityIdSet(arrayLiteral: entityId) } notify(FamilyMemberAdded(member: entityId, to: family.traits)) @@ -122,15 +113,8 @@ extension Nexus { fileprivate func remove(from family: Family, entityId: EntityIdentifier) { let traitHash: FamilyTraitSetHash = family.traits.hashValue - let trash: TraitEntityIdHash = calculateTrash(traitHash: traitHash, entityId: entityId) - guard let index: EntityIdInFamilyIndex = trashMap[trash] else { - assert(false, "removing entity id \(entityId) that is not in family \(family)") - report("removing entity id \(entityId) that is not in family \(family)") - return - } - - guard let removed: EntityIdentifier = familyMembersByTraitHash[traitHash]?.remove(at: index) else { + guard let removed: EntityIdentifier = familyMembersByTraitHash[traitHash]?.remove(entityId) else { assert(false, "removing entity id \(entityId) that is not in family \(family)") report("removing entity id \(entityId) that is not in family \(family)") return diff --git a/Sources/FirebladeECS/Nexus.swift b/Sources/FirebladeECS/Nexus.swift index 599873b..6079fb7 100644 --- a/Sources/FirebladeECS/Nexus.swift +++ b/Sources/FirebladeECS/Nexus.swift @@ -7,18 +7,13 @@ /// entity id ^ component identifier hash public typealias EntityComponentHash = Int -public typealias ComponentIndex = Int -public extension ComponentIndex { - static let invalid: ComponentIndex = Int.min -} public typealias ComponentIdsByEntityIndex = Int public typealias ComponentTypeHash = Int // component object identifier hash value -public typealias UniformComponents = ContiguousArray +public typealias UniformComponents = ContiguousComponentArray public typealias ComponentIdentifiers = ContiguousArray public typealias ComponentSet = Set public typealias Entities = ContiguousArray -public typealias EntityIds = ContiguousArray public typealias EntityIdSet = Set public typealias FamilyTraitSetHash = Int public typealias TraitEntityIdHash = Int @@ -35,10 +30,6 @@ public class Nexus { /// - Value: each element is a component instance of the same type (uniform). New component instances are appended. var componentsByType: [ComponentIdentifier: UniformComponents] - /// - Key: 'entity id' - 'component type' hash that uniquely links both - /// - Value: each element is an index pointing to the component instance index in the componentsByType map. - var componentIndexByEntityComponentHash: [EntityComponentHash: ComponentIndex] - /// - Key: entity id as index /// - Value: each element is a component identifier associated with this entity var componentIdsByEntity: [EntityIndex: ComponentIdentifiers] @@ -51,19 +42,16 @@ public class Nexus { var freeEntities: ContiguousArray var familiyByTraitHash: [FamilyTraitSetHash: Family] - var trashMap: TraitEntityIdHashSet - var familyMembersByTraitHash: [FamilyTraitSetHash: EntityIds] + var familyMembersByTraitHash: [FamilyTraitSetHash: EntityIdSet] public init() { entities = Entities() componentsByType = [:] - componentIndexByEntityComponentHash = [:] componentIdsByEntity = [:] componentIdsByEntityLookup = [:] freeEntities = ContiguousArray() familiyByTraitHash = [:] familyMembersByTraitHash = [:] - trashMap = [:] } } diff --git a/Tests/FirebladeECSTests/FamilyTests.swift b/Tests/FirebladeECSTests/FamilyTests.swift index d59212a..5a98caf 100644 --- a/Tests/FirebladeECSTests/FamilyTests.swift +++ b/Tests/FirebladeECSTests/FamilyTests.swift @@ -56,7 +56,7 @@ class FamilyTests: XCTestCase { let isMatch = nexus.family(requiresAll: [Position.self, Velocity.self], excludesAll: [Party.self], needsAtLeastOne: [Name.self, EmptyComponent.self]) measure { - for _ in 0..<1_000_000 { + for _ in 0..<10_000 { let success = isMatch.canBecomeMember(a) XCTAssert(success) } @@ -131,4 +131,35 @@ class FamilyTests: XCTestCase { } + func testFamilyExchange() { + let nexus = Nexus() + let number: Int = 10 + + for i in 0..