10 — Stress / Sanity System (BPC_StressSystem)
Purpose
Tracks the player's psychological state via a stress meter that accumulates from environmental horror, enemy proximity, health deficits, and narrative events. Drives visual distortion, audio hallucinations, gameplay penalties, and narrative branching based on sanity thresholds.
Dependencies
- Requires:
BPC_HealthSystem (low health triggers stress), GI_GameFramework (phase checks)
- Required By:
BPC_PlayerController (shader/audio effects), WBP_HUD (stress vignette), NarrativeManager (sanity-gated content), BPC_AdaptiveEnvironment (world distortion)
- Engine/Plugin Requirements: GameplayTags, Timers, Material Parameter Collections (for post-process effects)
Class Info
| Property |
Value |
| Parent Class |
ActorComponent |
| Class Type |
Blueprint Component |
| Asset Path |
Content/Framework/Player/BPC_StressSystem |
| Implements Interfaces |
None |
1. Enums
E_StressTier
| Value |
Description |
Calm = 0 |
No stress effects |
Uneasy = 1 |
Subtle audio cues, slight FOV shift |
Distressed = 2 |
Visual noise, heartbeat audio, reduced stamina regen |
Panicked = 3 |
Heavy distortion, screen shake, movement penalty |
Terrified = 4 |
Hallucinations, audio distortion, random input inversion |
Catatonic = 5 |
Brief freeze/stumble, forced slow walk |
E_StressSource
| Value |
Description |
EnemyProximity = 0 |
Nearby hostile presence |
EnvironmentalHorror = 1 |
Gore, darkness, unsettling areas |
HealthDeficit = 2 |
Low health triggers fear |
NarrativeEvent = 3 |
Scripted story beats |
Supernatural = 4 |
Ghosts, paranormal activity |
Isolation = 5 |
Long periods alone |
TraumaTrigger = 6 |
PTSD flashback spots |
2. Structs
S_StressThresholds
| Field |
Type |
Description |
UneasyThreshold |
Float |
Stress value to enter Uneasy tier |
DistressedThreshold |
Float |
Stress value to enter Distressed |
PanickedThreshold |
Float |
Stress value to enter Panicked |
TerrifiedThreshold |
Float |
Stress value to enter Terrified |
CatatonicThreshold |
Float |
Stress value to enter Catatonic |
S_StressSourceData
| Field |
Type |
Description |
SourceType |
E_StressSource |
Category of source |
CurrentIntensity |
Float |
Current contribution from this source [0..Max] |
MaxIntensity |
Float |
Maximum contribution this source can apply |
DecayRate |
Float |
How fast this source fades when no longer active |
SourceTags |
GameplayTagContainer |
Contextual tags for filtering |
SourceIdentifier |
FName |
Unique name for this specific source instance |
S_StressEvent
| Field |
Type |
Description |
Amount |
Float |
Stress to add or remove |
SourceType |
E_StressSource |
Category of source |
Instigator |
Actor |
What caused the stress change |
bIsInstant |
Boolean |
If true, bypasses buildup curve |
EventTags |
GameplayTagContainer |
Additional context |
3. Variables
Configuration (Instance Editable, Expose On Spawn)
| Variable |
Type |
Default |
Category |
Description |
MaxStress |
Float |
100.0 |
Stress Config |
Maximum stress value |
Thresholds |
S_StressThresholds |
(15, 30, 50, 75, 90) |
Stress Config |
Threshold values for each tier |
PassiveDecayRate |
Float |
2.0 |
Stress Config |
Stress lost per second when in calm area |
PanicDecayDelay |
Float |
5.0 |
Stress Config |
Seconds at max tier before forced decay begins |
bEnableHallucinations |
Boolean |
true |
Stress Config |
Allow visual/audio hallucinations at high stress |
bCanGoCatatonic |
Boolean |
true |
Stress Config |
Allow catatonic freeze state |
bStressAffectsMovement |
Boolean |
true |
Stress Config |
Apply movement penalties from stress |
HealthDeficitMultiplier |
Float |
1.5 |
Stress Config |
How much low health amplifies incoming stress |
Internal (Private / Protected, No Expose)
| Variable |
Type |
Default |
Category |
Description |
CurrentStress |
Float |
0.0 |
Stress State |
Current accumulated stress |
CurrentTier |
E_StressTier |
Calm |
Stress State |
Current stress tier |
ActiveSources |
Map<FName, S_StressSourceData> |
{} |
Stress State |
Currently contributing stress sources |
StressDecayTimer |
FTimerHandle |
- |
Stress State |
Timer for passive decay tick |
bIsInSafeZone |
Boolean |
true |
Stress State |
True when player is in a calm area |
LastStressTime |
Float |
0.0 |
Stress State |
World time of last stress change |
bHallucinationsActive |
Boolean |
false |
Stress State |
Whether hallucinations are currently triggered |
TierEntryTimes |
Map<E_StressTier, float> |
{} |
Stress State |
World time when each tier was entered |
Replicated (if multiplayer)
| Variable |
Type |
Condition |
Description |
CurrentStress |
Float |
RepNotify |
Replicated for UI and effects |
CurrentTier |
E_StressTier |
RepNotify |
Replicated tier state |
4. Functions
Public Functions
AddStress → Float (actual stress added)
- Description: Adds stress from a source. Returns amount actually added.
- Parameters:
| Param |
Type |
Description |
Event |
S_StressEvent |
Stress event to apply |
- Blueprint Authority: Any
- Flow:
- If CurrentTier == Catatonic and !Event.bIsInstant: return 0 (immune at max)
- If HealthDeficitMultiplier > 1.0 and health < 50%: scale amount
- If Event.bIsInstant: CurrentStress += Event.Amount
- Else: add/update ActiveSources with Event data, recalculate from sources
- Clamp CurrentStress to [0, MaxStress]
- Update CurrentTier
- Fire OnStressChanged
- If tier changed: fire OnStressTierChanged
- If tier >= Panicked: start tick for hallucinations
- Return delta
RemoveStress → Float
- Description: Reduces stress directly or removes a specific source.
- Parameters:
| Param |
Type |
Description |
Amount |
Float |
Amount to reduce |
SourceIdentifier |
FName |
Optional — if set, remove this source entirely |
- Flow:
- If SourceIdentifier set: remove from ActiveSources, recalculate total
- Else: CurrentStress = FMath::Max(0, CurrentStress - Amount)
- Update CurrentTier
- Fire OnStressChanged
- If tier changed: fire OnStressTierChanged
AddStressSource → void
- Parameters:
| Param |
Type |
Description |
SourceData |
S_StressSourceData |
Source configuration to add |
- Flow:
- If SourceData.SourceIdentifier already in ActiveSources: update MaxIntensity and DecayRate
- Else: add new entry
- Recalculate total stress from all sources
RemoveStressSource → void
- Parameters:
| Param |
Type |
Description |
SourceIdentifier |
FName |
Source to remove |
- Flow:
- Remove from ActiveSources
- Recalculate total stress
SetSafeZone → void
- Parameters:
| Param |
Type |
Description |
bSafe |
Boolean |
Whether player is in a safe zone |
- Flow:
- bIsInSafeZone = bSafe
- If bSafe: begin passive decay
- If !bSafe: stop passive decay
- Fire OnSafeZoneChanged
GetStressNormalised → Float [0.0 - 1.0]
- Flow: Return CurrentStress / MaxStress
GetTierDuration → Float
- Parameters:
| Param |
Type |
Description |
Tier |
E_StressTier |
Which tier to query |
- Description: Returns how long the player has been in the given tier.
- Flow: If TierEntryTimes contains Tier: return CurrentWorldTime - TierEntryTimes[Tier]; else return 0
Protected / Private Functions
UpdateTier → void
- Flow:
- OldTier = CurrentTier
- Check CurrentStress against Thresholds in descending order
- NewTier = matching tier (highest threshold exceeded)
- If NewTier != OldTier:
- CurrentTier = NewTier
- Record TierEntryTimes[NewTier] = CurrentWorldTime
- Fire OnStressTierChanged
RecalculateFromSources → void
- Flow:
- Total = 0.0
- For each ActiveSource: Total += Source.CurrentIntensity
- CurrentStress = FMath::Clamp(Total, 0, MaxStress)
- UpdateTier
- Fire OnStressChanged
PassiveDecayTick → void
- Flow:
- If bIsInSafeZone or CurrentTier < Distressed or CurrentStress <= 0: return
- DecayAmount = PassiveDecayRate * 0.1
- CurrentStress = FMath::Max(0, CurrentStress - DecayAmount)
- Fire OnStressChanged
HandleHallucinationCheck → void
- Flow:
- If CurrentTier < Terrified or !bEnableHallucinations:
- If bHallucinationsActive: StopHallucinations
- return
- Random chance each tick (e.g., 5% per second at Terrified)
- If roll succeeds: trigger random hallucination event
- Fire OnHallucinationTriggered
5. Event Dispatchers
| Dispatcher |
Parameters |
Bind Access |
Description |
OnStressChanged |
float OldStress, float NewStress |
Public |
Fired on any stress change |
OnStressTierChanged |
E_StressTier OldTier, E_StressTier NewTier |
Public |
Fired when stress tier changes |
OnStressSourceAdded |
FName SourceIdentifier, E_StressSource SourceType |
Public |
Fired when a new stress source is added |
OnStressSourceRemoved |
FName SourceIdentifier |
Public |
Fired when a stress source is removed |
OnSafeZoneChanged |
bool bIsSafe |
Public |
Fired when entering/leaving safe zone |
OnHallucinationTriggered |
E_HallucinationType Type |
Public |
Fired when a hallucination event occurs |
OnCatatonicStateEntered |
none |
Public |
Fired when entering Catatonic tier |
OnStressRecovered |
none |
Public |
Fired when stress drops back to Calm |
6. Overridden Events / Custom Events
Event: BeginPlay
- Description: Initialises stress system, binds to health component if available.
- Flow:
- CurrentStress = 0.0
- CurrentTier = Calm
- Find BPC_HealthSystem on owner
- If found: bind OnDamageTaken to a handler that adds stress for damage taken
- Start passive decay timer (interval 0.1s)
Custom Event: OnDamageTakenHandler
- Parameters:
S_DamageEvent DamageEvent, float EffectiveDamage
- Description: Stress response to taking damage.
- Flow:
- StressAmount = EffectiveDamage * 0.5
- If DamageType == Fear: StressAmount *= 2.0
- Build S_StressEvent with SourceType = HealthDeficit
- Call AddStress
Custom Event: ForcePanic
- Parameters:
float Duration
- Description: Forcibly raises stress to Panicked tier for a duration, used by narrative events.
- Flow:
- CurrentStress = Thresholds.PanickedThreshold
- UpdateTier
- Start timer for Duration
- On timer end: gradually return stress to previous level
7. Blueprint Graph Logic Flow
8. Communication Matrix
| Who Talks |
How |
What Is Sent |
BPC_StressSystem |
Dispatcher |
OnStressTierChanged -> PC_PlayerController (post-process), ABP_GASP (anim), WBP_HUD (vignette) |
BPC_StressSystem |
Dispatcher |
OnHallucinationTriggered -> BP_HallucinationManager, BP_AudioManager |
BPC_StressSystem |
Dispatcher |
OnStressChanged -> WBP_HUD (stress bar) |
BPC_StressSystem |
Dispatcher |
OnCatatonicStateEntered -> BPC_PlayerController (input lockdown) |
External (Enemies) |
Direct |
Calls AddStressSource on detection |
External (BP_DeathZone) |
Direct |
Calls AddStress via interface |
BPC_HealthSystem |
Listener |
Binds to OnDamageTaken for health-deficit stress |
BPC_StressSystem |
Dispatcher |
OnSafeZoneChanged -> BPC_AdaptiveAtmosphere |
9. Validation / Testing Checklist
10. Reuse Notes
- Can be used on NPCs for a simplified "morale" system (remove hallucination features).
- Stress tiers can gate narrative content — questgivers behave differently based on player stress.
- Hallucination types can be configured via Data Asset (DA_HallucinationConfig).
- For multiplayer: Stress is server-authoritative; clients receive tier changes for local effects.
11. Multiplayer Networking (Expanded)
Server RPCs
| RPC |
Direction |
Description |
Server_AddStress |
Client→Server |
Client reports stress event. Server validates source, applies. |
Server_AddStressSource |
Client→Server |
Client enters enemy proximity zone. Server adds/updates source. |
Server_RemoveStressSource |
Client→Server |
Client leaves enemy zone. Server removes source. |
Server_SetSafeZone |
Client→Server |
Client reports safe zone status. Server validates, applies decay. |
Authority
- Server calculates total stress from all sources, updates tier, replicates.
- Client plays local hallucination/audio effects based on replicated tier.
ForcePanic() is server-only (narrative system calls it directly on server).
Client Prediction
- Stress bar: predicted on client, corrected by
OnRep_CurrentStress.
- Hallucinations: randomly triggered by client based on replicated tier (cosmetic only).
- Safe zone: server-authoritative; clients see OnSafeZoneChanged dispatcher.
- The HealthDeficitMultiplier creates a death spiral feel — use carefully for horror pacing.
- Safe zones double as narrative checkpoints and stress recovery areas.
Blueprint Spec: Stress/Sanity System. Conforms to TEMPLATE.md v1.0 — part of the UE5 Modular Game Framework.