Fix family assign/remove problem
This commit is contained in:
parent
8c38f76e5a
commit
a33281b1fa
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = [:]
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue