410 lines
18 KiB
Markdown
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.* |