Behavior and lambda scripts receive two UIKit globals:
UIKit
UIKitPointerEventsUse UIKit for in-scene panels, diegetic UI, HUD overlays, labels, buttons, and interactive 3D controls.
let panel;
let scoreText;
this.init = function (game) {
UIKitPointerEvents.initialize(game);
};
this.onStart = function () {
panel = new UIKit.Container({
width: 220,
height: 80,
backgroundColor: 0x222222,
backgroundOpacity: 0.85,
borderRadius: 8,
padding: 12,
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
pointerEvents: "auto",
});
scoreText = new UIKit.Text({
text: "Score: 0",
fontSize: 22,
color: 0xffffff,
});
panel.add(scoreText);
this.target.add(panel);
UIKitPointerEvents.registerRoot(panel);
};
this.update = function (deltaTime) {
UIKitPointerEvents.update(deltaTime);
};
this.dispose = function () {
if (panel) {
UIKitPointerEvents.unregisterRoot(panel);
panel.dispose();
panel = null;
}
UIKitPointerEvents.deinitialize();
};UIKit.Fullscreen should be parented to a camera. Use game.uiCamera when available, with game.camera as the fallback.
let hud;
this.init = function (game) {
this.game = game;
UIKitPointerEvents.initialize(game);
};
this.onStart = function () {
hud = new UIKit.Fullscreen(this.game.renderer, {
flexDirection: "column",
pointerEvents: "auto",
});
hud.add(new UIKit.Text({
text: "Wave 1",
fontSize: 28,
color: 0xffffff,
}));
const camera = this.game.uiCamera ?? this.game.camera;
camera.add(hud);
UIKitPointerEvents.registerRoot(hud);
};
this.update = function (deltaTime) {
UIKitPointerEvents.update(deltaTime);
};
this.dispose = function () {
if (hud) {
UIKitPointerEvents.unregisterRoot(hud);
hud.parent?.remove(hud);
hud.dispose();
hud = null;
}
UIKitPointerEvents.deinitialize();
};UIKit.Container is the layout building block. It supports fixed sizing, flexbox-style layout, backgrounds, borders, overflow, and pointer handlers.
const button = new UIKit.Container({
width: 120,
height: 40,
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
backgroundColor: 0x3344aa,
borderRadius: 6,
pointerEvents: "auto",
hover: {backgroundColor: 0x4455cc},
active: {backgroundColor: 0x223388},
onClick: () => console.log("clicked"),
});Common properties:
width
height
backgroundColor
backgroundOpacity
borderRadius
borderWidth
borderColor
padding
paddingTop
paddingBottom
paddingLeft
paddingRight
margin
flexDirection
justifyContent
alignItems
gap
overflow
pointerEvents
hover
active
onClick
onPointerEnter
onPointerLeaveconst label = new UIKit.Text({
text: "Ready",
fontSize: 24,
fontWeight: "bold",
color: 0xffffff,
opacity: 1,
textAlign: "center",
verticalAlign: "center",
lineHeight: 1.2,
maxLines: 2,
});const icon = new UIKit.Image({
src: "https://example.com/icon.png",
width: 48,
height: 48,
objectFit: "contain",
borderRadius: 6,
});const nameInput = new UIKit.Input({
value: "",
placeholder: "Name",
fontSize: 18,
color: 0xffffff,
backgroundColor: 0x222222,
borderRadius: 4,
padding: 8,
onValueChange: (value) => {
this.erth.store.set("player.name", value);
},
});| Component | Use |
|---|---|
UIKit.Fullscreen |
Camera-attached viewport UI |
UIKit.Content |
Scrollable content inside a container |
UIKit.Svg |
SVG graphics |
UIKit.Video |
Video surfaces |
Call setProperties() to update one or more properties.
scoreText.setProperties({text: `Score: ${score}`});
button.setProperties({
backgroundColor: disabled ? 0x555555 : 0x3344aa,
pointerEvents: disabled ? "none" : "auto",
});UIKitPointerEvents is reference counted so multiple behaviors can use it at the same time.
UIKitPointerEvents.initialize(game);
UIKitPointerEvents.registerRoot(root);
UIKitPointerEvents.update(deltaTime);
UIKitPointerEvents.unregisterRoot(root);
UIKitPointerEvents.deinitialize();Available methods:
| Method | Use |
|---|---|
initialize(game) |
Store GameManager and increment the init reference count |
deinitialize() |
Decrement the init reference count |
registerRoot(component) |
Enable pointer events for a root component |
unregisterRoot(component) |
Remove a root component |
update(deltaTime?) |
Update pointer state and registered roots |
forceDispose() |
Force cleanup, bypassing reference counts |
isActive() |
True when pointer events are running with roots |
isInitialized() |
True when a game reference is present |
getRootCount() |
Number of registered roots |
getInitRefCount() |
Current initialization reference count |
Always pair initialize() with deinitialize(), and registerRoot() with unregisterRoot().
function createHealthBar(width, height, health, maxHealth) {
const root = new UIKit.Container({
width,
height,
backgroundColor: 0x333333,
borderRadius: 4,
overflow: "hidden",
});
const fill = new UIKit.Container({
width: (health / maxHealth) * width,
height: "100%",
backgroundColor: health > 30 ? 0x44aa44 : 0xaa4444,
});
root.add(fill);
return {root, fill};
}const button = new UIKit.Container({
width: 140,
height: 44,
justifyContent: "center",
alignItems: "center",
backgroundColor: 0x224488,
borderRadius: 6,
pointerEvents: "auto",
onClick: () => this.game.behaviorManager.sendEventToObjectBehaviors(this.target, "menu.start"),
});
button.add(new UIKit.Text({
text: "Start",
fontSize: 18,
color: 0xffffff,
}));These examples are adapted from UIKit-heavy games such as puzzle boards, title screens, lobbies, and combat HUDs.
The built-in uikit-dual-mode script is useful when a HUD should render both in the editor preview and in play mode. Import it from a behavior script, build your UI once, then let the helper attach and update the root.
@import "uikit-dual-mode" as uikit;
this.init = function (game) {
this.game = game;
this._uikitCtx = uikit.createPlayContext(game);
this._uiRoot = uikit.buildRoot(this._uikitCtx, {
pointerEvents: "auto",
});
this.buildHud();
uikit.attach(this, this._uikitCtx);
};
this.buildHud = function () {
this.scoreText = new UIKit.Text({
text: "Score 0",
fontSize: 28,
color: 0xffffff,
});
this._uiRoot.add(this.scoreText);
};
this.update = function (deltaTime) {
uikit.tick(this, deltaTime);
const score = this.erth.store.get("score") ?? 0;
this.scoreText.setProperties({text: `Score ${score}`});
};
this.dispose = function () {
uikit.teardown(this);
};For editor preview support, add onEditorAdded, onEditorAttributesUpdated, and onEditorDispose using the helper's createEditorContext() and teardown() methods.
Chess and lobby screens use UIKit buttons to send targeted behavior events back into game logic. This keeps UI code from directly mutating board or match state.
this.createHudButton = function (label, eventName, payload) {
const button = new UIKit.Container({
width: 160,
height: 44,
justifyContent: "center",
alignItems: "center",
backgroundColor: 0x26334d,
borderRadius: 6,
pointerEvents: "auto",
hover: {backgroundColor: 0x314263},
active: {backgroundColor: 0x1b2538},
onClick: () => {
this.game.behaviorManager.sendEventToObjectBehaviors(
this.target,
eventName,
payload ?? {},
);
},
});
button.add(new UIKit.Text({
text: label,
fontSize: 18,
color: 0xffffff,
}));
return button;
};
this._uiRoot.add(this.createHudButton("Reset", "resetGame"));
this._uiRoot.add(this.createHudButton("Promote", "promotePiece", {piece: "queen"}));The receiver implements onEvent(msg, data) in the gameplay behavior.
Puzzle games often support both UI clicks and keyboard/gamepad input. Keep input reads in one method, then let the game update decide what state changes are legal.
this.readControls = function () {
const input = this.game.inputManager;
const lateral = input.getMotion("lateral");
return {
left: input.getAction("drop7_Left") || lateral < -0.5,
right: input.getAction("drop7_Right") || lateral > 0.5,
drop: input.getAction("drop7_Drop") ||
input.getAction("jump") ||
input.getAction("use"),
restart: input.getAction("drop7_Restart"),
};
};
this.update = function (deltaTime) {
UIKitPointerEvents.update(deltaTime);
const controls = this.readControls();
if (controls.left) this.moveCursor(-1);
if (controls.right) this.moveCursor(1);
if (controls.drop) this.dropPiece();
if (controls.restart) this.resetBoard();
};For pointer interactions, put onClick handlers on the relevant UIKit.Container cells or buttons. For continuous input, poll game.inputManager in update().