Skip to content

UI

The @shell/ui package shows a HUD, a menu, or any other UI over a 2D game as entities and components: a Canvas is the root of a tree of Elements, laid out with flexbox and scaled with the screen. Engine2D installs it, and @shell/engine-2d exports its names.

import { Box, Canvas, Element, Sprite, Text, Tint } from '@shell/engine-2d';
const canvas = commands.spawn().root;
canvas.addComponent(Canvas);
canvas.addComponent(Box, { direction: 'column', padding: 24, gap: 12, align: 'start' });
const panel = commands.spawn().root;
panel.addComponent(Element, { width: '360' });
panel.addComponent(Box, { direction: 'column', padding: 16, gap: 8 });
panel.addComponent(Sprite);
panel.addComponent(Tint, { color: 0x334455 });
panel.setParent(canvas);
const score = commands.spawn().root;
score.addComponent(Element);
score.addComponent(Text, { text: 'Score: 0', fontSize: 28 });
score.setParent(panel);

The layout places every Element, so an Element has no Position, Size, Rotation, or Scale. What it shows is @shell/pixi’s own: Sprite with Tint for a coloured rectangle, with Texture for a picture, with NineSlice to keep a picture’s corners, and Text for words. Words wrap at the width of their Element.

The root of one tree of Elements. It fills the screen, whatever its own Element says. Requires Element.

FieldTypeDefaultMeans
ordernumber0A Canvas of a higher order is drawn in front

Anything in a Canvas that the layout places. A length is a string, written as CSS writes it: "120" (units of the reference resolution), "50%" (of the parent), "auto", or "" for not set. A string that is none of these is told once in the console and counts as not set.

FieldTypeDefaultMeans
width, heightlength'auto'auto is as large as what it holds
minWidth, minHeight, maxWidth, maxHeightlength''Bounds
positionstring'flow'flow in its parent’s row or column, absolute by its insets
left, top, right, bottomlength''Insets from the parent’s edges, when absolute
grownumber0Its share of the room left over
shrinknumber1How readily it gives way
marginnumber0All four sides
alignSelfstring'auto'auto, start, center, end, stretch
hiddenbooleanfalseNot laid out and not drawn, with its children
blocksPointerbooleanfalseA press on it does not reach the game’s world
rotationnumber0Radians, about its centre; children follow
scaleX, scaleYnumber1About its centre; children follow

An Element that arranges its children in a row or a column. Requires Element. An Element without Box arranges its children as a Box with every field at its default.

FieldTypeDefaultValues
directionstring'row'row, column
justifystring'start'start, center, end, space-between, space-around, space-evenly
alignstring'stretch'start, center, end, stretch
wrapbooleanfalse
gapnumber0
paddingnumber0All four sides

A Control is one entity with one component, and is what the player operates. It draws itself as rounded shapes in its colours, and is as large as its kind is until its Element says a size. Each requires Element. An entity is one Control.

ComponentFieldsNatural size
Buttonaction, label, labelSize, labelColor, color, disabled160 × 48
Togglechecked, label, labelSize, labelColor, color, checkedColor, disabled160 × 28
Slidervalue, min, max, step, color, fillColor, handleColor, disabled200 × 24
TextFieldvalue, placeholder, maxLength, textSize, textColor, color, disabled240 × 40
ProgressBarvalue (0 to 1), color, fillColor. Shown, and not operated by the player200 × 16

A press on a Button triggers the event Pressed, with the Button’s action and its entity, at the start of the next tick:

import { Define, Pressed } from '@shell/engine-2d';
export const StartGame = Define.system()
.observes(Pressed)
.update(({ events }) => {
if (events.get(Pressed)!.data.action === 'start') {
// …
}
});

What the player set on a Toggle, a Slider, or a TextField is written to its checked or value at the start of the next tick; read it with a Changed query. A value a system writes is shown, and is not told back to it.

A press that starts on a Control, or on an Element whose blocksPointer is on, is not a click in the game’s world: @shell/ui claims it from the pointer resource of @shell/dom, so a system that reads pointer.buttons needs no check. In an edit world a Control does not react.

Where the layout put an Element: x, y, width, height in its Canvas, before rotation and scale. Written by the layout, never saved, and added with Element.

Every Canvas of a Project scales alike. The UI is laid out for the reference resolution, and on a screen of another size it is drawn larger or smaller by one scale:

scale = 2 ^ lerp(log2(screenWidth / referenceWidth), log2(screenHeight / referenceHeight), match)

The values are a settings asset of the type UiScaling (ui:scaling):

FieldTypeDefaultMeans
referenceWidthnumber1280The width the UI is laid out for
referenceHeightnumber720The height the UI is laid out for
matchnumber0.50 follows the screen’s width, 1 its height

Registered in the prefabs resource, for the editor’s instantiate_prefab code:<key>:

KeyPrefabComponents
ui:canvasCanvasPrefabCanvas, Element, Box
ui:boxBoxPrefabElement, Box
ui:imageImagePrefabElement, Sprite, Texture
ui:textTextPrefabElement, Text
ui:buttonButtonPrefabElement, Button
ui:progressBarProgressBarPrefabElement, ProgressBar
ui:sliderSliderPrefabElement, Slider
ui:toggleTogglePrefabElement, Toggle
ui:textFieldTextFieldPrefabElement, TextField

resources.get('ui') is the Ui resource: scaleOf(width, height) gives the scale of a screen, length(written) reads a length, and elementAt(clientX, clientY) gives the front-most Control or blocking Element at a point of the page. UiControls and then UiLayout run in Schedule.Render, after @shell/pixi has made the nodes and before it draws them, also in an edit world. UiSignals runs in Schedule.First and hands over what the players of Controls did.

In an edit world a Canvas is of the reference resolution and its nodes are in the world, centred on the origin or on the Canvas’s Position, so the editor’s scene view shows and picks them. In a game the Position of a Canvas is ignored.

  • Nothing clips, so there is no scroll view, list, or dropdown.
  • A Canvas is on the screen and not in the world.
  • A Control is drawn in its colours and has no picture of its own yet.
  • No keyboard or gamepad focus moves between Controls.