Fix family assign/remove problem

This commit is contained in:
Christian Treffs 2017-10-28 13:55:02 +02:00
parent 8c38f76e5a
commit a33281b1fa
7 changed files with 115 additions and 72 deletions

View File

@ -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<Element?>
public init(minEntityCount minCount: Int) {
let count = nearestToPow2(minCount)
_store = ContiguousArray<Element?>(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<Element?> = ContiguousArray<Element?>.init(repeating: nil, count: count)
_store.reserveCapacity(newCapacity)
_store.append(contentsOf: nilElements)
}
}
}

View File

@ -54,7 +54,7 @@ public extension Entity {
} }
public final func has(_ uct: ComponentIdentifier) -> Bool { 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 { public final var hasComponents: Bool {

View File

@ -43,7 +43,7 @@ extension Family {
/*public var members: LazyMapCollection<LazyFilterCollection<LazyMapCollection<EntityIdSet, Entity?>>, Entity> { /*public var members: LazyMapCollection<LazyFilterCollection<LazyMapCollection<EntityIdSet, Entity?>>, Entity> {
return nexus.members(of: self) return nexus.members(of: self)
}*/ }*/
internal var memberIds: EntityIds { internal var memberIds: EntityIdSet {
return nexus.members(of: self) return nexus.members(of: self)
} }
} }

View File

@ -7,13 +7,9 @@
extension Nexus { extension Nexus {
public func has(component: ComponentIdentifier, entity: EntityIdentifier) -> Bool { public func has(componentId: ComponentIdentifier, entityIdx: EntityIndex) -> Bool {
let hash: EntityComponentHash = component.hashValue(using: entity.index) guard let uniforms = componentsByType[componentId] else { return false }
return has(hash) return uniforms.has(entityIdx)
}
fileprivate func has(_ hash: EntityComponentHash) -> Bool {
return componentIndexByEntityComponentHash[hash] != nil
} }
public func count(components entityId: EntityIdentifier) -> Int { public func count(components entityId: EntityIdentifier) -> Int {
@ -30,18 +26,16 @@ extension Nexus {
let entityIdx = entity.identifier.index let entityIdx = entity.identifier.index
let hash: EntityComponentHash = componentId.hashValue(using: entityIdx) let hash: EntityComponentHash = componentId.hashValue(using: entityIdx)
/// test if component is already assigned /// 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)") report("ComponentAdd collision: \(entityIdx) already has a component \(component)")
// TODO: replace component?! copy properties?! // TODO: replace component?! copy properties?!
return return
} }
var newComponentIndex: ComponentIndex = ComponentIndex.invalid
if componentsByType[componentId] != nil { if componentsByType[componentId] != nil {
newComponentIndex = componentsByType[componentId]!.count // TODO: get next free index componentsByType[componentId]!.insert(component, at: entityIdx)
componentsByType[componentId]!.append(component) // Amortized O(1)
} else { } else {
newComponentIndex = 0 componentsByType[componentId] = UniformComponents(minEntityCount: entities.count)
componentsByType[componentId] = UniformComponents(arrayLiteral: component) componentsByType[componentId]!.insert(component, at: entityIdx)
} }
// assigns the component id to the entity id // assigns the component id to the entity id
@ -54,9 +48,6 @@ extension Nexus {
componentIdsByEntityLookup[hash] = 0 componentIdsByEntityLookup[hash] = 0
} }
// assign entity / component to index
componentIndexByEntityComponentHash[hash] = newComponentIndex
// FIXME: this is costly for many families // FIXME: this is costly for many families
let entityId: EntityIdentifier = entity.identifier let entityId: EntityIdentifier = entity.identifier
for (_, family) in familiyByTraitHash { for (_, family) in familiyByTraitHash {
@ -71,22 +62,18 @@ extension Nexus {
} }
public func get(component componentId: ComponentIdentifier, for entityId: EntityIdentifier) -> Component? { 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 } guard let uniformComponents: UniformComponents = componentsByType[componentId] else { return nil }
return uniformComponents[componentIdx] return uniformComponents.get(at: entityId.index)
} }
public func get<C>(for entityId: EntityIdentifier) -> C? where C: Component { public func get<C>(for entityId: EntityIdentifier) -> C? where C: Component {
let componentId: ComponentIdentifier = C.identifier let componentId: ComponentIdentifier = C.identifier
let hash: EntityComponentHash = componentId.hashValue(using: entityId) return get(componentId: componentId, entityIdx: entityId.index)
return get(componentId: componentId, hash: hash)
} }
fileprivate func get<C>(componentId: ComponentIdentifier, hash: EntityComponentHash) -> C? where C: Component { fileprivate func get<C>(componentId: ComponentIdentifier, entityIdx: EntityIndex) -> C? where C: Component {
guard let componentIdx: ComponentIndex = componentIndexByEntityComponentHash[hash] else { return nil }
guard let uniformComponents: UniformComponents = componentsByType[componentId] else { return nil } 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? { public func get(components entityId: EntityIdentifier) -> ComponentIdentifiers? {
@ -98,20 +85,10 @@ extension Nexus {
let hash: EntityComponentHash = componentId.hashValue(using: entityId.index) let hash: EntityComponentHash = componentId.hashValue(using: entityId.index)
// MARK: delete component instance // MARK: delete component instance
guard let componentIdx: ComponentIndex = componentIndexByEntityComponentHash.removeValue(forKey: hash) else { componentsByType[componentId]?.remove(at: entityId.index)
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
}
// MARK: unassign component // MARK: unassign component
guard let removeIndex: ComponentIdsByEntityIndex = componentIdsByEntityLookup.removeValue(forKey: hash) else { 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") report("ComponentRemove failure: no component found to be removed")
return false return false
} }
@ -127,6 +104,7 @@ extension Nexus {
// FIXME: may be expensive but is cheap for small entities // FIXME: may be expensive but is cheap for small entities
for (index, compId) in remainingComponents.enumerated() { for (index, compId) in remainingComponents.enumerated() {
let cHash: EntityComponentHash = compId.hashValue(using: entityId.index) let cHash: EntityComponentHash = compId.hashValue(using: entityId.index)
assert(cHash != hash)
componentIdsByEntityLookup[cHash] = index componentIdsByEntityLookup[cHash] = index
} }
} }

View File

@ -39,7 +39,7 @@ extension Nexus {
return family.traits.isMatch(components: componentSet) 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 let traitHash: FamilyTraitSetHash = family.traits.hashValue
return familyMembersByTraitHash[traitHash] ?? [] // FIXME: fail? 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) { fileprivate func add(to family: Family, entityId: EntityIdentifier) {
let traitHash: FamilyTraitSetHash = family.traits.hashValue let traitHash: FamilyTraitSetHash = family.traits.hashValue
let trash: TraitEntityIdHash = calculateTrash(traitHash: traitHash, entityId: entityId)
if familyMembersByTraitHash[traitHash] != nil { if familyMembersByTraitHash[traitHash] != nil {
let newIndex: Int = familyMembersByTraitHash[traitHash]!.count familyMembersByTraitHash[traitHash]?.insert(entityId)
trashMap[trash] = newIndex
familyMembersByTraitHash[traitHash]!.append(entityId)
} else { } else {
familyMembersByTraitHash[traitHash] = EntityIds(arrayLiteral: entityId) familyMembersByTraitHash[traitHash] = EntityIdSet(arrayLiteral: entityId)
trashMap[trash] = 0
} }
notify(FamilyMemberAdded(member: entityId, to: family.traits)) notify(FamilyMemberAdded(member: entityId, to: family.traits))
@ -122,15 +113,8 @@ extension Nexus {
fileprivate func remove(from family: Family, entityId: EntityIdentifier) { fileprivate func remove(from family: Family, entityId: EntityIdentifier) {
let traitHash: FamilyTraitSetHash = family.traits.hashValue let traitHash: FamilyTraitSetHash = family.traits.hashValue
let trash: TraitEntityIdHash = calculateTrash(traitHash: traitHash, entityId: entityId)
guard let index: EntityIdInFamilyIndex = trashMap[trash] 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
}
guard let removed: EntityIdentifier = familyMembersByTraitHash[traitHash]?.remove(at: index) else {
assert(false, "removing entity id \(entityId) that is not in family \(family)") assert(false, "removing entity id \(entityId) that is not in family \(family)")
report("removing entity id \(entityId) that is not in family \(family)") report("removing entity id \(entityId) that is not in family \(family)")
return return

View File

@ -7,18 +7,13 @@
/// entity id ^ component identifier hash /// entity id ^ component identifier hash
public typealias EntityComponentHash = Int public typealias EntityComponentHash = Int
public typealias ComponentIndex = Int
public extension ComponentIndex {
static let invalid: ComponentIndex = Int.min
}
public typealias ComponentIdsByEntityIndex = Int public typealias ComponentIdsByEntityIndex = Int
public typealias ComponentTypeHash = Int // component object identifier hash value public typealias ComponentTypeHash = Int // component object identifier hash value
public typealias UniformComponents = ContiguousArray<Component> public typealias UniformComponents = ContiguousComponentArray
public typealias ComponentIdentifiers = ContiguousArray<ComponentIdentifier> public typealias ComponentIdentifiers = ContiguousArray<ComponentIdentifier>
public typealias ComponentSet = Set<ComponentIdentifier> public typealias ComponentSet = Set<ComponentIdentifier>
public typealias Entities = ContiguousArray<Entity> public typealias Entities = ContiguousArray<Entity>
public typealias EntityIds = ContiguousArray<EntityIdentifier>
public typealias EntityIdSet = Set<EntityIdentifier> public typealias EntityIdSet = Set<EntityIdentifier>
public typealias FamilyTraitSetHash = Int public typealias FamilyTraitSetHash = Int
public typealias TraitEntityIdHash = 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. /// - Value: each element is a component instance of the same type (uniform). New component instances are appended.
var componentsByType: [ComponentIdentifier: UniformComponents] 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 /// - Key: entity id as index
/// - Value: each element is a component identifier associated with this entity /// - Value: each element is a component identifier associated with this entity
var componentIdsByEntity: [EntityIndex: ComponentIdentifiers] var componentIdsByEntity: [EntityIndex: ComponentIdentifiers]
@ -51,19 +42,16 @@ public class Nexus {
var freeEntities: ContiguousArray<EntityIdentifier> var freeEntities: ContiguousArray<EntityIdentifier>
var familiyByTraitHash: [FamilyTraitSetHash: Family] var familiyByTraitHash: [FamilyTraitSetHash: Family]
var trashMap: TraitEntityIdHashSet var familyMembersByTraitHash: [FamilyTraitSetHash: EntityIdSet]
var familyMembersByTraitHash: [FamilyTraitSetHash: EntityIds]
public init() { public init() {
entities = Entities() entities = Entities()
componentsByType = [:] componentsByType = [:]
componentIndexByEntityComponentHash = [:]
componentIdsByEntity = [:] componentIdsByEntity = [:]
componentIdsByEntityLookup = [:] componentIdsByEntityLookup = [:]
freeEntities = ContiguousArray<EntityIdentifier>() freeEntities = ContiguousArray<EntityIdentifier>()
familiyByTraitHash = [:] familiyByTraitHash = [:]
familyMembersByTraitHash = [:] familyMembersByTraitHash = [:]
trashMap = [:]
} }
} }

View File

@ -56,7 +56,7 @@ class FamilyTests: XCTestCase {
let isMatch = nexus.family(requiresAll: [Position.self, Velocity.self], excludesAll: [Party.self], needsAtLeastOne: [Name.self, EmptyComponent.self]) let isMatch = nexus.family(requiresAll: [Position.self, Velocity.self], excludesAll: [Party.self], needsAtLeastOne: [Name.self, EmptyComponent.self])
measure { measure {
for _ in 0..<1_000_000 { for _ in 0..<10_000 {
let success = isMatch.canBecomeMember(a) let success = isMatch.canBecomeMember(a)
XCTAssert(success) XCTAssert(success)
} }
@ -131,4 +131,35 @@ class FamilyTests: XCTestCase {
} }
func testFamilyExchange() {
let nexus = Nexus()
let number: Int = 10
for i in 0..<number {
nexus.create(entity: "\(i)").assign(Position(x: i+1, y: i+2))
}
let familyA = nexus.family(requiresAll: [Position.self], excludesAll: [Velocity.self])
let familyB = nexus.family(requiresAll: [Velocity.self], excludesAll: [Position.self])
var countA: Int = 0
familyA.iterate(components: Position.self) { (entityId, _) in
let e = nexus.get(entity: entityId)
e.assign(Velocity(a: 3.14))
e.remove(Position.self)
countA += 1
}
XCTAssert(countA == number)
var countB: Int = 0
familyB.iterate(components: Velocity.self) { (eId, velocity) in
let e = nexus.get(entity: eId)
e.assign(Position(x: 1, y: 2))
e.remove(velocity!)
countB += 1
}
XCTAssert(countB == number)
}
} }