Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
}
6 changes: 5 additions & 1 deletion Sources/PlugIns/Graphic2D/Nodes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public final class Nodes: ResourceProtocol {

var store = [Entity: SKNode]()

// MARK: - public

/// node hierarchy に存在しない SKNode を entity に紐付けます.
public func create<Node: SKNode>(node: Node) -> NodeCreate<Node> {
return .init(
Expand Down Expand Up @@ -70,11 +72,13 @@ public final class Nodes: ResourceProtocol {
)
}

// MARK: - internal

func regiester<Node: SKNode>(entity: Entity, node: Node) {
store[entity] = node
}

func removeNode(forEntity entity: Entity) {
@discardableResult func removeNode(forEntity entity: Entity) -> SKNode? {
store.removeValue(forKey: entity)
}
}
Loading