Concept: Lifecycle & Show Conditions

The whole plugin is one loop: every frame, UCrimsonLoadingScreenManager asks 'does anything need a loading screen?'. If any condition says yes, the screen shows; only when all say no (plus a hold delay) does it hide. This is far more robust than 'show on OpenLevel, hide on map loaded' - on a client, the map being loaded does not mean the GameState has replicated or the PlayerController exists yet. Those are conditions here, so they are handled for free.

Why it survives level travel

The manager is a UGameInstanceSubsystem - it belongs to the game instance, not to any world, so destroying the old world does not touch it. The widget is likewise added to the viewport's Slate layer, above and outside any world. During a non-seamless travel the old world is fully destroyed before the new one loads; during seamless travel a small transition map bridges the gap while the destination loads in the background. Either way, manager and widget persist through it.

The 16 show conditions (evaluated in order)

#ConditionDebug reason string
1CVar CrimsonLoadingScreen.AlwaysShow is true"CrimsonLoadingScreen.AlwaysShow is true"
2Game instance has no FWorldContext"The game instance has a null WorldContext"
3World is null"We have no world (FWorldContext's World() is null)"
4GameState is null (not yet replicated)"GameState hasn't yet replicated (it's null)"
5Inside LoadMap (bCurrentlyInLoadMap)"bCurrentlyInLoadMap is true"
6TravelURL is non-empty (travel requested)"We have pending travel (the TravelURL is not empty)"
7PendingNetGame set (connecting to a server)"We are connecting to another server (PendingNetGame != nullptr)"
8World has not begun play"World hasn't begun play"
9World is in seamless travel"We are in seamless travel"
10GameState implements the process interface and votes trueSet by the implementor
11Any GameState component votes trueSet by the implementor
12Any externally registered processor (incl. process tasks) votes trueSet by the implementor
13Any local PlayerController votes trueSet by the implementor
14Any local PlayerController component votes trueSet by the implementor
15Splitscreen: a local player is missing its PlayerController"At least one missing local player controller in splitscreen"
16Not splitscreen: no local PlayerController at all"Need at least one local player controller"

When everything clears, the debug reason reads "(nothing wants to show it anymore)". In non-shipping builds the -NoLoadingScreen command-line flag short-circuits everything to hidden.

The hold delay

After all conditions clear, the screen is held for a configurable few seconds so texture streaming can finish - otherwise players see the world pop in blurry. During the hold, world rendering is force-enabled so the GPU can actually stream. The Timing -> Hold Loading Screen Additional Secs Project Setting seeds the CrimsonLoadingScreen.HoldLoadingScreenAdditionalSecs CVar when the manager initializes; a console or command-line set of that CVar overrides the setting. In the editor the hold is skipped unless Debugging -> Hold Loading Screen Additional Secs Even In Editor is ticked.

What happens on show

  1. Pick the tip and background (respecting the pick-new-each-load flags).
  2. Gather and cache the character info (ICrimsonLoadingCharacterProvider) - snapshot taken while the pawn still exists.
  3. Notify ICrimsonLoadingCinematicInterface actors in the current world (OnLoadingScreenStarted).
  4. Broadcast content-ready (native + Blueprint delegates).
  5. Start blocking input (packaged builds), broadcast visibility-changed(true).
  6. Create the widget from Loading Screen Widget and add it to the viewport at Loading Screen ZOrder (fallback: SThrobber + error log).
  7. Switch performance settings (below) and force one Slate tick so the widget renders immediately.

What happens on hide

  1. Stop blocking input.
  2. Force a full garbage collection (frees loading-time objects before gameplay).
  3. Remove the widget from the viewport and restore performance settings.
  4. Notify cinematic actors (OnLoadingScreenEnded), broadcast visibility-changed(false).
  5. Log the total visible duration.
The engine's own startup screen wins
While the engine's PreLoadScreen (startup movie / early-load screen) is active, the manager tracks state and fires content-ready but does not create a widget, block input, or change performance settings - the engine screen already owns the display. Normal behavior resumes once it ends.

Performance hooks

Engine systemOn showOn hide
FShaderPipelineCache batch modeFast (compile shaders with the idle CPU)Background
UGameViewportClient::bDisableWorldRenderingtrue - unless bUseLivePreviewfalse
AWorldSettings::bHighPriorityLoadingLocaltrue (prioritize streaming)false
FThreadHeartBeat duration multiplierLoadingScreenHangDurationMultiplier from [Core.System] in Engine.ini1.0
FGameThreadHitchHeartBeatsuspended (no false hitch reports)resumed

Input blocking

While the screen is up, a Slate input pre-processor registered at priority 0 swallows every key, mouse, analog, and motion event - in packaged builds only. In the editor (PIE) it deliberately passes everything through, so you can keep working; do not use PIE to test that clicks are blocked.

Dedicated servers and multiplayer

ShouldCreateSubsystem returns false on dedicated servers - no manager, no tick, zero cost. On clients the system is purely local presentation: conditions 4, 13-16 make clients naturally wait for replication and possession, and your own votes (see How-To: Hold the Loading Screen from Game Code) typically read replicated state that the server drives. Nothing here mutates gameplay state, so there are no authority concerns.