Skip to content

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 codeb.AddClickListener("Method") in Start() (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 no On Click — use an interactable in Press mode and handle OnUsed instead.

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 every On Click at the NexusComponent.
  • Never point an On Click at your source C# method directly. The source MonoBehaviour is destroyed at Awake — 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 / VerticalLayoutGroupspacing, childControlWidth/Height, childForceExpandWidth/Height.
  • LayoutElement — per‑child minWidth/Height, preferredWidth/Height, flexibleWidth/Height, ignoreLayout.
  • ContentSizeFitterhorizontalFit / verticalFit to 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.