diff --git a/.circleci/config.yml b/.circleci/config.yml index 489011f..a9fc8df 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ version: 2.1 jobs: build: macos: - xcode: 16.1.0 + xcode: 16.4.0 steps: - checkout # - run: apt-get update && apt-get install -y curl 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/ECS/Event/CommandsEvent/CommandsEvent+Buffer.swift b/Sources/ECS/Event/CommandsEvent/CommandsEvent+Buffer.swift deleted file mode 100644 index 062051b..0000000 --- a/Sources/ECS/Event/CommandsEvent/CommandsEvent+Buffer.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// CommandsEvent+WorldStorage.swift -// -// -// Created by rrbox on 2023/08/29. -// - -extension AnyMap { - func commandsEventReceiver(eventOfType type: T.Type) -> CommandsEventReceiver? { - valueRef(ofType: CommandsEventReceiver.self)?.body - } - - mutating func registerCommandsEventReceiver(eventType: T.Type) { - push(CommandsEventReceiver()) - } - - func commandsEventWriter(eventOfType type: T.Type) -> CommandsEventWriter? { - valueRef(ofType: CommandsEventWriter.self)?.body - } - - mutating func registerCommandsEventWriter(eventType: T.Type) { - let receiver = valueRef(ofType: CommandsEventReceiver.self)!.body - push(CommandsEventWriter(receiver: receiver)) - } - - func commandsEventResponder(eventOfType type: T.Type) -> EventResponder? { - valueRef(ofType: EventResponder.self)?.body - } - - mutating func resisterCommandsEventResponder(eventType: T.Type) { - push(EventResponder()) - } -} diff --git a/Sources/ECS/Event/CommandsEvent/CommandsEvent.swift b/Sources/ECS/Event/CommandsEvent/CommandsEvent.swift deleted file mode 100644 index e074c3b..0000000 --- a/Sources/ECS/Event/CommandsEvent/CommandsEvent.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// CommandsEvent.swift -// -// -// Created by rrbox on 2023/08/29. -// - -/// Commands 実行中に発信されるイベントです. -protocol CommandsEventProtocol { - -} - -struct OnCommandsEvent: Hashable { - -} - -extension Schedule { - static func onCommandsEvent(ofType type: T.Type) -> Schedule { - Schedule(id: OnCommandsEvent()) - } -} diff --git a/Sources/ECS/Event/CommandsEvent/CommandsEventReceiver.swift b/Sources/ECS/Event/CommandsEvent/CommandsEventReceiver.swift deleted file mode 100644 index fe8080b..0000000 --- a/Sources/ECS/Event/CommandsEvent/CommandsEventReceiver.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// CommandsEventReceiver.swift -// ECS_Swift -// -// Created by rrbox on 2025/07/06. -// - -final class CommandsEventReceiver: AnyEventReceiver, EventStorageElement { - var eventBuffer = [T]() - - override func receive(worldStorage: WorldStorageRef) { - let events = eventBuffer - eventBuffer = [] - guard !events.isEmpty else { return } - worldStorage.eventStorage.push(EventReader(events: events)) - if let systems = worldStorage.eventStorage.commandsEventResponder(eventOfType: T.self)!.systems[.update] { - for system in systems { - system.execute(worldStorage) - } - } - for schedule in worldStorage.stateStorage.currentEventSchedulesWhichAssociatedStates() { - guard let systems = worldStorage.eventStorage.commandsEventResponder(eventOfType: T.self)!.systems[schedule] else { continue } - for system in systems { - system.execute(worldStorage) - } - } - worldStorage.eventStorage.pop(EventReader.self) - } -} diff --git a/Sources/ECS/Event/CommandsEvent/CommandsEventWriter.swift b/Sources/ECS/Event/CommandsEvent/CommandsEventWriter.swift deleted file mode 100644 index 3b27de9..0000000 --- a/Sources/ECS/Event/CommandsEvent/CommandsEventWriter.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// CommandsEventWriter.swift -// -// -// Created by rrbox on 2023/08/29. -// - -final class CommandsEventWriter: SystemParameter, EventStorageElement { - unowned let receiver: CommandsEventReceiver - - init(receiver: CommandsEventReceiver) { - self.receiver = receiver - } - - public func send(value: T) { - receiver.eventBuffer.append(value) - } - - public static func register(to worldStorage: WorldStorageRef) { - - } - - public static func getParameter(from worldStorage: WorldStorageRef) -> CommandsEventWriter? { - worldStorage.eventStorage.commandsEventWriter(eventOfType: T.self) - } -} diff --git a/Sources/ECS/Event/EventStreaming/EventReader.swift b/Sources/ECS/Event/EventStreaming/EventReader.swift index b5f42d2..3026f4b 100644 --- a/Sources/ECS/Event/EventStreaming/EventReader.swift +++ b/Sources/ECS/Event/EventStreaming/EventReader.swift @@ -5,11 +5,23 @@ // Created by rrbox on 2023/08/14. // -final public class EventReader: SystemParameter, EventStorageElement { - public let events: [T] +final public class EventReader: SystemParameter, EventStorageElement { + unowned let queue: EventQueue - init(events: [T]) { - self.events = events + init(queue: EventQueue) { + self.queue = queue + } + + public var count: Int { + queue.countOfEvents + } + + public var isEmpty: Bool { + count == 0 + } + + public func forEach(_ body: (T) -> ()) { + queue.forEach(body) } public static func register(to worldStorage: WorldStorageRef) { @@ -17,6 +29,9 @@ final public class EventReader: SystemParameter, EventStorageElement { } public static func getParameter(from worldStorage: WorldStorageRef) -> EventReader? { - return worldStorage.eventStorage.valueRef(ofType: EventReader.self)?.body + worldStorage + .eventStorage + .valueRef(ofType: EventReader.self)? + .body } } diff --git a/Sources/ECS/Event/EventStreaming/EventReceiver.swift b/Sources/ECS/Event/EventStreaming/EventReceiver.swift index 335dbe2..1502eba 100644 --- a/Sources/ECS/Event/EventStreaming/EventReceiver.swift +++ b/Sources/ECS/Event/EventStreaming/EventReceiver.swift @@ -5,25 +5,36 @@ // Created by rrbox on 2025/07/06. // -final class EventReceiver: AnyEventReceiver, EventStorageElement { - var eventBuffer = [T]() +final class EventQueues: EventStorageElement { + var body = [any AnyEventQueue]() +} - override func receive(worldStorage: WorldStorageRef) { - let events = eventBuffer - eventBuffer = [] - guard !events.isEmpty else { return } - worldStorage.eventStorage.push(EventReader(events: events)) - if let systems = worldStorage.eventStorage.eventResponder(eventOfType: T.self)!.systems[.update] { - for system in systems { - system.execute(worldStorage) - } - } - for schedule in worldStorage.stateStorage.currentEventSchedulesWhichAssociatedStates() { - guard let systems = worldStorage.eventStorage.eventResponder(eventOfType: T.self)!.systems[schedule] else { continue } - for system in systems { - system.execute(worldStorage) - } +protocol AnyEventQueue { + func applyEventsWritingBuffer() +} + +final class EventQueue: AnyEventQueue, EventStorageElement { + var eventWritingBuffer = [T]() + // 3フェーズ分の event buffer を1フレームの間キャッシュする + var eventBufferQueue: [[T]] = [[], [], []] + + var countOfEvents: Int { + eventBufferQueue.reduce(0) { $0 + $1.count } + } + + func write(event: T) { + eventWritingBuffer.append(event) + } + + func applyEventsWritingBuffer() { + eventBufferQueue.removeFirst() + eventBufferQueue.append(eventWritingBuffer) + eventWritingBuffer.removeAll() + } + + func forEach(_ body: (T) -> ()) { + eventBufferQueue.forEach { eventsBuffer in + eventsBuffer.forEach(body) } - worldStorage.eventStorage.pop(EventReader.self) } } diff --git a/Sources/ECS/Event/EventStreaming/EventReceivers.swift b/Sources/ECS/Event/EventStreaming/EventReceivers.swift deleted file mode 100644 index 8127f94..0000000 --- a/Sources/ECS/Event/EventStreaming/EventReceivers.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// EventReceivers.swift -// ECS_Swift -// -// Created by rrbox on 2025/07/06. -// - -final class EventReceivers: EventStorageElement { - var eventReceivers = [AnyEventReceiver]() -} diff --git a/Sources/ECS/Event/EventStreaming/EventResponder.swift b/Sources/ECS/Event/EventStreaming/EventResponder.swift deleted file mode 100644 index a1c3e01..0000000 --- a/Sources/ECS/Event/EventStreaming/EventResponder.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// EventResponder.swift -// -// -// Created by rrbox on 2023/11/11. -// - -import Foundation - -final public class EventResponderBuilder { - unowned let worldStorage: WorldStorageRef - var systems: [EventSchedule: [SystemExecute]] = [:] - - init(worldStorage: WorldStorageRef) { - self.worldStorage = worldStorage - } -} - -final public class EventResponder: EventStorageElement { - var systems: [EventSchedule: [SystemExecute]] = [:] -} - -public extension World { - @discardableResult func buildEventResponder(_ eventType: T.Type, _ build: (EventResponderBuilder) -> ()) -> World { - let builder = EventResponderBuilder(worldStorage: self.worldStorage) - build(builder) - - self.worldStorage.eventStorage.eventResponder(eventOfType: T.self)! - .systems - .merge(builder.systems) { fromWorldStorage, new in - fromWorldStorage + new - } - - return self - } - - private func buildCommandsEventResponder(_ eventType: T.Type, _ build: (EventResponderBuilder) -> ()) { - let builder = EventResponderBuilder(worldStorage: self.worldStorage) - build(builder) - - self.worldStorage.eventStorage.commandsEventResponder(eventOfType: T.self)! - .systems - .merge(builder.systems) { fromWorldStorage, new in - fromWorldStorage + new - } - } - - @discardableResult func buildDidSpawnResponder(_ build: (EventResponderBuilder) -> ()) -> World { - self.buildCommandsEventResponder(DidSpawnEvent.self, build) - return self - } - - @discardableResult func buildWillDespawnResponder(_ build: (EventResponderBuilder) -> ()) -> World { - self.buildCommandsEventResponder(WillDespawnEvent.self, build) - return self - } -} diff --git a/Sources/ECS/Event/EventStreaming/EventStorage.swift b/Sources/ECS/Event/EventStreaming/EventStorage.swift index 2cd0d7a..e2369de 100644 --- a/Sources/ECS/Event/EventStreaming/EventStorage.swift +++ b/Sources/ECS/Event/EventStreaming/EventStorage.swift @@ -29,39 +29,28 @@ extension AnyMap where Mode == EventStorage { // world buffer にプロパティをつけておく extension AnyMap { - func eventReceivers() -> EventReceivers? { - valueRef(ofType: EventReceivers.self)?.body + mutating func registerEventQueues() { + push(EventQueues()) } - mutating func registerEventReceivers() { - push(EventReceivers()) + mutating func registerEventStreamer(eventType: T.Type) { + let queues = eventQueues() + let queue = EventQueue() + queues?.body.append(queue) + push(queue) + push(EventWriter(queue: queue)) + push(EventReader(queue: queue)) } - func eventReceiver(eventOfType type: T.Type) -> EventReceiver? { - valueRef(ofType: EventReceiver.self)?.body + func eventQueues() -> EventQueues? { + valueRef(ofType: EventQueues.self)?.body } - mutating func registerEventReceiver(eventType: T.Type) { - let eventReceiver = EventReceiver() - let eventReceivers = eventReceivers() - eventReceivers?.eventReceivers.append(eventReceiver) - push(eventReceiver) - } - - func eventWriter(eventOfType type: T.Type) -> EventWriter? { + func eventWriter(typeOf type: T.Type) -> EventWriter? { valueRef(ofType: EventWriter.self)?.body } - mutating func registerEventWriter(eventType: T.Type) { - let receiver = valueRef(ofType: EventReceiver.self)!.body - push(EventWriter(receiver: receiver)) - } - - func eventResponder(eventOfType type: T.Type) -> EventResponder? { - valueRef(ofType: EventResponder.self)?.body - } - - mutating func registerEventResponder(eventType: T.Type) { - push(EventResponder()) + func eventQueue(typeOf type: T.Type) -> EventQueue? { + valueRef(ofType: EventQueue.self)?.body } } diff --git a/Sources/ECS/Event/EventStreaming/EventWriter.swift b/Sources/ECS/Event/EventStreaming/EventWriter.swift index c782637..c279e1c 100644 --- a/Sources/ECS/Event/EventStreaming/EventWriter.swift +++ b/Sources/ECS/Event/EventStreaming/EventWriter.swift @@ -5,16 +5,15 @@ // Created by rrbox on 2023/08/14. // -// Commands と基本的な仕組みは同じ. final public class EventWriter: SystemParameter, EventStorageElement { - unowned let receiver: EventReceiver + unowned let queue: EventQueue - init(receiver: EventReceiver) { - self.receiver = receiver + init(queue: EventQueue) { + self.queue = queue } - public func send(value: T) { - receiver.eventBuffer.append(value) + public func send(_ value: T) { + queue.write(event: value) } public static func register(to worldStorage: WorldStorageRef) { @@ -22,6 +21,9 @@ final public class EventWriter: SystemParameter, EventStorageE } public static func getParameter(from worldStorage: WorldStorageRef) -> EventWriter? { - worldStorage.eventStorage.eventWriter(eventOfType: T.self) + worldStorage + .eventStorage + .valueRef(ofType: EventWriter.self)? + .body } } diff --git a/Sources/ECS/Event/Removed/CommandsEventReceiver.swift b/Sources/ECS/Event/Removed/CommandsEventReceiver.swift new file mode 100644 index 0000000..d334ebb --- /dev/null +++ b/Sources/ECS/Event/Removed/CommandsEventReceiver.swift @@ -0,0 +1,42 @@ +// +// RemovedEventReceiver.swift +// ECS_Swift +// +// Created by rrbox on 2025/07/06. +// + +final class RemovedEventReceiver: AnyEventReceiver, EventStorageElement { + private var eventWritingBuffer = [Entity]() + + var count: Int { + eventWritingBuffer.count + } + + override func receive(worldStorage: WorldStorageRef) { + let removedEntities = eventWritingBuffer + guard !removedEntities.isEmpty else { + return + } + eventWritingBuffer.removeAll() + worldStorage.eventStorage.push(Removed(entities: removedEntities)) + for system in worldStorage.systemStorage.systems(.removed) { + system.execute(worldStorage) + } + + for schedule in worldStorage.stateStorage.currentRemovedSchedulesWhichAssociatedStates() { + let systems = worldStorage.systemStorage.systems(schedule) + for system in systems { + system.execute(worldStorage) + } + } + worldStorage.eventStorage.pop(Removed.self) + } + + func pushDespawned(_ entity: Entity) { + eventWritingBuffer.append(entity) + } + + func forEach(_ body: (Entity) -> ()) { + eventWritingBuffer.forEach(body) + } +} diff --git a/Sources/ECS/Event/Removed/Removed.swift b/Sources/ECS/Event/Removed/Removed.swift new file mode 100644 index 0000000..c89628b --- /dev/null +++ b/Sources/ECS/Event/Removed/Removed.swift @@ -0,0 +1,27 @@ +// +// File.swift +// ECS_Swift +// +// Created by rrbox on 2025/12/09. +// + +final public class Removed: SystemParameter, EventStorageElement { + public let entities: [Entity] + + init(entities: [Entity]) { + self.entities = entities + } + + public func forEach(_ body: (Entity) -> ()) { + entities.forEach(body) + } + + public static func register(to worldStorage: WorldStorageRef) { + + } + + public static func getParameter(from worldStorage: WorldStorageRef) -> Removed? { + worldStorage.eventStorage.valueRef(ofType: Removed.self)?.body + } +} + diff --git a/Sources/ECS/Event/Removed/RemovedEvent+Buffer.swift b/Sources/ECS/Event/Removed/RemovedEvent+Buffer.swift new file mode 100644 index 0000000..99fd215 --- /dev/null +++ b/Sources/ECS/Event/Removed/RemovedEvent+Buffer.swift @@ -0,0 +1,17 @@ +// +// RemovedEvent+Buffer.swift +// +// +// Created by rrbox on 2023/08/29. +// + +extension AnyMap { + mutating func registerRemovedEventStreamer() { + let receiver = RemovedEventReceiver() + push(receiver) + } + + func removedEventReceiver() -> RemovedEventReceiver? { + valueRef(ofType: RemovedEventReceiver.self)?.body + } +} diff --git a/Sources/ECS/Event/World+EventStreamer.swift b/Sources/ECS/Event/World+EventStreamer.swift index a08f7f5..9d81ce0 100644 --- a/Sources/ECS/Event/World+EventStreamer.swift +++ b/Sources/ECS/Event/World+EventStreamer.swift @@ -10,34 +10,29 @@ public extension World { /// /// `Event` をイベントシステムで扱う前に, World に EventStreamer を追加する必要があります. @discardableResult func addEventStreamer(eventType: T.Type) -> World { - worldStorage.eventStorage.registerEventReceiver(eventType: T.self) - worldStorage.eventStorage.registerEventWriter(eventType: T.self) - worldStorage.eventStorage.registerEventResponder(eventType: T.self) + worldStorage.eventStorage.registerEventStreamer(eventType: T.self) return self } } extension World { - func addCommandsEventStreamer(eventType: T.Type) { - worldStorage.systemStorage.insertSchedule(.onCommandsEvent(ofType: T.self)) - worldStorage.eventStorage.registerCommandsEventReceiver(eventType: T.self) - worldStorage.eventStorage.registerCommandsEventWriter(eventType: T.self) - worldStorage.eventStorage.resisterCommandsEventResponder(eventType: T.self) + func addRemovedEventStreamer() { + worldStorage.eventStorage.registerRemovedEventStreamer() } } extension World { func applyEventQueue() { - let receivers = self.worldStorage.eventStorage.eventReceivers()! - for receiver in receivers.eventReceivers { - receiver.receive(worldStorage: worldStorage) + let queues = worldStorage.eventStorage.eventQueues()! + for queue in queues.body { + queue.applyEventsWritingBuffer() } } - func applyCommandsEventQueue(eventOfType: T.Type) { + func applyRemovedEventQueue() { let eventStorage = self.worldStorage.eventStorage - let receiver = eventStorage.commandsEventReceiver(eventOfType: T.self)! - receiver.receive(worldStorage: worldStorage) + let receiver = eventStorage.removedEventReceiver() + receiver?.receive(worldStorage: worldStorage) } } @@ -48,6 +43,6 @@ public extension World { System 内で Event を発信する場合は ``EventWriter`` を参照してください. */ func sendEvent(_ value: T) { - self.worldStorage.eventStorage.eventWriter(eventOfType: T.self)?.send(value: value) + self.worldStorage.eventStorage.eventWriter(typeOf: T.self)?.send(value) } } diff --git a/Sources/ECS/Schedule/Schedule.swift b/Sources/ECS/Schedule/Schedule.swift index aceedab..5c4a7a1 100644 --- a/Sources/ECS/Schedule/Schedule.swift +++ b/Sources/ECS/Schedule/Schedule.swift @@ -41,6 +41,7 @@ enum DefaultSchedule { case preUpdate case update case postUpdate + case removed } public extension Schedule { @@ -94,6 +95,7 @@ public extension Schedule { */ static let update: Schedule = Schedule(id: DefaultSchedule.update) static let postUpdate: Schedule = .init(id: DefaultSchedule.postUpdate) + static let removed: Schedule = .init(id: DefaultSchedule.removed) static func customSchedule(_ value: T) -> Schedule { Schedule(id: value) diff --git a/Sources/ECS/States/State.swift b/Sources/ECS/States/State.swift index 11c49d3..0ec0f70 100644 --- a/Sources/ECS/States/State.swift +++ b/Sources/ECS/States/State.swift @@ -39,7 +39,7 @@ extension AnyMap where Mode == StateStorage { final class StateAssociatedSchedules: StateStorageElement { var schedules = Set() - var eventSchedules = Set() + var removedSchedules = Set() } final class StateTransitionQueue: StateStorageElement { @@ -53,76 +53,65 @@ final class StateTransitionQueue: StateStorageElement { private(set) var onStackUpdatePreviousStateQueue = [Schedule]() private(set) var onInactiveUpdateNewStateQueue = [Schedule]() private(set) var onInactiveUpdatePreviousStateQueue = [Schedule]() - - private(set) var didEnterEventQueue = [EventSchedule]() - private(set) var willExitEventQueue = [EventSchedule]() - private(set) var onResumeEventQueue = [EventSchedule]() - private(set) var onPauseEventQueue = [EventSchedule]() - private(set) var onUpdateNewStateEventQueue = [EventSchedule]() - private(set) var onUpdatePreviousStateEventQueue = [EventSchedule]() - private(set) var onStackUpdateNewStateEventQueue = [EventSchedule]() - private(set) var onStackUpdatePreviousStateEventQueue = [EventSchedule]() - private(set) var onInactiveUpdateNewStateEventQueue = [EventSchedule]() - private(set) var onInactiveUpdatePreviousStateEventQueue = [EventSchedule]() + private(set) var removedOnNewStateQueue = [Schedule]() + private(set) var removedOnPreviousStateQueue = [Schedule]() + private(set) var removedOnStackNewStateQueue = [Schedule]() + private(set) var removedOnStackPreviousStateQueue = [Schedule]() + private(set) var removedOnInactiveNewStateQueue = [Schedule]() + private(set) var removedOnInactivePreviousStateQueue = [Schedule]() // enter func enqueueEntered(state: T) { didEnterQueue.append(.didEnter(state)) - didEnterEventQueue.append(.didEnter(state)) onUpdateNewStateQueue.append(.onUpdate(state)) - onUpdateNewStateEventQueue.append(.onUpdate(state)) onStackUpdateNewStateQueue.append(.onStackUpdate(state)) - onStackUpdateNewStateEventQueue.append(.onStackUpdate(state)) + removedOnNewStateQueue.append(.removedOn(state)) + removedOnStackNewStateQueue.append(.removedOnStack(state)) } func enqueueExited(state: T) { willExitQueue.append(.willExit(state)) - willExitEventQueue.append(.willExit(state)) onUpdatePreviousStateQueue.append(.onUpdate(state)) - onUpdatePreviousStateEventQueue.append(.onUpdate(state)) onStackUpdatePreviousStateQueue.append(.onStackUpdate(state)) - onStackUpdatePreviousStateEventQueue.append(.onStackUpdate(state)) + removedOnPreviousStateQueue.append(.removedOn(state)) + removedOnStackPreviousStateQueue.append(.removedOnStack(state)) } // push func enqueuePushed(state: T) { didEnterQueue.append(.didEnter(state)) - didEnterEventQueue.append(.didEnter(state)) onUpdateNewStateQueue.append(.onUpdate(state)) - onUpdateNewStateEventQueue.append(.onUpdate(state)) onStackUpdateNewStateQueue.append(.onStackUpdate(state)) - onStackUpdateNewStateEventQueue.append(.onStackUpdate(state)) + removedOnNewStateQueue.append(.removedOn(state)) + removedOnStackNewStateQueue.append(.removedOnStack(state)) } func enqueuePaused(state: T) { onPauseQueue.append(.onPause(state)) - onPauseEventQueue.append(.onPause(state)) onUpdatePreviousStateQueue.append(.onUpdate(state)) - onUpdatePreviousStateEventQueue.append(.onUpdate(state)) onInactiveUpdateNewStateQueue.append(.onInactiveUpdate(state)) - onInactiveUpdateNewStateEventQueue.append(.onInactiveUpdate(state)) + removedOnPreviousStateQueue.append(.removedOn(state)) + removedOnInactiveNewStateQueue.append(.removedOnInactive(state)) } // pop func enqueuePopped(state: T) { onUpdatePreviousStateQueue.append(.onUpdate(state)) - onUpdatePreviousStateEventQueue.append(.onUpdate(state)) onStackUpdatePreviousStateQueue.append(.onStackUpdate(state)) - onStackUpdatePreviousStateEventQueue.append(.onStackUpdate(state)) willExitQueue.append(.willExit(state)) - willExitEventQueue.append(.willExit(state)) + removedOnPreviousStateQueue.append(.removedOn(state)) + removedOnStackPreviousStateQueue.append(.removedOnStack(state)) } func enqueueResumed(state: T) { onResumeQueue.append(.onResume(state)) - onResumeEventQueue.append(.onResume(state)) onUpdateNewStateQueue.append(.onUpdate(state)) - onUpdateNewStateEventQueue.append(.onUpdate(state)) onInactiveUpdatePreviousStateQueue.append(.onInactiveUpdate(state)) - onInactiveUpdatePreviousStateEventQueue.append(.onInactiveUpdate(state)) + removedOnNewStateQueue.append(.removedOn(state)) + removedOnInactivePreviousStateQueue.append(.removedOnInactive(state)) } // clear @@ -138,17 +127,12 @@ final class StateTransitionQueue: StateStorageElement { onStackUpdateNewStateQueue = [] onInactiveUpdatePreviousStateQueue = [] onInactiveUpdateNewStateQueue = [] - - didEnterEventQueue = [] - willExitEventQueue = [] - onResumeEventQueue = [] - onPauseEventQueue = [] - onUpdateNewStateEventQueue = [] - onUpdatePreviousStateEventQueue = [] - onStackUpdateNewStateEventQueue = [] - onStackUpdatePreviousStateEventQueue = [] - onInactiveUpdateNewStateEventQueue = [] - onInactiveUpdatePreviousStateEventQueue = [] + removedOnNewStateQueue = [] + removedOnPreviousStateQueue = [] + removedOnStackNewStateQueue = [] + removedOnStackPreviousStateQueue = [] + removedOnInactiveNewStateQueue = [] + removedOnInactivePreviousStateQueue = [] } } @@ -173,8 +157,8 @@ extension AnyMap { valueRef(ofType: StateAssociatedSchedules.self)!.body.schedules } - func currentEventSchedulesWhichAssociatedStates() -> Set { - valueRef(ofType: StateAssociatedSchedules.self)!.body.eventSchedules + func currentRemovedSchedulesWhichAssociatedStates() -> Set { + valueRef(ofType: StateAssociatedSchedules.self)!.body.removedSchedules } // MARK: - queue @@ -239,6 +223,42 @@ extension AnyMap { .onInactiveUpdatePreviousStateQueue } + func removedOnNewStateQueue() -> [Schedule] { + valueRef(ofType: StateTransitionQueue.self)! + .body + .removedOnNewStateQueue + } + + func removedOnPreviousStateQueue() -> [Schedule] { + valueRef(ofType: StateTransitionQueue.self)! + .body + .removedOnPreviousStateQueue + } + + func removedOnStackNewStateQueue() -> [Schedule] { + valueRef(ofType: StateTransitionQueue.self)! + .body + .removedOnStackNewStateQueue + } + + func removedOnStackPreviousStateQueue() -> [Schedule] { + valueRef(ofType: StateTransitionQueue.self)! + .body + .removedOnStackPreviousStateQueue + } + + func removedOnInactiveNewStateQueue() -> [Schedule] { + valueRef(ofType: StateTransitionQueue.self)! + .body + .removedOnInactiveNewStateQueue + } + + func removedOnInactivePreviousStateQueue() -> [Schedule] { + valueRef(ofType: StateTransitionQueue.self)! + .body + .removedOnInactivePreviousStateQueue + } + func clearQueue() { valueRef(ofType: StateTransitionQueue.self)! .body.clear() @@ -309,6 +329,9 @@ public extension World { self.worldStorage.systemStorage.insertSchedule(.willExit(state)) self.worldStorage.systemStorage.insertSchedule(.onPause(state)) self.worldStorage.systemStorage.insertSchedule(.onResume(state)) + self.worldStorage.systemStorage.insertSchedule(.removedOn(state)) + self.worldStorage.systemStorage.insertSchedule(.removedOnStack(state)) + self.worldStorage.systemStorage.insertSchedule(.removedOnInactive(state)) } return self diff --git a/Sources/ECS/States/StateAssociatedEventSchedules.swift b/Sources/ECS/States/StateAssociatedEventSchedules.swift deleted file mode 100644 index 8781554..0000000 --- a/Sources/ECS/States/StateAssociatedEventSchedules.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// StateAssociatedEventSchedules.swift -// ECS_Swift -// -// Created by rrbox on 2025/06/15. -// - -public extension EventSchedule { - /// `state` が active の間の ``World/update(currentTime:)`` 実行時にイベントを受信します. - static func onUpdate(_ state: T) -> EventSchedule { - EventSchedule(id: OnUpdate(value: state)) - } - - /// `state` が inactive の間の ``World/update(currentTime:)`` 実行時にイベントを受信します. - static func onInactiveUpdate(_ state: T) -> EventSchedule { - EventSchedule(id: OnInactiveUpdate(value: state)) - } - - /// `state` が active/inactive 関係なくスタックされている間の ``World/update(currentTime:)`` 実行時にイベントを受信します. - static func onStackUpdate(_ state: T) -> EventSchedule { - EventSchedule(id: OnStackUpdate(value: state)) - } - - /// `state` を active にした時にイベントを受信します. - static func didEnter(_ state: T) -> EventSchedule { - EventSchedule(id: DidEnter(value: state)) - } - - /// `state` を inactive にした時にイベントを受信します. - static func willExit(_ state: T) -> EventSchedule { - EventSchedule(id: WillExit(value: state)) - } - - /// `state` を pause した時にイベントを受信します. - static func onPause(_ state: T) -> EventSchedule { - EventSchedule(id: OnPause(value: state)) - } - - /// `state` が resume された時にイベントを受信します. - static func onResume(_ state: T) -> EventSchedule { - EventSchedule(id: OnResume(value: state)) - } -} - diff --git a/Sources/ECS/States/StateAssociatedSchedules.swift b/Sources/ECS/States/StateAssociatedSchedules.swift index ef7b1ca..4dfe337 100644 --- a/Sources/ECS/States/StateAssociatedSchedules.swift +++ b/Sources/ECS/States/StateAssociatedSchedules.swift @@ -33,6 +33,19 @@ struct OnResume: Hashable { let value: T } +struct RemovedOn: Hashable { + let value: T +} + +struct RemovedOnStack: Hashable { + let value: T +} + +struct RemovedOnInactive: Hashable { + let value: T +} + + public extension Schedule { /// `state` が active の間の ``World/update(currentTime:)`` 実行時にシステムを実行します. static func onUpdate(_ state: T) -> Schedule { @@ -67,3 +80,19 @@ public extension Schedule { Schedule(id: OnResume(value: state)) } } + +// MARK: - removed + +public extension Schedule { + static func removedOn(_ state: T) -> Schedule { + Schedule(id: RemovedOn(value: state)) + } + + static func removedOnStack(_ state: T) -> Schedule { + Schedule(id: RemovedOnStack(value: state)) + } + + static func removedOnInactive(_ state: T) -> Schedule { + Schedule(id: RemovedOnInactive(value: state)) + } +} diff --git a/Sources/ECS/System/SystemBuffer.swift b/Sources/ECS/System/SystemBuffer.swift index ff8a0c6..db37243 100644 --- a/Sources/ECS/System/SystemBuffer.swift +++ b/Sources/ECS/System/SystemBuffer.swift @@ -33,7 +33,6 @@ extension AnyMap where Mode == SystemStorage { } } - extension AnyMap { public func systems(_ schedule: Schedule) -> [SystemExecute] { diff --git a/Sources/ECS/Systems/MultiParametersSystem.swift b/Sources/ECS/Systems/MultiParametersSystem.swift index 2330709..0139589 100644 --- a/Sources/ECS/Systems/MultiParametersSystem.swift +++ b/Sources/ECS/Systems/MultiParametersSystem.swift @@ -11,9 +11,6 @@ macro System(_ n: Int) = #externalMacro(module: "ECS_Macros", type: "SystemMacro @freestanding(declaration, names: named(addSystem(_:_:))) macro addSystemForWorld(_ n: Int) = #externalMacro(module: "ECS_Macros", type: "AddSystemMacroForWorld") -@freestanding(declaration, names: named(addSystem(_:_:))) -macro addSystemMacroForEventResponderBuilder(_ n: Int) = #externalMacro(module: "ECS_Macros", type: "AddSystemMacroForEventResponderBuilder") - enum Systems { #System(2) #System(3) @@ -34,7 +31,3 @@ enum Systems { public extension World { #addSystemForWorld(15) } - -public extension EventResponderBuilder { - #addSystemMacroForEventResponderBuilder(15) -} diff --git a/Sources/ECS/Systems/System.swift b/Sources/ECS/Systems/System.swift index e83613f..9e02a45 100644 --- a/Sources/ECS/Systems/System.swift +++ b/Sources/ECS/Systems/System.swift @@ -24,15 +24,3 @@ public extension World { return self } } - -public extension EventResponderBuilder { - @discardableResult func addSystem(_ schedule: EventSchedule, _ system: @escaping (P) -> ()) -> EventResponderBuilder { - if !self.systems.keys.contains(schedule) { - self.systems[schedule] = [] - } - - self.systems[schedule]!.append(System

(system)) - P.register(to: self.worldStorage) - return self - } -} diff --git a/Sources/ECS/WorldMethods/World+Init.swift b/Sources/ECS/WorldMethods/World+Init.swift index c242ec0..6cc938e 100644 --- a/Sources/ECS/WorldMethods/World+Init.swift +++ b/Sources/ECS/WorldMethods/World+Init.swift @@ -32,15 +32,18 @@ public extension World { worldStorage.systemStorage.insertSchedule(.update) worldStorage.systemStorage.insertSchedule(.postUpdate) + // world buffer に removed system を保持する領域を確保します. + worldStorage.systemStorage.insertSchedule(.removed) + // state storage に schedule 管理をするための準備をします. worldStorage.stateStorage.setUp() // world buffer に event queue を作成します. - worldStorage.eventStorage.registerEventReceivers() + worldStorage.eventStorage.registerEventQueues() - // world buffer に spawn/despawn event の streamer を登録します. - addCommandsEventStreamer(eventType: DidSpawnEvent.self) - addCommandsEventStreamer(eventType: WillDespawnEvent.self) + // world buffer に spawn event の streamer を登録します. + addEventStreamer(eventType: Spawned.self) + addRemovedEventStreamer() // world に一番最初のフレームで実行されるシステムを追加します. worldStorage.systemStorage.addSystem(.preStartUp, System(preUpdateSystemFirstFrameSystem(commands:))) diff --git a/Sources/ECS/WorldMethods/World+Spawn.swift b/Sources/ECS/WorldMethods/World+Spawn.swift index 740833c..26bbf7a 100644 --- a/Sources/ECS/WorldMethods/World+Spawn.swift +++ b/Sources/ECS/WorldMethods/World+Spawn.swift @@ -5,19 +5,10 @@ // Created by rrbox on 2023/08/10. // -public struct DidSpawnEvent: CommandsEventProtocol { +public struct Spawned: EventProtocol { public let spawnedEntity: Entity } -public struct WillDespawnEvent: CommandsEventProtocol { - public let despawnedEntity: Entity -} - -public extension Schedule { - static let didSpawn: Schedule = .onCommandsEvent(ofType: DidSpawnEvent.self) - static let willDespawn: Schedule = .onCommandsEvent(ofType: WillDespawnEvent.self) -} - extension World { /// Entity を登録します. /// @@ -32,10 +23,7 @@ extension World { self.worldStorage .chunkStorageRef .pushSpawned(entityRecord: entityRecord) - self.worldStorage - .eventStorage - .commandsEventWriter(eventOfType: DidSpawnEvent.self)! - .send(value: DidSpawnEvent(spawnedEntity: entityRecord.entity)) + self.sendEvent(Spawned(spawnedEntity: entityRecord.entity)) } /// Entity を削除します. @@ -48,7 +36,7 @@ extension World { .despawn(entity: entity) self.worldStorage .eventStorage - .commandsEventWriter(eventOfType: WillDespawnEvent.self)! - .send(value: WillDespawnEvent(despawnedEntity: entity)) + .removedEventReceiver()? + .pushDespawned(entity) } } diff --git a/Sources/ECS/WorldMethods/World+Update.swift b/Sources/ECS/WorldMethods/World+Update.swift index 36c82f6..ba7f610 100644 --- a/Sources/ECS/WorldMethods/World+Update.swift +++ b/Sources/ECS/WorldMethods/World+Update.swift @@ -64,6 +64,12 @@ extension World { let onStackUpdateNewStateQueue = stateStorage.onStackUpdateNewStateQueue() let onInactiveUpdatePreviousStateQueue = stateStorage.onInactivePreviousStateQueue() let onInactiveUpdateNewStateQueue = stateStorage.onInactiveNewStateQueue() + let removedOnNewStateQueue = stateStorage.removedOnNewStateQueue() + let removedOnPreviousStateQueue = stateStorage.removedOnPreviousStateQueue() + let removedOnStackNewStateQueue = stateStorage.removedOnStackNewStateQueue() + let removedOnStackPreviousStateQueue = stateStorage.removedOnStackPreviousStateQueue() + let removedOnInactiveNewStateQueue = stateStorage.removedOnInactiveNewStateQueue() + let removedOnInavtivePreviousStateQueue = stateStorage.removedOnInactivePreviousStateQueue() stateStorage.clearQueue() @@ -86,6 +92,18 @@ extension World { stateSchedulesManager.schedules.remove(previousState) } + for previousState in removedOnPreviousStateQueue { + stateSchedulesManager.removedSchedules.remove(previousState) + } + + for previousState in removedOnStackPreviousStateQueue { + stateSchedulesManager.removedSchedules.remove(previousState) + } + + for previousState in removedOnStackPreviousStateQueue { + stateSchedulesManager.removedSchedules.remove(previousState) + } + for willExit in willExitQueue { for system in systemStorage.systems(willExit) { system.execute(worldStorage) @@ -122,16 +140,31 @@ extension World { stateSchedulesManager.schedules.insert(newState) } + for newState in removedOnNewStateQueue { + stateSchedulesManager.removedSchedules.insert(newState) + } + + for newState in removedOnStackNewStateQueue { + stateSchedulesManager.removedSchedules.insert(newState) + } + + for newState in removedOnInactiveNewStateQueue { + stateSchedulesManager.removedSchedules.insert(newState) + } + self.applyEventQueue() } func updatePhase() { - for system in self.worldStorage.systemStorage.systems(self.updateSchedule) { + let systemStorage = worldStorage.systemStorage + let stateStorage = worldStorage.stateStorage + + for system in systemStorage.systems(self.updateSchedule) { system.execute(self.worldStorage) } // activate な state を shcedule によって紐づけられた system を実行します. - for schedule in self.worldStorage.stateStorage.currentSchedulesWhichAssociatedStates() { + for schedule in stateStorage.currentSchedulesWhichAssociatedStates() { for system in self.worldStorage.systemStorage.systems(schedule) { system.execute(self.worldStorage) } @@ -151,8 +184,8 @@ extension World { // 各システムが動いた後に実行される func applyCommandsPhase(_ commands: Commands) { - // will despawn event を配信します. - self.applyCommandsEventQueue(eventOfType: WillDespawnEvent.self) + // removed event を配信します. + self.applyRemovedEventQueue() // これから spawn する entity を chunk storage 内で enqueue // despawn 登録された entity を削除 @@ -163,9 +196,6 @@ extension World { // apply commands の際に push された entity を chunk に割り振ります(spawn). self.worldStorage.chunkStorageRef.applySpawnedEntityQueue() - // Did Spawn event を event system に発信します. - self.applyCommandsEventQueue(eventOfType: DidSpawnEvent.self) - self.applyCommands(commands: commands) // world 内の entity のコンポーネントの追加/削除. diff --git a/Sources/ECS_Macros/ECSMacros.swift b/Sources/ECS_Macros/ECSMacros.swift index b2c374b..dce3ff4 100644 --- a/Sources/ECS_Macros/ECSMacros.swift +++ b/Sources/ECS_Macros/ECSMacros.swift @@ -16,6 +16,5 @@ struct ECSMacros: CompilerPlugin { QueryMacro.self, SystemMacro.self, AddSystemMacroForWorld.self, - AddSystemMacroForEventResponderBuilder.self, ] } diff --git a/Sources/ECS_Macros/SystemMacro.swift b/Sources/ECS_Macros/SystemMacro.swift index 234afc7..fa23b4a 100644 --- a/Sources/ECS_Macros/SystemMacro.swift +++ b/Sources/ECS_Macros/SystemMacro.swift @@ -91,49 +91,3 @@ struct AddSystemMacroForWorld: DeclarationMacro { return declarations } } - -struct AddSystemMacroForEventResponderBuilder: DeclarationMacro { - static func createAddSystemDeclaration(_ n: Int) -> DeclSyntax { - let genericArguments = (0..(_ schedule: EventSchedule, _ system: @escaping (\(raw: valueTypes)) -> ()) -> EventResponderBuilder { - if !self.systems.keys.contains(schedule) { - self.systems[schedule] = [] - } - self.systems[schedule]?.append(Systems.System\(raw: n)<\(raw: valueTypes)>(system)) - \(raw: registerExpressions) - return self - } - """ - - return result - } - - static func expansion( - of node: some FreestandingMacroExpansionSyntax, - in context: some MacroExpansionContext - ) throws -> [DeclSyntax] { - guard let argument = node.argumentList.first?.expression else { - fatalError("compiler bug: the macro does not have any arguments") - } - guard let intArg = argument.as(IntegerLiteralExprSyntax.self)?.literal else { - fatalError("compiler bug: argument is not integer literal") - } - let n = Int(intArg.text)! - - let declarations = (2...n).reduce(into: []) { partialResult, i in - partialResult.append(createAddSystemDeclaration(i)) - } - - return declarations - } -} diff --git a/Sources/PlugIns/Graphic2D/Commands/EntityCommands+Graphic.swift b/Sources/PlugIns/Graphic2D/Commands/EntityCommands+Graphic.swift index 88063d1..773a34d 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()) } } @@ -104,45 +94,98 @@ public extension EntityCommands { } } +// TODO: - Rerouce を使用する(WillDespawnEvent を廃止したい) + func removeChildIfDespawned( - despawnEvent: EventReader, - query: Query, - parentQuery: Query + removed: Removed, + hierarchy: Resource, + commands: Commands ) { - 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 hierarchy = hierarchy.resource + for despawnedEntity in removed.entities { + let parent = hierarchy.parent(of: despawnedEntity) + hierarchy.removeFromParent(despawnedEntity) + 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, + removed: Removed, + hierarchy: Resource, commands: Commands ) { - // despawn した entity と自分の親が一致する子を despawn する. - for event in despawnedEntityEvent.events { - let despawnedEntity = event.despawnedEntity - despawnChildRecursive(despawnedEntity: despawnedEntity, - children: children, - commands: commands) + removed.forEach { despawnedEntity in + despawnChildRecursive( + despawnedEntity: despawnedEntity, + hierarchy: hierarchy, + commands: commands + ) + hierarchy.resource.removeRecursively(entity: despawnedEntity) + } +} + +/// - hirarchy から削除された child は despawn しません +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) + } +} + +// FIXME: - post update で despawn が呼ばれた場合、Nodes の紐付けを削除できない(Removed 実装後) +// - Nodes の仕組み上2重で削除しても問題ないので、防衛的にこのシステムで切り離してもいいかも +@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..160f86e 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,36 @@ 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) { - for event in despawn.events { - let despawnedEntity = event.despawnedEntity - nodes.resource.removeNode(forEntity: despawnedEntity) +func _removeNodeIfDespawned( + removed: Removed, + nodes: Resource +) { + removed.forEach { despawnedEntity in + nodes.resource + .removeNode(forEntity: despawnedEntity)? + .removeFromParent() } } @@ -100,17 +120,14 @@ 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:)) - - .buildWillDespawnResponder { responder in - responder - .addSystem(.update, removeChildIfDespawned(despawnEvent:query:parentQuery:)) - .addSystem(.update, despawnChildIfParentDespawned(despawnedEntityEvent:children:commands:)) - .addSystem(.update, _removeNodeIfDespawned(despawn:nodes:)) - } + .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:)) + .addSystem(.removed, removeChildIfDespawned(removed:hierarchy:commands:)) + .addSystem(.removed, despawnChildIfParentDespawned(removed:hierarchy:commands:)) + .addSystem(.removed, _removeNodeIfDespawned(removed: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() // ここは通過しません. diff --git a/Tests/ecs-swiftTests/EventTests.swift b/Tests/ecs-swiftTests/EventTests.swift index 77ccec6..373a6bf 100644 --- a/Tests/ecs-swiftTests/EventTests.swift +++ b/Tests/ecs-swiftTests/EventTests.swift @@ -12,15 +12,26 @@ struct TestEvent: EventProtocol { let name: String } -func testEvent(event: EventReader, eventWriter: EventWriter, commands: Commands, currentTime: Resource) { - for event in event.events { +enum EventTestState: StateProtocol { + case stateA + case stateB + case stateC +} + +func testEvent( + events: EventReader, + eventWriter: EventWriter, + commands: Commands, + currentTime: Resource +) { + events.forEach { event in print("---test event read---") print("frame:", currentTime.resource.value) print("<- read event:", event.name) let spawned = commands.spawn().addComponent(TestComponent(content: event.name)).id() print("-> spawn:", spawned) print("-> event send:", "\"link\"") - eventWriter.send(value: TestEvent(name: "[\(currentTime.resource.value)]: link")) + eventWriter.send(TestEvent(name: "[\(currentTime.resource.value)]: link")) print("---") print() } @@ -29,13 +40,17 @@ func testEvent(event: EventReader, eventWriter: EventWriter) { print("---set up---") print("-> event send:", "\"test event\"") - eventWriter.send(value: TestEvent(name: "test event")) + eventWriter.send(TestEvent(name: "test event")) print("---") print() } -func spawnedEntitySystem(eventReader: EventReader, commands: Commands, currentTime: Resource) { - for event in eventReader.events { +func spawnedEntitySystem( + events: EventReader, + commands: Commands, + currentTime: Resource +) { + events.forEach { event in print("---spawned entity event read---") print("frame:", currentTime.resource.value) print("<- spawned(receive):", event.spawnedEntity) @@ -46,11 +61,15 @@ func spawnedEntitySystem(eventReader: EventReader, commands: Comm } } -func despanedEntitySystem(eventReader: EventReader, commands: Commands, currentTime: Resource) { - for event in eventReader.events { +func despanedEntitySystem( + removed: Removed, + commands: Commands, + currentTime: Resource +) { + removed.forEach { removedEntity in print("---despawned entity event read---") print("frame:", currentTime.resource.value) - print("<- despawned(receive):", event.despawnedEntity) + print("<- despawned(receive):", removedEntity) print("---") print() } @@ -62,12 +81,10 @@ final class EventTests: XCTestCase { let world = World() .addEventStreamer(eventType: TestEvent.self) - .buildEventResponder(TestEvent.self, { responder in - responder.addSystem(.update, testEvent(event:eventWriter:commands:currentTime:)) - }) + .addSystem(.update, testEvent(events:eventWriter:commands:currentTime:)) .addSystem(.startUp, setUp(eventWriter:)) - .addSystem(.didSpawn, spawnedEntitySystem(eventReader:commands:currentTime:)) - .addSystem(.willDespawn, despanedEntitySystem(eventReader:commands:currentTime:)) + .addSystem(.update, spawnedEntitySystem(events:commands:currentTime:)) + .addSystem(.removed, despanedEntitySystem(removed:commands:currentTime:)) world.setUpWorld() world.update(currentTime: -1) @@ -83,60 +100,63 @@ final class EventTests: XCTestCase { let world = World() .addEventStreamer(eventType: TestEvent.self) - .addSystem(.startUp, { (eventWriter: EventWriter) in - eventWriter.send(value: .init(name: "test event")) + .addSystem(.startUp) { (eventWriter: EventWriter) in + eventWriter.send(.init(name: "test event")) ECSTAssertStepOrder(currentStep: 0, steps: &flags) - }) - .buildEventResponder(TestEvent.self) { responder in - responder.addSystem(.update) { (event: EventReader, commands: Commands) in - for event in event.events { - ECSTAssertStepOrder(currentStep: 1, steps: &flags) - commands.spawn().addComponent(TestComponent(content: event.name)) - } + } + .addSystem(.update) { (event: EventReader, commands: Commands) in + event.forEach { event in + ECSTAssertStepOrder(currentStep: 1, steps: &flags) + commands.spawn().addComponent(TestComponent(content: event.name)) } } - .buildDidSpawnResponder { responder in - responder - .addSystem(.update) { (event: EventReader, commands: Commands) in - for event in event.events { - ECSTAssertStepOrder(currentStep: 2, steps: &flags) - commands.despawn(entity: event.spawnedEntity) - } - } + .addSystem(.removed) { (removed: Removed, query: Query) in + ECSTAssertStepOrder(currentStep: 3, steps: &flags) } - .buildWillDespawnResponder { responder in - responder - .addSystem(.update) { (event: EventReader) in - ECSTAssertStepOrder(currentStep: 3, steps: &flags) - } + .addSystem(.update) { (events: EventReader, commands: Commands) in + events.forEach { spawned in + ECSTAssertStepOrder(currentStep: 2, steps: &flags) + commands.despawn(entity: spawned.spawnedEntity) + } } world.setUpWorld() world.update(currentTime: -1) world.update(currentTime: 0) + world.update(currentTime: 1) XCTAssertEqual(flags, [1, 1, 1, 1]) } func testSendTwoEventsInOneUpdate() { - var count = 0 + var receivedEventCounts = [0, 0] + var flags = [0, 0, 0] let world = World() .addEventStreamer(eventType: TestEvent.self) .addSystem(.startUp, { (eventWriter: EventWriter) in - eventWriter.send(value: .init(name: "event 1")) - eventWriter.send(value: .init(name: "event 2")) + eventWriter.send(.init(name: "event 1")) + eventWriter.send(.init(name: "event 2")) + ECSTAssertStepOrder(currentStep: 0, steps: &flags) + }) + .addSystem(.postStartUp, { (events: EventReader) in + receivedEventCounts[0] += events.count + ECSTAssertStepOrder(currentStep: 1, steps: &flags) }) - .buildEventResponder(TestEvent.self) { responder in - responder.addSystem(.update) { (event: EventReader) in - count += event.events.count + .addSystem(.update) { (events: EventReader) in + receivedEventCounts[1] += events.count + if !events.isEmpty { + ECSTAssertStepOrder(currentStep: 2, steps: &flags) } } world.setUpWorld() + world.update(currentTime: -1) world.update(currentTime: 0) + world.update(currentTime: 1) - XCTAssertEqual(count, 2) + XCTAssertEqual(receivedEventCounts, [2, 2]) + XCTAssertEqual(flags, [1, 1, 1]) } func testSystemExecutesOnceWithTwoEvents() { @@ -144,19 +164,127 @@ final class EventTests: XCTestCase { let world = World() .addEventStreamer(eventType: TestEvent.self) - .addSystem(.startUp, { (eventWriter: EventWriter) in - eventWriter.send(value: .init(name: "event 1")) - eventWriter.send(value: .init(name: "event 2")) - }) - .buildEventResponder(TestEvent.self) { responder in - responder.addSystem(.update) { (_: EventReader) in + .addSystem(.startUp) { (eventWriter: EventWriter) in + eventWriter.send(.init(name: "event 1")) + eventWriter.send(.init(name: "event 2")) + } + .addSystem(.update) { (events: EventReader) in + if !events.isEmpty { executionCount += 1 } } world.setUpWorld() + world.update(currentTime: -1) world.update(currentTime: 0) XCTAssertEqual(executionCount, 1) } + + func testRemovedOnEvent() { + var flags = [0, 0, 0] + let world = World() + .addEventStreamer(eventType: TestEvent.self) + .addState(initialState: EventTestState.stateA, states: [ + .stateA, .stateB, .stateC + ]) + .addSystem(.startUp) { (commands: Commands, state: State) in + commands.spawn() + state.enter(.stateA) + } + .addSystem(.update) { (spawned: EventReader, commands: Commands) in + spawned.forEach { event in + commands.despawn(entity: event.spawnedEntity) + } + } + .addSystem(.removedOn(EventTestState.stateA)) { (removed: Removed, commands: Commands, state: State) in + ECSTAssertStepOrder(currentStep: 0, steps: &flags) + state.enter(.stateB) + commands.spawn() + } + .addSystem(.removedOn(EventTestState.stateB)) { (removed: Removed, commands: Commands, state: State) in + ECSTAssertStepOrder(currentStep: 1, steps: &flags) + state.push(.stateC) + commands.spawn() + } + .addSystem(.removedOn(EventTestState.stateC)) { (removed: Removed) in + ECSTAssertStepOrder(currentStep: 2, steps: &flags) + } + world.update(currentTime: -1) + world.update(currentTime: 0) + world.update(currentTime: 1) + world.update(currentTime: 2) + XCTAssertEqual(flags, [1, 1, 1]) + } + + func testRemovedOnStackEvent() { + var flagsA = [0, 0] + var flagsB = [0, 0] + let world = World() + .addEventStreamer(eventType: TestEvent.self) + .addState(initialState: EventTestState.stateA, states: [ + .stateA, .stateB + ]) + .addSystem(.startUp) { (commands: Commands, state: State) in + commands.spawn() + } + .addSystem(.update) { (spawned: EventReader, commands: Commands) in + spawned.forEach { event in + commands.despawn(entity: event.spawnedEntity) + } + } + .addSystem(.removedOnStack(EventTestState.stateA)) { (removed: Removed, commands: Commands, state: State, currentTime: Resource) in + switch currentTime.resource.value { + case -1: XCTFail() + case 0: + ECSTAssertStepOrder(currentStep: 0, steps: &flagsA) + ECSTAssertStepOrder(currentStep: 0, steps: &flagsB) + state.push(.stateB) + commands.spawn() + case 1: + ECSTAssertStepOrder(currentStep: 1, steps: &flagsA) + default: + XCTFail() + } + } + .addSystem(.removedOnStack(EventTestState.stateB)) { (removed: Removed, commands: Commands, state: State) in + ECSTAssertStepOrder(currentStep: 1, steps: &flagsB) + commands.spawn() + } + world.update(currentTime: -1) + world.update(currentTime: 0) + world.update(currentTime: 1) + XCTAssertEqual(flagsA, [1, 1]) + XCTAssertEqual(flagsB, [1, 1]) + } + + func testRemovedOnInactiveEvent() { + var flags = [0, 0] + let world = World() + .addEventStreamer(eventType: TestEvent.self) + .addState(initialState: EventTestState.stateA, states: [ + .stateA, .stateB + ]) + .addSystem(.startUp) { (commands: Commands, state: State) in + commands.spawn() + state.enter(.stateA) + } + .addSystem(.update) { (spawned: EventReader, commands: Commands) in + spawned.forEach { event in + commands.despawn(entity: event.spawnedEntity) + } + } + .addSystem(.removedOn(EventTestState.stateA)) { (removed: Removed, commands: Commands, state: State) in + ECSTAssertStepOrder(currentStep: 0, steps: &flags) + state.push(.stateB) + commands.spawn() + } + .addSystem(.removedOnInactive(EventTestState.stateA), { (removed: Removed) in + ECSTAssertStepOrder(currentStep: 1, steps: &flags) + }) + world.update(currentTime: -1) + world.update(currentTime: 0) + world.update(currentTime: 1) + XCTAssertEqual(flags, [1, 1]) + } }