From 7c7b38253b6d156d7ede0ce14f7303d37818d83f Mon Sep 17 00:00:00 2001 From: Christian Treffs Date: Mon, 30 Oct 2017 09:02:03 +0100 Subject: [PATCH] Optimized a lot --- Package.swift | 9 +- .../ContiguousComponentArray.swift | 62 ------- Sources/FirebladeECS/Family.swift | 2 +- .../FirebladeECS/ManagedContiguousArray.swift | 119 ++++++++++++ Sources/FirebladeECS/Nexus+Component.swift | 4 +- Sources/FirebladeECS/Nexus+Family.swift | 50 +++-- Sources/FirebladeECS/Nexus.swift | 4 +- Sources/FirebladeECS/Profiler.swift | 10 + Sources/FirebladeECS/Timer.swift | 50 +++++ Sources/FirebladeECSDemo/main.swift | 174 ++++++++++++++++++ 10 files changed, 399 insertions(+), 85 deletions(-) delete mode 100644 Sources/FirebladeECS/ContiguousComponentArray.swift create mode 100644 Sources/FirebladeECS/ManagedContiguousArray.swift create mode 100644 Sources/FirebladeECS/Profiler.swift create mode 100644 Sources/FirebladeECS/Timer.swift create mode 100644 Sources/FirebladeECSDemo/main.swift diff --git a/Package.swift b/Package.swift index 2531b31..cb5210f 100644 --- a/Package.swift +++ b/Package.swift @@ -9,11 +9,15 @@ let package = Package( // Products define the executables and libraries produced by a package, and make them visible to other packages. .library( name: "FirebladeECS", - targets: ["FirebladeECS"]) + targets: ["FirebladeECS"]), + .executable( + name: "FirebladeECSDemo", + targets: ["FirebladeECSDemo"]) ], dependencies: [ // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), + .package(url: "https://github.com/PureSwift/CSDL2.git", .branch("master")) ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -21,6 +25,9 @@ let package = Package( .target( name: "FirebladeECS", dependencies: []), + .target( + name: "FirebladeECSDemo", + dependencies: ["FirebladeECS"]), .testTarget( name: "FirebladeECSTests", dependencies: ["FirebladeECS"]) diff --git a/Sources/FirebladeECS/ContiguousComponentArray.swift b/Sources/FirebladeECS/ContiguousComponentArray.swift deleted file mode 100644 index fd8a659..0000000 --- a/Sources/FirebladeECS/ContiguousComponentArray.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// ContiguousComponentArray.swift -// FirebladeECS -// -// Created by Christian Treffs on 28.10.17. -// - -private let pow2: [Int] = [ 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576, 2097152, 4194304, 8388608, 16777216, 33554432, 67108864, 134217728, 268435456, 536870912, 1073741824, 2147483648, 4294967296 -] - -private func nearestToPow2(_ value: Int) -> Int { - let exp = (value.bitWidth-value.leadingZeroBitCount) - return pow2[exp] -} - -public class ContiguousComponentArray { - public typealias Element = Component - - private var _store: ContiguousArray - - public init(minEntityCount minCount: Int) { - let count = nearestToPow2(minCount) - _store = ContiguousArray(repeating: nil, count: count) - } - - public func insert(_ element: Element, at entityIdx: EntityIndex) { - - if needsToGrow(entityIdx) { - grow(to: entityIdx) - } - _store[entityIdx] = element - } - - public func has(_ entityIdx: EntityIndex) -> Bool { - if _store.count <= entityIdx { return false } - return _store[entityIdx] != nil - } - - public func get(at entityIdx: EntityIndex) -> Element? { - return _store[entityIdx] - } - - public func remove(at entityIdx: EntityIndex) { - return _store[entityIdx] = nil - } - - fileprivate func needsToGrow(_ entityId: EntityIndex) -> Bool { - return entityId > _store.count - 1 - } - - fileprivate func grow(to minIndex: Int) { - if minIndex >= _store.count { - let newCapacity: Int = nearestToPow2(minIndex) - let count: Int = newCapacity-_store.count - let nilElements: ContiguousArray = ContiguousArray.init(repeating: nil, count: count) - - _store.reserveCapacity(newCapacity) - _store.append(contentsOf: nilElements) - } - } - -} diff --git a/Sources/FirebladeECS/Family.swift b/Sources/FirebladeECS/Family.swift index 411246e..1608dd6 100644 --- a/Sources/FirebladeECS/Family.swift +++ b/Sources/FirebladeECS/Family.swift @@ -43,7 +43,7 @@ extension Family { /*public var members: LazyMapCollection>, Entity> { return nexus.members(of: self) }*/ - internal var memberIds: EntityIdSet { + internal var memberIds: [EntityIdentifier] { return nexus.members(of: self) } } diff --git a/Sources/FirebladeECS/ManagedContiguousArray.swift b/Sources/FirebladeECS/ManagedContiguousArray.swift new file mode 100644 index 0000000..b3a1256 --- /dev/null +++ b/Sources/FirebladeECS/ManagedContiguousArray.swift @@ -0,0 +1,119 @@ +// +// ManagedContiguousArray.swift +// FirebladeECS +// +// Created by Christian Treffs on 28.10.17. +// + +private let pow2: [Int] = [ 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576, 2097152, 4194304, 8388608, 16777216, 33554432, 67108864, 134217728, 268435456, 536870912, 1073741824, 2147483648, 4294967296 +] + +private func nearestToPow2(_ value: Int) -> Int { + let exp = (value.bitWidth-value.leadingZeroBitCount) + return pow2[exp] +} + +public protocol ManagedContiguousArrayProtocol: class { + associatedtype Element + static var chunkSize: Int { get } + init(minCount: Int) + func insert(_ element: Element, at index: Int) + func has(_ index: Int) -> Bool + func get(at index: Int) -> Element? + func remove(at index: Int) +} + +public class ManagedContiguousArray: ManagedContiguousArrayProtocol { + public static var chunkSize: Int = 4096 + + public typealias Element = Any + var _store: ContiguousArray = [] + public required init(minCount: Int = chunkSize) { + _store = ContiguousArray(repeating: nil, count: minCount) + } + + public func insert(_ element: Element, at index: Int) { + if needsToGrow(index) { + grow(including: index) + } + _store[index] = element + } + public func has(_ index: Int) -> Bool { + if _store.count <= index { return false } + return _store[index] != nil + } + + public func get(at index: Int) -> Element? { + return _store[index] + } + + public func remove(at index: Int) { + return _store[index] = nil + } + + internal func needsToGrow(_ index: Int) -> Bool { + return index > _store.count - 1 + } + + internal func grow(including index: Int) { + //var t = Timer() + //t.start() + let newCapacity: Int = nearest(to: index) + let count: Int = newCapacity-_store.count + //_store.reserveCapacity(newCapacity) + for _ in 0.. Int { + let delta = Float(index) / Float(ManagedContiguousArray.chunkSize) + let multiplier = Int(delta) + 1 + return multiplier * ManagedContiguousArray.chunkSize + } +} + +public class ContiguousComponentArray: ManagedContiguousArray { + public typealias Element = Component +} + +public class ContiguousEntityIdArray: ManagedContiguousArray { + public typealias Element = EntityIdentifier +} + +/* + public func insert(_ element: Element, at entityIdx: EntityIndex) { + super.insert(element, at: entityIdx) + } + + public func has(_ entityIdx: EntityIndex) -> Bool { + if _store.count <= entityIdx { return false } + return _store[entityIdx] != nil + } + + public func get(at entityIdx: EntityIndex) -> Element? { + return _store[entityIdx] + } + + public func remove(at entityIdx: EntityIndex) { + return _store[entityIdx] = nil + } + + fileprivate func needsToGrow(_ entityId: EntityIndex) -> Bool { + return entityId > _store.count - 1 + } + + fileprivate func grow(to minIndex: Int) { + + let newCapacity: Int = nearestToPow2(minIndex) + let count: Int = newCapacity-_store.count + let nilElements: ContiguousArray = ContiguousArray.init(repeating: nil, count: count) + + _store.reserveCapacity(newCapacity) + _store.append(contentsOf: nilElements) + + } + +*/ diff --git a/Sources/FirebladeECS/Nexus+Component.swift b/Sources/FirebladeECS/Nexus+Component.swift index e5d8580..916e5db 100644 --- a/Sources/FirebladeECS/Nexus+Component.swift +++ b/Sources/FirebladeECS/Nexus+Component.swift @@ -34,7 +34,7 @@ extension Nexus { if componentsByType[componentId] != nil { componentsByType[componentId]!.insert(component, at: entityIdx) } else { - componentsByType[componentId] = UniformComponents(minEntityCount: entities.count) + componentsByType[componentId] = UniformComponents() componentsByType[componentId]!.insert(component, at: entityIdx) } @@ -63,7 +63,7 @@ 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) + return uniformComponents.get(at: entityId.index) as? Component } public func get(for entityId: EntityIdentifier) -> C? where C: Component { diff --git a/Sources/FirebladeECS/Nexus+Family.swift b/Sources/FirebladeECS/Nexus+Family.swift index 988fa42..1456c81 100644 --- a/Sources/FirebladeECS/Nexus+Family.swift +++ b/Sources/FirebladeECS/Nexus+Family.swift @@ -39,23 +39,24 @@ extension Nexus { return family.traits.isMatch(components: componentSet) } - public func members(of family: Family) -> EntityIdSet { + public func members(of family: Family) -> [EntityIdentifier] { let traitHash: FamilyTraitSetHash = family.traits.hashValue return familyMembersByTraitHash[traitHash] ?? [] // FIXME: fail? } - /*public func members(of family: Family) -> LazyMapCollection>, Entity> { - return members(of: family).lazy.flatMap { self.get(entity: $0) } - }*/ - public func isMember(_ entity: Entity, in family: Family) -> Bool { return isMember(entity.identifier, in: family) } + public func isMember(byHash traitSetEntityIdHash: TraitEntityIdHash) -> Bool { + return familyContainsEntityId[traitSetEntityIdHash] ?? false + } + public func isMember(_ entityId: EntityIdentifier, in family: Family) -> Bool { let traitHash: FamilyTraitSetHash = family.traits.hashValue - // FIXME: this may be costly for many entities in family - return familyMembersByTraitHash[traitHash]?.contains(entityId) ?? false + // FIXME: this is costly! + guard let members: [EntityIdentifier] = familyMembersByTraitHash[traitHash] else { return false } + return members.contains(entityId) } fileprivate func get(family traits: FamilyTraitSet) -> Family? { @@ -82,44 +83,57 @@ extension Nexus { // MARK: - update family membership + fileprivate func calculateTraitEntityIdHash(traitHash: FamilyTraitSetHash, entityIdx: EntityIndex) -> TraitEntityIdHash { + return hash(combine: traitHash, entityIdx) + } + func update(membership family: Family, for entityId: EntityIdentifier) { let entityIdx: EntityIndex = entityId.index + let traitHash: FamilyTraitSetHash = family.traits.hashValue guard let componentIds: ComponentIdentifiers = componentIdsByEntity[entityIdx] else { return } - // FIXME: bottle neck + + let trash: TraitEntityIdHash = calculateTraitEntityIdHash(traitHash: traitHash, entityIdx: entityIdx) + let is_Member: Bool = isMember(byHash: trash) + let componentsSet: ComponentSet = ComponentSet.init(componentIds) - let isMember: Bool = family.isMember(entityId) let isMatch: Bool = family.traits.isMatch(components: componentsSet) - switch (isMatch, isMember) { + switch (isMatch, is_Member) { case (true, false): - add(to: family, entityId: entityId) + add(to: family, entityId: entityId, with: trash) case (false, true): - remove(from: family, entityId: entityId) + remove(from: family, entityId: entityId, with: trash) default: break } } - fileprivate func add(to family: Family, entityId: EntityIdentifier) { + fileprivate func add(to family: Family, entityId: EntityIdentifier, with traitEntityIdHash: TraitEntityIdHash) { let traitHash: FamilyTraitSetHash = family.traits.hashValue - if familyMembersByTraitHash[traitHash] != nil { - familyMembersByTraitHash[traitHash]?.insert(entityId) + // here we already checked if entity is a member + familyMembersByTraitHash[traitHash]!.append(entityId) } else { - familyMembersByTraitHash[traitHash] = EntityIdSet(arrayLiteral: entityId) + familyMembersByTraitHash[traitHash] = [EntityIdentifier].init(arrayLiteral: entityId) + familyMembersByTraitHash.reserveCapacity(4096) } + familyContainsEntityId[traitEntityIdHash] = true + notify(FamilyMemberAdded(member: entityId, to: family.traits)) } - fileprivate func remove(from family: Family, entityId: EntityIdentifier) { + fileprivate func remove(from family: Family, entityId: EntityIdentifier, with traitEntityIdHash: TraitEntityIdHash) { let traitHash: FamilyTraitSetHash = family.traits.hashValue - guard let removed: EntityIdentifier = familyMembersByTraitHash[traitHash]?.remove(entityId) else { + // FIXME: index of is not cheep + guard let indexInFamily = familyMembersByTraitHash[traitHash]?.index(of: entityId) else { assert(false, "removing entity id \(entityId) that is not in family \(family)") report("removing entity id \(entityId) that is not in family \(family)") return } + let removed: EntityIdentifier = familyMembersByTraitHash[traitHash]!.remove(at: indexInFamily) + familyContainsEntityId[traitEntityIdHash] = false notify(FamilyMemberRemoved(member: removed, from: family.traits)) } diff --git a/Sources/FirebladeECS/Nexus.swift b/Sources/FirebladeECS/Nexus.swift index 6079fb7..4a70e79 100644 --- a/Sources/FirebladeECS/Nexus.swift +++ b/Sources/FirebladeECS/Nexus.swift @@ -42,7 +42,8 @@ public class Nexus { var freeEntities: ContiguousArray var familiyByTraitHash: [FamilyTraitSetHash: Family] - var familyMembersByTraitHash: [FamilyTraitSetHash: EntityIdSet] + var familyMembersByTraitHash: [FamilyTraitSetHash: [EntityIdentifier]] + var familyContainsEntityId: [TraitEntityIdHash: Bool] public init() { entities = Entities() @@ -52,6 +53,7 @@ public class Nexus { freeEntities = ContiguousArray() familiyByTraitHash = [:] familyMembersByTraitHash = [:] + familyContainsEntityId = [:] } } diff --git a/Sources/FirebladeECS/Profiler.swift b/Sources/FirebladeECS/Profiler.swift new file mode 100644 index 0000000..7b3c941 --- /dev/null +++ b/Sources/FirebladeECS/Profiler.swift @@ -0,0 +1,10 @@ +// +// Profiler.swift +// FirebladeECS +// +// Created by Christian Treffs on 28.10.17. +// + +struct Profiler { + +} diff --git a/Sources/FirebladeECS/Timer.swift b/Sources/FirebladeECS/Timer.swift new file mode 100644 index 0000000..11ff31d --- /dev/null +++ b/Sources/FirebladeECS/Timer.swift @@ -0,0 +1,50 @@ +// +// Timer.swift +// FirebladeECS +// +// Created by Christian Treffs on 28.10.17. +// + +import Darwin.Mach.mach_time + +struct Timer { + private let numerator: UInt64 + private let denominator: UInt64 + private var startTime: UInt64 = 0 + private var stopTime: UInt64 = 0 + + init() { + var timeBaseInfo = mach_timebase_info.init(numer: 0, denom: 0 ) + let success: kern_return_t = mach_timebase_info(&timeBaseInfo) + assert(KERN_SUCCESS == success) + numerator = UInt64(timeBaseInfo.numer) + denominator = UInt64(timeBaseInfo.denom) + } + + mutating func start() { + startTime = mach_absolute_time() + } + mutating func stop() { + stopTime = mach_absolute_time() + } + mutating func reset() { + startTime = 0 + stopTime = 0 + } + + var nanoSeconds: UInt64 { + return ((stopTime - startTime) * numerator) / denominator + } + + var microSeconds: Double { + return Double(nanoSeconds) / 1.0e3 + } + + var milliSeconds: Double { + return Double(nanoSeconds) / 1.0e6 + } + + var seconds: Double { + return Double(nanoSeconds) / 1.0e9 + } +} diff --git a/Sources/FirebladeECSDemo/main.swift b/Sources/FirebladeECSDemo/main.swift new file mode 100644 index 0000000..6c985bb --- /dev/null +++ b/Sources/FirebladeECSDemo/main.swift @@ -0,0 +1,174 @@ +import CSDL2 +import FirebladeECS + +if SDL_Init(SDL_INIT_VIDEO) != 0 { + fatalError("could not init video") +} +let width: Int32 = 640 +let height: Int32 = 480 +let hWin = SDL_CreateWindow("Fireblade ECS demo", 100, 100, width, height, SDL_WINDOW_SHOWN.rawValue) +if hWin == nil { + SDL_Quit() + fatalError("could not crate window") +} + +func randNorm() -> Double { + return Double(arc4random()) / Double(UInt32.max) +} + +// won't produce pure black +func randColor() -> UInt8 { + return UInt8(randNorm() * 254) + 1 +} + +let nexus = Nexus() + +class Position: Component { + var x: Int32 = width/2 + var y: Int32 = height/2 +} +class Color: Component { + var r: UInt8 = randColor() + var g: UInt8 = randColor() + var b: UInt8 = randColor() +} + +func createScene() { + + let numEntities: Int = 10_000 + + for i in 0.. width { + x = -x + } + if y < 0 || y > height { + y = -y + } + + pos!.x = x + pos!.y = y + } + } + +} + +class PositionResetSystem { + let family = nexus.family(requiresAll: [Position.self], excludesAll: []) + + func update() { + family.iterate(components: Position.self) { (_, pos) in + pos!.x = width/2 + pos!.y = height/2 + } + } +} + +class ColorSystem { + let family = nexus.family(requiresAll: [Color.self], excludesAll: []) + + func update() { + family.iterate(components: Color.self) { (_, color) in + + color!.r = randColor() + color!.g = randColor() + color!.b = randColor() + } + } +} + +class RenderSystem { + let hRenderer: OpaquePointer? + let family = nexus.family(requiresAll: [Position.self, Color.self], excludesAll: []) + + init(hWin: OpaquePointer?) { + hRenderer = SDL_CreateRenderer(hWin, -1, SDL_RENDERER_ACCELERATED.rawValue) + if hRenderer == nil { + SDL_DestroyWindow(hWin) + SDL_Quit() + fatalError("could not create renderer") + } + } + + deinit { + SDL_DestroyRenderer(hRenderer) + } + + func render() { + + SDL_SetRenderDrawColor( hRenderer, 0, 0, 0, 255 ) // black + SDL_RenderClear(hRenderer) // clear screen + + family.iterate(components: Position.self, Color.self) { [unowned self] (_, pos, color) in + var rect = SDL_Rect(x: pos!.x, y: pos!.y, w: 2, h: 2) + + SDL_SetRenderDrawColor(self.hRenderer, color!.r, color!.g, color!.b, 255) + SDL_RenderFillRect(self.hRenderer, &rect) + } + + SDL_RenderPresent(hRenderer) + } +} +let positionSystem = PositionSystem() +let positionResetSystem = PositionResetSystem() +let renderSystem = RenderSystem(hWin: hWin) +let colorSystem = ColorSystem() + +createScene() + +var event: SDL_Event = SDL_Event() +var quit: Bool = false +while quit == false { + while SDL_PollEvent(&event) == 1 { + switch SDL_EventType(rawValue: event.type) { + case SDL_QUIT: + quit = true + break + case SDL_KEYDOWN: + switch Int(event.key.keysym.sym) { + case SDLK_ESCAPE: + quit = true + break + case SDLK_c: + colorSystem.update() + case SDLK_r: + positionResetSystem.update() + case SDLK_s: + positionSystem.acceleration = 0.0 + case SDLK_PLUS: + positionSystem.acceleration += 0.1 + case SDLK_MINUS: + positionSystem.acceleration -= 0.1 + case SDLK_SPACE: + positionSystem.acceleration = 4.0 + default: + break + } + default: + break + } + } + + positionSystem.update() + + renderSystem.render() +} + +SDL_DestroyWindow(hWin) +SDL_Quit()