Enemy Performance Tiering¶
Summary: A three-tier LOD scheme (
UEnemySignificanceSubsystem) reduces per-enemy simulation cost at distance: Full Detail → Low Detail → Hibernated. A spawn-side density policy onUDomainDungeonConfigcaps environment spawners. Together these let dungeons run dozens of enemies without frame drops.
Table of Contents¶
- Why Tiering
- Architecture Overview
- Distance Tiers
- Hibernation
- Density Policy
- Flat Optimizations
- Configuration
- Source References
- Related Systems
- Recent Changes
Why Tiering¶
Unreal's default skeletal-mesh tick, cloth sim, and AI perception scale linearly with enemy count. A 30-enemy dungeon room at full fidelity costs ~28ms on the game thread. The tiering system brought per-enemy cost from ~0.94ms to ~0.35ms for distant enemies, making dense encounters viable.
Key Tradeoffs¶
| Decision | Benefit | Cost |
|---|---|---|
| Three tiers instead of two | Cloth/URO off at mid-range, full freeze at far range — smooth degradation | More state transitions to manage |
| Combat-state override | Engaged enemies always look good | A ranged enemy duelling at 4000u still pays full cost |
| Death unregisters immediately | Ragdolls and despawn VFX never freeze or jank | Corpse animation is full-cost until despawn |
| Spawn-side density cap | Hard ceiling on active enemies | Content rooms exempt — cap only affects filler spawners |
Architecture Overview¶
Spawn
│
├── Density Policy (FEnemyDensityPolicy on UDomainDungeonConfig)
│ Gates which environment-tile spawners fire
│ Content-room spawners always fire
│
▼
Enemy alive, registered with UEnemySignificanceSubsystem
│
├── Full Detail (Awake): dist ≤ DetailDistance OR in combat
│ No URO, cloth active, full anim graph eval
│
├── Low Detail (Awake): DetailDistance < dist ≤ SleepDistance, not in combat
│ URO on all skeletal meshes, cloth suspended
│
├── Hibernated: dist > SleepDistance, not exempt
│ Everything off: tick, AI, BT, CMC, mesh, poise, perception
│
├── Wake triggers: dist < WakeDistance, or any damage
│ Restores to Full Detail (low-detail re-evaluated next pass)
│
└── Death: immediate unregister → full detail restored
Ragdoll and VFX never frozen or URO-throttled
UEnemySignificanceSubsystem is a UTickableWorldSubsystem (server-only). It maintains a TMap<TWeakObjectPtr<AEternalEnemy>, FTrackedEnemy> of all registered enemies.
Registration Lifecycle¶
RegisterEnemy()— called fromAEternalEnemy::BeginPlay()(server-only)UnregisterEnemy()— called fromEndPlay()andMulticast_HandleDeath()(death unregisters early)- On unregister, hibernated enemies are woken and low-detail enemies are restored
Distance Tiers¶
A distance pass (RunDistancePass) runs every CVarPassInterval seconds (default 0.25s). For each tracked enemy, the minimum squared distance to any player pawn is computed.
Tier Table¶
| Tier | Condition | URO | Cloth | AI / BT | Tick |
|---|---|---|---|---|---|
| Full Detail | dist ≤ 3000 OR in combat state | Off | Active | Active | Active |
| Low Detail | 3000 < dist ≤ 7000, not in combat | On (all skeletal meshes) | Suspended | Active | Active |
| Hibernated | dist > 7000, awake > 2s, not exempt | N/A (mesh tick off) | Off | Paused | Disabled |
Combat-State Override¶
IsInCombatState() checks the AI controller's EEnemyState:
| State | Counts as Combat |
|---|---|
| Engaged | Yes |
| Attacking | Yes |
| HitReact | Yes |
| PhaseTransition | Yes |
| All others | No |
An engaged enemy at 4000u still gets full animation fidelity. Combat state also exempts from hibernation via ShouldNeverHibernate.
URO on the ALS Driver Mesh¶
The ALS driver mesh is always hidden (bHiddenInGame=true). When URO is enabled, the engine treats it as non-rendered and applies maximum throttle — the animation graph evaluates at a heavily interpolated skip rate. This is fine at distance but catastrophic at melee range (janky combat anims) and on death (URO interpolation fights the physics-driven ragdoll). The combat-state and death-unregister overrides exist specifically to prevent this.
Hibernation¶
What Gets Disabled¶
| System | How |
|---|---|
| Actor tick (ALS/pawn) | SetActorTickEnabled(false) |
| CharacterMovementComponent | Tick disabled + StopMovementImmediately |
| All skeletal mesh components | SetComponentTickEnabled(false) |
| PoiseSystemComponent | Tick disabled |
| AI Controller | Tick disabled |
| BrainComponent (BT) | PauseLogic("EnemyHibernation") |
| AI movement | Stopped |
| Perception senses | Sight + Hearing disabled |
Wake Triggers¶
- Distance: player enters
WakeDistance(default 5500u) — hysteresis prevents re-freeze oscillation - Damage: delegate on
Healthattribute change firesNotifyEnemyDamaged()— immediate wake - Master toggle: CVar
Eternal.AI.Hibernationset to 0 wakes everything next pass
Never-Hibernate Exemptions¶
AEternalBossclass orThreatTier == Boss- Any combat state (Engaged, Attacking, HitReact, PhaseTransition)
Safety Guards¶
| Guard | Purpose |
|---|---|
| Mid-air check | IsMovingOnGround() must be true — prevents freeze mid-jump |
| Max sleeps per pass | Capped at 8 (wakes uncapped) — prevents frame spike from mass-hibernate |
| Min-awake window | 2s after spawn or wake — prevents re-freeze before init completes |
| Death unregister | Immediate — corpse anims/ragdolls/VFX never frozen |
Density Policy¶
FEnemyDensityPolicy is authored on UDomainDungeonConfig::EnemyDensity. Two knobs:
| Knob | Type | Default | Purpose |
|---|---|---|---|
EnvironmentSpawnerChance |
float 0–1 | 1.0 | Per-spawner activation roll |
MaxEnvironmentEnemies |
int32 | 0 (uncapped) | Hard cap on environment-tile enemies |
Only environment-tile spawners (filler/edge tiles from the Phase B space-fill) are policed; content-room spawners always fire. The activation pass runs in UDungeonSubsystem::MarkRoomsReady(), spawners sorted by path name for determinism, using a seeded FRandomStream from the dungeon seed. Non-dungeon maps keep immediate spawn unchanged.
Flat Optimizations¶
These apply to all enemies unconditionally, independent of distance tier:
| Optimization | Location | Effect |
|---|---|---|
Visual-mesh OnlyTickPoseWhenRendered |
PostInitializeComponents |
Overlay and weapon meshes skip pose eval when off-screen |
| HitTrace tick gating | HitTraceActorComponent |
bStartWithTickEnabled = false; tick enabled only during active trace windows |
| Physics interaction disabled | Constructor | bEnablePhysicsInteraction = false on movement component |
| AI perception server-only | Not replicated | Perception component is server-only |
The ALS driver mesh keeps AlwaysTickPoseAndRefreshBones because it drives server-side animation state. All other skeletal meshes (retarget overlay, weapons) use OnlyTickPoseWhenRendered.
Configuration¶
| Mechanism | Location | Type |
|---|---|---|
| Density policy | UDomainDungeonConfig.EnemyDensity |
Data asset (FEnemyDensityPolicy) |
| Hibernation distances | CVars Eternal.AI.Hibernation.* |
Runtime CVars |
| Detail distance | CVar Eternal.AI.Hibernation.DetailDistance |
Runtime CVar |
| Debug dump | Console Eternal.AI.Hibernation.Dump |
Console command |
CVars¶
| CVar | Default | Purpose |
|---|---|---|
Eternal.AI.Hibernation |
1 | Master toggle (0 wakes all) |
Eternal.AI.Hibernation.SleepDistance |
7000 | Hibernate threshold (uu) |
Eternal.AI.Hibernation.WakeDistance |
5500 | Wake threshold — hysteresis gap |
Eternal.AI.Hibernation.DetailDistance |
3000 | Low-detail threshold (uu) |
Eternal.AI.Hibernation.Interval |
0.25 | Seconds between distance passes |
Eternal.AI.Hibernation.MaxSleepsPerPass |
8 | Cap on hibernate transitions per tick |
Source References¶
| Class | File |
|---|---|
| UEnemySignificanceSubsystem | Source/ProjectEternal/Public/AI/Significance/EnemySignificanceSubsystem.h |
| FTrackedEnemy | Source/ProjectEternal/Public/AI/Significance/EnemySignificanceSubsystem.h |
| FEnemyDensityPolicy | Source/ProjectEternal/Public/Dungeon/DataAssets/DomainDungeonConfig.h |
| AEternalEnemy (registration) | Source/ProjectEternal/Private/AI/Enemy/EternalEnemy.cpp |
| UDungeonSubsystem (density activation) | Source/ProjectEternal/Private/Dungeon/DungeonSubsystem.cpp |
Related Systems¶
- Enemy AI — behavior trees, combat states, perception
- Dungeon System — environment tiles, spawner activation
- Spawning System — encounter spawner lifecycle
Recent Changes¶
| Date | Change | Impact |
|---|---|---|
| 2026-07-29 | Full-detail override for combat state and corpses; URO becomes a distance tier instead of spawn-time default | Close/engaged enemies and ragdolls no longer jank from URO interpolation |
| 2026-07-29 | Per-enemy cost cut 0.94ms → 0.35ms (URO, visual-mesh tick gating, cloth distance tier) | Dense encounters viable without frame drops |
| 2026-07-29 | Enemy density policy + significance hibernation for many-enemy dungeons | Environment-tile spawner cap, three-tier distance LOD, damage-wake |