Reference · Updated 2552.10.14.11.52

Loading Screen — SL-35

Every map load currently shows a frozen frame of the world being left: main menu → lobby → match → menu. This doc picks the mechanism, lists every load it has to cover, and gives t

Every map load currently shows a frozen frame of the world being left: main menu → lobby → match → menu. This doc picks the mechanism, lists every load it has to cover, and gives the build order. Spec only — no code yet.


#1. Decision — adopt Epic's CommonLoadingScreen plugin (agreed 2026-10-12)

MoviePlayer (engine)CommonLoadingScreen (Lyra plugin)
Animates during the blocking LoadMap✅ own Slate thread❌ ticks Slate once, then holds that frame
Covers the gaps around LoadMap — connecting, pending travel, waiting for the game state to replicate, waiting for BeginPlay❌ stops when LoadMap returns, so the frozen frame comes back for the client's handshake✅ polls each tick, see §3
"Hold until we're actually ready" hook❌✅ ILoadingProcessInterface on the game state / its components
WidgetPure Slate only — UMG is unsafe on the loading threadAny UMG UUserWidget
Runs in the editor❌ disabled when GIsEditor (MoviePlayer.cpp:125)✅ in PIE (input-eating and the extra hold are off in editor)
Eats input underneath❌✅ input pre-processor, so the menu under it can't be clicked

Why CommonLoadingScreen. Our worst frozen frame is not the LoadMap itself — LAN maps load fast — it is the client side of a hard travel: connect, PendingNetGame, LoadMap, then a world with no game state and no pawn. MoviePlayer covers only the middle step. CommonLoadingScreen covers all of them and lets the match say "not yet" until the local players have a pawn and a HUD. It also works in PIE, so we can iterate on it.

What we give up. The spinner freezes for the duration of LoadMap itself. If that ever reads badly in a packaged build, MoviePlayer can be layered under it for just that window (Lyra's own startup module already does this — §6). Not in scope now.

Source. A copy is at C:\3D-DEV\Lyra\Plugins\CommonLoadingScreen (Lyra 5.4). It depends only on engine modules (Core, Engine, Slate, UMG, PreLoadScreen, RenderCore, DeveloperSettings, MoviePlayer) — no Lyra code. Copy it unmodified into Plugins/CommonLoadingScreen/ so it can be refreshed from a newer Lyra later. ⚠ It has not been compiled against 5.7; that is the first step of the build (§5).


#2. Every load it has to cover

LoadCode pathWho loadsCovered by
Host from main menu → LobbySLSessionService_LAN.cpp:138 ServerTravelhostbCurrentlyInLoadMap
Join a LAN gameSLSessionService_LAN.cpp:302 ClientTravelclientPendingNetGame → LoadMap → no game state yet
Start match: Lobby → mapSLLobbyGameMode.cpp:92 ServerTravel ?listenhost, then clients follow by reconnectinghost: TravelURL/LoadMap; clients: as Join
Leave → main menuSLSessionService_LAN.cpp:351 OpenLevel absolutewhoever leavesLoadMap
Client dropped by host leavingengine network-failure travel to the default mapclientLoadMap
Boot into the front endengine startup—Out of scope; the intro skip already owns this

All hard travel goes through UEngine::LoadMap, which broadcasts PreLoadMapWithContext / PostLoadMapWithWorld — the manager's hooks. Seamless travel (deferred, LobbyAndMapSelect.md §1) is also covered by World->IsInSeamlessTravel(), so switching later costs nothing here.


#3. When it shows and hides

The manager (ULoadingScreenManager, a UGameInstanceSubsystem) re-evaluates every tick and shows the screen while any of these hold, in this order:

  1. No world, or no game state replicated yet
  1. Inside LoadMap
  1. Pending travel (TravelURL set) or connecting (PendingNetGame)
  1. World hasn't begun play, or is in seamless travel
  1. The game state, any component on it, or any externally registered processor says so through
  2. ILoadingProcessInterface::ShouldShowLoadingScreen (ULoadingProcessTask is a ready-made one)

Once all clear, it holds for HoldLoadingScreenAdditionalSecs (default 2 s, packaged only) to let textures stream, then hides.

#USLLoadingScreenSubsystem — our say (item 5)

Public/UI/SLLoadingScreenSubsystem.h, a UGameInstanceSubsystem registered with the manager via RegisterLoadingProcessor. Built 2026-10-12, it holds the screen up for:

  • Pending server travel. A hard ServerTravel on a listen server waits ServerTravelPause (4 s,
  • BaseEngine.ini) with the destination in UWorld::NextURL so clients hear the travel first. The manager only watches the world context's TravelURL, which is set after that wait — so Start Match sat on a live, frozen-looking lobby for 4 s before the screen appeared (found in PIE 2026-10-12). Covering the wait beats shortening it: a lower pause risks the host leaving before clients are told to follow.

  • Minimum display time. MinimumDisplaySeconds (config, DefaultGame.ini, default 2.0) from the moment
  • the screen shows. Works in PIE, unlike the plugin's HoldLoadingScreenAdditionalSecs (packaged only; its in-editor switch is Transient, so it can't be saved). That hold is cut to 0.5 s so packaged builds don't stack 2 s on top of ours.

#Our readiness rule (still to build)

On the match map, keep the screen up until every local player on this machine has a possessed pawn and its HUD has been initialised. That is the moment the frozen frame / black screen ends today, and it is already a known timing edge (Footguns.md → HUD init: PossessedBy fires before ASLPlayerHUD::BeginPlay).

  • The match map has no SL game state (only the lobby has ASLLobbyGameState), so don't hang it there.
  • Add it to USLLoadingScreenSubsystem as a third condition: on a match world, "does every local player have a pawn and a HUD?"

  • Lobby and front end need nothing extra: items 1–4 are enough, because their widgets push in BeginPlay.
  • ⚠ Must have a timeout fallback (log + release) so a failed spawn can never trap a player behind the screen.

#4. Split-screen

The screen is added to the game viewport (AddViewportWidgetContent, Z-order 10000), not to a player's region, so one full-window screen covers every local player. That is what we want — players travel together. The widget must not assume player one: no owning-player bindings, no per-player profile name.

⚠ USLLocalJoinSubsystem::ApplySplitscreenPolicy re-applies the split on world init. The loading widget sits above the split regardless, but check in §7 that the split doesn't flash for a frame between the screen hiding and the HUDs appearing.


#5. Build order

  1. Plugin in. Copy the folder, add CommonLoadingScreen to SystemLink.uproject plugins, build. Fix any
  2. 5.4 → 5.7 API drift in place and note each fix at the top of this section. ✅ 2026-10-12: copied unmodified from Lyra 5.4 — both modules compile against 5.7.4 with no changes.

  1. Widget. WBP_SL_LoadingScreen under /Game/SystemLink/UI/LoadingScreen/ — plain UUserWidget, not
  2. CommonUI activatable (it lives outside the layer stack). Full-bleed background, game logo, a looping spinner. Map name/preview later (§6).

  1. Settings. Project Settings → Common Loading Screen: LoadingScreenWidget = the widget. Leave the other
  2. defaults. Lands in DefaultGame.ini. ✅ 2026-10-12: widget built via the bridge — opaque backdrop, halo-gradiant at the WBP_SL_Page tint, MI_UI_SL_Logo centred at 512, bottom-right LOADING (Style_Text-Medium) + CircularThrobber. The settings properties are not Blueprint-visible, so Python can't set them — the ini line was written by hand; a running editor only picks it up after a restart.

  1. PIE check — §7 tests 1–3. Engine conditions alone should already remove most frozen frames.
  1. Readiness rule (§3) in C++, with its timeout.
  2. ⏸ 2026-10-13: dropped for now (Beepers). Packaged tests 4–7 never showed a pawn-less or half-built frame; the engine conditions cover it. Build it if a slow machine or a heavy map ever shows one.

  1. Standalone / packaged check — §7 tests 4–6; input eating and the 2 s hold only run outside the editor.

Steps 1–4 can be verified on their own; 5 is separately testable. One card (SL-35) covers both.

#Update 2026-10-13 — rotating backgrounds

WBP_SL_LoadingScreen is parented to USLLoadingScreenWidget, which shows one image from its Backgrounds list per load.

  • Shuffled, not random: USLLoadingScreenSubsystem::TakeNextBackgroundIndex deals from a shuffled order, so
  • each image comes up once before any repeats and a new round never opens on the image just shown. The order lives in the subsystem because the plugin builds a new widget for every load.

  • Soft references, loaded synchronously on construct: only the chosen image is in memory, and an async load
  • would show an empty frame first.

  • Images: 3840 × 2160 from HighResShot 3840x2160. Texture Group UI, NoMipmaps, Never Stream, BC7,
  • Compress Without Alpha. BackgroundImage sits in a Scale Box (Scale To Fill), so ultrawide and 16:10 crop the edges; keep the subject in the middle ~75%.

  • Empty list: BackgroundImage collapses and the gradient backdrop shows, as before.

#6. Later, not now

  • Prefer the destination map's image over the shuffle when the map is known (it pairs with the map preview
  • below).

  • Map preview + name on the loading screen for Lobby → match: read the selected USLMapDefinition from a
  • UGameInstance-lifetime source, since the lobby world is gone by then.

  • Gameplay tip / Halo-style map flyover — art problem, not systems.
  • MoviePlayer underlay for the LoadMap window if the frozen spinner reads badly in a packaged build.
  • CommonStartupLoadingScreen module (comes with the plugin) shows a PreLoadScreen during engine
  • startup. Leave it in but inert unless it fights the intro skip; if it does, set its module to not load.


#7. Testing

#ModeDoExpect
1PIE, 1 playerMain menu → HostLoading screen instead of a frozen menu; lobby appears with its widget live
2PIE, 2 players (listen)Lobby → StartBoth windows show it; it drops on each only once that player's pawn + HUD exist
3PIEIn a match → LeaveScreen, then main menu with input live
4Standalone ×2Join by LANCovers connect → load → spawn with no frozen or black frame in between
5Standalone, split-screen 2Lobby → StartOne screen over the whole window; no split flash on hide
6StandaloneMash buttons during a loadNothing under the screen reacts
7AnyKill the host mid-loadClient lands on the main menu, not stuck behind the screen

First packaged pass, 2026-10-12 (Development build, this PC hosting, Stormtrooper joining by typed IP): host → lobby, join → lobby and lobby → match all looked right, with no frozen frame noticed. That was an informal pass, not the table above. Re-run every row once SL-10 removes the typed IP. A failed connect (wrong address) showed the screen and then returned to the main menu, which is the behaviour test 7 wants.

Test 4 ✅ 2026-10-13 (Development package, this PC hosting, Stormtrooper joining from the SL-10 server list): join → lobby and lobby → match showed the screen throughout, with no frozen or black frame. The widget's look is due a cosmetic pass, which is not a blocker for this card.

Test 6 ✅ 2026-10-13: mashing during loads reached nothing underneath. Mashing on the main menu found an unrelated menu-stack bug (BUG-033, SL-42), which is not a loading-screen leak.

Test 7 ✅ 2026-10-13 (host killed during lobby → match travel, Stormtrooper as client): the client stayed behind the loading screen until ConnectionTimeout fired at 20 s, then loaded MainMenu?closed with input live. The wait is the engine's net timeout, not the loading screen. Players see 20 s of "loading" with no sign the host is gone. If that reads badly in playtests, lower ConnectionTimeout on the pending net driver or show the reason on the screen; not a blocker.

Test 5 ✅ 2026-10-13 (two controllers on this PC, lobby → match): one screen over the whole window, no split-screen frame on show or hide.

Console: CommonLoadingScreen.LogLoadingScreenReasonEveryFrame 1 prints why it is showing — first stop when it won't hide. CommonLoadingScreen.AlwaysShow 1 pins it up for styling the widget.