Skip to main content

Getting Started

An in-canvas user interface is a tree of entities. At its root is an entity with a screen component, which defines the space the interface lives in. Below it, entities with element components are the rectangles you see: images, text and invisible groups that hold other elements. Components such as buttons and scroll views add behavior on top. This page sets up UI on each surface and builds a small interface: a panel with a label and a button that counts its clicks.

A dark panel in the middle of the screen with the text "Hello, PlayCanvas!" above an orange "Click me" button

Setting Up​

Screens and elements are drawn by any application that includes their component systems. To respond to input, an application also needs an ElementInput, the object that turns mouse, touch and XR events into events on elements.

Register the UI component systems, the font and texture handlers, and an ElementInput when you create the application:

const canvas = document.getElementById('application');
const device = await pc.createGraphicsDevice(canvas, {
deviceTypes: [pc.DEVICETYPE_WEBGPU]
});

const options = new pc.AppOptions();
options.graphicsDevice = device;

// Create the ElementInput before the mouse and touch devices, so that calling
// stopPropagation() in a UI event handler also hides the event from them
options.elementInput = new pc.ElementInput(canvas);
options.mouse = new pc.Mouse(canvas);
options.touch = new pc.TouchDevice(canvas);

options.componentSystems = [
pc.CameraComponentSystem,
pc.ScreenComponentSystem,
pc.ElementComponentSystem,
pc.ButtonComponentSystem
];
options.resourceHandlers = [
pc.FontHandler,
pc.TextureHandler
];

const app = new pc.AppBase(canvas);
app.init(options);
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
app.setCanvasResolution(pc.RESOLUTION_AUTO);
app.start();

window.addEventListener('resize', () => app.resizeCanvas());

Add the systems of the other UI components as you use them: pc.ScrollViewComponentSystem, pc.ScrollbarComponentSystem, pc.LayoutGroupComponentSystem and pc.LayoutChildComponentSystem, plus pc.SpriteHandler and pc.TextureAtlasHandler for sprites. pc.Application registers every system and handler for you, but it does not create an ElementInput either, so pass one in its options. See Using the Engine Standalone for the rest of the setup.

Your First Interface​

The interface in the image above is a screen with a panel on it. The panel holds a label and a button, and the button holds its own text. Text needs a font asset; the examples load one called arial.json, which you can generate from any font file.

With the application set up as above:

// Load a font for the text
const font = new pc.Asset('arial', 'font', { url: 'fonts/arial.json' });
app.assets.add(font);
await new Promise((resolve) => {
font.ready(resolve);
app.assets.load(font);
});

const camera = new pc.Entity('camera');
camera.addComponent('camera', { clearColor: new pc.Color(0.1, 0.11, 0.13) });
app.root.addChild(camera);

// A screen-space screen that scales with the canvas
const screen = new pc.Entity('screen');
screen.addComponent('screen', {
screenSpace: true,
referenceResolution: [1280, 720],
scaleMode: pc.SCALEMODE_BLEND,
scaleBlend: 0.5
});
app.root.addChild(screen);

// A panel in the middle of the screen
const panel = new pc.Entity('panel');
panel.addComponent('element', {
type: pc.ELEMENTTYPE_IMAGE,
anchor: [0.5, 0.5, 0.5, 0.5],
pivot: [0.5, 0.5],
width: 420,
height: 240,
color: new pc.Color(0.16, 0.18, 0.23),
opacity: 0.9
});
screen.addChild(panel);

// A label near the top of the panel
const label = new pc.Entity('label');
label.addComponent('element', {
type: pc.ELEMENTTYPE_TEXT,
fontAsset: font.id,
text: 'Hello, PlayCanvas!',
fontSize: 36,
anchor: [0.5, 1, 0.5, 1],
pivot: [0.5, 1]
});
label.setLocalPosition(0, -40, 0);
panel.addChild(label);

// A button near the bottom: an image element that receives input, and a
// button component that tints the image when it is hovered and pressed
const button = new pc.Entity('button');
button.addComponent('element', {
type: pc.ELEMENTTYPE_IMAGE,
anchor: [0.5, 0, 0.5, 0],
pivot: [0.5, 0],
width: 200,
height: 60,
color: new pc.Color(1, 0.55, 0.2),
useInput: true
});
button.addComponent('button', {
imageEntity: button,
hoverTint: new pc.Color(1, 0.7, 0.45),
pressedTint: new pc.Color(0.8, 0.4, 0.1)
});
button.setLocalPosition(0, 40, 0);
panel.addChild(button);

const buttonText = new pc.Entity('text');
buttonText.addComponent('element', {
type: pc.ELEMENTTYPE_TEXT,
fontAsset: font.id,
text: 'Click me',
fontSize: 28,
color: new pc.Color(0.1, 0.1, 0.1),
anchor: [0.5, 0.5, 0.5, 0.5],
pivot: [0.5, 0.5]
});
button.addChild(buttonText);

// Count the clicks
let clicks = 0;
button.button.on('click', () => {
clicks++;
label.element.text = `Clicked ${clicks} times`;
});
Basic Button

How It Fits Together​

The entity tree of the interface above is:

screen screen component: the root, which defines the space of the interface
└── panel image element
├── label text element
└── button image element that receives input, and a button component
└── text text element

A few rules follow from it, and the rest of this section builds on them:

  • Elements are laid out in screen units. On a screen-space screen that scales, like this one, a unit is a pixel at the 1280 × 720 reference resolution, and everything grows and shrinks with the canvas. See Screens.
  • Children are positioned relative to their parent. Each element's anchor picks a point or an edge of its parent, and its pivot picks the point of the element that sits there. The label is anchored to the top of the panel, so moving or resizing the panel carries it along. The y axis points up. See Elements.
  • The hierarchy is the draw order. A parent is drawn before its children, and earlier siblings before later ones, so the button's text is drawn over the button. See Draw Order and Performance.
  • Only elements with input enabled receive input. The button's image element has useInput on, and its events bubble up to its ancestors. See Input.

Naming Across Surfaces​

Every surface drives the same components, so a property has one name in four spellings:

SurfaceStyleExample
EnginecamelCase properties. Vectors and colors are pc.Vec2, pc.Vec4 and pc.Color objects, or arrays when you pass them to addComponentfontSize: 36, anchor: [0.5, 1, 0.5, 1]
EditorTitle Case inspector fieldsFont Size, Anchor
ReactcamelCase props. Vectors are arrays and colors are CSS color stringsfontSize={36}, anchor={[0.5, 1, 0.5, 1]}, color="#ff8c33"
Web Componentskebab-case attributes. Vectors are space-separated numbersfont-size="36", anchor="0.5 1 0.5 1"

A few properties have no React prop or Web Components attribute. For example, React reserves the key prop, and <pc-element> has no attribute for layers or rect. Set these on the engine component instead: in React, through the entity that useParent() or an <Entity ref> gives you, and in Web Components, through the element's component property once whenReady resolves.

Defaults Differ Between Surfaces​

The component defaults are the engine's, but React changes some of them, so a screen or element created without options does not look the same everywhere:

EngineReactWeb Components
A screen with no optionsWorld space, Scale Mode None, 640 × 320Screen space, Scale Mode Blend, reference resolution 1280 × 720World space, Scale Mode None, 640 × 320
An element's anchor and pivotBottom-left: 0, 0, 0, 0 and 0, 0Bottom-leftBottom-left
An element's size32 × 3232 × 3232 × 32
Input devicesThe ones you pass to AppOptionsMouse, touch and element inputMouse, keyboard and element input

Set the properties you rely on explicitly, as the examples on this page do. The Editor's inspector shows every value of a new entity, so check it there.

See Also​