diff --git a/Sources/ECS/Commons/FeatureFlags.swift b/Sources/ECS/Commons/FeatureFlags.swift new file mode 100644 index 0000000..977521b --- /dev/null +++ b/Sources/ECS/Commons/FeatureFlags.swift @@ -0,0 +1,15 @@ +// +// FeatureFlags.swift +// ECS_Swift +// +// Created by rrbox on 2025/09/27. +// + +struct FeatureFlags: OptionSet { + let rawValue: UInt8 + static let enabled: FeatureFlags = [] + + static func isEnabled(_ flags: FeatureFlags) -> Bool { + Self.enabled.contains(flags) + } +} diff --git a/Sources/PlugIns/Graphic2D/Commands/EntityCommands+Graphic.swift b/Sources/PlugIns/Graphic2D/Commands/EntityCommands+Graphic.swift index 88063d1..33984fd 100644 --- a/Sources/PlugIns/Graphic2D/Commands/EntityCommands+Graphic.swift +++ b/Sources/PlugIns/Graphic2D/Commands/EntityCommands+Graphic.swift @@ -8,23 +8,15 @@ import SpriteKit import ECS -public struct Child: Component { - var _parent: Entity - public var parent: Entity { - self._parent - } -} +public struct Child: Component {} -public struct Parent: Component { - var _children: Set - public var children: Set { - self._children - } -} +public struct Parent: Component {} -struct _RemoveFromParentTransaction: Component { +struct _RemoveFromParentTransaction: Component {} -} +struct _RemoveAllChildrenTransaction: Component {} + +struct _DespawnAllChildrenTransaction: Component {} final class AddChild: EntityCommand { let child: Entity @@ -36,22 +28,20 @@ final class AddChild: EntityCommand { override func runCommand(forRecord record: EntityRecordRef, inWorld world: World) { let childRecord = world.entityRecord(forEntity: self.child)! childRecord.addComponent(_AddChildNodeTransaction(parentEntity: self.entity)) + world.worldStorage.chunkStorageRef.pushUpdated(entityRecord: childRecord) } } final class RemoveAllChildren: EntityCommand { override func runCommand(forRecord record: EntityRecordRef, inWorld world: World) { - let node = record.component(ofType: Graphic.self)!.nodeRef - node.removeAllChildren() - record.componentRef(ofType: Parent.self)?.value._children = [] - - for child in record.componentRef(ofType: Parent.self)!.value.children { - let childRecord = world.entityRecord(forEntity: child)! - childRecord.removeComponent(ofType: Child.self) - world.worldStorage.chunkStorageRef.pushUpdated(entityRecord: childRecord) - } + record.addComponent(_RemoveAllChildrenTransaction()) + } +} +final class DespawnAllChildren: EntityCommand { + override func runCommand(forRecord record: EntityRecordRef, inWorld world: World) { + record.addComponent(_DespawnAllChildrenTransaction()) } } @@ -106,43 +96,96 @@ public extension EntityCommands { func removeChildIfDespawned( despawnEvent: EventReader, - query: Query, - parentQuery: Query + hierarchy: Resource, + commands: Commands ) { + let hierarchy = hierarchy.resource for event in despawnEvent.events { let entity = event.despawnedEntity - guard let parent = query.components(forEntity: entity)?.parent else { continue } - parentQuery.update(parent) { p in - p._children.remove(entity) - } + let parent = hierarchy.parent(of: entity) + hierarchy.removeFromParent(entity) + guard let parent, hierarchy.childrenIsEmpty(for: parent) else { continue } + commands.entity(parent) + .removeComponent(ofType: Parent.self) } } -// これが実行される時点ですでに parentEntity から despawn した parent が消えている. func despawnChildRecursive( despawnedEntity: Entity, - children: Query2, + hierarchy: Resource, commands: Commands ) { - children.update { entity, child in - if child.parent == despawnedEntity { - despawnChildRecursive(despawnedEntity: entity, children: children, commands: commands) - commands.despawn(entity: entity) - } + guard let children = hierarchy.resource.children(of: despawnedEntity) else { return } + for child in children { + despawnChildRecursive( + despawnedEntity: child, + hierarchy: hierarchy, + commands: commands + ) + commands.despawn(entity: child) } } -// これが実行される時点ですでに parentEntity から despawn した parent が消えている. func despawnChildIfParentDespawned( despawnedEntityEvent: EventReader, - children: Query2, + hierarchy: Resource, commands: Commands ) { - // despawn した entity と自分の親が一致する子を despawn する. for event in despawnedEntityEvent.events { let despawnedEntity = event.despawnedEntity - despawnChildRecursive(despawnedEntity: despawnedEntity, - children: children, - commands: commands) + despawnChildRecursive( + despawnedEntity: despawnedEntity, + hierarchy: hierarchy, + commands: commands + ) + hierarchy.resource.removeRecursively(entity: despawnedEntity) + } +} + +// TODO: child を despawn するかどうか検討する +// - despawn する場合: post update で状態を反映させる方法を検討する +// - despawn しない場合: child から Child component を外す +func removeAllChildren( + targetNodes: Filtered>, And, With<_RemoveAllChildrenTransaction>>>, + hierarchy: Resource, + commands: Commands +) { + targetNodes.update { entity, node in + node.nodeRef.removeAllChildren() + commands + .entity(entity) + .removeComponent(ofType: Parent.self) + .removeComponent(ofType: _RemoveAllChildrenTransaction.self) + let children = hierarchy.resource.children(of: entity) + children?.forEach { child in + commands + .entity(child) + .removeComponent(ofType: Child.self) + } + hierarchy.resource.removeAllChildren(fromEntity: entity) + } +} + +@MainActor +func despawnAllChildren( + targetNodes: Filtered>, And, With<_DespawnAllChildrenTransaction>>>, + hierarchy: Resource, + nodes: Resource, + commands: Commands +) { + targetNodes.update { entity, node in + node.nodeRef.removeAllChildren() + commands + .entity(entity) + .removeComponent(ofType: Parent.self) + .removeComponent(ofType: _DespawnAllChildrenTransaction.self) + + let children = hierarchy.resource.children(of: entity) + children?.forEach { child in + commands.despawn(entity: child) + // 防衛的に Nodes 経由で entity と SKNode の紐付けを削除する + nodes.resource.removeNode(forEntity: child) + } + hierarchy.resource.removeAllChildren(fromEntity: entity) } } diff --git a/Sources/PlugIns/Graphic2D/Commands/SetGraphic.swift b/Sources/PlugIns/Graphic2D/Commands/SetGraphic.swift index 9c7d628..e0211ce 100644 --- a/Sources/PlugIns/Graphic2D/Commands/SetGraphic.swift +++ b/Sources/PlugIns/Graphic2D/Commands/SetGraphic.swift @@ -27,8 +27,6 @@ final class SetGraphic: EntityCommand { override func runCommand(forRecord record: EntityRecordRef, inWorld world: World) { self.setEntityInfoForNode(entity) - - record.addComponent(Parent(_children: [])) } } diff --git a/Sources/PlugIns/Graphic2D/Hierarchy.swift b/Sources/PlugIns/Graphic2D/Hierarchy.swift new file mode 100644 index 0000000..7e90e83 --- /dev/null +++ b/Sources/PlugIns/Graphic2D/Hierarchy.swift @@ -0,0 +1,72 @@ +// +// Hierarchy.swift +// ECS_Swift +// +// Created by rrbox on 2025/09/30. +// + +import ECS + +public final class Hierarchy: ResourceProtocol { + private(set) var childrenMap = [Entity: Set]() + private(set) var parentMap = [Entity: Entity]() + + // MARK: - public + + public func children(of parentEntity: Entity) -> Set? { + childrenMap[parentEntity] + } + + public func parent(of childEntity: Entity) -> Entity? { + parentMap[childEntity] + } + + public func hasParentSlot(_ parent: Entity) -> Bool { + return childrenMap.keys.contains(parent) + } + + public func childrenIsEmpty(for parent: Entity) -> Bool { + childrenMap[parent]?.isEmpty ?? true + } + + // MARK: - internal + + func insertChild(_ childEntity: Entity, forParent parentEntity: Entity) { + insertChildToSlot(childEntity: childEntity, parentEntity: parentEntity) + setParentToSlot(parentEntity: parentEntity, childEntity: childEntity) + } + + /// 指定した entity を hierarchy グラフから完全に削除します + func removeRecursively(entity: Entity) { + childrenMap[entity]?.forEach { removeRecursively(entity: $0) } + childrenMap.removeValue(forKey: entity) + guard let parent = parentMap.removeValue(forKey: entity) else { return } + childrenMap[parent]?.remove(entity) + } + + func removeAllChildren(fromEntity entity: Entity) { + guard let children = childrenMap[entity] else { return } + children.forEach { child in + parentMap.removeValue(forKey: child) + } + childrenMap.removeValue(forKey: entity) + } + + func removeFromParent(_ child: Entity) { + guard let parent = parentMap.removeValue(forKey: child) else { return } + childrenMap[parent]?.remove(child) + if childrenMap[parent]?.count == 0 { + childrenMap.removeValue(forKey: parent) + } + } + + // MARK: - private + + private func insertChildToSlot(childEntity: Entity, parentEntity: Entity) { + childrenMap[parentEntity, default: []].insert(childEntity) + } + + private func setParentToSlot(parentEntity: Entity, childEntity: Entity) { + parentMap[childEntity] = parentEntity + } +} diff --git a/Sources/PlugIns/Graphic2D/Nodes.swift b/Sources/PlugIns/Graphic2D/Nodes.swift index 701cddd..b6851a9 100644 --- a/Sources/PlugIns/Graphic2D/Nodes.swift +++ b/Sources/PlugIns/Graphic2D/Nodes.swift @@ -22,6 +22,8 @@ public final class Nodes: ResourceProtocol { var store = [Entity: SKNode]() + // MARK: - public + /// node hierarchy に存在しない SKNode を entity に紐付けます. public func create(node: Node) -> NodeCreate { return .init( @@ -70,11 +72,13 @@ public final class Nodes: ResourceProtocol { ) } + // MARK: - internal + func regiester(entity: Entity, node: Node) { store[entity] = node } - func removeNode(forEntity entity: Entity) { + @discardableResult func removeNode(forEntity entity: Entity) -> SKNode? { store.removeValue(forKey: entity) } } diff --git a/Sources/PlugIns/Graphic2D/PlugInExport.swift b/Sources/PlugIns/Graphic2D/PlugInExport.swift index 16ca6c1..7ac3958 100644 --- a/Sources/PlugIns/Graphic2D/PlugInExport.swift +++ b/Sources/PlugIns/Graphic2D/PlugInExport.swift @@ -13,25 +13,31 @@ import ECS /// - query: entity heirarchy に入っていない entity の query. /// - graphics: 親 entity の SKNode を検索するための query. /// - scene: 親 entity が指定されていない場合に配置先となる scene. -/// - commands: `_AddChildNodeTransaction` を削除するための commands. +/// - commands: `_AddChildNodeTransaction` を削除するための commands.¥ func _addChildNodeSystem( query: Filtered>, WithOut>, - graphics: Query2, Parent>, + graphics: Query>, scene: Resource, + hierarchy: Resource, commands: Commands ) { - query.update { childEntity, parent, graphic in - if let parentEntity = parent.parentEntity { - graphics.update(parentEntity) { parentNode, children in + query.update { childEntity, transaction, graphic in + let childEntity = childEntity + let graphic = graphic + if let parentEntity = transaction.parentEntity { + graphics.update(parentEntity) { parentNode in parentNode.nodeRef.addChild(graphic.nodeRef) - children._children.insert(childEntity) + if !hierarchy.resource.hasParentSlot(parentEntity) { + commands.entity(parentEntity) + .addComponent(Parent()) + } + hierarchy.resource.insertChild(childEntity, forParent: parentEntity) commands.entity(childEntity) - .addComponent(Child(_parent: parentEntity)) + .addComponent(Child()) } } else { scene.resource.scene.addChild(graphic.nodeRef) } - commands .entity(childEntity) .removeComponent(ofType: _AddChildNodeTransaction.self) @@ -45,17 +51,24 @@ func _addChildNodeSystem( /// - commands: `_AddChildNodeTransaction` を削除するための commands. func _addChildNodeSystem( query: Filtered>, With>, - graphics: Query2, Parent>, + graphics: Query>, + hierarchy: Resource, commands: Commands ) { - query.update { childEntity, parent, graphic in - if let parentEntity = parent.parentEntity { + query.update { childEntity, transaction, graphic in + let childEntity = childEntity + let graphic = graphic + if let parentEntity = transaction.parentEntity { graphic.nodeRef.removeFromParent() - graphics.update(parentEntity) { parentNode, children in + graphics.update(parentEntity) { parentNode in parentNode.nodeRef.addChild(graphic.nodeRef) - children._children.insert(childEntity) + if !hierarchy.resource.hasParentSlot(parentEntity) { + commands.entity(parentEntity) + .addComponent(Parent()) + } + hierarchy.resource.insertChild(childEntity, forParent: parentEntity) commands.entity(childEntity) - .addComponent(Child(_parent: parentEntity)) + .addComponent(Child()) } } else { fatalError("parent entity not found") @@ -69,29 +82,37 @@ func _addChildNodeSystem( @MainActor func _removeFromParentSystem( - query: Filtered, Child>, With<_RemoveFromParentTransaction>>, - parents: Query, + query: Filtered>, And, With<_RemoveFromParentTransaction>>>, nodes: Resource, + hierarchy: Resource, commands: Commands ) { - query.update { childEntity, childNode, child in + query.update { childEntity, childNode in childNode.nodeRef.removeFromParent() nodes.resource.removeNode(forEntity: childEntity) commands.entity(childEntity) .removeComponent(ofType: Child.self) .removeComponent(ofType: _RemoveFromParentTransaction.self) - parents.update(child.parent) { parent in - parent._children.remove(childEntity) + guard let parent = hierarchy.resource.parent(of: childEntity) else { return } + hierarchy.resource.removeFromParent(childEntity) + if hierarchy.resource.childrenIsEmpty(for: parent) { + commands.entity(parent) + .removeComponent(ofType: Parent.self) } } } @MainActor -func _removeNodeIfDespawned(despawn: EventReader, nodes: Resource) { +func _removeNodeIfDespawned( + despawn: EventReader, + nodes: Resource +) { for event in despawn.events { let despawnedEntity = event.despawnedEntity - nodes.resource.removeNode(forEntity: despawnedEntity) + nodes.resource + .removeNode(forEntity: despawnedEntity)? + .removeFromParent() } } @@ -100,17 +121,18 @@ func _removeNodeIfDespawned(despawn: EventReader, nodes: Resou public func graphicPlugIn(world: World) { world .addResource(Nodes()) - .addSystem(.postStartUp, _addChildNodeSystem(query:graphics:scene:commands:)) - .addSystem(.postStartUp, _addChildNodeSystem(query:graphics:commands:)) - .addSystem(.postStartUp, _removeFromParentSystem(query:parents:nodes:commands:)) - .addSystem(.postUpdate, _addChildNodeSystem(query:graphics:scene:commands:)) - .addSystem(.postUpdate, _addChildNodeSystem(query:graphics:commands:)) - .addSystem(.postUpdate, _removeFromParentSystem(query:parents:nodes:commands:)) + .addResource(Hierarchy()) + .addSystem(.postStartUp, _addChildNodeSystem(query:graphics:hierarchy:commands:)) + .addSystem(.postStartUp, _addChildNodeSystem(query:graphics:scene:hierarchy:commands:)) + .addSystem(.postStartUp, _removeFromParentSystem(query:nodes:hierarchy:commands:)) + .addSystem(.postUpdate, _addChildNodeSystem(query:graphics:hierarchy:commands:)) + .addSystem(.postUpdate, _addChildNodeSystem(query:graphics:scene:hierarchy:commands:)) + .addSystem(.postUpdate, _removeFromParentSystem(query:nodes:hierarchy:commands:)) .buildWillDespawnResponder { responder in responder - .addSystem(.update, removeChildIfDespawned(despawnEvent:query:parentQuery:)) - .addSystem(.update, despawnChildIfParentDespawned(despawnedEntityEvent:children:commands:)) + .addSystem(.update, removeChildIfDespawned(despawnEvent:hierarchy:commands:)) + .addSystem(.update, despawnChildIfParentDespawned(despawnedEntityEvent:hierarchy:commands:)) .addSystem(.update, _removeNodeIfDespawned(despawn:nodes:)) } } diff --git a/Tests/GraphicPlugInTests/GraphicPlugInTests.swift b/Tests/GraphicPlugInTests/GraphicPlugInTests.swift index 0eb8f5a..f8dfb95 100644 --- a/Tests/GraphicPlugInTests/GraphicPlugInTests.swift +++ b/Tests/GraphicPlugInTests/GraphicPlugInTests.swift @@ -70,6 +70,65 @@ final class GraphicPlugInTests: XCTestCase { XCTAssertEqual(flags, [1]) } + // entity hierarchy から取り外す処理のテスト + func testDespawn() { + let scene = SKScene() + let node = SKNode() + var flags = [0, 0, 0] + let world = World() + .addResource(SceneResource(scene)) + .addPlugIn(graphicPlugIn(world:)) + .addSystem(.startUp) { (commands: Commands, nodes: Resource) in + commands.spawn() + .setGraphic(nodes.resource.create(node: node)) + } + .addSystem(.update) { ( + currentTime: Resource, + entities: Query + ) in + // add child 関数が機能しているのかをチェック + switch currentTime.resource.value { + case -1: fatalError() // ここは通過しない. + case 0: + flags[0] += 1 + XCTAssertEqual(scene.children.count, 1) + XCTAssertEqual(entities.components.data.count, 1) + default: return + } + } + .addSystem(.update) { ( + entities: Query, + parents: Query, + commands: Commands, + currentTime: Resource, + hierarchy: Resource, + nodes: Resource + ) in + // remove from parent 関数の効果をチェック + switch currentTime.resource.value { + case 1: + flags[1] += 1 + entities.update { entity in + commands.despawn(entity: entity) + } + case 2: + flags[2] += 1 + XCTAssertEqual(scene.children.count, 0) + XCTAssertEqual(entities.components.data.count, 0) + XCTAssertEqual(nodes.resource.store.count, 0) + default: break + } + } + + world.setUpWorld() + world.update(currentTime: -1) + world.update(currentTime: 0) + world.update(currentTime: 1) // この一番最後で _remove from parent tarnsaction 追加 + world.update(currentTime: 2) // remove from parent system 実行, 一番最後に component に変更反映 | ここで結果が出る + + XCTAssertEqual(flags, [1, 1, 1]) + } + func testAddChildOnUpdate() { let scene = SKScene() let parentNode = SKNode() @@ -93,6 +152,7 @@ final class GraphicPlugInTests: XCTestCase { parents: Query3>, currentTime: Resource, commands: Commands, + hierarchy: Resource, nodes: Resource ) in switch currentTime.resource.value { @@ -101,12 +161,16 @@ final class GraphicPlugInTests: XCTestCase { XCTAssertEqual(parents.components.data.count, 0) XCTAssertEqual(parentNode.children.count, 0) XCTAssertEqual(children.components.data.count, 0) + XCTAssertEqual(hierarchy.resource.childrenMap.count, 0) + XCTAssertEqual(hierarchy.resource.parentMap.count, 0) XCTAssertEqual(nodes.resource.store.count, 2) flags[0] += 1 case 1: - XCTAssertEqual(parents.components.data.count, 2) + XCTAssertEqual(parents.components.data.count, 1) XCTAssertEqual(parentNode.children.count, 1) XCTAssertEqual(children.components.data.count, 1) + XCTAssertEqual(hierarchy.resource.childrenMap.count, 1) + XCTAssertEqual(hierarchy.resource.parentMap.count, 1) XCTAssertEqual(nodes.resource.store.count, 2) flags[1] += 1 default: return @@ -147,11 +211,14 @@ final class GraphicPlugInTests: XCTestCase { .addSystem(.update) { ( children: Query, parents: Query2, + hierarchy: Resource, nodes: Resource ) in flags[0] += 1 - XCTAssertEqual(parents.components.data.count, 2) + XCTAssertEqual(parents.components.data.count, 0) XCTAssertEqual(children.components.data.count, 0) + XCTAssertEqual(hierarchy.resource.childrenMap.count, 0) + XCTAssertEqual(hierarchy.resource.parentMap.count, 0) XCTAssertEqual(nodes.resource.store.count, 2) } @@ -164,6 +231,80 @@ final class GraphicPlugInTests: XCTestCase { } + // entity hierarchy から取り外す処理のテスト + func testDespawnChild() { + let scene = SKScene() + let parentNode = SKNode() + var flags = [0, 0] + let world = World() + .addResource(SceneResource(scene)) + .addPlugIn(graphicPlugIn(world:)) + .addSystem(.startUp) { (commands: Commands, nodes: Resource) in + let childNode = SKNode() + + let child = commands.spawn() + .setGraphic(nodes.resource.create(node: childNode)) + .id() + commands.spawn() + .setGraphic(nodes.resource.create(node: parentNode)) + .addChild(child) + } + .addSystem(.update) { ( + children: Query, + parents: Query2, + hierarchy: Resource, + currentTime: Resource + ) in + // add child 関数が機能しているのかをチェック + switch currentTime.resource.value { + case -1: fatalError() // ここは通過しない. + case 0: + flags[0] += 1 + XCTAssertEqual(parents.components.data.count, 1) + XCTAssertEqual(parentNode.children.count, 1) + XCTAssertEqual(hierarchy.resource.childrenMap.count, 1) + XCTAssertEqual(hierarchy.resource.parentMap.count, 1) + XCTAssertEqual(children.components.data.count, 1) + default: return + } + } + .addSystem(.update) { ( + children: Filtered, + With>, + parents: Query, + commands: Commands, + currentTime: Resource, + hierarchy: Resource, + nodes: Resource + ) in + // remove from parent 関数の効果をチェック + switch currentTime.resource.value { + case 1: + flags[1] += 1 + children.update { entity in + commands.despawn(entity: entity) + } + case 2: + flags[1] += 1 + XCTAssertEqual(parents.components.data.count, 0) + XCTAssertEqual(children.query.components.data.count, 0) + XCTAssertEqual(parentNode.children.count, 0) + XCTAssertEqual(hierarchy.resource.childrenMap.count, 0) + XCTAssertEqual(hierarchy.resource.parentMap.count, 0) + XCTAssertEqual(nodes.resource.store.count, 1) + default: break + } + } + + world.setUpWorld() + world.update(currentTime: -1) + world.update(currentTime: 0) + world.update(currentTime: 1) // この一番最後で _remove from parent tarnsaction 追加 + world.update(currentTime: 2) // remove from parent system 実行, 一番最後に component に変更反映 | ここで結果が出る + + XCTAssertEqual(flags, [1, 2]) + } + // entity hierarchy から取り外す処理のテスト func testRemoveFromParent() { let scene = SKScene() @@ -182,14 +323,21 @@ final class GraphicPlugInTests: XCTestCase { .setGraphic(nodes.resource.create(node: parentNode)) .addChild(child) } - .addSystem(.update) { (children: Query, parents: Query2, currentTime: Resource) in + .addSystem(.update) { ( + children: Query, + parents: Query2, + hierarchy: Resource, + currentTime: Resource + ) in // add child 関数が機能しているのかをチェック - XCTAssertEqual(parents.components.data.count, 2) switch currentTime.resource.value { case -1: fatalError() // ここは通過しない. case 0: flags[0] += 1 + XCTAssertEqual(parents.components.data.count, 1) XCTAssertEqual(parentNode.children.count, 1) + XCTAssertEqual(hierarchy.resource.childrenMap.count, 1) + XCTAssertEqual(hierarchy.resource.parentMap.count, 1) XCTAssertEqual(children.components.data.count, 1) default: return } @@ -200,10 +348,10 @@ final class GraphicPlugInTests: XCTestCase { parents: Query, commands: Commands, currentTime: Resource, + hierarchy: Resource, nodes: Resource ) in // remove from parent 関数の効果をチェック - XCTAssertEqual(parents.components.data.count, 2) switch currentTime.resource.value { case 1: flags[1] += 1 @@ -213,8 +361,11 @@ final class GraphicPlugInTests: XCTestCase { } case 2: flags[1] += 1 + XCTAssertEqual(parents.components.data.count, 0) XCTAssertEqual(children.query.components.data.count, 0) XCTAssertEqual(parentNode.children.count, 0) + XCTAssertEqual(hierarchy.resource.childrenMap.count, 0) + XCTAssertEqual(hierarchy.resource.parentMap.count, 0) XCTAssertEqual(nodes.resource.store.count, 1) default: break } @@ -257,35 +408,42 @@ final class GraphicPlugInTests: XCTestCase { currentTime: Resource, commands: Commands, children: Query, - parents: Query2, + parents: Filtered, With>, totalEntities: Query, + hierarchy: Resource, nodes: Resource ) in switch currentTime.resource.value { case -1: fatalError() // ここは通過しません. case 0: flags[0] += 1 - XCTAssertEqual(parents.components.data.count, 3) + XCTAssertEqual(parents.query.components.data.count, 2) XCTAssertEqual(children.components.data.count, 2) XCTAssertEqual(totalEntities.components.data.count, 3) + XCTAssertEqual(hierarchy.resource.childrenMap.count, 2) + XCTAssertEqual(hierarchy.resource.parentMap.count, 2) XCTAssertEqual(nodes.resource.store.count, 3) case 1: flags[1] += 1 - parents.update { entity, parent in - if parent.children.count == 1 { + parents.update { entity in + if hierarchy.resource.children(of: entity)?.count == 1 { commands.despawn(entity: entity) } } - XCTAssertEqual(parents.components.data.count, 3) + XCTAssertEqual(parents.query.components.data.count, 2) XCTAssertEqual(children.components.data.count, 2) XCTAssertEqual(totalEntities.components.data.count, 3) + XCTAssertEqual(hierarchy.resource.childrenMap.count, 2) + XCTAssertEqual(hierarchy.resource.parentMap.count, 2) XCTAssertEqual(nodes.resource.store.count, 3) case 2: flags[2] += 1 - XCTAssertEqual(parents.components.data.count, 0) + XCTAssertEqual(parents.query.components.data.count, 0) XCTAssertEqual(children.components.data.count, 0) XCTAssertEqual(totalEntities.components.data.count, 0) + XCTAssertEqual(hierarchy.resource.childrenMap.count, 0) + XCTAssertEqual(hierarchy.resource.parentMap.count, 0) XCTAssertEqual(nodes.resource.store.count, 0) default: fatalError() @@ -318,27 +476,40 @@ final class GraphicPlugInTests: XCTestCase { .setGraphic(nodes.resource.create(node: parentNode)) .addChild(child) } - .addSystem(.update) { (currentTime: Resource, commands: Commands, children: Filtered, With>, parents: Query2, totalEntities: Query) in + .addSystem(.update) { ( + currentTime: Resource, + commands: Commands, + children: Filtered, With>, + parents: Query2, + hierarchy: Resource, + totalEntities: Query + ) in switch currentTime.resource.value { case -1: fatalError() // ここは通過しません. case 0: XCTAssertStepOrder(currentStep: 0, steps: &flags) - XCTAssertEqual(parents.components.data.count, 2) + XCTAssertEqual(parents.components.data.count, 1) XCTAssertEqual(children.query.components.data.count, 1) + XCTAssertEqual(hierarchy.resource.childrenMap.count, 1) + XCTAssertEqual(hierarchy.resource.parentMap.count, 1) XCTAssertEqual(totalEntities.components.data.count, 2) case 1: XCTAssertStepOrder(currentStep: 1, steps: &flags) children.update { entity in commands.despawn(entity: entity) } - - XCTAssertEqual(parents.components.data.count, 2) + + XCTAssertEqual(parents.components.data.count, 1) XCTAssertEqual(children.query.components.data.count, 1) + XCTAssertEqual(hierarchy.resource.childrenMap.count, 1) + XCTAssertEqual(hierarchy.resource.parentMap.count, 1) XCTAssertEqual(totalEntities.components.data.count, 2) case 2: XCTAssertStepOrder(currentStep: 2, steps: &flags) - XCTAssertEqual(parents.components.data.count, 1) + XCTAssertEqual(parents.components.data.count, 0) XCTAssertEqual(children.query.components.data.count, 0) + XCTAssertEqual(hierarchy.resource.childrenMap.count, 0) + XCTAssertEqual(hierarchy.resource.parentMap.count, 0) XCTAssertEqual(totalEntities.components.data.count, 1) default: fatalError() // ここは通過しません.