Use Spotlight for onboarding and tutorial walkthroughs: it focuses the player on one UI element by dimming everything else, then glides the highlight from element to element ("Tap here to start" → Play → Inventory…).
Unlike a per-element UI Effect — which is scoped to its own element — a spotlight dims the whole panel except one element. (Under the hood it's a fullscreen overlay, a sibling to Circle Hole, whose SDF filter shader punches a soft, rounded-rect hole at the target — the same SDF the effects use — and animates that hole between targets.)
Spotlight has three ways to use it — pick by how much you need.
A multi-step tour — the fluent SpotlightTour builder:
using CupkekGames.Luna.Spotlight;
// `root` is your panel's visual tree (e.g. from a PanelRenderer reload callback).
var tour = new SpotlightTour()
.Step("play-button", "Tap here to start your first run.")
.Step("inventory-panel", "Your loot lives here.").Title("Inventory")
.Step("settings-gear", "Audio and controls live in Settings.")
.OnComplete(() => PlayerPrefs.SetInt("onboarded", 1))
.OnSkip(() => PlayerPrefs.SetInt("onboarded", 1))
.CreatePlayer(root)
.Begin();Title / Caption are text you render — Spotlight draws none itself; see
Captions for how.
A single highlight, in code — the static LunaSpotlight facade:
var handle = LunaSpotlight.Focus(root.Q("daily-reward"));
// ...later
LunaSpotlight.MoveTo(root.Q("shop-button")); // animates the hole across
LunaSpotlight.Dismiss(); // fades outA single highlight, no code — add a SpotlightFocus
component to the UI GameObject, point its Target selector at an element, and enable
Focus On Load (or call Focus() / Dismiss() from script).
Spotlight works out of the box — with no setup it uses a Painter2D fallback (hard edges, no
feather). For soft, feathered edges, wire the filter once: assign a Spotlight Filter
Settings asset to LunaUIManager → Spotlight Settings
(Essentials/Effects/SpotlightFilterSettings).
Tour only — single highlights use the SpotlightFocus
Input field instead.
Each step has an advance mode (how the player moves on) plus an optional auto-advance timer that fires on top of it.
Advance mode (SpotlightAdvance):
| Mode | Behavior |
|---|---|
ClickAnywhere (default) | The dim layer blocks the UI; a click anywhere advances. |
Manual | Input blocked; advance only when the game calls player.Next() — wire your own "Next" button. |
ClickTarget | Only the highlighted element is clickable (input passes through the hole); clicking it advances — "learn by doing". |
Spotlight ships no Next / Skip buttons — they're yours. Wire your own to player.Next() /
player.Skip() (and Previous()). Because the dim blocks the UI underneath, they must render
above it — see Captions for how.
Auto-advance is opt-in: set a step's auto-advance seconds > 0 and it also advances after
that delay (the player can still advance early). Leave it 0 and the step waits for input. Set
it per step with .AutoAdvanceAfter(seconds) (and the advance mode with .AdvanceOn(mode));
set tour defaults with .DefaultAdvance(mode) / .DefaultAutoAdvance(seconds). (On the
SpotlightTourController, each step has an Auto Advance Seconds field.)
Spotlight draws no caption — rendering it is your job, so the text matches your art, voice,
and localization. You author a caption per step (.Step(target, "caption") / .Title(...), or
in the inspector), and Spotlight hands it back for you to draw.
Render it from the step-changed callback — SpotlightTour.OnStepChanged (code) or
SpotlightTourController.StepChanged (component). The SpotlightStep carries what you need:
ResolvedTarget (the live highlighted element, to position next to) and Title / Caption
(the text).
controller.StepChanged += (index, step) =>
{
myBubble.Q<Label>().text = step.Caption; // your own UI element
PositionNextTo(myBubble, step.ResolvedTarget); // you decide placement
myBubble.style.display = DisplayStyle.Flex;
};
controller.Completed += () => myBubble.style.display = DisplayStyle.None;
controller.Skipped += () => myBubble.style.display = DisplayStyle.None;The dim overlay sits on top of your UI, so your tutorial UI (caption + Next / Skip) must
render above it — parent it to root.panel.visualTree (a sibling of the overlay, added
after) and re-BringToFront() it on controller.StepChanged so it stays on top, or use a
higher-sorted panel. The bundled SpotlightTour sample builds its own tutorial card (caption +
Skip / Next) above the dim, doing exactly this.
The dim overlay uses .luna-spotlight-overlay (in LibSpotlight.uss, imported by the Luna
theme) — override it to restyle the dim. Everything else (your caption text, Next / Skip
buttons) is your own UI, styled however you like.
To author a tour in the inspector instead of code, add a SpotlightTourController to the
UI GameObject (the one with the PanelRenderer, or any child of it — it auto-finds the
renderer):
#name / .class / Type), plus the caption / title text
and advance mode. Tour-level defaults (dim, padding, shape, default advance) live on the
same component.controller.Play() (or Stop()) whenever you want. With
Play On Load it starts itself once the UI is ready (after a small Start Delay so
layout settles). To replay from a button, call Play() from that button's click — e.g.
root.Q<Button>("help").clicked += controller.Play;. Wire your Next / Skip buttons to
controller.Player.Next() / .Skip(), and listen to controller.StepChanged /
Completed / Skipped to drive them.A tour is screen-specific, so the steps live directly on the controller — there's no separate
asset. It's a self-contained controller (in the spirit of UIAttractor): it resolves the
panel root, builds the tour from its inline steps, and plays it.
SpotlightFocus dims one element as a drop-on component — no tour, no caption. Add it to the
UI GameObject (it auto-finds the PanelRenderer) and set its Target selector. That target
is just the default — used by Focus() and Focus On Load.
Move the highlight at runtime with the same targeting vocabulary as the inspector — the
component resolves it under its own UI root, so you never grab the root yourself. Every
Focus(...) animates the hole across if a spotlight is already showing:
spotlightFocus.Focus(); // the inspector default target
spotlightFocus.Focus("inventory-panel"); // by element name
spotlightFocus.Focus(new ElementSelector { Selector = "#shop > .badge" }); // by selector
spotlightFocus.Focus(someVisualElement); // by explicit element
spotlightFocus.Dismiss(); // fade outIts Input field chooses how the dim treats pointer input:
| Input | Behavior |
|---|---|
PassiveNoBlock (default) | Purely visual — the UI stays interactive. |
ClickAnywhere | Block the UI; a click raises the handle's OnAdvance. |
ClickTarget | Block everything except the highlighted element. |
BlockAll | Block all input. |
The visual fields (dim color, padding, shape, corner radius, softness, durations, easing)
mirror SpotlightOptions.
Each step (or SpotlightFocus) targets its element with a Luna ElementSelector — the same
CSS-like picker used elsewhere in Luna:
#play-button — by name.hud-play — by USS classButton — by type (inheritance-aware: a selector for a base type also matches its subclasses)A B (descendant), A > B (child), A, B (union)The first match (DOM order) is highlighted, resolved under the UI root at play time. In code you can target three ways:
new SpotlightTour()
.Step(playButtonElement, "...") // an explicit VisualElement
.Step("play-button", "...") // by name (root.Q)
.Step(new ElementSelector { Selector = "#play-button" }, "..."); // selectorUse the code builder when you need dynamic targets or per-step side effects (.OnEnter(...));
use the SpotlightTourController component for static, designer-authored tours.
SpotlightOptions (per call, or .DefaultOptions(...) / .WithOptions(...) per step):
| Field | Default | Description |
|---|---|---|
DimColor | rgba(0,0,0,0.72) | Overlay fill outside the hole. |
Padding | 8 | Breathing room (px) around the target. |
Shape | RoundedRect | RoundedRect / Circle / Pill / Rect. |
CornerRadius | -1 | RoundedRect corner px; negative = follow the target's own border-radius. |
Softness | 6 | Soft feather (px) at the hole edge (shader renderer only). |
MoveDuration | 0.35 | Seconds to animate the hole between targets. |
FadeDuration | 0.25 | Seconds to fade the overlay in/out. |
Easing | EaseOutCubic | Easing for move + fade. |
SpotlightTour) — OnStepChanged(index, step), OnComplete(), OnSkip().SpotlightTourController) — StepChanged(index, step), Completed, Skipped: the player's callbacks forwarded as C# events (wire once; they survive Play() restarts).SpotlightTourPlayer) — Begin(), Next(), Previous(), Skip(), Stop().SpotlightHandle, from LunaSpotlight.Focus) — OnAdvance, OnDismissed.Settings
Theme
Light
Contrast
Material
Dark
Dim
Material Dark
System
Sidebar(Light & Contrast only)
Font Family
DM Sans
Wix
Inclusive Sans
AR One Sans
Direction