diff --git a/Sources/FirebladeECS/ManagedContiguousArray.swift b/Sources/FirebladeECS/ManagedContiguousArray.swift index b3a1256..f9a985d 100644 --- a/Sources/FirebladeECS/ManagedContiguousArray.swift +++ b/Sources/FirebladeECS/ManagedContiguousArray.swift @@ -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 = [] public required init(minCount: Int = chunkSize) { _store = ContiguousArray(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 } diff --git a/Sources/FirebladeECS/Nexus+Component.swift b/Sources/FirebladeECS/Nexus+Component.swift index 916e5db..8c5e1db 100644 --- a/Sources/FirebladeECS/Nexus+Component.swift +++ b/Sources/FirebladeECS/Nexus+Component.swift @@ -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) diff --git a/Sources/FirebladeECS/Nexus+Entity.swift b/Sources/FirebladeECS/Nexus+Entity.swift index 0fc534c..1c32974 100644 --- a/Sources/FirebladeECS/Nexus+Entity.swift +++ b/Sources/FirebladeECS/Nexus+Entity.swift @@ -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) diff --git a/Sources/FirebladeECS/Nexus+Family.swift b/Sources/FirebladeECS/Nexus+Family.swift index 1456c81..0ee1c2e 100644 --- a/Sources/FirebladeECS/Nexus+Family.swift +++ b/Sources/FirebladeECS/Nexus+Family.swift @@ -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) } diff --git a/Sources/FirebladeECS/Nexus.swift b/Sources/FirebladeECS/Nexus.swift index 4a70e79..f4674c1 100644 --- a/Sources/FirebladeECS/Nexus.swift +++ b/Sources/FirebladeECS/Nexus.swift @@ -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 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 = [:] diff --git a/Sources/FirebladeECS/SparseSet.swift b/Sources/FirebladeECS/SparseSet.swift new file mode 100644 index 0000000..b51995b --- /dev/null +++ b/Sources/FirebladeECS/SparseSet.swift @@ -0,0 +1,68 @@ +// +// SparseSet.swift +// FirebladeECS +// +// Created by Christian Treffs on 30.10.17. +// + +public struct SparseComponentSet { + fileprivate typealias ComponentIdx = Int + fileprivate typealias EntryTuple = (entityId: EntityIdentifier, component: Element) + fileprivate var dense: ContiguousArray + fileprivate var sparse: [EntityIdentifier: ComponentIdx] + + public init(_ min: Int = 1024) { + dense = ContiguousArray() + 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 { + var iterator = dense.makeIterator() + + return AnyIterator { + iterator.next()??.component + } + } +} diff --git a/Sources/FirebladeECSDemo/main.swift b/Sources/FirebladeECSDemo/main.swift index b317ea9..1d36ccd 100644 --- a/Sources/FirebladeECSDemo/main.swift +++ b/Sources/FirebladeECSDemo/main.swift @@ -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.. 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) diff --git a/Tests/FirebladeECSTests/FamilyTests.swift b/Tests/FirebladeECSTests/FamilyTests.swift index 5a98caf..cd92aa0 100644 --- a/Tests/FirebladeECSTests/FamilyTests.swift +++ b/Tests/FirebladeECSTests/FamilyTests.swift @@ -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 diff --git a/Tests/FirebladeECSTests/NexusTests.swift b/Tests/FirebladeECSTests/NexusTests.swift index ced81bf..d368034 100644 --- a/Tests/FirebladeECSTests/NexusTests.swift +++ b/Tests/FirebladeECSTests/NexusTests.swift @@ -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)) diff --git a/Tests/FirebladeECSTests/SparseComponentSetTests.swift b/Tests/FirebladeECSTests/SparseComponentSetTests.swift new file mode 100644 index 0000000..2595cb6 --- /dev/null +++ b/Tests/FirebladeECSTests/SparseComponentSetTests.swift @@ -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() + + 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) + } +}