UI from Scripts
Scripts drive the in-game interface through api.ui, addressing UI elements and screens by their entity name, and build UI at runtime by spawning UI prefabs into a screen.
Two layers put text and controls on screen during Play: UINode elements and screens you lay out in the editor (see In-Game UI), and a single HUD string (api.setHud) for quick status text. Both are covered here.
Names are the address
api.ui finds an element by the name of its object in the Hierarchy, and a screen by the name of the object carrying UIScreen.
- Every element verb acts on every
UINodewith that name. A layout variant lays the same elements out again under the same names, sosetText("Score", …)updates whichever variant is showing. - Every screen verb acts on every screen with that name (a screen and its variants).
- A name that matches nothing is a silent no-op, so a renamed label never crashes a script.
- Names are one global namespace: two buttons called "Close" on two screens are one address. The Screens tab tags such names N dup.
The UIApi
| Member | Signature | Description |
|---|---|---|
ui.setText | (name, text): void | Set the text of every element with the name. |
ui.setImage | (name, imageId): void | Set the sprite: a UI image id or a texture id. |
ui.show | (name): void | visible = true. Warns once if the element's screen is hidden. |
ui.hide | (name): void | visible = false. |
ui.onClick | (name, cb): void | Click handler for a button element. Never fires for a panel, text or image. |
ui.onPress | (name, cb): void | Pointer-down handler for any element. Registering makes the element take presses. |
ui.onPointer | (name, cb(x, y, clicks)): void | Click handler for any element, with x / y as 0..1 across it and clicks the burst count (2 = double-click). Registering makes the element take presses. |
ui.hitTest | (x, y): string | null | The name of the element a press at canvas point (x, y) would land on, or null when it would reach the game. |
ui.rect | (name): { x, y, width, height } | null | Where the element is drawn, canvas CSS px. See Where an element is. |
ui.play | (name, clip): void | Play a UI animation clip on the widget (adds or restarts its UIAnimator). |
ui.stop | (name): void | Freeze the widget's UI animation. |
ui.showScreen | (name): void | Show every screen with the name. |
ui.hideScreen | (name): void | Hide every screen with the name. |
ui.toggleScreen | (name): void | Flip them (from the first one's state). |
ui.switchScreen | (name): void | Show it and hide every other screen that is not pinned. An unknown name does nothing. |
ui.screenVisible | (name): boolean | Whether a screen of that name is showing at the current view size; false for one that does not exist. |

Text, visibility and screens
ts
export default class implements ScriptBehavior {
onStart(api: ScriptApi) {
api.state.lives ??= 3;
api.ui.onClick("Resume", () => api.ui.hideScreen("Pause Menu"));
api.ui.onClick("Pause", () => api.ui.switchScreen("Pause Menu")); // the pinned HUD stays up
}
onUpdate(api: ScriptApi) {
api.ui.setText("Lives", `Lives: ${api.state.lives}`);
if (api.state.lives <= 0 && !api.ui.screenVisible("Game Over")) api.ui.switchScreen("Game Over");
}
}api.ui.show on an element inside a hidden screen sets its flag but draws nothing, and logs [awaken] ui.show('Resume'): its screen 'Pause Menu' is hidden… once per name: show the screen instead.
Presses
onClickis for buttons: a button element is a real<button>, and its click fires the handler.onPressfires on pointer down, on any element: the start of a drag out of a tray slot.onPointerfires on a click, on any element, with where in it the click landed: a minimap, a colour picker.- There is one handler per name and kind: a second
onClick("Close", …)from another script replaces the first. Handlers are cleared when Play stops and when a scene loads, and they outlive a destroyed element until then. - Panels, text and images are click-through until something registers
onPressoronPointerfor them. A modal card's backdrop that must swallow presses registers a handler:api.ui.onPress("Backdrop", () => {}). - A press on an element inside another (Follow parent) is the inner element's only.
A press on the UI is also a game pointer press (api.input), in the player and the editor's Game view alike. A script that reads presses for the game asks hitTest first:
ts
onUpdate(api: ScriptApi) {
if (!api.input.wasPressed(0)) return;
const x = api.input.pointerX, y = api.input.pointerY;
if (api.ui.hitTest(x, y) !== null) return; // a press on the HUD, not on the board
// …the game's own press at (x, y)…
}hitTest counts only elements that take presses (buttons, and elements with an onPress or onPointer handler), names the innermost one, and answers with its current name, also after a script renamed it. Only the primary touch reaches scripts.
Swapping images
ts
api.ui.setImage("Weapon Icon", "icon_bow");The id is a UI image id or a texture id, as the element's image field takes in the Inspector. Every UI image ships in the exported game. A texture named only in a script ships only with Always include in exports ticked in its inspector, because an export ships the textures its scenes and prefabs use and cannot see a name inside a script.
Playing UI animations
ts
api.ui.play("Game Over Panel", "slideIn"); // a UIClip, imported from a Unity .anim
api.ui.stop("Game Over Panel");play gives the widget a UIAnimator (or reuses its own), points it at the clip and restarts it from 0. UI animations keep running while api.timeScale is 0, so a pause card can animate. See UI animation.
Where an element is
api.ui.rect(name) answers where an element is drawn: { x, y, width, height } in canvas CSS px from the game view's top-left corner, the space of api.input.pointerX / pointerY, api.input.viewWidth / viewHeight and api.screenToWorld.
- It is measured from the overlay as laid out: anchor, the layout variant chosen for this window, the screen's scale, the safe area and the element it follows are all included.
- A rotated or scaled element answers with the box around its turned shape.
- With several elements of the name (layout variants), it answers for the one showing.
nullwhen none is showing (hidden, in a hidden screen, or a variant for another window size) and when nothing draws UI (an edit-time script, a headless test).- An element not drawn yet (made this frame, or asked in
onStart) is laid out on the spot.
Use it to fit the game around authored UI. This script, on an orthographic camera looking down -Z at a board centred on the origin, keeps the board centred in the part of the view to the right of a side panel, whichever layout variant is showing:
ts
export default class FrameBesidePanel implements ScriptBehavior {
onUpdate(api: ScriptApi) {
const panel = api.ui.rect("Side Panel");
if (!panel) return; // not showing at this window size
const w = api.input.viewWidth, h = api.input.viewHeight;
// Where a view point lands on the board's plane (z = 0).
const onBoard = (px: number, py: number) => {
const ray = api.screenToWorld(px, py);
if (!ray || ray.dir.z === 0) return null;
const t = -ray.origin.z / ray.dir.z;
return { x: ray.origin.x + ray.dir.x * t, y: ray.origin.y + ray.dir.y * t };
};
const mid = onBoard(w / 2, h / 2);
const free = onBoard((panel.x + panel.width + w) / 2, h / 2); // the middle of the free area
if (!mid || !free) return;
const p = api.getPosition();
api.setPosition(mid.x - free.x, p.y, p.z); // the board's centre now lands in the free area
}
}For UI that is always up, a Camera with Fit: Contain and the HUD screen's edge insets does the same without a script.
Building UI at runtime
Lay a repeated piece of UI out once (a level tile: a button with a label and an icon inside it, each with Follow parent on), save it with Save as Prefab, and spawn one per item:
| Call | What it does |
|---|---|
api.spawn(prefab, { parent, name, position, rotation }) | Instantiates the prefab. parent puts the new root under that entity, so a UI prefab spawned into a screen is gated and scaled by it and, with Follow parent, laid out inside the parent element. name renames the new root. Returns the root, or null for an unknown prefab. A dead parent leaves it at the scene root. |
api.destroy(e?) | Destroys e (or this entity, with no argument) and everything under it. |
api.world.destroyEntity(e) | The same, from the world. |
ts
// On the screen that holds a "Grid" panel. "Level Tile" is a prefab: a button "Tile" with a text "Tile Label"
// inside it (Follow parent on for both).
export default class LevelGrid implements ScriptBehavior {
private tiles: Entity[] = [];
onStart(api: ScriptApi) {
const UI = api.world.registry.get("UINode");
const grid = api.find("Grid");
if (grid === null) return;
const levels = ["Harbour", "Reef", "Trench", "Abyss"];
levels.forEach((title, i) => {
const tile = api.spawn("Level Tile", { parent: grid, name: `Tile ${i}` });
if (tile === null) return;
const node = api.world.get(tile, UI);
node.x = 20 + (i % 2) * 140; // inside Grid's box, because the tile follows its parent
node.y = 20 + Math.floor(i / 2) * 140;
// The tile's parts keep the prefab's names: give each this tile's own, and point its pressWith at the new name.
const rename = (e: Entity) => {
for (const part of api.world.getChildren(e)) {
api.world.setName(part, `${api.world.getName(part)} ${i}`);
const n = api.world.get(part, UI);
if (n?.pressWith) n.pressWith = `${n.pressWith} ${i}`;
rename(part);
}
};
rename(tile);
api.ui.setText(`Tile Label ${i}`, title);
api.ui.onClick(`Tile ${i}`, () => api.loadScene(title));
this.tiles.push(tile);
});
}
clear(api: ScriptApi) {
for (const t of this.tiles) api.destroy(t); // each tile goes with its label
this.tiles = [];
}
}A field written directly (node.x = …) is drawn on the next frame. In a shipped game that has gone idle (no input and nothing moving), the next frame can be up to 100 ms away; api.requestDraw() asks for one now.
The World methods scripts can use
api.world is the live ECS world. These members are declared in the Code panel's IntelliSense:
| Member | Signature | Description |
|---|---|---|
createEntity | (): Entity | A new, empty entity. |
destroyEntity | (e): void | Destroy an entity and everything under it. |
isAlive | (e): boolean | Whether the handle still names a live entity. |
getName / setName | (e) / (e, name) | An entity's name; api.ui and api.find address by it. |
get | (e, type): any | The component of that type on the entity (write its fields directly), or undefined. |
add | (e, type, init?): any | Add a component with its defaults, overridden by init. |
has / remove | (e, type) | Whether it carries the component / take it off. |
query | (...types): Entity[] | Every entity carrying all of these component types. |
findNamed | (name, type): Entity | null | The first entity carrying type with this name (names are indexed). |
findAllNamed | (name, type): Entity[] | Every one: an element and its layout variants. |
getParent / getChildren | (e) | The hierarchy; 0 is the scene root. |
setParent | (child, parent): void | Reparent (0 = the root). An element under a screen is gated and scaled by it. |
isActive / setActive | (e) / (e, active) | The object's active flag. The UI overlay does not read it: hide UI with visible or its screen. |
isStatic | (e): boolean | The static flag. |
markMoved | (e): void | Tell the renderer a script moved this Transform. |
registry.get | (componentName) | The component type to pass to get / add / query, e.g. registry.get("UINode"). |
setHud - the quick overlay
For simple status text you do not need a UINode. api.setHud(text) sets one on-screen string:
ts
onUpdate(api: ScriptApi) {
api.setHud(`Score ${api.state.score ?? 0} · Time ${api.time.toFixed(1)}s`);
}It is one string with no styling or layout, cleared when Play stops. Use UINode elements for positioned panels, several labels or buttons.
Play matches the shipped game
The UI service scripts talk to (handlers, hitTest, rect) is the same in the editor's Game view and the standalone Player, and both run the same overlay. A menu wired with api.ui.onClick behaves the same when you press Play and when a player opens your exported game.
See also
- In-Game UI - elements, screens, layout variants, Follow parent and styling
- ScriptApi Reference -
api.ui,api.spawnandapi.worldin the full API - Input -
api.input, the pointer and the safe area - Prefabs - saving the UI you spawn
- Camera - Fit Contain and the game area