Concept: The Cinematic Preview Pipeline

The showcase is three roles joined by one struct. Your game implements a provider (what does this character look like?) and registers the result; the manager holds those registrations and turns them into a roster when a screen opens; the showcase host renders that roster into a render target the widget displays. The plugin never touches your equipment system, and your equipment system never learns that a loading screen exists.

Who hosts the showcase

The plugin ships the host. UCrimsonShowcaseSubsystem creates a dedicated UWorld - a pocket world - when the loading screen appears, puts an ACrimsonShowcaseStage in it, and destroys the whole thing when the screen hides.

That choice is the point of the feature. A capture living in the game world dies with it: a non-seamless LoadMap destroys every actor before the destination exists, and a client joining a server has no world of its own yet at all. A pocket world belongs to the game instance instead, so it outlives every travel type - non-seamless loads, client joins, returns to the main menu - with no transition map to author and no bUseSeamlessTravel requirement anywhere.

PieceResponsibility
UCrimsonShowcaseSubsystemOwns the pocket world, the optional stage level, the tick and the 30 Hz capture, and the teardown
ACrimsonShowcaseStageBuilds the characters from the roster, places them, frames the capture, holds the built-in light rig
ACrimsonShowcaseSlotLevel-placed mark: where character i stands and which way it faces
ACrimsonShowcaseCameraLevel-placed shot for a given head count
ICrimsonLoadingCinematicInterfaceThe escape hatch: your own director, presenting the roster its own way

The flow on every show

  1. ShowLoadingScreen fires (a condition became true - often the travel itself).
  2. The manager builds the roster from what your game registered: the primary first, then valid companions in registration order, clamped to MaxShowcaseCharacters. Nothing registered, or a presentation with bSuppressShowcase, produces an empty roster - and the manager logs which case applied.
  3. Every actor in the current world implementing ICrimsonLoadingCinematicInterface receives OnLoadingScreenStarted(Roster). This runs first, so a custom director can claim the render target before the built-in host would.
  4. OnLoadingScreenContentReady(Roster) broadcasts to non-actor listeners - among them the showcase subsystem, which bootstraps its pocket world and queues the rest. It stands down here if a director already declared the showcase live.
  5. The widget is created and added to the viewport. At this moment the showcase is normally not live yet.
  6. On following end-of-frame ticks the stage level streams in (if one is set), the stage is resolved or spawned, the roster is applied, the first frame is captured - and only then does SetShowcaseLive(true) fire OnShowcaseLiveChanged, which is the widget's cue to swap its background to the render target.
  7. On hide, cinematic actors receive OnLoadingScreenEnded, the render target is cleared to near-black, and the pocket world is torn down on the next end-of-frame tick.
Nothing that touches level streaming may run in those callbacks
ShowLoadingScreen and HideLoadingScreen fire from inside the game world's tick or inside UEngine::LoadMap, and UWorld::UpdateLevelStreaming guards reentrancy with a process-global check that is fatal on a nested call. That is why the subsystem only bootstraps in the callback and does all level work from FCoreDelegates::OnEndFrame - and why the showcase goes live a few frames late, which OnShowcaseLiveChanged exists to announce.

The roster API

FunctionSourceUse when
GetCachedShowcaseRoster()What this load is presenting: the registered primary at index 0 then the companions, or empty when nothing is registered or the showcase is suppressedAnywhere during loading - the pawns it describes may already be destroyed
UpdateCachedCharacterInfo(Info)Your registration of the local player, made whenever the look becomes final or changes (save restore, equip, dye, respawn)The only way the primary reaches the loading screen
AddShowcaseCompanion(Id, Info)One more character filed under a key you own; adding the same id again replaces it in placeYour game decided somebody else belongs on this screen
RemoveShowcaseCompanion(Id) / ClearShowcaseCompanions()Retracts companionsThat character is no longer loading with the player
ClearCachedCharacterInfo()Retracts the primary and every companion togetherThe registered look stops being the player's - a character switch, or leaving a session

Two deliberate asymmetries. There is no live query - the manager never searches the world for a provider, because a search has to trust whichever object answers first, and the first answer is often a half-initialized pawn or a front-end placeholder. And only a new companion rebuilds a screen that is already up: a replacement is the same character re-registering as its equipment replicates in, which would churn the stage for nothing, while a genuinely new arrival is a player who just joined this load.

Why a struct and not a pawn reference

During a non-seamless load the old world - pawn, equipment components, everything - is destroyed. A pointer to any of it would dangle. FCrimsonLoadingCharacterInfo copies out only what the showcase needs: mesh assets, material assets, an anim class, transforms. Assets outlive worlds, so the snapshot stays valid for the whole load. It is also why the roster is built immediately at show time rather than when the widget appears.

The same reasoning runs through the host in the other direction: every reference the subsystem keeps into a streamed stage level - the stage, the slots, the cameras - is a weak pointer, and the stage copies slot transforms out rather than storing the slot actors. A hard reference that outlives the level's removal is a check() in Debug game builds and a leaked-world fatal at the next map load.

Design rules encoded in the data

  • Exactly one leader entry (bIsLeader = true) per character - it drives the skeleton, and every overlay follows it through SetLeaderPoseComponent.
  • A null `Mesh` is meaningful: the slot exists but is empty (a bare head), and no component is built for it.
  • AdditionalActors carry exactly one of Mesh (skeletal) or StaticMesh, because equipment visuals are usually static meshes; AttachSocketName parents the entry to the leader's socket, and None places it freely.
  • Roster index 0 is always the primary - the local player, slot 0, the hero of the shot.
  • IsValid() / GetLeaderComponent() on the struct are C++ helpers; Blueprint reads the MeshComponents array directly (every field is readable and writable).

Where the picture comes from

The stage poses the scene; getting pixels onto the loading screen is the render-target pipeline. The stage's USceneCaptureComponent2D writes the render target named in Project Settings, bUseLivePreview keeps world rendering alive during the load, the host declares the capture running through SetShowcaseLive (without that declaration GetShowcaseRenderTarget stays null and widgets fall back to the background material - never a stale frame), and the widget reads GetShowcaseRenderTarget into a dynamic material. See How-To: Live Render Target Preview for the setup, and How-To: Author a Stage Level for replacing the empty world with a real set.