From 853d0bba83b4812acb2b80e6f84da7c4e1582292 Mon Sep 17 00:00:00 2001 From: rrbox <87851278+rrbox@users.noreply.github.com> Date: Sat, 27 Sep 2025 17:53:26 +0900 Subject: [PATCH] Implement ContiguousSparseSet and update Query and World classes to support contiguous array storage --- Sources/ECS/Commons/FeatureFlags.swift | 7 ++- Sources/ECS/Commons/SparseSet.swift | 56 +++++++++++++++++ Sources/ECS/Query/Query.swift | 71 +++++++++++++++++----- Sources/ECS/World/World+Entities.swift | 21 +++++-- Sources/ECS/World/World.swift | 6 +- Sources/ECS/WorldMethods/World+Spawn.swift | 6 +- Sources/ECS_Macros/QueryMacro.swift | 65 ++++++++++++++++---- Tests/ecs-swiftTests/CommandsTests.swift | 4 +- Tests/ecs-swiftTests/ecs_swiftTests.swift | 47 ++++++++++++-- 9 files changed, 239 insertions(+), 44 deletions(-) diff --git a/Sources/ECS/Commons/FeatureFlags.swift b/Sources/ECS/Commons/FeatureFlags.swift index 977521b..514dab0 100644 --- a/Sources/ECS/Commons/FeatureFlags.swift +++ b/Sources/ECS/Commons/FeatureFlags.swift @@ -7,7 +7,12 @@ struct FeatureFlags: OptionSet { let rawValue: UInt8 - static let enabled: FeatureFlags = [] + + static let contiguousArrayStorage = FeatureFlags(rawValue: 1 << 0) + + static var enabled: FeatureFlags = [ + .contiguousArrayStorage + ] static func isEnabled(_ flags: FeatureFlags) -> Bool { Self.enabled.contains(flags) diff --git a/Sources/ECS/Commons/SparseSet.swift b/Sources/ECS/Commons/SparseSet.swift index d576b23..7ec8975 100644 --- a/Sources/ECS/Commons/SparseSet.swift +++ b/Sources/ECS/Commons/SparseSet.swift @@ -60,3 +60,59 @@ public struct SparseSet { return self.dense.indices.contains(i) } } + +public struct ContiguousSparseSet { + typealias DenseIndex = Int + + var sparse: [DenseIndex?] + var dense: [Entity] + var data: ContiguousArray + + public func value(forEntity entity: Entity) -> T? { + guard let i = self.sparse[entity.slot] else { return nil } + guard self.dense[i] == entity else { return nil } + return self.data[i] + } + + public mutating func update(forEntity entity: Entity, _ execute: (inout T) -> ()) { + guard let i = self.sparse[entity.slot] else { return } + guard self.dense[i].generation == entity.generation else { return } + execute(&self.data[i]) + } + + public mutating func update(_ execute: (inout T) -> ()) { + for i in self.data.indices { + execute(&self.data[i]) + } + } + + public mutating func allocate() { + self.sparse.append(nil) + } + + public mutating func insert(_ value: T, withEntity entity: Entity) { + let denseIndex = self.dense.count + self.sparse[entity.slot] = denseIndex + self.dense.append(entity) + self.data.append(value) + } + + public mutating func pop(entity: Entity) { + assert(entity.generation == self.dense[self.sparse[entity.slot]!].generation) + let denseIndexLast = self.dense.count-1 + let removeIndex = self.sparse[entity.slot]! + + self.sparse[self.dense[denseIndexLast].slot] = removeIndex + self.sparse[self.dense[removeIndex].slot] = nil + self.dense.swapAt(removeIndex, denseIndexLast) + self.data.swapAt(removeIndex, denseIndexLast) + self.data.removeLast() + self.dense.removeLast() + } + + public func contains(_ entity: Entity) -> Bool { + guard self.sparse.indices.contains(entity.slot) else { return false } + guard let i = self.sparse[entity.slot] else { return false } + return self.dense.indices.contains(i) + } +} diff --git a/Sources/ECS/Query/Query.swift b/Sources/ECS/Query/Query.swift index 0c0328a..a13ae28 100644 --- a/Sources/ECS/Query/Query.swift +++ b/Sources/ECS/Query/Query.swift @@ -7,26 +7,44 @@ final public class Query: Chunk, SystemParameter { var components = SparseSet>(sparse: [], dense: [], data: []) + var contiguousComponents = ContiguousSparseSet>(sparse: [], dense: [], data: []) public override init() {} public func allocate() { - self.components.allocate() + if FeatureFlags.isEnabled(.contiguousArrayStorage) { + self.contiguousComponents.allocate() + } else { + self.components.allocate() + } } public func insert(entityRecord: EntityRecordRef) { guard let componentRef = entityRecord.ref(C.self) else { return } - self.components.insert(componentRef, withEntity: entityRecord.entity) + if FeatureFlags.isEnabled(.contiguousArrayStorage) { + self.contiguousComponents.insert(componentRef, withEntity: entityRecord.entity) + } else { + self.components.insert(componentRef, withEntity: entityRecord.entity) + } } public func remove(entity: Entity) { - guard self.components.contains(entity) else { return } - self.components.pop(entity: entity) + if FeatureFlags.isEnabled(.contiguousArrayStorage) { + guard self.contiguousComponents.contains(entity) else { return } + self.contiguousComponents.pop(entity: entity) + } else { + guard self.components.contains(entity) else { return } + self.components.pop(entity: entity) + } } override func spawn(entityRecord: EntityRecordRef) { if entityRecord.entity.generation == 0 { - self.components.allocate() + if FeatureFlags.isEnabled(.contiguousArrayStorage) { + self.contiguousComponents.allocate() + } else { + self.components.allocate() + } } self.insert(entityRecord: entityRecord) } @@ -40,27 +58,50 @@ final public class Query: Chunk, SystemParameter { self.despawn(entity: entityRecord.entity) return } - guard !components.contains(entityRecord.entity) else { return } - self.components.insert( - componentRef, - withEntity: entityRecord.entity - ) + if FeatureFlags.isEnabled(.contiguousArrayStorage) { + guard !contiguousComponents.contains(entityRecord.entity) else { return } + self.contiguousComponents.insert( + componentRef, + withEntity: entityRecord.entity + ) + } else { + guard !components.contains(entityRecord.entity) else { return } + self.components.insert( + componentRef, + withEntity: entityRecord.entity + ) + } } /// Query で指定した Component を持つ entity を world から取得し, イテレーションします. public func update(_ f: (inout C) -> ()) { - for ref in self.components.data { - f(&ref.value) + if FeatureFlags.isEnabled(.contiguousArrayStorage) { + for ref in self.contiguousComponents.data { + f(&ref.value) + } + } else { + for ref in self.components.data { + f(&ref.value) + } } } public func update(_ entity: Entity, _ f: (inout C) -> ()) { - guard let ref = self.components.value(forEntity: entity) else { return } - f(&ref.value) + if FeatureFlags.isEnabled(.contiguousArrayStorage) { + guard let ref = self.contiguousComponents.value(forEntity: entity) else { return } + f(&ref.value) + } else { + guard let ref = self.components.value(forEntity: entity) else { return } + f(&ref.value) + } } public func components(forEntity entity: Entity) -> C? { - self.components.value(forEntity: entity)?.value + if FeatureFlags.isEnabled(.contiguousArrayStorage) { + self.contiguousComponents.value(forEntity: entity)?.value + } else { + self.components.value(forEntity: entity)?.value + } } public static func register(to worldStorage: WorldStorageRef) { diff --git a/Sources/ECS/World/World+Entities.swift b/Sources/ECS/World/World+Entities.swift index 209e10e..c4395a6 100644 --- a/Sources/ECS/World/World+Entities.swift +++ b/Sources/ECS/World/World+Entities.swift @@ -7,16 +7,29 @@ public extension World { func insert(entityRecord: EntityRecordRef) { - self.entities.insert(entityRecord, withEntity: entityRecord.entity) + if FeatureFlags.isEnabled(.contiguousArrayStorage) { + self.contiguousEntities.insert(entityRecord, withEntity: entityRecord.entity) + } else { + self.defaultEntities.insert(entityRecord, withEntity: entityRecord.entity) + } } func remove(entity: Entity) { - guard self.entities.contains(entity) else { return } - self.entities.pop(entity: entity) + if FeatureFlags.isEnabled(.contiguousArrayStorage) { + guard self.contiguousEntities.contains(entity) else { return } + self.contiguousEntities.pop(entity: entity) + } else { + guard self.defaultEntities.contains(entity) else { return } + self.defaultEntities.pop(entity: entity) + } } func entityRecord(forEntity entity: Entity) -> EntityRecordRef? { - self.entities.value(forEntity: entity) + if FeatureFlags.isEnabled(.contiguousArrayStorage) { + self.contiguousEntities.value(forEntity: entity) + } else { + self.defaultEntities.value(forEntity: entity) + } } } diff --git a/Sources/ECS/World/World.swift b/Sources/ECS/World/World.swift index 28940b6..ee32af2 100644 --- a/Sources/ECS/World/World.swift +++ b/Sources/ECS/World/World.swift @@ -43,14 +43,16 @@ */ final public class World { - var entities: SparseSet + var defaultEntities: SparseSet + var contiguousEntities: ContiguousSparseSet var preUpdateSchedule: Schedule var updateSchedule: Schedule var postUpdateSchedule: Schedule public let worldStorage: WorldStorageRef init(worldStorage: WorldStorageRef) { - self.entities = SparseSet(sparse: [], dense: [], data: []) + self.defaultEntities = SparseSet(sparse: [], dense: [], data: []) + self.contiguousEntities = ContiguousSparseSet(sparse: [], dense: [], data: []) self.preUpdateSchedule = .preStartUp self.updateSchedule = .startUp self.postUpdateSchedule = .postStartUp diff --git a/Sources/ECS/WorldMethods/World+Spawn.swift b/Sources/ECS/WorldMethods/World+Spawn.swift index 740833c..0d0c0c3 100644 --- a/Sources/ECS/WorldMethods/World+Spawn.swift +++ b/Sources/ECS/WorldMethods/World+Spawn.swift @@ -25,7 +25,11 @@ extension World { /// entity へのコンポーネントの登録などは, push の後に行われます. func push(entityRecord: EntityRecordRef) { if entityRecord.entity.generation == 0 { - self.entities.allocate() + if FeatureFlags.isEnabled(.contiguousArrayStorage) { + self.contiguousEntities.allocate() + } else { + self.defaultEntities.allocate() + } } self.insert(entityRecord: entityRecord) diff --git a/Sources/ECS_Macros/QueryMacro.swift b/Sources/ECS_Macros/QueryMacro.swift index 289fe99..6cfd4d2 100644 --- a/Sources/ECS_Macros/QueryMacro.swift +++ b/Sources/ECS_Macros/QueryMacro.swift @@ -50,26 +50,44 @@ struct QueryMacro: DeclarationMacro { """ final public class Query\(raw: n)<\(raw: genericArguments)>: Chunk, SystemParameter, QueryProtocol { public var components = SparseSet<(\(raw: refTypes))>(sparse: [], dense: [], data: []) + public var contiguousComponents = ContiguousSparseSet<(\(raw: refTypes))>(sparse: [], dense: [], data: []) public override init() {} public func allocate() { - self.components.allocate() + if FeatureFlags.isEnabled(.contiguousArrayStorage) { + self.contiguousComponents.allocate() + } else { + self.components.allocate() + } } public func insert(entityRecord: EntityRecordRef) { guard \(raw: refDeclarationsFromRecord) else { return } - self.components.insert((\(raw: refs)), withEntity: entityRecord.entity) + if FeatureFlags.isEnabled(.contiguousArrayStorage) { + self.contiguousComponents.insert((\(raw: refs)), withEntity: entityRecord.entity) + } else { + self.components.insert((\(raw: refs)), withEntity: entityRecord.entity) + } } public func remove(entity: Entity) { - guard self.components.contains(entity) else { return } - self.components.pop(entity: entity) + if FeatureFlags.isEnabled(.contiguousArrayStorage) { + guard self.contiguousComponents.contains(entity) else { return } + self.contiguousComponents.pop(entity: entity) + } else { + guard self.components.contains(entity) else { return } + self.components.pop(entity: entity) + } } public override func spawn(entityRecord: EntityRecordRef) { if entityRecord.entity.generation == 0 { - self.components.allocate() + if FeatureFlags.isEnabled(.contiguousArrayStorage) { + self.contiguousComponents.allocate() + } else { + self.components.allocate() + } } self.insert(entityRecord: entityRecord) } @@ -83,24 +101,45 @@ struct QueryMacro: DeclarationMacro { self.despawn(entity: entityRecord.entity) return } - guard !components.contains(entityRecord.entity) else { return } - self.components.insert((\(raw: refs)), withEntity: entityRecord.entity) + if FeatureFlags.isEnabled(.contiguousArrayStorage) { + guard !contiguousComponents.contains(entityRecord.entity) else { return } + self.contiguousComponents.insert((\(raw: refs)), withEntity: entityRecord.entity) + } else { + guard !components.contains(entityRecord.entity) else { return } + self.components.insert((\(raw: refs)), withEntity: entityRecord.entity) + } } public func update(_ f: (\(raw: parameters)) -> ()) { - self.components.data.forEach { components in - f(\(raw: componentRefs)) + if FeatureFlags.isEnabled(.contiguousArrayStorage) { + self.contiguousComponents.data.forEach { components in + f(\(raw: componentRefs)) + } + } else { + self.components.data.forEach { components in + f(\(raw: componentRefs)) + } } } public func update(_ entity: Entity, _ f: (\(raw: parameters)) -> ()) { - guard let components = self.components.value(forEntity: entity) else { return } - f(\(raw: componentRefs)) + if FeatureFlags.isEnabled(.contiguousArrayStorage) { + guard let components = self.contiguousComponents.value(forEntity: entity) else { return } + f(\(raw: componentRefs)) + } else { + guard let components = self.components.value(forEntity: entity) else { return } + f(\(raw: componentRefs)) + } } public func components(forEntity entity: Entity) -> (\(raw: valueTypes))? { - guard let components = components.value(forEntity: entity) else { return nil } - return (\(raw: componentValuess)) + if FeatureFlags.isEnabled(.contiguousArrayStorage) { + guard let components = contiguousComponents.value(forEntity: entity) else { return nil } + return (\(raw: componentValuess)) + } else { + guard let components = components.value(forEntity: entity) else { return nil } + return (\(raw: componentValuess)) + } } public static func register(to worldStorage: WorldStorageRef) { diff --git a/Tests/ecs-swiftTests/CommandsTests.swift b/Tests/ecs-swiftTests/CommandsTests.swift index cd99ee2..873dd74 100644 --- a/Tests/ecs-swiftTests/CommandsTests.swift +++ b/Tests/ecs-swiftTests/CommandsTests.swift @@ -43,7 +43,7 @@ final class CommandsTests: XCTestCase { world.applyCommands(commands: commands) XCTAssertEqual(commands.commandQueue.count, 0) - XCTAssertEqual(world.entities.data.count, 3) + XCTAssertEqual(world.defaultEntities.data.count, 3) for testEntity in testEntities { commands.push(command: TestCommand_Despawn(entity: testEntity)) @@ -53,6 +53,6 @@ final class CommandsTests: XCTestCase { world.applyCommands(commands: commands) XCTAssertEqual(commands.commandQueue.count, 0) - XCTAssertEqual(world.entities.data.count, 0) + XCTAssertEqual(world.defaultEntities.data.count, 0) } } diff --git a/Tests/ecs-swiftTests/ecs_swiftTests.swift b/Tests/ecs-swiftTests/ecs_swiftTests.swift index 51327e2..20d9fc7 100644 --- a/Tests/ecs-swiftTests/ecs_swiftTests.swift +++ b/Tests/ecs-swiftTests/ecs_swiftTests.swift @@ -6,14 +6,14 @@ struct Text: Component { } func entitycreate(commands: Commands) { - for i in 1...20000 { + for i in 1...100000 { commands.spawn() .addComponent(Text(v: "\(i)")) } } func entitycreate2(commands: Commands) { - for i in 1...20000 { + for i in 1...100000 { commands.spawn() .addComponent(Text(v: "\(i)")) } @@ -49,14 +49,30 @@ final class ecs_swiftTests: XCTestCase { // set up: 1 // update: 1 // 0.00478 s - func testPerformance() { + func testPerformanceDefaultStorage() { + FeatureFlags.enabled.remove(.contiguousArrayStorage) let world = World() .addSystem(.startUp, entitycreate(commands:)) .addSystem(.update, update(query:)) world.setUpWorld() world.update(currentTime: -1) - print(world.entities.data.count) + print(world.defaultEntities.data.count) + + measure { + world.update(currentTime: 0) + } + } + + func testPerformanceContiguousStorage() { + FeatureFlags.enabled.insert(.contiguousArrayStorage) + let world = World() + .addSystem(.startUp, entitycreate(commands:)) + .addSystem(.update, update(query:)) + world.setUpWorld() + world.update(currentTime: -1) + + print(world.contiguousEntities.data.count) measure { world.update(currentTime: 0) @@ -67,7 +83,26 @@ final class ecs_swiftTests: XCTestCase { // set up: 1 // update: 4 // 0.0158 s -> およそ 4 倍 - func testUpdate4Performance() { + func testUpdate4PerformanceDefaultStorage() { + FeatureFlags.enabled.remove(.contiguousArrayStorage) + let world = World() + .addSystem(.startUp, entitycreate(commands:)) + .addSystem(.update, update(query:)) + .addSystem(.update, update2(query:)) + .addSystem(.update, update3(query:)) + .addSystem(.update, update4(query:)) + world.setUpWorld() + world.update(currentTime: -1) + + print(world.defaultEntities.data.count) + + measure { + world.update(currentTime: 0) + } + } + + func testUpdate4PerformanceContiguousStorage() { + FeatureFlags.enabled.insert(.contiguousArrayStorage) let world = World() .addSystem(.startUp, entitycreate(commands:)) .addSystem(.update, update(query:)) @@ -77,7 +112,7 @@ final class ecs_swiftTests: XCTestCase { world.setUpWorld() world.update(currentTime: -1) - print(world.entities.data.count) + print(world.contiguousEntities.data.count) measure { world.update(currentTime: 0)