Files
UE5-Modular-Game-Framework/docs/blueprints/02-player/08_BPC_HealthSystem.md

410 lines
18 KiB
Markdown

# 08 — Health System (`BPC_HealthSystem`)
> **⚡ C++ Status: Stub** — `Source/PG_Framework/Public/Player/BPC_HealthSystem.h` provides the UCLASS shell, `MaxHealth`/`CurrentHealth` variables, `OnHealthChanged` and `OnDeath` event dispatchers. **The C++ stub has NO gameplay logic** — it exists so other C++ classes (`BPC_StateManager`, `BPC_DamageReceptionSystem`) can forward-reference it. **Create a BP child** and build ALL logic from this spec: `TakeDamage()`, `Heal()`, `OnDeath()` transition, damage resistance modifiers, health regen tick, `I_Damageable` interface. See `docs/developer/cpp-integration-guide.md` for setup steps.
>
> ---
## Purpose
Manages the player's health pool, damage application with type-based resistance, health regeneration, death state, and critical-health events. Serves as the central survivability component on the player character.
## Dependencies
- **Requires:** `FL_GameUtilities` (for safe interface casts), `I_Damageable` (interface on the owner), `GI_GameFramework` (for GamePhase queries)
- **Required By:** `BPC_StressSystem` (health below threshold triggers fear events), `BPC_PlayerMetricsTracker` (death tracking), `WBP_HUD` (health bar), `AI_EnemyController` (player health as awareness trigger), `BPC_InteractionDetector` (low-health checks)
- **Engine/Plugin Requirements:** GameplayTags, DeveloperSettings
## Class Info
| Property | Value |
|----------|-------|
| **Parent Class** | `ActorComponent` |
| **Class Type** | Blueprint Component |
| **Asset Path** | `Content/Framework/Player/BPC_HealthSystem` |
| **Implements Interfaces** | `I_Damageable` |
---
## 1. Enums
### `E_DamageType`
| Value | Description |
|-------|-------------|
| `Physical = 0` | Bullets, blunt force, falls |
| `Arcane = 1` | Magic, curse, psychic damage |
| `Fire = 2` | Burning, environmental heat |
| `Poison = 3` | Toxins, venom, chemical |
| `Fear = 4` | Psychological damage (stress overflow) |
| `Environmental = 5` | Traps, crushing, drowning |
| `True = 6` | Bypasses all resistances, no reduction |
### `E_DeathState`
| Value | Description |
|-------|-------------|
| `Alive = 0` | Normal gameplay |
| `Dying = 1` | Downed state, waiting for recovery or final death |
| `Dead = 2` | Character is dead, no further actions possible |
| `PermaDeath = 3` | Save-scrubbing locked, forced reload |
---
## 2. Structs
### `S_DamageEvent`
| Field | Type | Description |
|-------|------|-------------|
| `BaseAmount` | `Float` | Raw damage before resistance |
| `DamageType` | `E_DamageType` | Category of damage |
| `Instigator` | `Actor` | Who caused the damage |
| `HitLocation` | `Vector` | World location of impact |
| `DamageTags` | `GameplayTagContainer` | Additional contextual tags |
| `bIsCriticalHit` | `Boolean` | True if this is a headshot / weak-point hit |
| `SourceDescription` | `Text` | Human-readable source (for log / UI) |
### `S_DamageResistance`
| Field | Type | Description |
|-------|------|-------------|
| `DamageType` | `E_DamageType` | Type this resistance applies to |
| `Multiplier` | `Float` | 1.0 = normal, 0.5 = half damage, 2.0 = double |
| `bIsStackable` | `Boolean` | Can this resistance stack with others? |
---
## 3. Variables
### Configuration (Instance Editable, Expose On Spawn)
| Variable | Type | Default | Category | Description |
|----------|------|---------|----------|-------------|
| `MaxHealth` | `Float` | `100.0` | `Health Config` | Maximum hit points |
| `bRegenEnabled` | `Boolean` | `true` | `Health Config` | Allow passive health regeneration |
| `RegenDelay` | `Float` | `3.0` | `Health Config` | Seconds after last damage before regen starts |
| `RegenRate` | `Float` | `2.0` | `Health Config` | HP recovered per second during regen |
| `RegenDelayUntil` | `Float` | `0.0` | `Health Config` | World time when regen can resume |
| `CriticalHealthPercent` | `Float` | `0.25` | `Health Config` | Below this fraction of MaxHealth, fire critical event |
| `DefaultResistances` | `Array<S_DamageResistance>` | `[]` | `Health Config` | Base damage resistances for this character |
| `bEnableFriendlyFire` | `Boolean` | `false` | `Health Config` | Can the player damage themselves? |
### Internal (Private / Protected, No Expose)
| Variable | Type | Default | Category | Description |
|----------|------|---------|----------|-------------|
| `CurrentHealth` | `Float` | `100.0` | `Health State` | Current HP, clamped [0..MaxHealth] |
| `DeathState` | `E_DeathState` | `Alive` | `Health State` | Current death state |
| `ActiveDamageOverTime` | `Map<FName, FTimerHandle>` | `{}` | `Health State` | Active DoT timers keyed by source name |
| `InvincibilityTimer` | `FTimerHandle` | `-` | `Health State` | Timer handle for temporary invincibility |
| `RegenTimerHandle` | `FTimerHandle` | `-` | `Health State` | Timer handle for regen tick |
| `bIsInvincible` | `Boolean` | `false` | `Health State` | True during invincibility window |
| `LastDamageInstigator` | `Actor` | `None` | `Health State` | Most recent damage source (for kill credit) |
| `LastDamageTime` | `Float` | `0.0` | `Health State` | World time of last damage taken |
### Replicated (if multiplayer)
| Variable | Type | Condition | Description |
|----------|------|-----------|-------------|
| `CurrentHealth` | `Float` | `RepNotify` | Replicated with OnRep handler for UI updates |
| `DeathState` | `E_DeathState` | `RepNotify` | Replicated death state |
---
## 4. Functions
### Public Functions
#### `ApplyDamage` → `S_DamageResult (custom struct)`
- **Description:** Applies damage to the health pool after resistance calculation. Returns effective damage and any side effects.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `DamageEvent` | `S_DamageEvent` | The damage event to process |
- **Blueprint Authority:** Server (if MP), Any (single-player)
- **Flow:**
1. Early-out if DeathState is Dead or bIsInvincible
2. Calculate effective damage from BaseAmount * type resistances
3. If damage > 0: reset RegenDelay timer, update CurrentHealth
4. Fire OnDamageTaken dispatcher
5. If CurrentHealth <= 0: call `HandleDeath`
6. If CurrentHealth <= CriticalHealthPercent: fire OnHealthCritical
7. Return damage result struct
#### `ApplyHealing` → `Float (effective healing)`
- **Description:** Adds health up to MaxHealth. Returns the amount actually healed.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `HealAmount` | `Float` | Raw healing value |
| `HealTags` | `GameplayTagContainer` | Contextual tags |
- **Blueprint Authority:** Server (if MP), Any (single-player)
- **Flow:**
1. Early-out if DeathState is Dead
2. Clamp: `CurrentHealth = FMath::Min(MaxHealth, CurrentHealth + HealAmount)`
3. Fire OnHealthChanged
4. Return actual heal amount
#### `GetHealthNormalised` → `Float [0.0 - 1.0]`
- **Description:** Returns NormalisedCurrentHealth for UI widgets.
- **Flow:** `Return CurrentHealth / MaxHealth`
#### `SetMaxHealth` → `void`
- **Description:** Changes MaxHealth and optionally heals to match new ratio or clamps current.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `NewMax` | `Float` | New maximum health value |
| `bMaintainRatio` | `Boolean` | If true, scale CurrentHealth to same ratio |
- **Flow:**
1. Clamp NewMax to >= 1.0
2. If bMaintainRatio: `CurrentHealth = (CurrentHealth / MaxHealth) * NewMax`
3. Else: `CurrentHealth = FMath::Min(CurrentHealth, NewMax)`
4. Set MaxHealth = NewMax
5. Fire OnHealthChanged
#### `KillInstant` → `void`
- **Description:** Bypasses all resistances, immediately sets health to 0 and triggers death.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `Instigator` | `Actor` | Who caused the kill |
| `DeathReason` | `FText` | Reason displayed on death screen |
- **Blueprint Authority:** Server (if MP), Any (single-player)
- **Flow:**
1. Set CurrentHealth = 0
2. Set DeathState = Dying
3. Fire OnHealthChanged with CurrentHealth = 0
4. Call HandleDeath with bInstant = true
#### `ApplyDamageOverTime` → `void`
- **Description:** Applies periodic damage with tick interval and total duration.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `DamagePerTick` | `Float` | Damage applied each interval |
| `TickInterval` | `Float` | Seconds between ticks |
| `TotalDuration` | `Float` | Total seconds the DoT lasts |
| `DamageType` | `E_DamageType` | Type of each damage tick |
| `SourceName` | `FName` | Unique name to prevent stacking same DoT |
| `Instigator` | `Actor` | Who applied the DoT |
- **Flow:**
1. If SourceName already exists in ActiveDamageOverTime, clear existing timer
2. Create a Timer by Event with loop = TickInterval
3. On each tick: build S_DamageEvent, call ApplyDamage
4. After TotalDuration: clear timer, remove from ActiveDamageOverTime
#### `IsAlive` → `Boolean`
- **Flow:** `Return DeathState == Alive`
#### `AddResistance` → `void`
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `NewResistance` | `S_DamageResistance` | Resistance to add or update |
- **Flow:** If resistance for this DamageType exists, update Multiplier; if not, add to array.
#### `RemoveResistance` → `void`
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `DamageType` | `E_DamageType` | Type to remove |
- **Flow:** Remove all entries matching this DamageType from resistance array.
### Protected / Private Functions
#### `HandleDeath` → `void`
- **Description:** Internal death processing. Sets state, fires dispatchers, notifies GameMode.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `bInstant` | `Boolean` | If true, skip dying state go straight to dead |
- **Flow:**
1. Set DeathState = (bInstant ? Dead : Dying)
2. Clear all timers (regen, DoT, invincibility)
3. Fire OnDeath with DeathState
4. Call `I_Damageable::OnOwnerDied` on owner if interface exists
5. Get `GM_CoreGameMode` via `GetWorld()->GetAuthGameMode()`
6. Call `GM_CoreGameMode::HandlePlayerDead(LastDamageInstigator)`
#### `RegenTick` → `void`
- **Description:** Timer callback — applies one tick of regeneration.
- **Flow:**
1. If CurrentHealth >= MaxHealth: stop regen timer, return
2. `CurrentHealth = FMath::Min(MaxHealth, CurrentHealth + RegenRate * TickInterval)`
3. Fire OnHealthChanged
4. If CurrentHealth >= MaxHealth: stop regen timer
---
## 5. Event Dispatchers
| Dispatcher | Parameters | Bind Access | Description |
|------------|-----------|-------------|-------------|
| `OnHealthChanged` | `float OldHealth`, `float NewHealth`, `float Delta` | `Public` | Fired when health changes by any amount |
| `OnDamageTaken` | `S_DamageEvent DamageEvent`, `float EffectiveDamage` | `Public` | Fired each time damage is applied |
| `OnHealingReceived` | `float HealAmount`, `GameplayTagContainer HealTags` | `Public` | Fired each time healing is applied |
| `OnHealthCritical` | `float CurrentHealth`, `float MaxHealth` | `Public` | Fired when health drops below critical threshold |
| `OnDeath` | `E_DeathState DeathState`, `Actor Instigator` | `Public` | Fired when character dies or enters dying state |
| `OnDeathStateChanged` | `E_DeathState OldState`, `E_DeathState NewState` | `Public` | Fired on any death state transition |
| `OnInvincibilityChanged` | `bool bIsInvincible` | `Public` | Fired when invincibility state toggles |
---
## 6. Overridden Events / Custom Events
### Event: `BeginPlay`
- **Description:** Initializes health, loads saved health if persisting, starts regen timer if enabled.
- **Flow:**
1. Set CurrentHealth = MaxHealth
2. Check if owner implements `I_Persistable`
3. If yes, try to load saved health value
4. If bRegenEnabled: start regen timer with initial delay = 0
5. Bind to any relevant GamePhase change dispatchers
### Event: `OnComponentDestroyed`
- **Description:** Clean up all active timers.
- **Flow:**
1. Clear all TimerHandles (Invincibility, Regen, all DoT timers)
2. Clear ActiveDamageOverTime map
### Custom Event: `ApplyTemporaryInvincibility`
- **Parameters:** `float Duration`
- **Description:** Grants brief invincibility, often used after respawn or dodge.
- **Flow:**
1. Set bIsInvincible = true
2. Fire OnInvincibilityChanged(true)
3. Start timer for Duration
4. On timer end: set bIsInvincible = false, fire OnInvincibilityChanged(false)
---
## 7. Blueprint Graph Logic Flow
```mermaid
flowchart TD
A[ApplyDamage called] --> B{DeathState == Dead?}
B -->|Yes| C[Return 0 damage]
B -->|No| D{bIsInvincible?}
D -->|Yes| C
D -->|No| E[Calculate resistance multiplier]
E --> F[EffectiveDamage = Base * Multiplier]
F --> G[CurrentHealth -= EffectiveDamage]
G --> H[Reset RegenDelay timer]
H --> I[Fire OnDamageTaken]
I --> J{CurrentHealth <= 0?}
J -->|Yes| K[HandleDeath]
J -->|No| L{CurrentHealth <= CriticalThreshold?}
L -->|Yes| M[Fire OnHealthCritical]
L -->|No| N[Fire OnHealthChanged]
K --> O[Fire OnDeath dispatcher]
K --> P[Notify GameMode: HandlePlayerDead]
K --> Q[Clear all timers]
M --> N
```
---
## 8. Communication Matrix
| Who Talks | How | What Is Sent |
|-----------|-----|-------------|
| `BPC_HealthSystem` | `Dispatcher` | `OnHealthChanged` -> `WBP_HUD`, `BPC_StressSystem` |
| `BPC_HealthSystem` | `Dispatcher` | `OnDeath` -> `GM_CoreGameMode`, `SS_SaveSystem`, `BPC_PlayerMetricsTracker` |
| `BPC_HealthSystem` | `Dispatcher` | `OnDamageTaken` -> `BPC_StressSystem`, `BPC_AdaptiveEnvironment` |
| `BPC_HealthSystem` | `Interface` | `I_Damageable.Execute_ApplyDamage` route |
| `BPC_HealthSystem` | `Interface` | `I_Persistable` for save/load of CurrentHealth |
| `External (Weapons, Traps)` | `Interface` | `I_Damageable -> ApplyDamage` |
| `BPC_HealthSystem` | `Direct` | `GM_CoreGameMode.HandlePlayerDead` on death |
| `BPC_HealthSystem` | `Subsystem` | `GI_GameFramework.GetGamePhase` for phase-restricted actions |
---
## 9. Validation / Testing Checklist
- [ ] ApplyDamage with all E_DamageType values produces correct resistance-modified damage
- [ ] Health never drops below 0 or exceeds MaxHealth
- [ ] Regeneration does not start while RegenDelay timer is active
- [ ] Regeneration stops when health reaches MaxHealth
- [ ] Death sets DeathState properly and fires OnDeath exactly once
- [ ] Temporary invincibility blocks all damage for its duration
- [ ] Damage-over-time correctly applies ticks and cleans up timer on death
- [ ] Critical health event fires only when crossing below threshold from above
- [ ] Edge case: ApplyHealing on a dead character returns 0 with no dispatchers
- [ ] Edge case: ApplyDamage with negative BaseAmount is treated as 0
- [ ] Edge case: Multiple simultaneous DoT sources with same SourceName do not stack
- [ ] All dispatchers are cleaned up on component destroy
---
## 10. Reuse Notes
- This component can be used on AI characters and enemy pawns by exposing the same configuration variables.
- For enemies, consider disabling regen (bRegenEnabled = false) unless a specific healing mechanic exists.
- The resistance system supports temporary buffs via AddResistance / RemoveResistance at runtime.
- If implementing a downed / bleeding state, extend E_DeathState with custom values and add a recovery timer check in HandleDeath.
- Damage numbers displayed as floating text should bind to OnDamageTaken.
- For multiplayer: all ApplyDamage logic runs on Server; clients receive repnotified CurrentHealth and fire local cosmetic dispatchers only.
---
## 11. Multiplayer Networking (Expanded)
### Replicated Variables (Existing + New)
| Variable | Type | Condition | Description |
|----------|------|-----------|-------------|
| `CurrentHealth` | `Float` | `Replicated Using OnRep_CurrentHealth` | Clients sync via OnRep → dispatcher |
| `DeathState` | `E_DeathState` | `Replicated Using OnRep_DeathState` | Death state synced; OnRep triggers death effects |
| `bIsInvincible` | `Boolean` | `Replicated` | Invincibility visible to other players |
| `MaxHealth` | `Float` | `Replicated` | Synced for UI percentage calculations |
### Server RPCs
| RPC | Direction | Description |
|-----|-----------|-------------|
| `Server_ApplyDamage` | Client→Server | Client reports taking damage. Server validates source, range, damage type. |
| `Server_ApplyHealing` | Client→Server | Client requests healing. Server validates heal source, applies. |
| `Server_KillInstant` | Client→Server | Only accepted from authoritative sources (GM, traps). Client calls are rejected. |
### Authority Gates
```
Function ApplyDamage(DamageEvent)
Switch HasAuthority
Authority:
→ Calculate resistance, apply damage, fire dispatchers
Remote:
→ Return (clients never modify health directly)
→ Server_ApplyDamage is called by damage source (weapon/trap), not victim
Function ApplyHealing(HealAmount)
Switch HasAuthority
Authority:
→ Apply healing, fire dispatchers
Remote:
→ Return
→ Consumable use calls Server_UseConsumable → server calls ApplyHealing
```
### Client Prediction
- **Health bar:** Client predicts damage flash on hit; server-corrected value arrives via `OnRep_CurrentHealth`.
- **Death:** No prediction. `OnRep_DeathState` triggers all death effects (screen fade, ragdoll).
- **Healing:** Client shows green flash immediately + predicted health; server corrects within one RTT.
- **Invincibility:** Visual effect (shield shimmer) is local cosmetic; state is replicated.
### OnRep Handlers
```
OnRep_CurrentHealth()
→ Broadcast OnHealthChanged(OldHealth, CurrentHealth, Delta)
→ UI updates identically to single-player path
OnRep_DeathState()
→ Broadcast OnDeathStateChanged(OldState, NewState)
→ If Dead: play death animation, screen effects, notify GM
```
### Anti-Cheat
- Server validates all `Server_ApplyDamage` calls: check instigator range, weapon fire rate, damage type validity.
- Server never trusts `DamageEvent.BaseAmount` from client — always recalculates from weapon data.
- `Server_KillInstant` is only callable from server-authoritative systems (traps, death zones, GM).
---
*Blueprint Spec: Health System. Conforms to TEMPLATE.md v1.0 — part of the UE5 Modular Game Framework.*