Damage Module
The damage module handles all health-reduction logic in the game. It is composed of three systems and components working together: DamageSystem applies damage from collisions each frame, and InvincibilitySystem ticks down and removes post-hit invincibility frames.
Components
| Component | Role |
|---|---|
ContactDamageComponent | Attached to enemies; defines how much damage they deal on contact with the player |
InvincibilityComponent | Temporarily attached to the player after taking damage; prevents follow-up hits until it expires |
Systems
DamageSystem
DamageSystem runs at priority: 40 and processes two categories of collision each frame, sourced from the CollisionEventBuffer:
| Event Buffer | Description |
|---|---|
projectileHitEnemy | A player projectile overlapped an enemy |
playerHitByEnemy | An enemy overlapped the player |
Projectile → Enemy
For each projectileHitEnemy event:
- Skip if either entity is no longer alive.
- Deduplicate — if the same projectile appears in multiple events this frame, damage is only applied once.
- Subtract
event.damagefrom the enemy'sHealthComponent. - Enqueue the projectile for destruction via
DestructionQueue(regardless of deduplication — all overlapping projectile events still destroy the projectile).
Enemy Contact → Player
For each playerHitByEnemy event:
- Skip if the player entity is no longer alive.
- Skip if the player currently has an
InvincibilityComponent(they are in i-frames). - Subtract
event.damagefrom the player'sHealthComponent. - Attach an
InvincibilityComponentto the player to prevent the next contact event from dealing damage immediately.
InvincibilitySystem
InvincibilitySystem runs at priority: 45 (after DamageSystem) and manages the lifespan of InvincibilityComponent.
Each frame, for every entity carrying an InvincibilityComponent:
- Subtract
deltaTimefromremainingTime. - If
remainingTimehas reached zero or below, remove the component — the entity can take damage again. - Otherwise, write the updated
remainingTimeback to the component.
Invincibility Frames (I-Frames)
After the player takes contact damage, an InvincibilityComponent is attached with a default duration of 0.5 seconds. During this window, all playerHitByEnemy events are ignored by DamageSystem.
This prevents a single collision from draining multiple health points across consecutive frames while the physics contact persists.
// Default i-frame window applied by DamageSystem after a contact hit
InvincibilityComponent(remainingTime: 0.5)
To adjust the i-frame window, change the remainingTime passed in DamageSystem.applyContactDamage().
Update Loop Summary
Each frame, in system priority order:
-
DamageSystem(priority 40)- Reads
CollisionEventBuffer.projectileHitEnemy→ applies damage to enemies, destroys projectiles. - Reads
CollisionEventBuffer.playerHitByEnemy→ applies damage to player if not invincible, then grants i-frames.
- Reads
-
InvincibilitySystem(priority 45)- Ticks down
InvincibilityComponent.remainingTimeon every entity that has one. - Removes the component once the timer expires.
- Ticks down
ContactDamageComponent
Attach this to any enemy entity to define how much damage it deals when it physically contacts the player. The value is read from the collision event — it is the responsibility of the collision detection layer to populate event.damage from this component.
public struct ContactDamageComponent: Component {
public var damage: Float
public init(damage: Float)
}
Example — setting contact damage when creating an enemy:
world.addComponent(
component: ContactDamageComponent(damage: 15),
to: enemy
)
Adding Damage to a New Entity Type
Enemy that deals contact damage
Add ContactDamageComponent to the entity at spawn time:
let enemy = EnemyEntityFactory(at: position, type: .charger, baseScale: scale).make(in: world)
world.addComponent(component: ContactDamageComponent(damage: 20), to: enemy)
Projectile that damages enemies
Populate the projectileHitEnemy buffer in your collision detection code with the appropriate damage value. DamageSystem will handle the rest — no changes to the system are needed.
Custom i-frame duration
If a specific entity type requires a longer or shorter invincibility window (e.g. a boss attack that should not grant i-frames, or a hazard with a shorter cooldown), attach InvincibilityComponent manually with a different remainingTime value immediately after applying damage:
// Short i-frame window for a hazard (0.2s instead of default 0.5s)
world.addComponent(component: InvincibilityComponent(remainingTime: 0.2), to: player)
Dependencies
| Dependency | Role |
|---|---|
CollisionEventBuffer | Source of projectileHitEnemy and playerHitByEnemy events |
DestructionQueue | Receives projectiles to be destroyed after a hit |
HealthComponent | Read and modified to apply damage |
InvincibilityComponent | Checked by DamageSystem; managed by InvincibilitySystem |
KnockbackSystem | DamageSystem has no direct dependency, but knockback is applied separately after damage |