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)
| # | Condition | Debug reason string |
|---|---|---|
| 1 | CVar CrimsonLoadingScreen.AlwaysShow is true | "CrimsonLoadingScreen.AlwaysShow is true" |
| 2 | Game instance has no FWorldContext | "The game instance has a null WorldContext" |
| 3 | World is null | "We have no world (FWorldContext's World() is null)" |
| 4 | GameState is null (not yet replicated) | "GameState hasn't yet replicated (it's null)" |
| 5 | Inside LoadMap (bCurrentlyInLoadMap) | "bCurrentlyInLoadMap is true" |
| 6 | TravelURL is non-empty (travel requested) | "We have pending travel (the TravelURL is not empty)" |
| 7 | PendingNetGame set (connecting to a server) | "We are connecting to another server (PendingNetGame != nullptr)" |
| 8 | World has not begun play | "World hasn't begun play" |
| 9 | World is in seamless travel | "We are in seamless travel" |
| 10 | GameState implements the process interface and votes true | Set by the implementor |
| 11 | Any GameState component votes true | Set by the implementor |
| 12 | Any externally registered processor (incl. process tasks) votes true | Set by the implementor |
| 13 | Any local PlayerController votes true | Set by the implementor |
| 14 | Any local PlayerController component votes true | Set by the implementor |
| 15 | Splitscreen: a local player is missing its PlayerController | "At least one missing local player controller in splitscreen" |
| 16 | Not 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
- Pick the tip and background (respecting the pick-new-each-load flags).
- Gather and cache the character info (
ICrimsonLoadingCharacterProvider) - snapshot taken while the pawn still exists. - Notify
ICrimsonLoadingCinematicInterfaceactors in the current world (OnLoadingScreenStarted). - Broadcast content-ready (native + Blueprint delegates).
- Start blocking input (packaged builds), broadcast visibility-changed(true).
- Create the widget from Loading Screen Widget and add it to the viewport at Loading Screen ZOrder (fallback:
SThrobber+ error log). - Switch performance settings (below) and force one Slate tick so the widget renders immediately.
What happens on hide
- Stop blocking input.
- Force a full garbage collection (frees loading-time objects before gameplay).
- Remove the widget from the viewport and restore performance settings.
- Notify cinematic actors (
OnLoadingScreenEnded), broadcast visibility-changed(false). - Log the total visible duration.
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 system | On show | On hide |
|---|---|---|
FShaderPipelineCache batch mode | Fast (compile shaders with the idle CPU) | Background |
UGameViewportClient::bDisableWorldRendering | true - unless bUseLivePreview | false |
AWorldSettings::bHighPriorityLoadingLocal | true (prioritize streaming) | false |
FThreadHeartBeat duration multiplier | LoadingScreenHangDurationMultiplier from [Core.System] in Engine.ini | 1.0 |
FGameThreadHitchHeartBeat | suspended (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.