Skip to main content

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

  1. Create a final class conforming to System.
  2. Override dependencies to declare prerequisites.
  3. Implement update(deltaTime:world:).
  4. 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/.

SystemProtocol DependencyPurpose
RenderSystemRenderingBackendSyncing sprite and transform data to nodes.
HUDSystemHUDBackendUpdating UI health, mana, and ammo bars.
CameraSystemCameraBackendManaging 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.