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.)

Quick start

Spotlight has three ways to use it — pick by how much you need.

A multi-step tour — the fluent SpotlightTour builder:

csharp
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:

csharp
var handle = LunaSpotlight.Focus(root.Q("daily-reward")); // ...later LunaSpotlight.MoveTo(root.Q("shop-button")); // animates the hole across LunaSpotlight.Dismiss(); // fades out

A 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).

Setup

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).

Advancing steps

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):

ModeBehavior
ClickAnywhere (default)The dim layer blocks the UI; a click anywhere advances.
ManualInput blocked; advance only when the game calls player.Next() — wire your own "Next" button.
ClickTargetOnly 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.)

Captions (you render the text)

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).

csharp
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.

Styling

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.

No-code authoring (the controller)

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):

  1. Author the steps inline in the inspector. Each step picks its target with a Luna ElementSelector (#name / .class / Type), plus the caption / title text and advance mode. Tour-level defaults (dim, padding, shape, default advance) live on the same component.
  2. Trigger from script — call 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.

Single highlight (the component)

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:

csharp
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 out

Its Input field chooses how the dim treats pointer input:

InputBehavior
PassiveNoBlock (default)Purely visual — the UI stays interactive.
ClickAnywhereBlock the UI; a click raises the handle's OnAdvance.
ClickTargetBlock everything except the highlighted element.
BlockAllBlock all input.

The visual fields (dim color, padding, shape, corner radius, softness, durations, easing) mirror SpotlightOptions.

Targeting

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 class
  • Button — by type (inheritance-aware: a selector for a base type also matches its subclasses)
  • combinators: 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:

csharp
new SpotlightTour() .Step(playButtonElement, "...") // an explicit VisualElement .Step("play-button", "...") // by name (root.Q) .Step(new ElementSelector { Selector = "#play-button" }, "..."); // selector

Use the code builder when you need dynamic targets or per-step side effects (.OnEnter(...)); use the SpotlightTourController component for static, designer-authored tours.

Options

SpotlightOptions (per call, or .DefaultOptions(...) / .WithOptions(...) per step):

FieldDefaultDescription
DimColorrgba(0,0,0,0.72)Overlay fill outside the hole.
Padding8Breathing room (px) around the target.
ShapeRoundedRectRoundedRect / Circle / Pill / Rect.
CornerRadius-1RoundedRect corner px; negative = follow the target's own border-radius.
Softness6Soft feather (px) at the hole edge (shader renderer only).
MoveDuration0.35Seconds to animate the hole between targets.
FadeDuration0.25Seconds to fade the overlay in/out.
EasingEaseOutCubicEasing for move + fade.

Events

  • Tour builder (SpotlightTour) — OnStepChanged(index, step), OnComplete(), OnSkip().
  • Controller (SpotlightTourController) — StepChanged(index, step), Completed, Skipped: the player's callbacks forwarded as C# events (wire once; they survive Play() restarts).
  • Player verbs (SpotlightTourPlayer) — Begin(), Next(), Previous(), Skip(), Stop().
  • Single highlight (SpotlightHandle, from LunaSpotlight.Focus) — OnAdvance, OnDismissed.

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