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

View File

@ -28,7 +28,7 @@ public class Nexus {
/// - Key: component type identifier
/// - 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
/// - 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.
//
public struct SparseComponentSet<Element: Component> {
public class SparseComponentSet {
public typealias Element = Component
public let chunkSize: Int = 4096
fileprivate typealias ComponentIdx = Int
fileprivate typealias EntryTuple = (entityId: EntityIdentifier, component: Element)
fileprivate var dense: ContiguousArray<EntryTuple?>
fileprivate var sparse: [EntityIdentifier: ComponentIdx]
fileprivate var size: Int = 0
public init(_ min: Int = 1024) {
dense = ContiguousArray<EntryTuple?>()
dense.reserveCapacity(min)
sparse = [EntityIdentifier: ComponentIdx](minimumCapacity: min)
fileprivate class Pair {
let key: EntityIndex
let value: Element
init(key: EntityIndex, value: Element) {
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 capacityDense: Int { return dense.count }
public func contains(_ entityId: EntityIdentifier) -> Bool {
guard let compIdx: ComponentIdx = sparse[entityId] else { return false }
public func contains(_ entityIdx: EntityIndex ) -> Bool {
guard let compIdx: ComponentIdx = sparse[entityIdx] else { return false }
return compIdx < count && dense[compIdx] != nil
}
@discardableResult
public mutating func add(_ element: Element, with entityId: EntityIdentifier) -> Bool {
if contains(entityId) { return false }
sparse[entityId] = count
let entry: EntryTuple = EntryTuple(entityId: entityId, component: element)
public func add(_ element: Element, with entityIdx: EntityIndex ) -> Bool {
if contains(entityIdx) { return false }
if needsToGrow(entityIdx) {
grow(including: entityIdx)
}
sparse[entityIdx] = count
let entry: Pair = Pair(key: entityIdx, value: element)
dense.append(entry)
size += 1
return true
}
public func get(_ entityId: EntityIdentifier) -> Element? {
guard let compIdx: ComponentIdx = sparse[entityId] else { return nil }
return dense[compIdx]?.component
public func get(at entityIdx: EntityIndex) -> Element? {
guard let compIdx: ComponentIdx = sparse[entityIdx] else { return nil }
return dense[compIdx]!.value
}
public mutating func remove(_ entityId: EntityIdentifier) -> Element? {
guard let compIdx: ComponentIdx = sparse[entityId] else { return nil }
dense.swapAt(compIdx, count-1)
sparse[entityId] = nil
let swapped: EntryTuple = dense[compIdx]!
sparse[swapped.entityId] = compIdx
let removed: EntryTuple = dense.popLast()!!
return removed.component
@discardableResult
public func remove(_ entityIdx: EntityIndex ) -> Element? {
guard let compIdx: ComponentIdx = sparse[entityIdx] else { return nil }
let last: Int = count-1
dense.swapAt(compIdx, last)
sparse[entityIdx] = nil
let swapped: Pair = dense[compIdx]!
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)
}
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 {
@ -62,7 +99,7 @@ extension SparseComponentSet: Sequence {
var iterator = dense.makeIterator()
return AnyIterator<Element> {
iterator.next()??.component
iterator.next()??.value
}
}
}

View File

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