Quick answer: Outside references prevent GC. Audit with obj refs <actor>. Clear references in GameInstance / save data / cross-level UPROPERTYs before UnloadStreamLevel.
An open-world game streams in regions. After leaving a region, actors are supposed to unload to free memory. Memory profile shows them persisting. Console shows higher actor count than expected.
Diagnose with obj refs
obj refs name=BP_Enemy_C_42
Lists all UObjects holding strong refs. Common culprits:
- GameInstance variables of cross-level actors.
- Save game data still in memory.
- Other still-loaded sub-levels.
- Subsystems / managers holding actor lists.
Fix: Clear External References
void UMyGameInstance::OnLevelStreamingDone(const UWorld* World) {
for (auto& ActorRef : ActiveEnemies) {
if (ActorRef.IsValid() && !ActorRef->GetLevel()->IsVisible()) {
ActorRef = nullptr;
}
}
}
Replace strong UPROPERTY refs with TWeakObjectPtr where you don’t need to keep the actor alive across levels.
Latent Unload Pattern
FLatentActionInfo LatentInfo;
LatentInfo.CallbackTarget = this;
LatentInfo.ExecutionFunction = "OnLevelUnloaded";
LatentInfo.UUID = FMath::Rand();
UGameplayStatics::UnloadStreamLevel(this, LevelName, LatentInfo, true);
bShouldBlockOnUnload = true forces the unload to complete before returning. Useful for transitions.
Verify via Memreport
memreport -full
stat levels
Compare actor counts before and after unload. Should drop by the level’s known content. If not, references still exist.
Verifying
Stream a level in, then out. memreport — level memory drops to baseline. obj refs on previously-active actors return empty. Stat levels confirms not-loaded.
“Unloading is GC-driven. Hold no strong references after you don’t need them, and the level unloads cleanly.”
For open-world games especially, use TWeakObjectPtr for any cross-system actor refs — lets GC do its job between regions.