LoadingScreen is a UIViewComponent overlay that covers the screen while your UI (and/or a scene) finishes loading, then fades away to reveal it. It runs at [DefaultExecutionOrder(-2000)] and lives on its own scene-root GameObject — deliberately not inside any nav graph.

It has one unified lifecycle reached through two entry shapes:

Entry shapeWhen to useWho drives the revealSample
Auto-revealSingle-scene boot — the loader covers the first frame while every nav host loads its UI, then fades itself out.The component itself, gated on the navgraph host readiness (LunaLayers.WhenAllReady).Showcase
Sequencer-drivenScene loads / multi-scene boot — the loader is shown by a scene transition and hidden when a sequencer node completes a deferred transition.A RevealLoadingScreenNodeSO in the loaded scene's sequence.GameFull

Loading

The component used to be called LoadingViewController with an Act As Boot Preloader toggle. It is now LoadingScreen and the toggle is Auto-reveal (boot loader, no sequencer) (_autoRevealOnAllReady). Old loader assets keep working — the renamed fields carry [FormerlySerializedAs] aliases, so no asset edits are needed.

Placement: a scene-root overlay, never a nav node

The loader is authored as a plain scene-root GameObject with its own PanelRenderer + PanelSettings, and a sorting order higher than every nav host so it occludes them. It is not a node in any NavGraphSO. That is structural, not stylistic — putting the loader in a nav graph would be circular:

  • The boot loader gates the nav hosts, so it cannot be one of their destinations — a host-spawned node is load-gated by the very host whose readiness it must wait on.
  • During boot it holds the nav push-queue (see below); a node living on a stack would itself be subject to the hold it is trying to manage.
  • Nav nodes route through channel/stack push · pop · re-push side effects (occlusion, dismiss modes, backdrops) that have nothing to do with a boot overlay.

Because it has no NavNode, the loader is born visible (a scene-root UIViewComponent with a null node starts shown). It talks to navigation only through two narrow seams: LunaLayers.WhenAllReady (readiness) and LunaNavigation.HoldBoot / ReleaseBoot (the push-queue). The fade-out is the reveal — nothing underneath is "shown"; the finished UI simply becomes visible as the higher-sorted overlay disappears.

Sort order matters. The loader's PanelSettings sorting order must be higher than each nav host's NavConfigSO.PanelSettings sorting order, or the UI will draw over the loader. GameFull's loader uses sorting order 1000.

Auto-reveal: gating the navgraph host at boot

This is the path you want when a single scene boots straight into its UI and you just need to hide the construction. Tick Auto-reveal (boot loader, no sequencer) (_autoRevealOnAllReady) on the LoadingScreen and you're done — the loader covers the first frame and fades out once every gating nav host has loaded.

How the navgraph host fits in

A quick recap of the host model (full detail in Navigation):

  • A NavHost mounts one NavGraphSO onto a physical layer (its NavConfigSO supplies the PanelSettings/sort order). Its Start Visible destinations are auto-pushed at boot.
  • LunaLayers is a static registry of every NavHost. A host counts toward readiness only when its Gates All Ready toggle is on (default). LunaLayers.AllReady is true once every gating host has finished loading.
  • A host is "ready" when all of its spawned views' panels have loaded and its Start Visible seed has run.

The loader subscribes LunaLayers.WhenAllReady(...), which fires once — immediately if everything is already ready (cache-hit), otherwise on the rising edge when the last gating host reports ready. Hosts that should not hold the loader open (e.g. a lazily-spawned HUD) turn Gates All Ready off; they still participate in PlayAll/HideAll but never delay the reveal.

The boot timeline

Execution order is the linchpin — LunaUIManager (-3000) → LoadingScreen (-2000) → NavHost (0):

  1. LunaUIManager.Awake constructs the navigation stack, so LunaNavigation is live.
  2. LoadingScreen.Awake (scene-root, -2000) calls LunaNavigation.HoldBoot() before base.Awake() (auto-reveal only). From now, every nav push — your Push calls, TabView auto-select, and each host's Start Visible seed — is queued, not executed.
  3. Each NavHost.Awake registers with LunaLayers (incrementing the not-ready count for gating hosts) and spawns its views; each view begins loading its panel.
  4. The loader's own panel loads → OnUILoaded runs: it engages the input lockout + tips, then subscribes LunaLayers.WhenAllReady. (Readiness is wired here, after the panel exists — not in Start.)
  5. As each host's views finish loading it calls FinishReady, which seeds its Start Visible destinations (these pushes are queued by the hold) and reports ready to LunaLayers.
  6. When the last gating host is ready, WhenAllReady fires the reveal.
  7. The loader waits out its timing floor (below), then fades itself out with _autoRevealFadeOutDuration + _easeModeOut.
  8. When the fade completes, LunaNavigation.ReleaseBoot() drains the queued pushes in arrival order — each now runs its full entry animation, after the loader is gone. The Start Visible views were already born visible, so the screen is correct the instant the overlay clears; the drain just replays their lifecycle hooks.

Why queue the pushes at all? So nothing animates underneath the cover. Without the hold, every Start Visible destination would play its entry transition while hidden behind the loader, and you'd reveal a static screen. With it, the entry animations play on reveal.

Timing fields (auto-reveal)

FieldDescription
_autoRevealFadeOutDurationFade-out duration for the self-hide (default 0.5). Sequencer/transition hides use the transition's own duration instead.
_minVisibleDurationMinimum seconds the loader stays up after it appears (not after Awake), even if everything was ready instantly — so users can perceive it. 0 = no floor.
_postReadyHoldDurationExtra hold after AllReady before the fade-out begins. 0 = fade as soon as ready (subject to the floor).

When AllReady fires, the effective wait is the larger of the two remainders, in unscaled time (so timeScale = 0 during boot doesn't stretch it):

wait = max(_minVisibleDuration − sinceShown, _postReadyHoldDuration)

where sinceShown is measured from when the loader appeared (OnUILoaded).

Sequencer-driven: reveal after a scene load

This is the path real games use (GameFull). The loader is shown by a scene transition while a scene loads, kept up while the loaded scene boots its services and nav graphs, and hidden by a sequencer node once everything is ready. It does not use HoldBoot/ReleaseBoot — leave _autoRevealOnAllReady off.

The deferred-transition idea

When you start a scene load you normally want the cover to fade out the moment loading finishes. But a freshly-loaded scene still has to register services, spawn nav graphs, and load panels. So the load is started with deferFadeOutUntilManualComplete: true: the transition's FadeIn shows the loader, the scene loads, but the matching FadeOut is stashed as pending instead of running. A sequencer node in the new scene completes it later, once the scene is actually ready.

Node sequence

In the loaded scene's SequenceRunner, after your service/dependency setup, run two nodes in order:

  1. BootNavGraphsNodeSO (CupkekGames/Sequencer/Boot Nav Graphs, no fields) → calls LunaLayers.BootDeferred(). This boots every nav host authored as NavHostBoot.Manual — they registered in Awake (so they count toward readiness) but waited to spawn views until now, when their service dependencies exist.
  2. RevealLoadingScreenNodeSO (CupkekGames/Sequencer/Reveal Loading Screen) → waits for nav readiness, then completes the deferred transition (which fades the loader out).

RevealLoadingScreenNodeSO folds in what used to be two separate nodes (a "wait for nav ready" node and a "complete deferred transition" node). If you have docs or assets referencing CompleteDeferredLoadingTransitionNodeSO, it has been removed — use RevealLoadingScreenNodeSO.

RevealLoadingScreenNodeSO fields

FieldDefaultDescription
_waitForNavReadytruePoll until the nav hosts are ready before completing the transition.
_treatNoHostsAsReadytrueIf no nav hosts exist in the scene, count as ready after one frame (avoids a same-frame registration race).
_minVisibleSeconds0Minimum cover time before the node may complete.
_timeoutSeconds8Safety cap — reveal anyway after this long even if readiness never fires.
_postReadyHoldSeconds0Extra hold after ready before completing.
_targetSceneLoaderBuildIndexWhich loader holds the pending transition: SceneLoaderBuildIndex or SceneLoaderAddressable.
_fadeOutWhenNoPendingSceneLoadtrueCold-start fallback (below).
_fallbackTransitionKey"Fade"Transition key used by the fallback.

Readiness gates on LunaLayers.AllReady (counting only Gates All Ready hosts). On completion the node calls SceneLoader.TryCompleteDeferredLoadingTransition() (or the SceneLoaderAddressable equivalent), which runs the stashed FadeOut.

Cold start. If you press Play directly in a scene that has no pending deferred load (so there's nothing to complete), TryCompleteDeferredLoadingTransition() returns false and — when _fadeOutWhenNoPendingSceneLoad is on — the node falls back to SceneLoader.FadeOutTransitionByKey(_fallbackTransitionKey) so a born-visible loader still hides.

Scene transitions & the bus

The loader and the transitions never reference each other; they communicate through two static signals on SceneTransition:

csharp
public static Action<bool, float> LoadingScreenToggleEvent; // (fadeIn, duration) public static Action<Action> LoadingScreenContinueRequested; // run after the player continues

LoadingScreen subscribes both in OnEnable. LoadingScreenToggleEvent(true, d)Show(d); (false, d)Hide(d). You load scenes through SceneLoader / SceneLoaderAddressable (from Scene Management) and pass a transition:

csharp
using CupkekGames.Luna; using CupkekGames.SceneManagement; SceneLoader.Instance.LoadScene( sceneIndex: 1, sceneLoadTransition: SceneTransitionDatabase.Instance.Transitions.GetValue("Fade"), deferFadeOutUntilManualComplete: true // a RevealLoadingScreenNodeSO completes it later );

Loading Transition

SceneTransitionDatabase

A singleton holding a keyed KeyValueDatabase of transition strategies, so you can add variants without touching the loader. Built-ins (CupkekGames.Luna):

TransitionNotes
SceneLoadTransitionFade_fadeInDuration (0.5), _fadeOutDuration (1, longer to absorb load lag). GetStartDelay() = _fadeInDuration + 0.2 so the cover is opaque before the heavy load begins.
SceneLoadTransitionInstantZero-duration toggle.
SceneLoadTransitionCircleDrives a circle-wipe; only toggles the loading overlay when its _showLoadingScreen is on. Uses Circle Hole.
SceneLoadTransitionCircleMaskUI Toolkit background-size mask wipe.

Writing a custom transition

Subclass SceneTransition and raise the bus:

csharp
public class SceneLoadTransitionFade : SceneTransition { public float _fadeInDuration = 0.5f; public float _fadeOutDuration = 1f; public override float GetStartDelay() => _fadeInDuration + 0.2f; public override void FadeIn() => LoadingScreenToggleEvent?.Invoke(true, _fadeInDuration); public override void FadeOut() => LoadingScreenToggleEvent?.Invoke(false, _fadeOutDuration); }
MemberDescription
GetStartDelay()Seconds between FadeIn() and the load actually starting.
FadeIn()Show the overlay (raise LoadingScreenToggleEvent(true, …)).
FadeOut()Hide it (raise LoadingScreenToggleEvent(false, …)).

Load progress (optional)

SceneLoader.OnLoadProgress is an Action<float> fired each frame during an async load, rescaled from Unity's 0…0.9 to a full 0…1. The GameFull sample's SceneLoadProgressBinder (drop it on the loader GameObject) subscribes it to swap the indeterminate RadialLoading spinner for a determinate RadialProgressBar (#LoadingProgress) during the load, restoring the spinner afterward. It is opt-in — not on the GameFull prefab by default, which just shows the spinner.

Manual use without the Scene Management package

LoadingScreen, SceneTransition, and the built-in transition classes all live in com.cupkekgames.luna. Only the SceneLoader orchestrator is in the separate com.cupkekgames.scenemanagement package, and the dependency runs one way (SceneLoader → luna, never the reverse). So a UI-only project that doesn't install Scene Management can still use the loading screen — you just drive it yourself around a plain UnityEngine.SceneManagement.SceneManager load.

Show(fadeInDuration) / Hide(fadeOutDuration) are public; they're the same entry/exit the transition bus uses (OnLoadingScreenToggle only calls them). Call them directly:

csharp
using System.Collections; using UnityEngine; using UnityEngine.SceneManagement; using CupkekGames.Luna; public class ManualSceneChange : MonoBehaviour { [SerializeField] LoadingScreen _loadingScreen; // the DontDestroyOnLoad loader (see below) [SerializeField] int _targetBuildIndex; public void Go() => StartCoroutine(LoadRoutine()); IEnumerator LoadRoutine() { _loadingScreen.Show(0.3f); // fade the cover in yield return new WaitForSecondsRealtime(0.3f); // let it become opaque before the load var op = SceneManager.LoadSceneAsync(_targetBuildIndex); // plain Unity load (Single mode) while (!op.isDone) yield return null; // op.progress → your own bar, if you want one _loadingScreen.Hide(0.4f); // fade out to reveal the loaded scene } }

That is the whole SceneLoader flow by hand — no Scene Management package, no SceneTransitionDatabase, no sequencer.

Two rules make it correct:

  • The loader must be DontDestroyOnLoad. A Single-mode load destroys the current scene; the loader has to survive it to keep covering the swap and to run the fade-out in the new scene. Put it (with its PanelRenderer + a LunaUIManager) on a DDOL root in your entry scene — created once, it persists into every scene you load afterward. This is the same "entry scene owns the persistent UI" pattern GameFull uses, minus the sequencer, so the rule is simply always start from that entry scene.
  • Show only after the loader's panel has loaded. LoadingScreen is a UIViewComponent; its Fade doesn't exist until the PanelRenderer delivers the tree. A born-visible scene-root loader is ready by the first frame; if in doubt, guard with _loadingScreen.WhenUILoaded(() => _loadingScreen.Show(0.3f)).

Optional — keep the transition as a strategy. If you'd rather express the fade as a reusable object, SceneLoadTransitionFade (also in luna) just raises the bus, so you can call transition.FadeIn() / transition.FadeOut() around the same manual load instead of Show/Hide — the subscribed LoadingScreen responds identically. Still no Scene Management needed.

No standalone loader prefab yet. The LoadingScreen component currently ships only assembled inside GameFull's LunaUIDemoFullDependencies.prefab. For a UI-only project, build a small loader GameObject yourself on a DontDestroyOnLoad root: a PanelRenderer (its PanelSettings sort order above your UI) + a loading UXML that satisfies the UXML contract (the Showcase Loading.uxml or GameFull's LoadingGameFull.uxml are ready-made templates) + the LoadingScreen component with _autoRevealOnAllReady off (you're driving it manually).

Press-to-continue (policy vs mechanism)

A "Press [key] to continue" reveal is split cleanly:

  • Policy — the transition. A …WithInput transition (GameFull's SceneLoadTransitionFadeWithInput / SceneLoadTransitionCircleWithInput, namespace CupkekGames.Luna.Demo.Game.Full) overrides FadeOut to raise SceneTransition.LoadingScreenContinueRequested(() => base.FadeOut()) instead of toggling the loader off.
  • Mechanism — the loader. LoadingScreen.WaitForContinue shows the continue prompt, waits on the loading continue action(s) (InputDeviceManager.GetLoadingContinueActions()) or a click on InputPromptContinue, then runs the supplied callback — which is the transition's real FadeOut, finally hiding the loader.

No reference flows between them; they meet only at the static LoadingScreenContinueRequested signal. So whether a reveal waits for input is purely the transition you chose — RevealLoadingScreenNodeSO has no input setting, it just completes whatever transition is pending.

The continue prompt requires a real luna:InputPrompt named InputPromptContinue (GameFull binds it to the Loading/Submit action). LunaUIManager configures the loading action map(s) ({"Loading"}) and continue action (Loading/Submit) via InputDeviceManager.

Public API

The same methods serve both paths (Show/Hide are the single entry/exit for every reveal):

MemberDescription
Show(float fadeInDuration)Fade the overlay in; start tips + engage the input lockout.
Hide(float fadeOutDuration)Fade out; release the input lockout (and, for auto-reveal, drain the boot hold). The single exit for every path.
WaitForContinue(Action onContinue)Show the continue prompt, wait for input, then run onContinue. Dormant until called.
OnLoadingScreenToggle(bool fadeIn, float duration)Bus entry point — delegates to Show/Hide.
InputPromptContinue (property)The resolved continue prompt element.

Input lockout

The loader owns input lockout so every loading screen freezes gameplay input consistently: InputDeviceManager.OnLoadingStart() engages whenever the loader appears (both paths funnel through the same internal "shown" step) and InputDeviceManager.OnLoadingEnd() releases in Hide.

Both paths share a rotating-tips carousel:

FieldDescription
_tipsList<string> of tip texts shown in the Speech label. A new tip never repeats the current one.
_tipIntervalMsRotation interval (UI Toolkit scheduler).
_playerInput(Input System only) PlayerInput whose action — named by the InputPromptNextTip element's InputActionName — skips to the next tip. Clicking the prompt also works.

Next-tip input (action and prompt click) is wired in both entry shapes — it engages whenever the loader appears, so the auto-reveal/boot loader can skip tips too.

The UXML contract

LoadingScreen resolves elements by name/class after the panel loads. Your loading UXML must provide:

ElementLookupRole
.loading-pageclass (one or more)The pages toggled by show-input / with-transition for the loading ↔ continue cross-fade.
TipsContainernameContainer shown/hidden with the tips.
Speechname (Label)The tip text.
InputPromptNextTipname (InputPrompt)Optional skip-tip prompt.
InputPromptContinuename (InputPrompt)Optional "press to continue" prompt.
#LoadingProgressname (RadialProgressBar)Optional determinate ring driven by SceneLoadProgressBinder.

SetInputContinueVisiblity(visible, withTransition) toggles the show-input / with-transition classes on the .loading-page elements. Preserve these names/classes in any UXML rewrite, or the lookups return null and silently no-op.

GameFull walkthrough

GameFull is the canonical sequencer-driven setup. Two SequenceRunners cooperate:

Boot scene (00_LunaUIDemoFullInitialization) runs four nodes:

  1. Register Services
  2. Instantiate Dependencies — spawns LunaUIDemoFullDependencies.prefab (DontDestroyOnLoad), which brings in the LoadingUI (PanelRenderer + LoadingScreen, sort order 1000, _autoRevealOnAllReady off), SceneLoader, SceneTransitionDatabase, the GlobalNavHost, and the transition objects.
  3. Game Services
  4. Scene Loader StartupSceneLoader.LoadScene(buildIndex 1, "FadeWithInput", deferFadeOutUntilManualComplete: true). The transition's FadeIn raises the bus → the loader shows.

Loaded scene (01_LunaUIDemoFullMainMenu) runs six nodes (Register/Instantiate/Game Services are OncePerPlaySession, so they no-op the second time): 1–3. Register / Instantiate / Game Services 4. Activate Game Object — re-enables the deactivated SceneParent content root. 5. Boot Nav GraphsLunaLayers.BootDeferred() spawns the Manual nav hosts now that services exist. 6. Reveal Loading ScreenRevealLoadingScreenNodeSO (_target = SceneLoaderBuildIndex, _minVisibleSeconds 0.3, _postReadyHoldSeconds 0.2, _timeoutSeconds 8) waits for LunaLayers.AllReady, then completes the deferred FadeWithInput transition → continue prompt → on input, the loader hides and the menu is revealed.

Demos

  • Showcase/Components/LoadingScreen/LoadingScreen.unity — a standalone styling/UX reference (simulated load, tip cycling, press-to-continue) driven by the sample's own LoadingScreenDemo on a raw PanelRenderer; it does not contain a LoadingScreen component.
  • Showcase/LunaShowcase.unity — runs the real component in auto-reveal mode; the startup loading screen is LoadingScreen live.
  • GameFull — the sequencer-driven path end to end (see the walkthrough above).

See also

Settings

Theme

Light

Contrast

Material

Dark

Dim

Material Dark

System

Sidebar(Light & Contrast only)

Light
Dark

Font Family

DM Sans

Wix

Inclusive Sans

AR One Sans

Direction

LTR
RTL