Skip to content
15 changes: 15 additions & 0 deletions Sources/ECS/Commons/FeatureFlags.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
127 changes: 85 additions & 42 deletions Sources/PlugIns/Graphic2D/Commands/EntityCommands+Graphic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Entity>
public var children: Set<Entity> {
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
Expand All @@ -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())
}
}

Expand Down Expand Up @@ -106,43 +96,96 @@ public extension EntityCommands {

func removeChildIfDespawned(
despawnEvent: EventReader<WillDespawnEvent>,
query: Query<Child>,
parentQuery: Query<Parent>
hierarchy: Resource<Hierarchy>,
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<Entity, Child>,
hierarchy: Resource<Hierarchy>,
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<WillDespawnEvent>,
children: Query2<Entity, Child>,
hierarchy: Resource<Hierarchy>,
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<Query2<Entity, Graphic<SKNode>>, And<With<Parent>, With<_RemoveAllChildrenTransaction>>>,
hierarchy: Resource<Hierarchy>,
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<Query2<Entity, Graphic<SKNode>>, And<With<Parent>, With<_DespawnAllChildrenTransaction>>>,
hierarchy: Resource<Hierarchy>,
nodes: Resource<Nodes>,
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)
}
}
2 changes: 0 additions & 2 deletions Sources/PlugIns/Graphic2D/Commands/SetGraphic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ final class SetGraphic: EntityCommand {

override func runCommand(forRecord record: EntityRecordRef, inWorld world: World) {
self.setEntityInfoForNode(entity)

record.addComponent(Parent(_children: []))
}

}
72 changes: 72 additions & 0 deletions Sources/PlugIns/Graphic2D/Hierarchy.swift
Original file line number Diff line number Diff line change
@@ -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<Entity>]()
private(set) var parentMap = [Entity: Entity]()

// MARK: - public

public func children(of parentEntity: Entity) -> Set<Entity>? {
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
}
}
Loading