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 |
| Widget | Pure Slate only — UMG is unsafe on the loading thread | Any 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
| Load | Code path | Who loads | Covered by |
|---|---|---|---|
| Host from main menu → Lobby | SLSessionService_LAN.cpp:138 ServerTravel | host | bCurrentlyInLoadMap |
| Join a LAN game | SLSessionService_LAN.cpp:302 ClientTravel | client | PendingNetGame → LoadMap → no game state yet |
| Start match: Lobby → map | SLLobbyGameMode.cpp:92 ServerTravel ?listen | host, then clients follow by reconnecting | host: TravelURL/LoadMap; clients: as Join |
| Leave → main menu | SLSessionService_LAN.cpp:351 OpenLevel absolute | whoever leaves | LoadMap |
| Client dropped by host leaving | engine network-failure travel to the default map | client | LoadMap |
| Boot into the front end | engine 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:
- No world, or no game state replicated yet
- Inside
LoadMap
- Pending travel (
TravelURLset) or connecting (PendingNetGame)
- World hasn't begun play, or is in seamless travel
- The game state, any component on it, or any externally registered processor says so through
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
ServerTravelon a listen server waitsServerTravelPause(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
- Plugin in. Copy the folder, add
CommonLoadingScreentoSystemLink.uprojectplugins, build. Fix any
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.
- Widget.
WBP_SL_LoadingScreenunder/Game/SystemLink/UI/LoadingScreen/— plainUUserWidget, not
CommonUI activatable (it lives outside the layer stack). Full-bleed background, game logo, a looping spinner. Map name/preview later (§6).
- Settings. Project Settings → Common Loading Screen:
LoadingScreenWidget= the widget. Leave the other
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.
- PIE check — §7 tests 1–3. Engine conditions alone should already remove most frozen frames.
- Readiness rule (§3) in C++, with its timeout.
⏸ 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.
- 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::TakeNextBackgroundIndexdeals 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:
BackgroundImagecollapses 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
USLMapDefinitionfrom 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
LoadMapwindow if the frozen spinner reads badly in a packaged build.
CommonStartupLoadingScreenmodule (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
| # | Mode | Do | Expect |
|---|---|---|---|
| 1 | PIE, 1 player | Main menu → Host | Loading screen instead of a frozen menu; lobby appears with its widget live |
| 2 | PIE, 2 players (listen) | Lobby → Start | Both windows show it; it drops on each only once that player's pawn + HUD exist |
| 3 | PIE | In a match → Leave | Screen, then main menu with input live |
| 4 | Standalone ×2 | Join by LAN | Covers connect → load → spawn with no frozen or black frame in between |
| 5 | Standalone, split-screen 2 | Lobby → Start | One screen over the whole window; no split flash on hide |
| 6 | Standalone | Mash buttons during a load | Nothing under the screen reacts |
| 7 | Any | Kill the host mid-load | Client 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.