Add SparseComponentSet
This commit is contained in:
parent
e643c7761a
commit
a491d457ec
|
|
@ -17,6 +17,7 @@ public protocol ManagedContiguousArrayProtocol: class {
|
|||
associatedtype Element
|
||||
static var chunkSize: Int { get }
|
||||
init(minCount: Int)
|
||||
var count: Int { get }
|
||||
func insert(_ element: Element, at index: Int)
|
||||
func has(_ index: Int) -> Bool
|
||||
func get(at index: Int) -> Element?
|
||||
|
|
@ -27,15 +28,23 @@ public class ManagedContiguousArray: ManagedContiguousArrayProtocol {
|
|||
public static var chunkSize: Int = 4096
|
||||
|
||||
public typealias Element = Any
|
||||
var _count: Int = 0
|
||||
var _store: ContiguousArray<Element?> = []
|
||||
public required init(minCount: Int = chunkSize) {
|
||||
_store = ContiguousArray<Element?>(repeating: nil, count: minCount)
|
||||
}
|
||||
|
||||
public var count: Int {
|
||||
return _count
|
||||
}
|
||||
|
||||
public func insert(_ element: Element, at index: Int) {
|
||||
if needsToGrow(index) {
|
||||
grow(including: index)
|
||||
}
|
||||
if _store[index] == nil {
|
||||
_count += 1
|
||||
}
|
||||
_store[index] = element
|
||||
}
|
||||
public func has(_ index: Int) -> Bool {
|
||||
|
|
@ -48,6 +57,9 @@ public class ManagedContiguousArray: ManagedContiguousArrayProtocol {
|
|||
}
|
||||
|
||||
public func remove(at index: Int) {
|
||||
if _store[index] != nil {
|
||||
_count -= 1
|
||||
}
|
||||
return _store[index] = nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,14 @@
|
|||
|
||||
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 {
|
||||
guard let uniforms = componentsByType[componentId] else { return false }
|
||||
return uniforms.has(entityIdx)
|
||||
|
|
|
|||
|
|
@ -7,9 +7,13 @@
|
|||
|
||||
extension Nexus {
|
||||
|
||||
public var entities: [Entity] {
|
||||
return entityStorage.filter { $0.isValid }
|
||||
}
|
||||
|
||||
fileprivate func nextEntityIdx() -> EntityIndex {
|
||||
guard let nextReused: EntityIdentifier = freeEntities.popLast() else {
|
||||
return entities.count
|
||||
return entityStorage.count
|
||||
}
|
||||
return nextReused.index
|
||||
}
|
||||
|
|
@ -17,8 +21,8 @@ extension Nexus {
|
|||
public func create(entity name: String? = nil) -> Entity {
|
||||
let newEntityIndex: EntityIndex = nextEntityIdx()
|
||||
let newEntityIdentifier: EntityIdentifier = newEntityIndex.identifier
|
||||
if entities.count > newEntityIndex {
|
||||
let reusedEntity: Entity = entities[newEntityIndex]
|
||||
if entityStorage.count > newEntityIndex {
|
||||
let reusedEntity: Entity = entityStorage[newEntityIndex]
|
||||
assert(reusedEntity.identifier == EntityIdentifier.invalid, "Stil valid entity \(reusedEntity)")
|
||||
reusedEntity.identifier = newEntityIdentifier
|
||||
reusedEntity.name = name
|
||||
|
|
@ -26,15 +30,15 @@ extension Nexus {
|
|||
return reusedEntity
|
||||
} else {
|
||||
let newEntity = Entity(nexus: self, id: newEntityIdentifier, name: name)
|
||||
entities.insert(newEntity, at: newEntityIndex)
|
||||
entityStorage.insert(newEntity, at: newEntityIndex)
|
||||
notify(EntityCreated(entityId: newEntityIdentifier))
|
||||
return newEntity
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of entities in nexus.
|
||||
public var count: Int {
|
||||
return entities.count - freeEntities.count
|
||||
public var numEntities: Int {
|
||||
return entityStorage.count - freeEntities.count
|
||||
}
|
||||
|
||||
func isValid(entity: Entity) -> Bool {
|
||||
|
|
@ -44,7 +48,7 @@ extension Nexus {
|
|||
func isValid(entity entitiyId: EntityIdentifier) -> Bool {
|
||||
return entitiyId != EntityIdentifier.invalid &&
|
||||
entitiyId.index >= 0 &&
|
||||
entitiyId.index < entities.count
|
||||
entitiyId.index < entityStorage.count
|
||||
}
|
||||
|
||||
public func has(entity entityId: EntityIdentifier) -> Bool {
|
||||
|
|
@ -52,7 +56,7 @@ extension Nexus {
|
|||
}
|
||||
|
||||
public func get(entity entityId: EntityIdentifier) -> Entity {
|
||||
return entities[entityId.index]
|
||||
return entityStorage[entityId.index]
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
|
|
@ -70,7 +74,7 @@ extension Nexus {
|
|||
|
||||
// replace with "new" invalid entity to keep capacity of array
|
||||
let invalidEntity = Entity(nexus: self, id: EntityIdentifier.invalid)
|
||||
entities[entityId.index] = invalidEntity
|
||||
entityStorage[entityId.index] = invalidEntity
|
||||
|
||||
freeEntities.append(entityId)
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ extension Nexus {
|
|||
assert(replaced == nil, "Family with exact trait hash already exists: \(traitHash)")
|
||||
|
||||
// FIXME: this is costly for many entities
|
||||
for entity: Entity in entities {
|
||||
for entity: Entity in entityStorage {
|
||||
update(membership: family, for: entity.identifier)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ public class Nexus {
|
|||
|
||||
/// - Index: index value matching entity identifier shifted to Int
|
||||
/// - Value: each element is a entity instance
|
||||
var entities: Entities
|
||||
var entityStorage: Entities
|
||||
|
||||
/// - Key: component type identifier
|
||||
/// - 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 familiyByTraitHash: [FamilyTraitSetHash: Family]
|
||||
var familyMembersByTraitHash: [FamilyTraitSetHash: [EntityIdentifier]]
|
||||
var familyMembersByTraitHash: [FamilyTraitSetHash: [EntityIdentifier]] // SparseSet for EntityIdentifier
|
||||
var familyContainsEntityId: [TraitEntityIdHash: Bool]
|
||||
|
||||
public init() {
|
||||
entities = Entities()
|
||||
entityStorage = Entities()
|
||||
componentsByType = [:]
|
||||
componentIdsByEntity = [:]
|
||||
componentIdsByEntityLookup = [:]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,23 @@
|
|||
import CSDL2
|
||||
import FirebladeECS
|
||||
var tFrame = Timer()
|
||||
var tSetup = Timer()
|
||||
tSetup.start()
|
||||
if SDL_Init(SDL_INIT_VIDEO) != 0 {
|
||||
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 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 {
|
||||
SDL_Quit()
|
||||
fatalError("could not crate window")
|
||||
|
|
@ -22,8 +32,6 @@ func randColor() -> UInt8 {
|
|||
return UInt8(randNorm() * 254) + 1
|
||||
}
|
||||
|
||||
let nexus = Nexus()
|
||||
|
||||
class Position: Component {
|
||||
var x: Int32 = width/2
|
||||
var y: Int32 = height/2
|
||||
|
|
@ -39,20 +47,43 @@ func createScene() {
|
|||
let numEntities: Int = 10_000
|
||||
|
||||
for i in 0..<numEntities {
|
||||
let e = nexus.create(entity: "\(i)")
|
||||
createDefaultEntity(name: "\(i)")
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
let family = nexus.family(requiresAll: [Position.self], excludesAll: [])
|
||||
var acceleration: Double = 4.0
|
||||
var velocity: Double = 4.0
|
||||
func update() {
|
||||
family.iterate(components: Position.self) { [unowned self](_, pos) in
|
||||
|
||||
let deltaX: Double = self.acceleration*((randNorm() * 2) - 1)
|
||||
let deltaY: Double = self.acceleration*((randNorm() * 2) - 1)
|
||||
let deltaX: Double = self.velocity*((randNorm() * 2) - 1)
|
||||
let deltaY: Double = self.velocity*((randNorm() * 2) - 1)
|
||||
var x = pos!.x + Int32(deltaX)
|
||||
var y = pos!.y + Int32(deltaY)
|
||||
|
||||
|
|
@ -142,6 +173,10 @@ func printHelp() {
|
|||
+ increase movement speed
|
||||
- reduce 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)
|
||||
}
|
||||
|
|
@ -155,8 +190,12 @@ printHelp()
|
|||
tRun.start()
|
||||
var event: SDL_Event = SDL_Event()
|
||||
var quit: Bool = false
|
||||
var currentTime: UInt32 = 0
|
||||
var lastTime: UInt32 = 0
|
||||
var frameTimes: [UInt64] = []
|
||||
print("================ RUNNING ================")
|
||||
while quit == false {
|
||||
tFrame.start()
|
||||
while SDL_PollEvent(&event) == 1 {
|
||||
switch SDL_EventType(rawValue: event.type) {
|
||||
case SDL_QUIT:
|
||||
|
|
@ -172,13 +211,21 @@ while quit == false {
|
|||
case SDLK_r:
|
||||
positionResetSystem.update()
|
||||
case SDLK_s:
|
||||
positionSystem.acceleration = 0.0
|
||||
positionSystem.velocity = 0.0
|
||||
case SDLK_PLUS:
|
||||
positionSystem.acceleration += 0.1
|
||||
positionSystem.velocity += 0.1
|
||||
case SDLK_MINUS:
|
||||
positionSystem.acceleration -= 0.1
|
||||
positionSystem.velocity -= 0.1
|
||||
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:
|
||||
break
|
||||
}
|
||||
|
|
@ -190,6 +237,28 @@ while quit == false {
|
|||
positionSystem.update()
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ class FamilyTests: XCTestCase {
|
|||
let family = nexus.family(requiresAll: [Position.self, Velocity.self], excludesAll: [Party.self], needsAtLeastOne: [Name.self, EmptyComponent.self])
|
||||
|
||||
XCTAssert(family.count == number)
|
||||
XCTAssert(nexus.count == number)
|
||||
XCTAssert(nexus.numEntities == number)
|
||||
|
||||
measure {
|
||||
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])
|
||||
|
||||
XCTAssert(family.count == number)
|
||||
XCTAssert(nexus.count == number)
|
||||
XCTAssert(nexus.numEntities == number)
|
||||
|
||||
measure {
|
||||
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])
|
||||
|
||||
XCTAssert(family.count == number)
|
||||
XCTAssert(nexus.count == number)
|
||||
XCTAssert(nexus.numEntities == number)
|
||||
|
||||
measure {
|
||||
family.iterate(components: Position.self, Velocity.self, Name.self) { (entityId, pos, vel, nm) in
|
||||
|
|
|
|||
|
|
@ -21,17 +21,17 @@ class NexusTests: XCTestCase {
|
|||
|
||||
func testCreateEntity() {
|
||||
let nexus: Nexus = Nexus()
|
||||
XCTAssert(nexus.count == 0)
|
||||
XCTAssert(nexus.numEntities == 0)
|
||||
|
||||
let e0 = nexus.create()
|
||||
XCTAssert(e0.identifier.index == 0)
|
||||
XCTAssert(e0.isValid)
|
||||
XCTAssert(nexus.count == 1)
|
||||
XCTAssert(nexus.numEntities == 1)
|
||||
|
||||
let e1 = nexus.create(entity: "Named e1")
|
||||
XCTAssert(e1.identifier.index == 1)
|
||||
XCTAssert(e1.isValid)
|
||||
XCTAssert(nexus.count == 2)
|
||||
XCTAssert(nexus.numEntities == 2)
|
||||
|
||||
XCTAssert(e0.name == nil)
|
||||
XCTAssert(e1.name == "Named e1")
|
||||
|
|
@ -43,28 +43,28 @@ class NexusTests: XCTestCase {
|
|||
|
||||
func testDestroyAndReuseEntity() {
|
||||
let nexus: Nexus = Nexus()
|
||||
XCTAssert(nexus.count == 0)
|
||||
XCTAssert(nexus.numEntities == 0)
|
||||
|
||||
let e0 = nexus.create(entity: "e0")
|
||||
XCTAssert(e0.isValid)
|
||||
XCTAssert(nexus.count == 1)
|
||||
XCTAssert(nexus.numEntities == 1)
|
||||
|
||||
let e1 = nexus.create(entity: "e1")
|
||||
XCTAssert(e1.isValid)
|
||||
XCTAssert(nexus.count == 2)
|
||||
XCTAssert(nexus.numEntities == 2)
|
||||
|
||||
e0.destroy()
|
||||
|
||||
XCTAssert(!e0.isValid)
|
||||
XCTAssert(e1.isValid)
|
||||
XCTAssert(nexus.count == 1)
|
||||
XCTAssert(nexus.numEntities == 1)
|
||||
|
||||
let e2 = nexus.create(entity: "e2")
|
||||
XCTAssert(!e0.isValid)
|
||||
XCTAssert(e1.isValid)
|
||||
XCTAssert(e2.isValid)
|
||||
|
||||
XCTAssert(nexus.count == 2)
|
||||
XCTAssert(nexus.numEntities == 2)
|
||||
|
||||
XCTAssert(!(e0 == e2))
|
||||
XCTAssert(!(e0 === e2))
|
||||
|
|
@ -72,7 +72,7 @@ class NexusTests: XCTestCase {
|
|||
|
||||
func testComponentCreation() {
|
||||
let nexus: Nexus = Nexus()
|
||||
XCTAssert(nexus.count == 0)
|
||||
XCTAssert(nexus.numEntities == 0)
|
||||
|
||||
let e0: Entity = nexus.create(entity: "e0")
|
||||
|
||||
|
|
@ -145,7 +145,7 @@ class NexusTests: XCTestCase {
|
|||
let b = nexus.create()
|
||||
let c = nexus.create()
|
||||
|
||||
XCTAssert(nexus.count == 3)
|
||||
XCTAssert(nexus.numEntities == 3)
|
||||
|
||||
a.assign(Position(x: 0, y: 0))
|
||||
b.assign(Position(x: 0, y: 0))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue