Health & Damage System
Problem
You want entities to have health, take damage, heal, and die when health reaches zero.
Solution
Components
[Component]
public partial struct Health : IComponent
{
public int Current;
public int Max;
}
[Component]
public partial struct DamageReceived : IComponent
{
public int Amount;
public Entity Source; // Who dealt the damage
}
[Component]
public partial struct HealReceived : IComponent
{
public int Amount;
}
[TagComponent]
public partial struct Dead : ITagComponent { }
[TagComponent]
public partial struct Invulnerable : ITagComponent { }
Damage Processing System
public class DamageSystem : SystemBase
{
public override SystemPhase Phase => SystemPhase.Update;
public override int Order => 100; // Process after gameplay systems
public override void Update(float deltaTime)
{
var buffer = World.GetCommandBuffer();
// Process damage
foreach (var entity in World.Query<Health, DamageReceived>().Without<Invulnerable>())
{
ref var health = ref World.Get<Health>(entity);
ref readonly var damage = ref World.Get<DamageReceived>(entity);
health.Current -= damage.Amount;
// Clamp to zero
if (health.Current < 0)
health.Current = 0;
// Remove the damage component (it's been processed)
buffer.Remove<DamageReceived>(entity);
// Check for death
if (health.Current == 0)
{
buffer.Add<Dead>(entity);
}
}
// Process healing
foreach (var entity in World.Query<Health, HealReceived>().Without<Dead>())
{
ref var health = ref World.Get<Health>(entity);
ref readonly var heal = ref World.Get<HealReceived>(entity);
health.Current += heal.Amount;
// Clamp to max
if (health.Current > health.Max)
health.Current = health.Max;
buffer.Remove<HealReceived>(entity);
}
buffer.Execute();
}
}
Death Handling System
public class DeathSystem : SystemBase
{
public override SystemPhase Phase => SystemPhase.LateUpdate;
public override void Update(float deltaTime)
{
var buffer = World.GetCommandBuffer();
foreach (var entity in World.Query<Dead>())
{
// Option 1: Destroy immediately
buffer.Despawn(entity);
// Option 2: Keep for death animation (see variations)
// buffer.Add(entity, new DeathAnimation { Timer = 2f });
}
buffer.Execute();
}
}
Usage
using var world = new World();
world.AddSystem<DamageSystem>();
world.AddSystem<DeathSystem>();
// Create an entity with health
var enemy = world.Spawn()
.With(new Health { Current = 100, Max = 100 })
.Build();
// Deal damage by adding a component
world.Add(enemy, new DamageReceived { Amount = 30, Source = player });
// Heal by adding a component
world.Add(enemy, new HealReceived { Amount = 20 });
// Make temporarily invulnerable
world.Add<Invulnerable>(enemy);
// Update processes all damage/healing
world.Update(deltaTime);
Why This Works
Event-Driven via Components
Instead of calling entity.TakeDamage(30), you add a DamageReceived component. Benefits:
- Decoupling: Damage source doesn't need to know about health systems
- Batching: All damage processed together in one system
- Queryable: Can find "all entities that took damage this frame"
- Auditable: The
Sourcefield tracks who dealt damage
One Frame Lifecycle
Damage/heal components are:
- Added by gameplay code
- Processed by the damage system
- Removed in the same frame
This prevents double-processing and keeps the component as a "this frame event."
Invulnerability via Tag
Using Without<Invulnerable> in the query means:
- Invulnerable entities are skipped entirely
- No conditional logic in the damage loop
- Easy to add/remove invulnerability frames
Death as State
The Dead tag serves multiple purposes:
- Prevents further healing (
Without<Dead>) - Queryable for death effects
- Can be checked by other systems (AI, rendering)
Variations
Damage Types
public enum DamageType
{
Physical,
Fire,
Ice,
Poison
}
[Component]
public partial struct DamageReceived : IComponent
{
public int Amount;
public DamageType Type;
public Entity Source;
}
[Component]
public partial struct DamageResistance : IComponent
{
public float Physical; // 0 = no resistance, 1 = immune
public float Fire;
public float Ice;
public float Poison;
}
// In system:
float resistance = type switch
{
DamageType.Physical => res.Physical,
DamageType.Fire => res.Fire,
// ...
};
int finalDamage = (int)(damage.Amount * (1f - resistance));
Damage Over Time
[Component]
public partial struct PoisonEffect : IComponent
{
public int DamagePerSecond;
public float RemainingDuration;
public Entity Source;
}
public class PoisonSystem : SystemBase
{
private float tickTimer = 0f;
private const float TickInterval = 1f;
public override void Update(float deltaTime)
{
tickTimer += deltaTime;
foreach (var entity in World.Query<PoisonEffect>())
{
ref var poison = ref World.Get<PoisonEffect>(entity);
poison.RemainingDuration -= deltaTime;
// Apply damage every tick
if (tickTimer >= TickInterval)
{
World.Add(entity, new DamageReceived
{
Amount = poison.DamagePerSecond,
Source = poison.Source
});
}
// Remove expired poison
if (poison.RemainingDuration <= 0)
{
World.Remove<PoisonEffect>(entity);
}
}
if (tickTimer >= TickInterval)
tickTimer = 0f;
}
}
Death Animation
[Component]
public partial struct DeathAnimation : IComponent
{
public float Timer;
}
public class DeathAnimationSystem : SystemBase
{
public override void Update(float deltaTime)
{
var buffer = World.GetCommandBuffer();
foreach (var entity in World.Query<Dead, DeathAnimation>())
{
ref var anim = ref World.Get<DeathAnimation>(entity);
anim.Timer -= deltaTime;
if (anim.Timer <= 0)
{
buffer.Despawn(entity);
}
}
buffer.Execute();
}
}
Shields Before Health
[Component]
public partial struct Shield : IComponent
{
public int Current;
public int Max;
}
// In damage system, check shield first:
foreach (var entity in World.Query<Health, Shield, DamageReceived>())
{
ref var health = ref World.Get<Health>(entity);
ref var shield = ref World.Get<Shield>(entity);
ref readonly var damage = ref World.Get<DamageReceived>(entity);
var remaining = damage.Amount;
// Absorb with shield first
if (shield.Current > 0)
{
var absorbed = Math.Min(shield.Current, remaining);
shield.Current -= absorbed;
remaining -= absorbed;
}
// Apply remaining to health
health.Current -= remaining;
// ...
}
See Also
- Events Guide - Entity lifecycle events
- Command Buffer - Safe entity modification
- State Machines - For complex AI death states