Skip to content

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 on UDomainDungeonConfig caps environment spawners. Together these let dungeons run dozens of enemies without frame drops.

Table of Contents


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 from AEternalEnemy::BeginPlay() (server-only)
  • UnregisterEnemy() — called from EndPlay() and Multicast_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

  1. Distance: player enters WakeDistance (default 5500u) — hysteresis prevents re-freeze oscillation
  2. Damage: delegate on Health attribute change fires NotifyEnemyDamaged() — immediate wake
  3. Master toggle: CVar Eternal.AI.Hibernation set to 0 wakes everything next pass

Never-Hibernate Exemptions

  • AEternalBoss class or ThreatTier == 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


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