Skip to content

UI and Widgets

RedEngine.UI provides an application-level widget stack. Screens represent navigation, windows layer temporary UI above them, notifications are queued transient messages, and the loading screen follows world travel automatically.

The system owns presentation only. Widgets read state and request actions; replicated gameplay remains in actors, abilities, inventories, and other Fusion-aware components.

Execution model

  • Authority — UI never becomes gameplay authority; requests are validated by the owning gameplay system.
  • Prediction — widgets may show pending or predicted intent without treating it as confirmed state.
  • Replicated — the actor, attribute, inventory, equipment, and match values displayed by widgets.
  • Local only — widgets, layers, windows, notifications, loading screens, markers, animation, and audio.

Placeholder: widget root, layers, screens, windows, and notifications

Configure widgets in EngineSettings

Open the project's EngineSettings asset and assign:

  • Default Widget Layer: a prefab with WidgetLayer used as the persistent root;
  • Loading Screen Widget: a prefab with ScreenWidget shown during GameInstance travel.

If no default canvas is assigned, WidgetSubsystem creates an overlay canvas at runtime with a 1920×1080 reference resolution. The root survives scene travel and belongs to the GameInstance.

Widget types

TypeUse
WidgetReusable UI block with child widgets, variables, and localization bindings
WidgetLayerPersistent root for instantiated widgets
ScreenWidgetNavigable full-screen state with show/hide animation and LayerIndex
WindowWidgetModal or additive UI above screens; can close itself
NotificationWidgetTemporary queued window with optional lifespan

ScreenWidget.LayerIndex controls sibling sorting. Use low values for normal screens, higher values for overlays, dialogs, and system UI. Show and hide animation clips are queued so lifecycle callbacks occur after their visual transition.

csharp
[SerializeField] private AssetReference<MainMenuWidget> mainMenu = new();
[SerializeField] private AssetReference<SettingsWidget> settings = new();

WidgetSubsystem widgets = gameInstance.GetSubsystem<WidgetSubsystem>()!;

MainMenuWidget? menu = widgets.ShowScreen(
    mainMenu,
    new ScreenShownParameters(
        openMode: EScreenOpenMode.ClearStackAndOpen));

SettingsWidget? settingsScreen = widgets.ShowScreen(
    settings,
    new ScreenShownParameters(
        openMode: EScreenOpenMode.Push));

Screen modes:

  • Push disables the current screen and resumes it when the new screen closes;
  • ReplaceCurrent closes the current screen before opening the next;
  • ClearStackAndOpen closes the entire navigation stack.

Call CloseCurrentScreen() for Back behavior, or close a specific screen returned from ShowScreen.

For a prefab-backed widget that is not a screen, window, or notification, call WidgetSubsystem.CreateWidget(reference). The subsystem loads it through AssetReference, parents it to the UI root, constructs its variable context and activates it. Destroy the returned widget when its owning gameplay presentation is removed.

Open windows

csharp
[SerializeField] private AssetReference<ConfirmPurchaseWindow> confirmPurchase = new();

ConfirmPurchaseWindow? window = widgets.OpenWindow(
    confirmPurchase,
    new WindowShownParameters(
        state: new ConfirmPurchaseState(itemId, price),
        group: "Store.Modal",
        openMode: EWindowOpenMode.Additive,
        instancePolicy: EWindowInstancePolicy.SingleInstanceInGroup));

Windows support additive display, replacement of all visible windows, and queued opening. Group policy is useful for ensuring only one dialog from the same flow remains open. A WindowWidget can call Close() from its button handler.

State objects are plain project-defined classes derived from ScreenState, WindowState, or NotificationState. They keep initialization data out of global fields and are available through the widget's Parameters property.

Show notifications

csharp
[SerializeField] private AssetReference<ItemReceivedNotification> itemReceived = new();

ItemReceivedNotification? toast = widgets.EnqueueNotification(
    itemReceived,
    new NotificationShownParameters(
        state: new ItemReceivedState(itemName, quantity)));

Set NotificationWidget.initialLifeSpan for automatic local dismissal, or call Hide() explicitly. Notification timing is presentation-only and therefore uses the game-instance timer manager rather than replicated Fusion state.

Variables and bindings

Every widget owns a VariablesContext that inherits values from its parent. SetVariable updates the context and notifies child widgets:

csharp
public sealed class PlayerHud : Widget
{
    public void Present(Character character)
    {
        SetVariable("playerName", character.name);
        SetVariable("health", character.Health);
        SetVariable("maxHealth", character.MaxHealth);
    }
}

The showcase's ShowcaseTextBinding reads these variables in OnVariablesChanged. Localized TMP text can also consume variables through RedEngine's localization markup. ValueBinding<T> is available for small polled getter/setter bindings.

Child contexts inherit parent values but can override a key locally. This makes it practical to set a player name once on a screen and provide per-slot values inside inventory child widgets.

World markers

World-marker responsibilities are split between three Framework types:

  • WorldMarker describes the world target, widget prefab, offset, distance and occlusion settings;
  • WorldMarkerWidget is the extensible screen-space presentation created for a registered marker;
  • WorldMarkerSubsystem creates marker widgets in the root WidgetLayer, then centrally projects, range-culls and occlusion-tests every marker in its World.

Use Bind(target, offset) for runtime-spawned actors and SetVisible for gameplay-driven availability. The subsystem requests the camera depth texture and runs occlusion queries in the target's PhysicsScene, so overlay views remain correctly isolated in Fusion Multi-Peer scenes. Compact Showcase uses this path for level labels, sample-specific interaction prompts and the widget-based NPC health bar; characters contain no marker scan.

Configure a marker

Add WorldMarker to the world object and configure:

  • Widget Prefab — a prefab containing WorldMarkerWidget;
  • Anchor and Local Offset — the transform and local position projected to the screen;
  • Culling Mode — distance plus viewport, or distance only for an off-screen indicator;
  • Fade Start Distance and Maximum Visible Distance — the range fade;
  • Occlusion ModeNone, Hide, or Fade;
  • Occlusion Mask, radius, and padding — what blocks the marker and how broadly it is sampled.

For an actor created at runtime, a project-specific marker can expose its gameplay model while the widget remains presentation-only:

csharp
public sealed class ObjectiveWorldMarker : WorldMarker
{
    public string Label { get; private set; } = string.Empty;

    public void Initialize(Transform target, string label)
    {
        Label = label;
        Bind(target, new Vector3(0f, 1.8f, 0f));
        SetVisible(true);
    }
}

public sealed class ObjectiveWorldMarkerWidget : WorldMarkerWidget
{
    [SerializeField] private TMP_Text label = null!;

    protected override void OnMarkerBound(WorldMarker marker)
    {
        if (marker is ObjectiveWorldMarker objective)
            label.text = objective.Label;
    }

    protected override void OnMarkerUnbound(WorldMarker marker)
    {
        label.text = string.Empty;
    }
}

Do not run Camera.WorldToScreenPoint independently in every marker component. The world subsystem batches ownership, projection, culling, and occlusion and creates the widget in the correct WidgetLayer for the active peer.

Typical uses include player names, NPC health bars, interaction prompts, quest objectives, damage directions, and off-screen targets. For a marker that must remain visible at the screen edge, use DistanceOnly and clamp its visual position in the derived widget.

Loading screen and lifecycle events

WidgetSubsystem subscribes to GameInstance.OnBeginLoadWorld and OnWorldLoadComplete. It shows the configured loading widget before travel and hides it when the new world is ready. You can also call ShowLoadingScreen and HideLoadingScreen manually for non-travel content work.

The subsystem publishes OnScreenShown, OnScreenHidden, OnWindowShown, OnWindowClosed, OnNotificationShown, and OnNotificationHidden for presentation coordination and analytics.

Build the loading screen

  1. Create a prefab derived from ScreenWidget.
  2. Add a full-screen background, progress or activity indicator, and optional status text.
  3. Assign a hide animation to the widget so the new world is revealed after a short transition.
  4. Assign the prefab to EngineSettings > Loading Screen Widget.

The subsystem shows it on GameInstance.OnBeginLoadWorld. It hides it on either OnWorldLoadComplete or OnTravelFailure, so a failed connection cannot leave the UI permanently covered. BeginHide lets the configured hide animation complete before the widget is destroyed.

For local content work that does not perform travel, bracket the operation manually:

csharp
WidgetSubsystem widgets = gameInstance.GetSubsystem<WidgetSubsystem>()!;

widgets.ShowLoadingScreen();
try
{
    await LoadFrontendCatalogAsync();
}
finally
{
    widgets.HideLoadingScreen();
}

The manual API is presentation state, not network synchronization. For additive Fusion levels, await World.LoadSceneAsync on the authoritative flow and decide separately whether every peer needs a full-screen overlay or a smaller streaming indicator.

Next: Diagnostics · Developer Console.

Updated:

RedEngine Framework documentation