Add SparseComponentSet

This commit is contained in:
Christian Treffs 2017-10-31 00:41:33 +01:00
parent e643c7761a
commit a491d457ec
10 changed files with 232 additions and 39 deletions

View File

@ -17,6 +17,7 @@ public protocol ManagedContiguousArrayProtocol: class {
associatedtype Element associatedtype Element
static var chunkSize: Int { get } static var chunkSize: Int { get }
init(minCount: Int) init(minCount: Int)
var count: Int { get }
func insert(_ element: Element, at index: Int) func insert(_ element: Element, at index: Int)
func has(_ index: Int) -> Bool func has(_ index: Int) -> Bool
func get(at index: Int) -> Element? func get(at index: Int) -> Element?
@ -27,15 +28,23 @@ public class ManagedContiguousArray: ManagedContiguousArrayProtocol {
public static var chunkSize: Int = 4096 public static var chunkSize: Int = 4096
public typealias Element = Any public typealias Element = Any
var _count: Int = 0
var _store: ContiguousArray<Element?> = [] var _store: ContiguousArray<Element?> = []
public required init(minCount: Int = chunkSize) { public required init(minCount: Int = chunkSize) {
_store = ContiguousArray<Element?>(repeating: nil, count: minCount) _store = ContiguousArray<Element?>(repeating: nil, count: minCount)
} }
public var count: Int {
return _count
}
public func insert(_ element: Element, at index: Int) { public func insert(_ element: Element, at index: Int) {
if needsToGrow(index) { if needsToGrow(index) {
grow(including: index) grow(including: index)
} }
if _store[index] == nil {
_count += 1
}
_store[index] = element _store[index] = element
} }
public func has(_ index: Int) -> Bool { public func has(_ index: Int) -> Bool {
@ -48,6 +57,9 @@ public class ManagedContiguousArray: ManagedContiguousArrayProtocol {
} }
public func remove(at index: Int) { public func remove(at index: Int) {
if _store[index] != nil {
_count -= 1
}
return _store[index] = nil return _store[index] = nil
} }

View File

@ -7,6 +7,14 @@
extension Nexus { extension Nexus {
public var numComponents: Int {
var count = 0
for (_, value) in componentsByType {
count += value._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.has(entityIdx)

View File

@ -7,9 +7,13 @@
extension Nexus { extension Nexus {
public var entities: [Entity] {
return entityStorage.filter { $0.isValid }
}
fileprivate func nextEntityIdx() -> EntityIndex { fileprivate func nextEntityIdx() -> EntityIndex {
guard let nextReused: EntityIdentifier = freeEntities.popLast() else { guard let nextReused: EntityIdentifier = freeEntities.popLast() else {
return entities.count return entityStorage.count
} }
return nextReused.index return nextReused.index
} }
@ -17,8 +21,8 @@ extension Nexus {
public func create(entity name: String? = nil) -> Entity { public func create(entity name: String? = nil) -> Entity {
let newEntityIndex: EntityIndex = nextEntityIdx() let newEntityIndex: EntityIndex = nextEntityIdx()
let newEntityIdentifier: EntityIdentifier = newEntityIndex.identifier let newEntityIdentifier: EntityIdentifier = newEntityIndex.identifier
if entities.count > newEntityIndex { if entityStorage.count > newEntityIndex {
let reusedEntity: Entity = entities[newEntityIndex] let reusedEntity: Entity = entityStorage[newEntityIndex]
assert(reusedEntity.identifier == EntityIdentifier.invalid, "Stil valid entity \(reusedEntity)") assert(reusedEntity.identifier == EntityIdentifier.invalid, "Stil valid entity \(reusedEntity)")
reusedEntity.identifier = newEntityIdentifier reusedEntity.identifier = newEntityIdentifier
reusedEntity.name = name reusedEntity.name = name
@ -26,15 +30,15 @@ extension Nexus {
return reusedEntity return reusedEntity
} else { } else {
let newEntity = Entity(nexus: self, id: newEntityIdentifier, name: name) let newEntity = Entity(nexus: self, id: newEntityIdentifier, name: name)
entities.insert(newEntity, at: newEntityIndex) entityStorage.insert(newEntity, at: newEntityIndex)
notify(EntityCreated(entityId: newEntityIdentifier)) notify(EntityCreated(entityId: newEntityIdentifier))
return newEntity return newEntity
} }
} }
/// Number of entities in nexus. /// Number of entities in nexus.
public var count: Int { public var numEntities: Int {
return entities.count - freeEntities.count return entityStorage.count - freeEntities.count
} }
func isValid(entity: Entity) -> Bool { func isValid(entity: Entity) -> Bool {
@ -44,7 +48,7 @@ extension Nexus {
func isValid(entity entitiyId: EntityIdentifier) -> Bool { func isValid(entity entitiyId: EntityIdentifier) -> Bool {
return entitiyId != EntityIdentifier.invalid && return entitiyId != EntityIdentifier.invalid &&
entitiyId.index >= 0 && entitiyId.index >= 0 &&
entitiyId.index < entities.count entitiyId.index < entityStorage.count
} }
public func has(entity entityId: EntityIdentifier) -> Bool { public func has(entity entityId: EntityIdentifier) -> Bool {
@ -52,7 +56,7 @@ extension Nexus {
} }
public func get(entity entityId: EntityIdentifier) -> Entity { public func get(entity entityId: EntityIdentifier) -> Entity {
return entities[entityId.index] return entityStorage[entityId.index]
} }
@discardableResult @discardableResult
@ -70,7 +74,7 @@ extension Nexus {
// replace with "new" invalid entity to keep capacity of array // replace with "new" invalid entity to keep capacity of array
let invalidEntity = Entity(nexus: self, id: EntityIdentifier.invalid) let invalidEntity = Entity(nexus: self, id: EntityIdentifier.invalid)
entities[entityId.index] = invalidEntity entityStorage[entityId.index] = invalidEntity
freeEntities.append(entityId) freeEntities.append(entityId)

View File

@ -71,7 +71,7 @@ extension Nexus {
assert(replaced == nil, "Family with exact trait hash already exists: \(traitHash)") assert(replaced == nil, "Family with exact trait hash already exists: \(traitHash)")
// FIXME: this is costly for many entities // FIXME: this is costly for many entities
for entity: Entity in entities { for entity: Entity in entityStorage {
update(membership: family, for: entity.identifier) update(membership: family, for: entity.identifier)
} }

View File

@ -24,7 +24,7 @@ public class Nexus {
/// - Index: index value matching entity identifier shifted to Int /// - Index: index value matching entity identifier shifted to Int
/// - Value: each element is a entity instance /// - Value: each element is a entity instance
var entities: Entities var entityStorage: Entities
/// - 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.
@ -42,11 +42,11 @@ public class Nexus {
var freeEntities: ContiguousArray<EntityIdentifier> var freeEntities: ContiguousArray<EntityIdentifier>
var familiyByTraitHash: [FamilyTraitSetHash: Family] var familiyByTraitHash: [FamilyTraitSetHash: Family]
var familyMembersByTraitHash: [FamilyTraitSetHash: [EntityIdentifier]] var familyMembersByTraitHash: [FamilyTraitSetHash: [EntityIdentifier]] // SparseSet for EntityIdentifier
var familyContainsEntityId: [TraitEntityIdHash: Bool] var familyContainsEntityId: [TraitEntityIdHash: Bool]
public init() { public init() {
entities = Entities() entityStorage = Entities()
componentsByType = [:] componentsByType = [:]
componentIdsByEntity = [:] componentIdsByEntity = [:]
componentIdsByEntityLookup = [:] componentIdsByEntityLookup = [:]

View File

@ -0,0 +1,68 @@
//
// SparseSet.swift
// FirebladeECS
//
// Created by Christian Treffs on 30.10.17.
//
public struct SparseComponentSet<Element: Component> {
fileprivate typealias ComponentIdx = Int
fileprivate typealias EntryTuple = (entityId: EntityIdentifier, component: Element)
fileprivate var dense: ContiguousArray<EntryTuple?>
fileprivate var sparse: [EntityIdentifier: ComponentIdx]
public init(_ min: Int = 1024) {
dense = ContiguousArray<EntryTuple?>()
dense.reserveCapacity(min)
sparse = [EntityIdentifier: ComponentIdx](minimumCapacity: min)
}
public var count: Int { return dense.count }
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 }
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)
dense.append(entry)
return true
}
public func get(_ entityId: EntityIdentifier) -> Element? {
guard let compIdx: ComponentIdx = sparse[entityId] else { return nil }
return dense[compIdx]?.component
}
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
}
public mutating func clear(keepingCapacity: Bool = false) {
dense.removeAll(keepingCapacity: keepingCapacity)
}
}
extension SparseComponentSet: Sequence {
public func makeIterator() -> AnyIterator<Element> {
var iterator = dense.makeIterator()
return AnyIterator<Element> {
iterator.next()??.component
}
}
}

View File

@ -1,13 +1,23 @@
import CSDL2 import CSDL2
import FirebladeECS import FirebladeECS
var tFrame = Timer()
var tSetup = Timer() var tSetup = Timer()
tSetup.start() tSetup.start()
if SDL_Init(SDL_INIT_VIDEO) != 0 { if SDL_Init(SDL_INIT_VIDEO) != 0 {
fatalError("could not init video") fatalError("could not init video")
} }
var frameCount: UInt = 0
var fps: Double = 0
let nexus = Nexus()
var windowTitle: String {
return "Fireblade ECS demo: [entities: \(nexus.numEntities), components: \(nexus.numComponents)] @ [FPS: \(fps), frames: \(frameCount)]"
}
let width: Int32 = 640 let width: Int32 = 640
let height: Int32 = 480 let height: Int32 = 480
let hWin = SDL_CreateWindow("Fireblade ECS demo", 100, 100, width, height, SDL_WINDOW_SHOWN.rawValue) let hWin = SDL_CreateWindow(windowTitle, 100, 100, width, height, SDL_WINDOW_SHOWN.rawValue)
if hWin == nil { if hWin == nil {
SDL_Quit() SDL_Quit()
fatalError("could not crate window") fatalError("could not crate window")
@ -22,8 +32,6 @@ func randColor() -> UInt8 {
return UInt8(randNorm() * 254) + 1 return UInt8(randNorm() * 254) + 1
} }
let nexus = Nexus()
class Position: Component { class Position: Component {
var x: Int32 = width/2 var x: Int32 = width/2
var y: Int32 = height/2 var y: Int32 = height/2
@ -39,20 +47,43 @@ func createScene() {
let numEntities: Int = 10_000 let numEntities: Int = 10_000
for i in 0..<numEntities { for i in 0..<numEntities {
let e = nexus.create(entity: "\(i)") createDefaultEntity(name: "\(i)")
e.assign(Position())
e.assign(Color())
} }
} }
func batchCreateEntities(count: Int) {
for _ in 0..<count {
createDefaultEntity(name: nil)
}
}
func batchDestroyEntities(count: Int) {
var i = count
for entity in nexus.entities {
if nexus.destroy(entity: entity) {
i -= 1
if i == 0 {
return
}
}
}
}
func createDefaultEntity(name: String?) {
let e = nexus.create(entity: name)
e.assign(Position())
e.assign(Color())
}
class PositionSystem { class PositionSystem {
let family = nexus.family(requiresAll: [Position.self], excludesAll: []) let family = nexus.family(requiresAll: [Position.self], excludesAll: [])
var acceleration: Double = 4.0 var velocity: Double = 4.0
func update() { func update() {
family.iterate(components: Position.self) { [unowned self](_, pos) in family.iterate(components: Position.self) { [unowned self](_, pos) in
let deltaX: Double = self.acceleration*((randNorm() * 2) - 1) let deltaX: Double = self.velocity*((randNorm() * 2) - 1)
let deltaY: Double = self.acceleration*((randNorm() * 2) - 1) let deltaY: Double = self.velocity*((randNorm() * 2) - 1)
var x = pos!.x + Int32(deltaX) var x = pos!.x + Int32(deltaX)
var y = pos!.y + Int32(deltaY) var y = pos!.y + Int32(deltaY)
@ -142,6 +173,10 @@ func printHelp() {
+ increase movement speed + increase movement speed
- reduce movement speed - reduce movement speed
space reset to default movement speed space reset to default movement speed
e create 1 entity
d destroy 1 entity
8 batch create 10k entities
9 batch destroy 10k entities
""" """
print(help) print(help)
} }
@ -155,8 +190,12 @@ printHelp()
tRun.start() tRun.start()
var event: SDL_Event = SDL_Event() var event: SDL_Event = SDL_Event()
var quit: Bool = false var quit: Bool = false
var currentTime: UInt32 = 0
var lastTime: UInt32 = 0
var frameTimes: [UInt64] = []
print("================ RUNNING ================") print("================ RUNNING ================")
while quit == false { while quit == false {
tFrame.start()
while SDL_PollEvent(&event) == 1 { while SDL_PollEvent(&event) == 1 {
switch SDL_EventType(rawValue: event.type) { switch SDL_EventType(rawValue: event.type) {
case SDL_QUIT: case SDL_QUIT:
@ -172,13 +211,21 @@ while quit == false {
case SDLK_r: case SDLK_r:
positionResetSystem.update() positionResetSystem.update()
case SDLK_s: case SDLK_s:
positionSystem.acceleration = 0.0 positionSystem.velocity = 0.0
case SDLK_PLUS: case SDLK_PLUS:
positionSystem.acceleration += 0.1 positionSystem.velocity += 0.1
case SDLK_MINUS: case SDLK_MINUS:
positionSystem.acceleration -= 0.1 positionSystem.velocity -= 0.1
case SDLK_SPACE: case SDLK_SPACE:
positionSystem.acceleration = 4.0 positionSystem.velocity = 4.0
case SDLK_e:
batchCreateEntities(count: 1)
case SDLK_d:
batchDestroyEntities(count: 1)
case SDLK_8:
batchCreateEntities(count: 10_000)
case SDLK_9:
batchDestroyEntities(count: 10_000)
default: default:
break break
} }
@ -190,6 +237,28 @@ while quit == false {
positionSystem.update() positionSystem.update()
renderSystem.render() renderSystem.render()
tFrame.stop()
frameTimes.append(tFrame.nanoSeconds)
// Print a report once per second
currentTime = SDL_GetTicks()
if (currentTime > lastTime + 1000) {
let count = UInt(frameTimes.count)
frameCount += count
let sum: UInt64 = frameTimes.reduce(0, { $0 + $1 })
frameTimes.removeAll(keepingCapacity: true)
let avergageNanos: Double = Double(sum)/Double(count)
fps = 1.0 / (avergageNanos * 1.0e-9)
fps.round()
SDL_SetWindowTitle(hWin, windowTitle)
lastTime = currentTime
}
tFrame.reset()
} }
SDL_DestroyWindow(hWin) SDL_DestroyWindow(hWin)

View File

@ -74,7 +74,7 @@ class FamilyTests: XCTestCase {
let family = nexus.family(requiresAll: [Position.self, Velocity.self], excludesAll: [Party.self], needsAtLeastOne: [Name.self, EmptyComponent.self]) let family = nexus.family(requiresAll: [Position.self, Velocity.self], excludesAll: [Party.self], needsAtLeastOne: [Name.self, EmptyComponent.self])
XCTAssert(family.count == number) XCTAssert(family.count == number)
XCTAssert(nexus.count == number) XCTAssert(nexus.numEntities == number)
measure { measure {
family.iterateMembers({ (entityId) in family.iterateMembers({ (entityId) in
@ -94,7 +94,7 @@ class FamilyTests: XCTestCase {
let family = nexus.family(requiresAll: [Position.self, Velocity.self], excludesAll: [Party.self], needsAtLeastOne: [Name.self, EmptyComponent.self]) let family = nexus.family(requiresAll: [Position.self, Velocity.self], excludesAll: [Party.self], needsAtLeastOne: [Name.self, EmptyComponent.self])
XCTAssert(family.count == number) XCTAssert(family.count == number)
XCTAssert(nexus.count == number) XCTAssert(nexus.numEntities == number)
measure { measure {
family.iterate(components: Velocity.self) { (_, vel) in family.iterate(components: Velocity.self) { (_, vel) in
@ -115,7 +115,7 @@ class FamilyTests: XCTestCase {
let family = nexus.family(requiresAll: [Position.self, Velocity.self], excludesAll: [Party.self], needsAtLeastOne: [Name.self, EmptyComponent.self]) let family = nexus.family(requiresAll: [Position.self, Velocity.self], excludesAll: [Party.self], needsAtLeastOne: [Name.self, EmptyComponent.self])
XCTAssert(family.count == number) XCTAssert(family.count == number)
XCTAssert(nexus.count == number) XCTAssert(nexus.numEntities == number)
measure { measure {
family.iterate(components: Position.self, Velocity.self, Name.self) { (entityId, pos, vel, nm) in family.iterate(components: Position.self, Velocity.self, Name.self) { (entityId, pos, vel, nm) in

View File

@ -21,17 +21,17 @@ class NexusTests: XCTestCase {
func testCreateEntity() { func testCreateEntity() {
let nexus: Nexus = Nexus() let nexus: Nexus = Nexus()
XCTAssert(nexus.count == 0) XCTAssert(nexus.numEntities == 0)
let e0 = nexus.create() let e0 = nexus.create()
XCTAssert(e0.identifier.index == 0) XCTAssert(e0.identifier.index == 0)
XCTAssert(e0.isValid) XCTAssert(e0.isValid)
XCTAssert(nexus.count == 1) XCTAssert(nexus.numEntities == 1)
let e1 = nexus.create(entity: "Named e1") let e1 = nexus.create(entity: "Named e1")
XCTAssert(e1.identifier.index == 1) XCTAssert(e1.identifier.index == 1)
XCTAssert(e1.isValid) XCTAssert(e1.isValid)
XCTAssert(nexus.count == 2) XCTAssert(nexus.numEntities == 2)
XCTAssert(e0.name == nil) XCTAssert(e0.name == nil)
XCTAssert(e1.name == "Named e1") XCTAssert(e1.name == "Named e1")
@ -43,28 +43,28 @@ class NexusTests: XCTestCase {
func testDestroyAndReuseEntity() { func testDestroyAndReuseEntity() {
let nexus: Nexus = Nexus() let nexus: Nexus = Nexus()
XCTAssert(nexus.count == 0) XCTAssert(nexus.numEntities == 0)
let e0 = nexus.create(entity: "e0") let e0 = nexus.create(entity: "e0")
XCTAssert(e0.isValid) XCTAssert(e0.isValid)
XCTAssert(nexus.count == 1) XCTAssert(nexus.numEntities == 1)
let e1 = nexus.create(entity: "e1") let e1 = nexus.create(entity: "e1")
XCTAssert(e1.isValid) XCTAssert(e1.isValid)
XCTAssert(nexus.count == 2) XCTAssert(nexus.numEntities == 2)
e0.destroy() e0.destroy()
XCTAssert(!e0.isValid) XCTAssert(!e0.isValid)
XCTAssert(e1.isValid) XCTAssert(e1.isValid)
XCTAssert(nexus.count == 1) XCTAssert(nexus.numEntities == 1)
let e2 = nexus.create(entity: "e2") let e2 = nexus.create(entity: "e2")
XCTAssert(!e0.isValid) XCTAssert(!e0.isValid)
XCTAssert(e1.isValid) XCTAssert(e1.isValid)
XCTAssert(e2.isValid) XCTAssert(e2.isValid)
XCTAssert(nexus.count == 2) XCTAssert(nexus.numEntities == 2)
XCTAssert(!(e0 == e2)) XCTAssert(!(e0 == e2))
XCTAssert(!(e0 === e2)) XCTAssert(!(e0 === e2))
@ -72,7 +72,7 @@ class NexusTests: XCTestCase {
func testComponentCreation() { func testComponentCreation() {
let nexus: Nexus = Nexus() let nexus: Nexus = Nexus()
XCTAssert(nexus.count == 0) XCTAssert(nexus.numEntities == 0)
let e0: Entity = nexus.create(entity: "e0") let e0: Entity = nexus.create(entity: "e0")
@ -145,7 +145,7 @@ class NexusTests: XCTestCase {
let b = nexus.create() let b = nexus.create()
let c = nexus.create() let c = nexus.create()
XCTAssert(nexus.count == 3) XCTAssert(nexus.numEntities == 3)
a.assign(Position(x: 0, y: 0)) a.assign(Position(x: 0, y: 0))
b.assign(Position(x: 0, y: 0)) b.assign(Position(x: 0, y: 0))

View File

@ -0,0 +1,32 @@
//
// SparseComponentSetTests.swift
// FirebladeECSTests
//
// Created by Christian Treffs on 31.10.17.
//
import XCTest
import FirebladeECS
class SparseComponentSetTests: XCTestCase {
func testSet() {
var s = SparseComponentSet<Position>()
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)
for p in s {
print(p.x, p.y)
}
s.remove(13)
for p in s {
print(p.x, p.y)
}
s.remove(234567890)
}
}