Systems
A system contains the game logic. Each frame, the SystemManager calls every registered system in dependency order. Systems communicate solely through changes in component data via the World instance.
The System Protocol
public protocol System: AnyObject {
/// Systems that must finish before this system runs each update.
var dependencies: [System.Type] { get }
/// Called once per game-loop tick.
func update(deltaTime: Double, world: World)
}
Systems are reference types (AnyObject), which allows them to hold internal state, such as node registries or weak references to external objects (factories, buffers, etc.).
Dependency Management (DAG)
Instead of manual priority numbers, systems declare their prerequisites. The SystemManager builds a Directed Acyclic Graph (DAG) and uses Kahn’s Algorithm to produce a valid execution order.
Why use a DAG?
- Extensibility: Adding a new system only requires knowing its direct prerequisites, not the global state of the entire pipeline.
- Safety: Cycle detection at startup prevents infinite loops or race conditions where two systems depend on each other.
Writing a New System
- Create a
final classconforming toSystem. - Override
dependenciesto declare prerequisites. - Implement
update(deltaTime:world:). - Register it with
SystemManager.
Code Example: Class-based Mutation
Since components are classes, you mutate them by reference. No write-back to the world is necessary.
final class PoisonSystem: System {
// Must run after damage is processed but before rendering
var dependencies: [System.Type] { [DamageSystem.self] }
func update(deltaTime: Double, world: World) {
for entity in world.entities(with: PoisonComponent.self) {
guard let health = world.getComponent(type: HealthComponent.self, for: entity) else { continue }
// Mutate directly on the reference
health.value.current -= 0.1 * Float(deltaTime)
health.value.clampToMin()
}
}
}
Execution Phases
Systems are conceptually grouped into several distinct phases within the global pipeline:
1. Input & AI Phase
The InputSystem processes buffered user commands, while the EnemyAISystem computes pathfinding and intent (move/aim vectors).
2. Physics Phase
The KnockbackSystem and MovementSystem integrate calculated velocities into world positions. The ProjectileSystem specifically handles the specialized physics of non-kinematic projectiles.
3. Combat Phase
The WeaponSystem ticks cooldowns and handles the sequential execution of Weapon Effects. The DamageSystem processes hit events generated by the CollisionSystem to apply health deductions.
4. World & Orchestration Phase
The LevelRoomTransitionSystem and RoomClearSystem interact with the LevelOrchestrator to handle room transitions, locks, and progression logic.
5. Output & Rendering Phase
The CameraSystem updates the viewport based on the focus target. Finally, the RenderSystem and HUDSystem pass final data to the backend protocols.
Decoupling from SpriteKit
To ensure the game is independently testable, output systems do not depend on SpriteKit types directly. They communicate via Protocols declared in ECS/Protocols/.
| System | Protocol Dependency | Purpose |
|---|---|---|
RenderSystem | RenderingBackend | Syncing sprite and transform data to nodes. |
HUDSystem | HUDBackend | Updating UI health, mana, and ammo bars. |
CameraSystem | CameraBackend | Managing the visual viewport and smoothing. |
Concrete Adapters (e.g., SpriteKitRenderingAdapter) are injected into these systems at startup. This allows unit tests to inject Mocks instead of real rendering nodes.