UI¶
NexusScript can build and drive a Unity uGUI interface at runtime: a canvas, panels, labels, buttons, sliders, toggles, and input fields. There are two ways in — build it from script with the UI.Create* helpers, or drive existing controls you've placed in the scene and dragged into public fields.
This is screen UI, and it's local
These helpers create a screen‑space overlay on the local player's screen. It's per‑client and not networked — ideal for HUDs, menus, scoreboards, and prompts.
Why this doesn't clash with the world‑space rule below: the platform disables screen‑space UI that is authored into the scene — once, when the world loads. UI your script creates at runtime (like everything on this page) loads after that pass, so a script‑built HUD works in game. Authored panel in the scene = must be world space; script‑built overlay = fine. To show shared state (everyone's score), read it from an authoritative source (Storage / an NPC action) and render it locally.
Build a UI from script¶
Every UI lives under a Canvas. Create one, then add children to it. Builders return a handle you keep and pass to the control wrappers.
using UnityEngine;
using NexusVM.Unity;
class ScoreHud : NexusBehaviour
{
Text scoreLabel;
protected override void Start()
{
UI.EnsureEventSystem(); // needed once for clicks to register
GameObject canvas = UI.CreateCanvas(); // screen-space overlay
// CreateLabel(parent, name, text, fontSize, posX, posY, sizeX, sizeY, r,g,b,a)
scoreLabel = UI.CreateLabel(canvas, "Score", "Score: 0", 28, 0, 400, 300, 60);
scoreLabel.color = Color.white;
// CreateButton(parent, name, label, posX, posY, sizeX, sizeY, r,g,b,a)
Button start = UI.CreateButton(canvas, "Start", "New Game", 0, -300, 200, 60);
start.AddClickListener("OnStartClicked"); // calls this script's OnStartClicked()
}
public void OnStartClicked() // public, matched by name
{
Debug.Log("Start pressed");
}
public void SetScore(int n)
{
scoreLabel.text = "Score: " + n;
}
}
| Builder | Signature → returns |
|---|---|
UI.CreateCanvas() |
new screen‑space canvas → GameObject |
UI.CreatePanel(parent, name, r,g,b,a) |
colored background Image |
UI.CreateImage(parent, name) |
white Image (set color/sprite after) |
UI.CreateLabel(parent, name, text, fontSize, posX, posY, sizeX, sizeY, r,g,b,a) |
Text |
UI.CreateButton(parent, name, label, posX, posY, sizeX, sizeY, r,g,b,a) |
Button (with child label) |
UI.CreateText(parent) |
bare Text (defaults) |
UI.CreateChild(parent, name) |
empty RectTransform GameObject |
UI.EnsureEventSystem() |
adds an EventSystem so buttons receive input |
Positioning & layout helpers: UI.SetRect(comp, posX, posY, sizeX, sizeY), UI.SetAnchors(comp, minX, minY, maxX, maxY, pivotX, pivotY), UI.Stretch(comp), UI.SetParent, UI.SetActive, UI.SetButtonColors, UI.AddRectMask2D.
TextMeshPro
Both legacy Text and TextMeshPro work. Type a field as TextMeshProUGUI (or TMP_Text) and set .text, .fontSize, .color directly:
public TextMeshProUGUI label;
void Show(int n) { if (label != null) label.text = "Score: " + n; }
Coordinates
posX/posY are anchored‑position offsets from the element's anchor (the builders center‑anchor by default, so 0,0 is screen center, +y is up). Sizes are in canvas units against a 1920×1080 reference that scales to the player's screen.
Drive controls you placed in the scene¶
If you'd rather lay UI out visually, expose the controls as public fields, drag them in, and drive them:
public Button fireButton;
public Slider volume;
public Toggle muted;
public Text status;
protected override void Start()
{
fireButton.AddClickListener("OnFire");
volume.value = 0.8f;
}
public void OnFire() { status.text = "Fired!"; }
You can also look controls up by name under a root: UI.FindButton(root, "Fire"), UI.FindText(root, "Status"), UI.GetButton/GetText/GetImage.
Two UI stacks, both fully supported
You can build world UI with uGUI (Canvas + Button/Slider/…) or with UI Toolkit (UIDocument +
UXML/USS). Both are clickable on desktop and in VR — pick whichever you prefer to author in. uGUI is
covered immediately below; UI Toolkit has its own section: UI Toolkit.
World UI must be World Space — screen-space UI is disabled at load
Your UI lives in the world, never over the player's screen. A Canvas must have Render Mode set to World Space; a UIDocument must use a World Space PanelSettings. Anything screen-space (Overlay or Screen Space – Camera) is disabled when the world loads, and the upload validator warns you first.
This isn't a limitation to work around — the platform's own menu, safety and report controls always sit on top, and a world that could paint over them could imitate them. Size a world Canvas in metres (a 2 m × 1 m panel, not 1920 × 1080) and place it where players can walk up to it.
Clickable world UI¶
World‑space UI is clickable in the live game, not just the editor — a uGUI Button fires from the player's gaze + left‑click on desktop and from the controller ray in VR, wired up automatically (no scene setup). It's graphic‑space: the ray hits the Button's graphic, so a world Button needs no Collider — only a normal raycastTarget graphic on a World Space Canvas. Wire the click to one of your script's methods:
myButton.AddClickListener("_OnPressed"); // fires _OnPressed() on click — no collider needed
This is the tool for flat menu panels. It's a different system from an interactable in Press mode, which is a physics button (a 3D prop hit by a reticle / controller ray) and therefore does need a Collider. Rule of thumb: UI panel → uGUI Button (no collider); 3D world prop → SSInteractable Press (collider).
Calling a script from a UI Button¶
A Button's click has to land on a NexusComponent method — not on your .cs source object. Three ways to wire it:
- From code —
b.AddClickListener("Method")inStart()(as above). Simplest for buttons you build or drag in. - From the inspector — drag the object's NexusComponent onto the Button's On Click () list, pick
NexusComponent.InvokeByName (string), and type the method name into the string field. - Non‑uGUI objects (a 3D prop, no
Button) have noOn Click— use an interactable in Press mode and handleOnUsedinstead.
Value controls pass their value to the handler, so the method signature must match: Slider → float, Toggle → bool, Dropdown → int, InputField → string. A plain Button passes nothing (no‑arg method).
Wiring UI events: two hard rules
- Always route clicks through
NexusComponent.InvokeByName("Method"). Named-event mechanisms from other platforms' scripting systems have no equivalent here — re‑point everyOn Clickat the NexusComponent. - Never point an
On Clickat your source C# method directly. The source MonoBehaviour is destroyed atAwake— only the NexusComponent survives — so a click wired to the source object silently no‑ops. Always target the NexusComponent +InvokeByName.
Underscore methods are invokable but hidden
Methods whose name starts with _ (e.g. _StartGame) are hidden from the inspector's method dropdown, but they're still fully invokable — type the exact name, leading _ included, into the InvokeByName string field by hand.
The controls¶
Each control is reached through its wrapper class, passing the handle first.
| Control | Read / write |
|---|---|
| Button | b.AddClickListener("Method") · b.RemoveAllListeners() · interactable |
| Text | text (read/write) · fontSize · color · alignment |
| Image | color (read/write) · fillAmount (0..1 — health bars, radial fills) |
| Slider | value · minValue · maxValue · wholeNumbers · interactable · onValueChanged |
| Toggle | isOn · interactable · onValueChanged |
| Dropdown | value (selected index) · interactable · onValueChanged |
| InputField | text · characterLimit · readOnly · onEndEdit · onValueChanged |
| ScrollRect | horizontal / vertical · horizontalNormalizedPosition · verticalNormalizedPosition |
| Scrollbar | value · size · numberOfSteps · interactable |
| CanvasGroup | alpha (fade a whole panel) · interactable · blocksRaycasts |
// A value-changed handler: the callback name is wired via the control's onValueChanged
public void OnVolumeChanged(float v) { AudioListener.set_volume(v); } // see Audio guide
// Fade a panel out
CanvasGroup grp = panel.GetComponent<CanvasGroup>();
grp.alpha = Mathf.Lerp(grp.alpha, 0f, Time.deltaTime * 4f);
grp.blocksRaycasts = false;
Layout groups¶
To auto‑arrange children (lists, rows of buttons) instead of hand‑placing them, add a layout group + sizing in the editor and tune from script:
- HorizontalLayoutGroup / VerticalLayoutGroup —
spacing,childControlWidth/Height,childForceExpandWidth/Height. - LayoutElement — per‑child
minWidth/Height,preferredWidth/Height,flexibleWidth/Height,ignoreLayout. - ContentSizeFitter —
horizontalFit/verticalFitto size a container to its contents (scrolling lists).
UI Toolkit (UXML + USS)¶
The other way to build UI — author the panel as UXML + USS in the editor, put a UIDocument on an
object, and drive it from script. It's a peer of uGUI, not a fallback: clicks and drags work on desktop and
in VR the same way.
A panel in the world needs a World Space PanelSettings
To put a UI Toolkit panel on a surface in your world, its PanelSettings asset must have
Render Mode = World Space (then size it with the UIDocument's world-space width/height). A
screen-space PanelSettings is a full-screen overlay, which the platform disables at world load — world
UI belongs in the world, never pasted over the player's view.
Input is wired for you at runtime, on desktop and in VR, so buttons and sliders on a world-space panel just work — nothing to add to your scene. In Play mode inside the SDK the test player sets the same thing up, so what you click offline is what players click in game.
UI Toolkit in VR — three honest limits to design around
The controller ray clicks, hovers, drags sliders and types into a world‑space UI Toolkit panel. Three things it can not do, on desktop-parity worlds or anywhere else:
- No wheel‑scroll from the ray. The thumbstick scrolls uGUI lists, but not a UI Toolkit
ScrollView. Give a long UITK list drag‑scrolling room or up/down buttons — players can always drag the list body. - No double‑click. A VR ray press always reads as a single click. Don't gate anything on
clickCount == 2. - Primary button only. There is no right‑click from a controller. Put secondary actions on their own buttons.
Desktop players have none of these limits. If your panel leans on any of them, test the VR path early.
Elements are addressed by the name you gave them in the UXML, so there's no element handle to hold on
to and you can rebuild the panel without breaking your script:
<!-- MarkerPanel.uxml -->
<ui:UXML xmlns:ui="UnityEngine.UIElements">
<ui:Label name="title" text="Marker" class="heading" />
<ui:Slider name="width" low-value="0" high-value="1" />
<ui:Toggle name="snap" label="Snap" />
<ui:Button name="reset" text="Reset" />
</ui:UXML>
UIDocument panel;
protected override void Start()
{
panel = GetComponent<UIDocument>(); // or a serialized `public UIDocument panel;`
panel.SetText("title", "Marker — red");
panel.SetFloat("width", 0.25f);
panel.OnChange("width", "OnWidth"); // slider → OnWidth(float)
panel.OnChange("snap", "OnSnap"); // toggle → OnSnap(bool)
panel.OnClick("reset", "OnReset"); // button → OnReset()
}
void OnWidth(float v) { strokeWidth = v; }
void OnSnap(bool on) { snapping = on; }
void OnReset() { panel.SetFloat("width", 0.25f); }
Bind in OnEnable, not Start — hiding a panel throws its controls away
A UIDocument rebuilds its whole set of controls every time it is re-enabled. OnChange / OnClick are
attached to the controls that existed at the moment you called them, so a panel bound once in Start()
works perfectly until something hides it — and is then permanently dead, with no error. It presents as
"nothing is wired" even though your references, handlers and names are all correct.
Bind in OnEnable so the hookup runs again with each rebuild:
void OnEnable()
{
// Returns false while the panel is still being built — retry next frame.
if (!panel.OnChange("width", "OnWidth")) return;
panel.OnClick("reset", "OnReset");
}
OnChange / OnClick return false when the controls aren't ready yet, which is what makes a retry work.
Worth knowing: a screen-space panel that is never hidden keeps its controls and never shows the fault, so
the same code can work in one place and be inert in another.
Styling from script. Prefer flipping a USS class over writing inline styles — it keeps the look in the stylesheet where you can restyle everything at once:
panel.AddClass("reset", "danger"); // .danger { background-color: ... } in your USS
panel.ToggleClass("title", "highlight");
bool lit = panel.HasClass("title", "highlight");
panel.SetVisible("snap", false); // display: none
panel.SetEnabled("reset", false); // greys out and stops events
panel.SetColor("title", Color.red); // background-color
panel.SetTextColor("title", Color.white);
panel.SetOpacity("title", 0.5f);
A name that doesn't match logs once
Names are case-sensitive. If nothing matches, you get a single console warning naming the element —
so a typo shows up immediately, but calling it every frame in Update won't flood the console. Use
panel.Exists("width") to branch on optional elements.
| Read/write text | GetText(name) · SetText(name, text) |
| Values | GetFloat/SetFloat (slider) · GetBool/SetBool (toggle) |
| Visibility | GetVisible/SetVisible · GetEnabled/SetEnabled |
| Style | SetColor · SetTextColor · SetOpacity |
| USS classes | AddClass · RemoveClass · ToggleClass · HasClass |
| Events | OnClick(name, "Method") · OnChange(name, "Method") |
| Guard | Exists(name) |
Quick reference¶
| Group | Calls |
|---|---|
| UI Toolkit | UIDocument: SetText/GetText · SetFloat/GetFloat · SetBool/GetBool · SetVisible/SetEnabled · AddClass/RemoveClass/ToggleClass/HasClass · OnClick/OnChange · Exists |
| Build | UI.CreateCanvas · CreatePanel · CreateImage · CreateLabel · CreateButton · CreateText · CreateChild · EnsureEventSystem |
| Place | UI.SetRect · SetAnchors · Stretch · SetParent · SetActive · SetButtonColors · AddRectMask2D |
| Find | UI.FindButton · FindText · GetButton · GetText · GetImage |
| Controls | Button · Text · Image · Slider · Toggle · Dropdown · InputField · ScrollRect · Scrollbar · CanvasGroup |
| Layout | HorizontalLayoutGroup · VerticalLayoutGroup · LayoutElement · ContentSizeFitter |
→ See the Score HUD recipe for a complete build‑from‑script HUD. Pair with Input & time for keyboard/mouse and Rendering for sprites and colors.