User Interface

User Interface

You can use scripting to create user interface elements like buttons or text fields in the world. When players interact with the UI elements, they generate events that you can subscribe to. The following examples will assume that you already know your way around Tabletop Playground scripting, see Scripting basics to get started.

Object UI

There are two ways to add UI elements: globally or attached to an object. In both cases, you create a widget (like a button or check box), and you create a UIElement to determine how and where your widget is shown in the game. For example, here’s how to add a large text above an object:

let ui = new UIElement();
ui.position = new Vector(0, 0, 5); 
ui.widget = new Text().setText("Look at me!").setFontSize(28);
refObject.addUI(ui);

Note how the set methods on the widget return the widget itself so you can use method chaining. When the object is moved, the text will be moved along with it.

Global UI

When you want to add UI at a fixed position instead of attached to an object, you can use the the methods from GameWorld. For example, a button at the center of the table that prints a message when pressed can be created like this:

let printButton = new Button().setText("Print");
printButton.onClicked.add((button, player) => {
    console.log(player.getName(), "pressed the button!"); 
});

let ui = new UIElement();
ui.position = new Vector(0, 0, 80.5); // For 80 cm table height 
ui.widget = printButton;
world.addUI(ui);

Hit point counter example

That’s already all you need to know for creating a custom user interface for your game! Let’s try a more complex example: a hit point counter. It will have three components: a progress bar to show the current hit points and two buttons to increase or decrease the hit point count. We will implement it as a reusable function. You could have this function in a separate file that you import using require, or even in an npm library! For this tutorial, we’ll just leave it in the object script file.

const { Vector, Rotator, ProgressBar, Button, UIElement, refObject, Canvas } = require('@tabletop-playground/api');

function addHitPointCounter(hitPointObj, maxHitPoints) {
    function setBarValue(obj) {
        obj.progressBar
            .setText((`${obj.hitPoints}/${obj.maxHitPoints}`))
            .setProgress(obj.hitPoints / obj.maxHitPoints);
        obj.plusButton.setEnabled(obj.hitPoints < maxHitPoints);
    }
    
    function minusPressed(button, player) {
        let owner = button.getOwningObject();
        if (owner.hitPoints <= 1) {
            // Final hit point lost, die!
            owner.destroy();
        }
        else {
            owner.hitPoints--;
            setBarValue(owner);

            // Store hitpoints in game states (number is automatically converted to string) 
            owner.setSavedData(owner.hitPoints);
        }
    }
    
    function plusPressed(button, player) {
        let owner = button.getOwningObject();
        owner.hitPoints = Math.min(owner.hitPoints + 1, owner.maxHitPoints);
        setBarValue(owner);
    }
    
    // Add hit point variables to the object
    hitPointObj.maxHitPoints = maxHitPoints;

    // If this object is loaded from a game state or copied from another object, it may 
    // already have a stored hit point value.
    hitPointObj.hitPoints = parseInt(refObject.getSavedData());
    if (isNaN(hitPointObj.hitPoints)) {
        hitPointObj.hitPoints = maxHitPoints;
    }
    
    // Create widgets
    let minusButton = new Button().setText("-").setFontSize(11);
    minusButton.onClicked.add(minusPressed);
    
    hitPointObj.plusButton = new Button().setText("+").setFontSize(11);
    hitPointObj.plusButton.onClicked.add(plusPressed);

    hitPointObj.progressBar = new ProgressBar();
    setBarValue(hitPointObj);

    // Place widgets next to each other on a canvas
    let canvas = new Canvas();
    canvas.addChild(minusButton, 0, 2, 25, 26);
    canvas.addChild(hitPointObj.progressBar, 25, 0, 60, 30);
    canvas.addChild(hitPointObj.plusButton, 85, 2, 25, 26);
    
    // Add the UI element at an angle
    let ui = new UIElement();
    ui.useWidgetSize = false;
    ui.width = 110;
    ui.height = 30;
    ui.position = new Vector(-3, 0, 0);
    ui.rotation = new Rotator(25, 0, 0);
    ui.scale = 0.5;
    ui.widget = canvas;
    hitPointObj.addUI(ui);
}

// Add a counter for 10 hit points to our reference object
addHitPointCounter(refObject, 10);

Note that we used more properties of UIElement in this example: By default, the UI element will just as large as necessary, and one UI pixel corresponds to one millimeter in the 3D world. When you set useWidgetSize to false, the pixel size is determined by the width and height properties. With scale, you can determine how big each pixel should be in the 3D world. With the default scale of 1, each pixel corresponds to one millimeter. When you use a Canvas to layout your widgets like in this example, you need to give an explicit width and height in the UIElement because a Canvas does not have a preferred size on its own.

Available widgets

You can find all the available widgets in the API documentation in the list of subclasses for Widget. Border is a special widget that is used as a container for other widgets when you want a background color. For example, a Slider usually has a transparent background, and you can wrap it in a border to make it easier to see:

let slider = new Slider().setMaxValue(10);
let ui = new UIElement();
ui.widget = new Border().setChild(slider);
refObject.addUI(ui);
Slider without and with border

The Canvas, HorizontalBox and VerticalBox widgets are containers, too. You can add child widgets to create composite interfaces, all within one UIElement! If you want to learn more about creating more complex UIs, check out the advanced UI tutorial.

Table of Contents