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 {
return nexus.has(component: uct, entity: identifier)
return nexus.has(componentId: uct, entityIdx: identifier.index)
}
public final var hasComponents: Bool {

View File

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

View File

@ -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<C>(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<C>(componentId: ComponentIdentifier, hash: EntityComponentHash) -> C? where C: Component {
guard let componentIdx: ComponentIndex = componentIndexByEntityComponentHash[hash] else { return nil }
fileprivate func get<C>(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
}
}

View File

@ -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

View File

@ -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<Component>
public typealias UniformComponents = ContiguousComponentArray
public typealias ComponentIdentifiers = ContiguousArray<ComponentIdentifier>
public typealias ComponentSet = Set<ComponentIdentifier>
public typealias Entities = ContiguousArray<Entity>
public typealias EntityIds = ContiguousArray<EntityIdentifier>
public typealias EntityIdSet = Set<EntityIdentifier>
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<EntityIdentifier>
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<EntityIdentifier>()
familiyByTraitHash = [:]
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])
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..<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)
}
}