Fixed SparseSet - needs optimizations

This commit is contained in:
Christian Treffs 2017-10-31 10:49:43 +01:00
parent a491d457ec
commit a559387891
4 changed files with 97 additions and 55 deletions

View File

@ -9,15 +9,15 @@ extension Nexus {
public var numComponents: Int { public var numComponents: Int {
var count = 0 var count = 0
for (_, value) in componentsByType { for (_, uniformComps) in componentsByType {
count += value._count count += uniformComps.count
} }
return count return count
} }
public func has(componentId: ComponentIdentifier, entityIdx: EntityIndex) -> Bool { public func has(componentId: ComponentIdentifier, entityIdx: EntityIndex) -> Bool {
guard let uniforms = componentsByType[componentId] else { return false } guard let uniforms = componentsByType[componentId] else { return false }
return uniforms.has(entityIdx) return uniforms.contains(entityIdx)
} }
public func count(components entityId: EntityIdentifier) -> Int { public func count(components entityId: EntityIdentifier) -> Int {
@ -40,10 +40,10 @@ extension Nexus {
return return
} }
if componentsByType[componentId] != nil { if componentsByType[componentId] != nil {
componentsByType[componentId]!.insert(component, at: entityIdx) componentsByType[componentId]!.add(component, with: entityIdx)
} else { } else {
componentsByType[componentId] = UniformComponents() componentsByType[componentId] = SparseComponentSet()
componentsByType[componentId]!.insert(component, at: entityIdx) componentsByType[componentId]!.add(component, with: entityIdx)
} }
// assigns the component id to the entity id // assigns the component id to the entity id
@ -70,8 +70,8 @@ extension Nexus {
} }
public func get(component componentId: ComponentIdentifier, for entityId: EntityIdentifier) -> Component? { public func get(component componentId: ComponentIdentifier, for entityId: EntityIdentifier) -> Component? {
guard let uniformComponents: UniformComponents = componentsByType[componentId] else { return nil } guard let uniformComponents: SparseComponentSet = componentsByType[componentId] else { return nil }
return uniformComponents.get(at: entityId.index) as? Component 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 {
@ -80,7 +80,7 @@ extension Nexus {
} }
fileprivate func get<C>(componentId: ComponentIdentifier, entityIdx: EntityIndex) -> C? where C: Component { fileprivate func get<C>(componentId: ComponentIdentifier, entityIdx: EntityIndex) -> C? where C: Component {
guard let uniformComponents: UniformComponents = componentsByType[componentId] else { return nil } guard let uniformComponents: SparseComponentSet = componentsByType[componentId] else { return nil }
return uniformComponents.get(at: entityIdx) as? C return uniformComponents.get(at: entityIdx) as? C
} }
@ -90,10 +90,11 @@ extension Nexus {
@discardableResult @discardableResult
public func remove(component componentId: ComponentIdentifier, from entityId: EntityIdentifier) -> Bool { public func remove(component componentId: ComponentIdentifier, from entityId: EntityIdentifier) -> Bool {
let hash: EntityComponentHash = componentId.hashValue(using: entityId.index) let entityIdx: EntityIndex = entityId.index
let hash: EntityComponentHash = componentId.hashValue(using: entityIdx)
// MARK: delete component instance // MARK: delete component instance
componentsByType[componentId]?.remove(at: entityId.index) componentsByType[componentId]?.remove(entityIdx)
// MARK: unassign component // MARK: unassign component
guard let removeIndex: ComponentIdsByEntityIndex = componentIdsByEntityLookup.removeValue(forKey: hash) else { guard let removeIndex: ComponentIdsByEntityIndex = componentIdsByEntityLookup.removeValue(forKey: hash) else {
@ -101,17 +102,17 @@ extension Nexus {
return false return false
} }
guard componentIdsByEntity[entityId.index]?.remove(at: removeIndex) != nil else { guard componentIdsByEntity[entityIdx]?.remove(at: removeIndex) != nil else {
assert(false, "ComponentRemove failure: nothing was removed") assert(false, "ComponentRemove failure: nothing was removed")
report("ComponentRemove failure: nothing was removed") report("ComponentRemove failure: nothing was removed")
return false return false
} }
// relocate remaining indices pointing in the componentsByEntity map // relocate remaining indices pointing in the componentsByEntity map
if let remainingComponents: ComponentIdentifiers = componentIdsByEntity[entityId.index] { if let remainingComponents: ComponentIdentifiers = componentIdsByEntity[entityIdx] {
// 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: entityIdx)
assert(cHash != hash) assert(cHash != hash)
componentIdsByEntityLookup[cHash] = index componentIdsByEntityLookup[cHash] = index
} }

View File

@ -28,7 +28,7 @@ public class Nexus {
/// - Key: component type identifier /// - Key: component type identifier
/// - 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: SparseComponentSet]
/// - 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

View File

@ -5,55 +5,92 @@
// Created by Christian Treffs on 30.10.17. // Created by Christian Treffs on 30.10.17.
// //
public struct SparseComponentSet<Element: Component> { public class SparseComponentSet {
public typealias Element = Component
public let chunkSize: Int = 4096
fileprivate typealias ComponentIdx = Int fileprivate typealias ComponentIdx = Int
fileprivate typealias EntryTuple = (entityId: EntityIdentifier, component: Element) fileprivate var size: Int = 0
fileprivate var dense: ContiguousArray<EntryTuple?>
fileprivate var sparse: [EntityIdentifier: ComponentIdx]
public init(_ min: Int = 1024) { fileprivate class Pair {
dense = ContiguousArray<EntryTuple?>() let key: EntityIndex
dense.reserveCapacity(min) let value: Element
init(key: EntityIndex, value: Element) {
sparse = [EntityIdentifier: ComponentIdx](minimumCapacity: min) self.key = key
self.value = value
}
} }
public var count: Int { return dense.count } fileprivate var dense: ContiguousArray<Pair?>
fileprivate var sparse: [EntityIndex: ComponentIdx]
public init() {
dense = ContiguousArray<Pair?>()
dense.reserveCapacity(chunkSize)
sparse = [EntityIndex: ComponentIdx].init(minimumCapacity: chunkSize)
}
public var count: Int { return size }
internal var capacitySparse: Int { return sparse.count } internal var capacitySparse: Int { return sparse.count }
internal var capacityDense: Int { return dense.count } internal var capacityDense: Int { return dense.count }
public func contains(_ entityId: EntityIdentifier) -> Bool { public func contains(_ entityIdx: EntityIndex ) -> Bool {
guard let compIdx: ComponentIdx = sparse[entityId] else { return false } guard let compIdx: ComponentIdx = sparse[entityIdx] else { return false }
return compIdx < count && dense[compIdx] != nil return compIdx < count && dense[compIdx] != nil
} }
@discardableResult @discardableResult
public mutating func add(_ element: Element, with entityId: EntityIdentifier) -> Bool { public func add(_ element: Element, with entityIdx: EntityIndex ) -> Bool {
if contains(entityId) { return false } if contains(entityIdx) { return false }
sparse[entityId] = count if needsToGrow(entityIdx) {
let entry: EntryTuple = EntryTuple(entityId: entityId, component: element) grow(including: entityIdx)
}
sparse[entityIdx] = count
let entry: Pair = Pair(key: entityIdx, value: element)
dense.append(entry) dense.append(entry)
size += 1
return true return true
} }
public func get(_ entityId: EntityIdentifier) -> Element? { public func get(at entityIdx: EntityIndex) -> Element? {
guard let compIdx: ComponentIdx = sparse[entityId] else { return nil } guard let compIdx: ComponentIdx = sparse[entityIdx] else { return nil }
return dense[compIdx]?.component return dense[compIdx]!.value
} }
public mutating func remove(_ entityId: EntityIdentifier) -> Element? { @discardableResult
guard let compIdx: ComponentIdx = sparse[entityId] else { return nil } public func remove(_ entityIdx: EntityIndex ) -> Element? {
dense.swapAt(compIdx, count-1) guard let compIdx: ComponentIdx = sparse[entityIdx] else { return nil }
sparse[entityId] = nil let last: Int = count-1
let swapped: EntryTuple = dense[compIdx]! dense.swapAt(compIdx, last)
sparse[swapped.entityId] = compIdx sparse[entityIdx] = nil
let removed: EntryTuple = dense.popLast()!! let swapped: Pair = dense[compIdx]!
return removed.component sparse[swapped.key] = compIdx
let removed: Pair = dense.popLast()!!
size -= 1
return removed.value
} }
public mutating func clear(keepingCapacity: Bool = false) { public func clear(keepingCapacity: Bool = false) {
dense.removeAll(keepingCapacity: keepingCapacity) dense.removeAll(keepingCapacity: keepingCapacity)
} }
fileprivate func needsToGrow(_ index: Int) -> Bool {
return index > count - 1
}
fileprivate func grow(including index: Int) {
let newCapacity: Int = nearest(to: index)
//let newCount: Int = newCapacity-count
dense.reserveCapacity(newCapacity)
/*for _ in 0..<newCount {
dense.append(nil)
}*/
}
fileprivate func nearest(to index: Int) -> Int {
let delta = Float(index) / Float(chunkSize)
let multiplier = Int(delta) + 1
return multiplier * chunkSize
}
} }
extension SparseComponentSet: Sequence { extension SparseComponentSet: Sequence {
@ -62,7 +99,7 @@ extension SparseComponentSet: Sequence {
var iterator = dense.makeIterator() var iterator = dense.makeIterator()
return AnyIterator<Element> { return AnyIterator<Element> {
iterator.next()??.component iterator.next()??.value
} }
} }
} }

View File

@ -6,27 +6,31 @@
// //
import XCTest import XCTest
import FirebladeECS @testable import FirebladeECS
class SparseComponentSetTests: XCTestCase { class SparseComponentSetTests: XCTestCase {
func testSet() { func testSet() {
var s = SparseComponentSet<Position>() let s = SparseComponentSet()
s.add(Position(x: 1, y: 2), with: 0) let num: Int = 100
s.add(Position(x: 13, y: 23), with: 13)
s.add(Position(x: 123, y: 123), with: 123)
for p in s { for i in 0..<num {
print(p.x, p.y) s.add(Position(x: i, y: i), with: EntityIndex(i))
} }
s.remove(13) XCTAssert(s.count == num)
for p in s { for i in 0..<num {
print(p.x, p.y) let idx = num-i-1
let p: Position = s.get(at: idx) as! Position
XCTAssertEqual(idx, p.x)
} }
s.remove(234567890) for i in 0..<num {
s.remove(EntityIndex(i))
}
XCTAssert(s.count == 0)
} }
} }