Curitor
Gameplay Systems · Topic Guide

UI & Theming

Every screen the kit ships — inventory, settings, results, the pause menu — is built from one small widget set reading one theme asset. Duplicate that asset, change the colours, and the whole game restyles. Nothing is a bespoke prefab you have to re-skin by hand.

One asset, every screen

A ThemeProvider sits on each canvas root and holds a UiThemeAsset. Every kit widget below it walks up to the nearest provider and paints itself from that asset's tokens — colours, fonts, corner sprite, spacing, bar and button heights. Nothing hardcodes a colour, so there is exactly one place to change.

  1. Duplicate the installed theme

    Install creates DefaultUiTheme under a Resources folder. Duplicate it — leave the original alone so Install keeps finding one.
  2. Change the tokens

    Primary, danger, panel and slot colours, the text tones, the font, the padding unit. The defaults in the asset are the shipped look, so you are always editing something complete.
  3. Assign it to a canvas root

    Drop the copy into that canvas's ThemeProvider. Every widget under it restyles — in the editor, immediately, with no play mode and no prefab edits.

A widget with no provider above it, or a provider with an empty field, falls back to the installed default. A build that somehow has neither still runs: the theme's own field defaults carry the shipped palette, so the fallback is a real theme rather than a null.

A chosen theme and volume survive a quit

GameSettingsStore.Currentis the one place a player's live settings actually live — a plain C# class, not aScriptableObject, because writing choices back into an asset works in the editor and silently fails in a built player. Reading it auto-creates and restores it, so the first thing that asks already sees the saved values rather than defaults.

FieldTypeDefaultWhat it does
MasterVolumefloat10 (silent) to 1 (full). SetMasterVolume(v) clamps, applies to AudioListener.volume immediately, and saves.
ThemeIdstring""The player's picked UiThemeAsset.ThemeId. Empty means "never chosen" — consumers fall back to their own default theme rather than guessing.

Both setters save immediately rather than waiting for a save point — a player who drags the volume slider and quits without ceremony keeps the change. See Saving & Loading for how this fits the rest of the persistence seam: settings are Shared scope and Player owned, so they follow the machine rather than the save slot.

The widget set

All of these live under Party Core Kit › UI Kit in the Add Component menu. They style; they do not decide what your screen says. Text content, option lists and selection state stay with the screen composing them.

FieldTypeWhat it does
PanelSurfaceAny themed surface — a card, a slot, a sunken well, the dim behind a popup, or an accent highlight.
LabelRoleThemed text: body, muted, title, caption or danger. Size follows the theme's font scale.
ButtonRoleA uGUI button whose hover, pressed and disabled looks derive from one role colour — including the highlight a gamepad needs.
ToggleA uGUI toggle with the same derived states as Button.
SliderSunken track, accent fill, a handle that reads as a small button.
InputA TextMeshPro input field: surface, text, placeholder, caret and selection colour.
StepperA left/right cycler through a fixed option set. Owns the index and wraps; you supply the count and the text.
Tab BarA row of tabs where exactly one is active. Raises SelectedChanged; the active tab wears the Primary role.
List RowA row surface with a selected look, for level pickers, results tables and settings lists.
Card GridA sunken grid whose padding and spacing come from the theme, so grids keep one rhythm across a reskin.
Stat BarKindA value bar for health or stamina; the colour shifts from the empty tone to the full one as it fills.
BadgeThe small count pill on a button or icon. Hides at zero, shows 99+ above ninety-nine.
Currency ChipAn icon-plus-amount chip for coins or gems.
Avatar FrameA framed portrait: accent border, sunken well, avatar sprite.
Top BarA full-width header with left, centre and right regions to parent widgets into.
Popup FrameThe shape every popup shares — full-screen dim, centred card, title. Put your content under Content.
Panel StackA stack of menu pages with open, back and close, and the gamepad focus handling that goes with them.
Collapsible PanelA sidebar or drawer that shrinks to a thin toggle strip and back, so extra information doesn't crowd the view when the player doesn't want it.
GradientDirectionShades any image toward the theme's gradient tint. One theme value restyles every gradient; white turns them off.
Theme ProviderHolds the theme for everything under it. One per canvas root.
FocusableThe Selectable to use inside a player's own hierarchy. See the warning below.
Objective IndicatorAn edge-of-screen arrow pointing at the player's current objective while it is out of view.

Windows, the cursor and the pad

An in-game window has three jobs beyond looking right: it needs the mouse, it needs the pad to land somewhere useful, and the player expects to be able to move it.

UI Focus Panelon the panel root handles the first two. While the panel is active, the owning player's camera releases the cursor and stops looking around — movement keys stay live — and keyboard or gamepad focus lands on the panel's first control so pad navigation works without touching the mouse. Both happen on enable and unwind on disable, so a window closed by any route leaves the cursor as it found it.

Draggable Window goes on the window's header, not the whole panel: the header is the grab handle, so dragging it moves the window while the body's own drag and drop — inventory slots, container slots — keeps working untouched. Positions are remembered for the session, so a window dragged into a corner reopens there even if it was rebuilt in between.

Escape unwinds in the order a player expects, and that is a single call: UiFocusPanel.TryCloseTopExcept(...)closes the most recently opened panel and reports whether it closed one. A false return is the pause menu's cue that nothing else was up.

Telling the player something

The notification centre on each player's HUD takes two kinds of message, and gameplay code never builds UI to send one:

Sending a notification
// Small, stacks in the corner: "Poison applied", "Picked up 3 Stone".
notifications.ShowToast("Picked up 3 Stone");

// Big, centred, one at a time: "You Died", "Level Up!".
notifications.ShowBanner("You Win!", NotificationStyle.Positive);

A banner arriving while another is on screen queues behind it instead of drawing over it. Stat events reach this through a relay, and the scripting library exposes it as an instruction — so “when this happens, tell the player” is composition, not code.

The Objective Indicatoranswers “where am I supposed to be going?”. It pins an arrow to the screen edge whenever the objective is out of view and hides itself the moment it comes into view. It binds one small interface rather than any game mode, and the kit ships two adapters for it: the goal-race one points at the start zone while players gather and the end zone once the round is running; the lap-circuit one points at each player's own next gate.

Two more, under Party Core Kit › UI

Everything above lives under the UI Kit menu folder because it reads the theme. A couple of other pieces live one folder over, under Party Core Kit › UI, because they don't:

FieldTypeWhat it does
Billboard To CameraTurns a world-space element -- an overhead stat bar, an interaction prompt -- to face the active camera every frame, so it stays readable from any angle.
Round Series ScoreboardCumulative points per participant, round progress and final standings for a Round Series. The stock results panel already shows this between rounds; add it yourself only for a custom results screen.

Extending it

A widget of your own that follows the theme is one class. Derive from ThemedWidget and paint in Apply; it is called when the widget is enabled and on every theme change, never per frame.

A themed widget of your own
using Curitor.PartyCoreKit.UI;
using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(Image))]
[AddComponentMenu("My Game/UI/Ribbon")]
public class Ribbon : ThemedWidget
{
    private Image _image;

    protected override void Apply(UiThemeAsset theme)
    {
        if (_image == null) _image = GetComponent<Image>();
        _image.color = theme.primary;
    }
}

Pointing the objective arrow at something the kit has never heard of is one class too — implement IObjectiveSource, register in OnEnable, unregister in OnDisable, and return false whenever you have nothing to point at.

Your own objective source
public class EscapePodObjective : MonoBehaviour, IObjectiveSource
{
    [SerializeField] private Transform pod;

    private void OnEnable()  => ObjectiveSources.Register(this);
    private void OnDisable() => ObjectiveSources.Unregister(this);

    public bool TryGetObjective(GameObject playerRoot, out Vector3 worldPosition)
    {
        worldPosition = pod != null ? pod.position : default;
        return pod != null;
    }
}