Components
A component is a plain data container: a Swift class that holds state but no logic. Every component conforms to the Component marker protocol:
public protocol Component {}
Representation Invariants
To maintain world consistency, components must adhere to the following rules:
- Reference Semantics: A component retrieved via
getComponentis a live reference—mutations are immediately visible to all systems. - Unique Ownership: Each component instance should be owned by exactly one entity. Sharing a component instance between two entities (e.g., two enemies sharing one
TransformComponent) is a misuse of the API and will lead to undefined behavior. - Pure Data: Components should not contain methods that encapsulate logic or side effects. Logic belongs in Systems.
Core Data Types: StatValue
Many components (Health, Mana, MoveSpeed) use the StatValue struct to manage numerical data. This structure is designed to support baseline values and temporary modifiers without losing the original state.
| Field | Purpose |
|---|---|
base | The unmodified starting value. Never changed by temporary gameplay effects; serves as a reference for percentage-based modifiers. |
current | The active runtime value. Modified by damage, healing, or status effects. |
max | An optional ceiling. If non-nil, current is clamped to this value. |
Invariants:
currentnever exceedsmaxifmaxis non-nil (after callingclampToMax()).currentnever falls below a specified floor (usually0) after callingclampToMin().
Component Storage
ComponentStorage acts as a 2D mapping between EntityID and ComponentType.
ComponentStore<T>: A dictionary mappingEntityIDtoT.ComponentStorage Registry: A map fromObjectIdentifier(T.self)to the correspondingComponentStore<T>.
This layout ensures O(1) lookup for any component type and allows systems to efficiently query all entities possessing a specific set of data.
Working with Components
Add a Component
world.addComponent(component: TransformComponent(position: .zero), to: entity)
Read and Mutate
Since components are classes, you mutate them directly on the reference:
if let health = world.getComponent(type: HealthComponent.self, for: entity) {
health.value.current -= 10
health.value.clampToMin() // StatValue helper
}
Remove a Component
world.removeComponent(type: VelocityComponent.self, from: entity)
Common Component Domains
Physics & Input
TransformComponent: Position, rotation, and scale.VelocityComponent: Linear and angular velocity vectors.MoveSpeedComponent: AStatValuewrapper around movement speed.InputComponent: Stores move/aim vectors and shooting intent.
Combat & Stats
HealthComponent: UsesStatValueto track HP.ManaComponent: UsesStatValueto track mana and regeneration rates.KnockbackComponent: A transient component added during knockback. Its presence serves as a state flag for movement systems.
Weapons & Projectiles
EquippedWeaponComponent: Stores references to primary and secondary weapon entities.WeaponAmmoComponent: Tracks magazine count and reload timers.ProjectileComponent: Stores the array ofProjectileHitEffectto apply on impact.