UICueBinder binds UI events to payoffs — a transition sequence, a sound, a one-shot UIRender particle burst — with zero C# at the call site. You author rows of selector → trigger → payoff in the inspector (or share them as profile assets), and the binder plays the right payoff whenever a matching element fires the trigger.
Luna's juice content was already asset-driven — sequence assets, attractor motion presets, reactions assets. The trigger — when something plays — still required a programmer: btn.clicked += … for every claim button, every tab, every hover. UI Cue generalizes the trigger half.
UICueBinder to the GameObject that holds the screen's PanelRenderer (it requires one)..claim-btn, Trigger Click, and drop a TransitionSequenceAsset + AudioClip into the payoff..claim-btn — including ones inside ListView rows — now punches and clinks.No code, no per-button wiring, and elements created after setup work too.
The binder registers one callback per trigger type on the panel root. When an event dispatches, it tests evt.target and its ancestors against each row's compiled selector and fires the payoff on match — the same pattern the web uses for dynamic content.
This matters for one reason above all: recycled elements just work. ListView and GridView rebind pooled rows without detaching them, so any system that registers callbacks per element at setup time silently misses rebound rows. Delegation matches at dispatch time instead — there is nothing to miss and nothing to leak.
The cost is a selector match per dispatched event × row count, which is negligible for discrete events (click, focus, change). That is also why there is deliberately no pointer-move or drag trigger — per-frame matching is the wrong tool.
| Trigger | Fires on | Notes |
|---|---|---|
Click | click (press + release on the same element) | the workhorse |
PointerDown / PointerUp | press / release | press feel |
PointerEnter / PointerLeave | pointer entering / leaving a matched element | see the hover note below |
FocusIn / FocusOut | keyboard/gamepad focus gained / lost | focus parity for hover payoffs |
ValueChanged | a control's value changes | gated by ValueFilter (bool) / ValueStep (numeric); see below |
ScreenEnter | panel (re)load | entrance juice; supports StaggerDelay |
Custom | UICue.Fire(element, key) from code | the game-logic bridge |
Hover note. UI Toolkit enter/leave events don't bubble — they fire once per entered element — so for PointerEnter/PointerLeave the selector must match the entered element itself, not an ancestor. Every other trigger also matches ancestors: a Click row with selector .card fires when any child of the card is clicked.
ValueChanged. UI Toolkit sends typed ChangeEvent<T>s; the binder listens for all four control value types — bool (Toggle, RadioButton), int (SliderInt, RadioButtonGroup, IntegerField), float (Slider), string (DropdownField, TextField). Two gates turn continuous streams into discrete cues:
ValueFilter (bool rows): fire on any change, only on check, or only on uncheck — two rows on one selector give check-pop vs uncheck-thud.ValueStep (numeric rows): fire only when the value crosses a multiple of the step. ValueStep: 0.1 on a 0–1 slider ticks at 0.1, 0.2, … 1.0 like a notched dial. Steps are raw values (a 0–100 bar wants 10), and a fast drag that jumps several steps in one event fires once, so a full-range fling can't spam sounds or render slots.An ungated row (filter Any, step 0, no cooldown) warns once at load: sliders change every frame of a drag and text fields fire per keystroke, so either gate it or set a small Cooldown if continuous firing is intentional.
| Field | Meaning |
|---|---|
Selector | CSS-style selector matched against the event target and its ancestors — .claim-btn, #MailButton, Button.primary |
Trigger | one of the triggers above |
CustomKey | only for Custom: the key passed to UICue.Fire |
ValueFilter | only for ValueChanged on bool controls: Any / True (check only) / False (uncheck only) |
ValueStep | only for ValueChanged on numeric controls: fire when the value crosses a multiple of this step, in raw values. 0 = every change |
ApplyToSelector | optional closest-ancestor selector to play the payoff on — click the button, punch the card. Empty = the matched element itself |
Payoff | the bundle below |
Replay | Restart (default) stops the row's running sequence on that element and replays from the top; IgnoreWhilePlaying skips fires until it finishes |
Cooldown | minimum seconds between fires per target element (unscaled time). Spam guard for punchy payoffs |
StaggerDelay | only for ScreenEnter: seconds × match index, for cascade-in feels |
| Field | Meaning |
|---|---|
Sequence | TransitionSequenceAsset played on the target |
Sound + PitchJitter | clip routed through LunaUIManager.AudioHandler, with optional ± random pitch per play |
Vfx + VfxSize / VfxAnchor / VfxOffset | one-shot ParticleSystem prefab via UIRenderManager.PlayBurst |
All three channels are optional and fire together at the moment of the event. A row with only Sound is a click-sfx rule; a row with only Sequence is pure motion. The shape deliberately mirrors UIAttractorDefaultReactionsSO so the two systems feel like one family.
The same dialect as every Luna ElementSelector: #Name, .class, Type, *, compounds (Button.primary), descendant (.card .icon), child (.list > .row), and comma unions. No pseudo-classes or attribute selectors.
Game code rarely knows what should play; it knows what happened. Fire the semantic fact and let rows own everything else:
UICue.Fire(claimButton, "quest.claimed");Row: Trigger = Custom, CustomKey = "quest.claimed". The event bubbles through the same delegation path, so the selector can match the fired element or any ancestor. This is also the intended path for dynamic appears — chat bubbles, spawned rows — where the code creating the element knows the moment it becomes meaningful.
UICueProfileSO (Create → Luna → UI Cue → Profile) holds a reusable row list — "app-wide button feel" lives here once instead of per screen. A binder takes any number of profiles plus its own inline rows, and precedence is additive: every matching row fires, nothing overrides. Juice composes.
The recommended split: generic UI feel → profiles; screen-specific payoffs → inline rows; attractor lifecycle → reactions assets; everything else → code.
On panel load (and UXML hot-reload) the binder runs one query per ScreenEnter row and fires the payoff on every match, StaggerDelay seconds apart in match order. An empty selector means the panel root itself.
Scope it right: ScreenEnter fires when the panel tree loads — not on every navigation push of a pooled view. For per-push entrance stagger on UIViewComponent screens, use Stagger Entry (its FadeInStart trigger re-fires on every push). ScreenEnter suits HUDs, standalone panels, and screens whose tree load is their appearance.
ApplyToSelector or a wrapper element when two payoffs must overlap.RestoreOnEnd) always composes. A hold pair works too — hover-in (Persist) matched by hover-out on the same property. What does not compose is interleaving other rows that animate the same property between a hold pair (e.g. recreating press-and-hold between hover-in and hover-out): the later restore captures a mid-hold value and the element settles off-rest. Full press+hover feel is Interaction Juice's job — its single driver owns all four slots.Cooldown and IgnoreWhilePlaying are the levers. Requires a UIRenderManager in the scene; the binder warns and skips otherwise.LunaUIManager/AudioHandler exists.Enable _debugTrace on the binder to log every match:
[UICueBinder] Home: ButtonFeel[2] '.tab-btn' Click matched 'QuestsTab' → play on 'QuestsTab' seq:TabPunch sfx:click_soft vfx:-
Rows that can never fire — an empty selector on a non-ScreenEnter trigger, a Custom row without a key — are warned about once at load, not silently dropped.
ValueChanged + ValueStep covers the slider case discretely).PlayAmbient) — a fire-and-forget row can't own a lifetime.The UI Cue showcase scene (Components/Juice/UICue in the imported Showcase sample) wires every trigger from Inspector rows: hover + click-pop button feel from a shared CueButtonFeel profile, an ApplyToSelector card punch, a toggle with check/uncheck ValueFilter rows, a slider ticking at ValueStep: 0.1 notches, a dropdown pick-pop, five ScreenEnter chips staggering in, and a reward chest where the scene's only line of gameplay code — UICue.Fire(chest, "demo.reward") — triggers two additive rows: a punch + sfx and a SparkleBurst via UIRender.
Runtime/Scripts/UICue/ — UICueBinder.cs, UICueRow.cs, UICueProfileSO.cs, UICueEvent.csSettings
Theme
Light
Contrast
Material
Dark
Dim
Material Dark
System
Sidebar(Light & Contrast only)
Font Family
DM Sans
Wix
Inclusive Sans
AR One Sans
Direction