Skip to content
On this page

Layout User Interface - LUI

LUI helps to create Buttons, Menus and Healthbars on the screen. Other than the normal Graphics Engine it's tightly bound to a layout system that helps to deal with different screen sizes.

Internally it makes use of the same render instance which means it can use the same Texture objects and SpriteSheet as the normal game. This helps to reduce draw calls even if a game mixes world-graphics objects with UI graphics objects.

LUI is implemented as a classic object orientated set of classes. This is good for developing the UI itself but feels a bit aged when designing UIs. To make things faster there is a swiss-army-object called: $ui(...). It's a utility that helps to creating LUI objects. The code examples in here will show both approaches.

HTML UIs

If you're already an expert in creating UIs with things like Vue or React and learning a new UI isn't your thing: it's a viable option to layer an HTML UI ontop of the game canvas.

It might break your framerate at times, and you have to write some glue code, but it's doable.

Create a LUIManager

For starters it's best to use the @GraphicsEngine:typesprite:

ini
# myworld.edf

[!Core]
@GraphicsEngine:typesprite

[!MyUI->Core]
@MyUI

It not just brings the normal game graphics engine but also provides a working LUI environment.

Created as a static component you can access it via @link in your Component(s):

ts
import {GraphicsEngine, Component} from 'typesprite'

export class MyUI extends Component {
    
    @link('GraphicsEngine:typesprite')
    private gfx:GraphicsEngine
    
    onInit() {
        this.gfx.gui;       // < LUILayer, predefined, infront of the game world
        this.gfx.guiBehind; // < LUILayer, predefined, behind the game world
        this.gfx.lui;       // < LUIManager, access to the root object 
    }
}
Custom LUIManager

Technically it's easy to create your own LUIManager and do things in a more custom way. It basically means that you have to write your own version of GraphicsEngine. Again, that's also not a big thing.

However, explaining all the details here in an approachable fashion would be an overkill. To dive deep into the inner workings of LUIManager, FatRenderer, canvas management etc. check out the implementation of GraphicsEngine.ts GitHub.

LUIElement - the new <DIV>

To create a UI with LUI you put together a tree of LUIElement instances. Like in so many other examples you have a root element, attach children to it and each child can have their own children.

bash
- LUIElement: "gui"
  - LUIElement: "el1"
  - LUIElement: "el2"
    - LUIElement: "el3"
  - LUIElement: "el4"
ts
import {$ui} from 'typesprite'
// In a component like MyUI.ts

$ui(this.gfx.gui)
    .newChild
        .setName("el1")
    .endChild
    .newChild
        .setName("el2")
        .newChild
            .setName("el3")
        .endChild
    .endChild
    .newChild
        .setName("el4")
    .endChild
;
ts
import {LUIElement} from 'typesprite'
// In a component like MyUI.ts

const el1 = new LUIElement().setName("el1");
const el2 = new LUIElement().setName("el2");
const el3 = new LUIElement().setName("el3");
const el4 = new LUIElement().setName("el4");

this.gfx.gui.addChild(el1)
this.gfx.gui.addChild(el2)
el2.addChild(el3)
this.gfx.gui.addChild(el4)

Layout Basics

A LUI instance is attached to a canvas and each LUILayer always has the size of the entire canvas (full height, full width).

When adding children to a parent element the parent is responsible for the layout of all it's direct children. Everytime the UI gets layouted each parent sets the size of the children.

To control this each LUIElement is equipped with a setContainerLayout() function. By providing a container layout object we change the way how the children of the objects get placed. To be able to adjust the layout behavior of the children we can also set layout properties which can be used by the container layout.

For example if we like to el1 to consume full width and 30px height of the canvas we would do this:

ts
// In a component like MyUI.ts

const layer1 = $ui(this.gfx.gui)
    .newChild.setName('layer1')
        .childrenLayoutSpace
        .newChild.setName('el1')
            .layoutProp(LUISpaceLayout.PropDir, LUISpaceLayout.DirTop)
            .layoutProp(LUISpaceLayout.PropSize, 30)
        .endChild
    .endChild
;
ts
// In a component like MyUI.ts

// define the layout behavior
const layer1 = new LUIElement().setName("layer1");
this.gfx.gui.addChild(layer1);
layer1.setContainerLayout(new LUISpaceLayout())

// let el1 consume 30 pixel of top space
const el1 = new LUIElement().setName('el1');
el1.setLayoutProperty(LUISpaceLayout.PropDir, LUISpaceLayout.DirTop);
el1.setLayoutProperty(LUISpaceLayout.PropSize, 30);

Padding

Each LUIElement has a padding property. This reduces the size of the object itself.

ts
el1.getPadding().setTop(10); // 10 pixel top padding

The input and rendering objects allow reacting on padding space or not.

Styles & Rendering

LUIElements do not draw a thing. Similar to DIVs they just occupy a rectangle area of screenspace that follows layout rules. To make them draw something one has to create and set LUIStyle objects.

A LUIStyle object is composed of LUIStyleElements. Those elements actually perform the drawing. One LUIStyles instance can be attached to multiple LUIElements. So bascially the idea is to design various style objects and attach them to the LUIElements.

ts
const style = $ui.style.fill("#f00").done;

// apply to elements
el1.setStyle(style); // el1 will be red now
el2.setStyle(style); // el2 is also red
ts
const style = new LUIStyle();
const fill = new LUIFill();
fill.color.parseFromString("#f00");
style.addChild(fill);
// apply to elements
el1.setStyle(style); // el1 will be red now
el2.setStyle(style); // el2 is also red

In cases were we do not need or want to share the style object we can shorten the code above like this:

ts
$ui(el1).styleFill("#f00");
ts
const fill = new LUIFill();
fill.color.parseFromString("#f00");
el1.addStyleElement(fill); // modifies or creates a style object within el1

The base class LUILayoutElement brings an option to only consume the padding space. Setting fill.onPaddingSpace = true would mean that the fill will also be rendered on padding space.

Layouts

All Layout classes are subclasses of LUIContainerLayouter. You can do your own or use one of TypeSprites standard implementations:

Class$ui nameCharacteristics
LUIFreeStackLayout.childrenLayoutFreeStack (default if no other ContainerLayout exists)Children are stacked in the order attached to parent. Per default a child consumes full space of parent. Can be freely adjusted with properties like top, left etc.
LUISpaceLayout.childrenLayoutSpaceEach child consumes a space of the parent area.
LUILineLayout .createXAligned.childrenLayoutXAlignedChildren are placed on X-Axis from left to right
LUILineLayout .createYAligned.childrenLayoutYAlignedChildren are placed on Y-Axis from top to bottom

There is also LUIStackLayout which is a limited version of LUIFreeStackLayout.

LUIFreeStackLayout:

LUI

ts
$ui(this.gfx.gui)
    .newChild
        .childrenLayoutFreeStack // (default)
        .styleFill("#515151")
        .newChild
            .styleFill("#ffcb4f") // Yellow (top, left)
            .layoutFreeStack({top: 10, left: 10, width: 50, height: 50})
        .endChild
        .newChild
            .styleFill("#d595cf") // Pink (top, right)
            .layoutFreeStack({top: 10, right: 10, width: 50, height: 50})
        .endChild
        .newChild
            .styleFill("#8acf94") // Green (middle)
            .layoutFreeStack({left: 10, right: 10, height: 50})
        .endChild
        .newChild
            .styleFill("#ff6e76") // Red (bottom)
            .layoutFreeStack({bottom: 10, left: 10, right: 10, height: 0.25})
        .endChild
        .newChild
            .styleFill("#f2f0ec", 0.8 /* alpha */) // Light Gray (overlaps)
            .layoutFreeStack({bottom: 20, right: 20, width: 0.5, height: 0.5})
        .endChild
    .endChild
;
ts
const panel = new LUIElement();
this.gfx.gui.addChild(panel);
panel.setContainerLayout(new LUIFreeStackLayout());
{
    const style = new LUIStyleFill();
    style.color.setFromHash("#515151")
    panel.addStyleElement(style);
}
{
    const child = new LUIElement();
    const style = new LUIStyleFill();
    style.color.setFromHash("#ffcb4f")
    child.addStyleElement(style);
    LUIFreeStackLayout.layout(child, {top: 10, left: 10, width: 50, height: 50})
    panel.addChild(child);
}
{
    const child = new LUIElement();
    const style = new LUIStyleFill();
    style.color.setFromHash("#d595cf")
    child.addStyleElement(style);
    LUIFreeStackLayout.layout(child, {top: 10, right: 10, width: 50, height: 50})
    panel.addChild(child);
}
{
    const child = new LUIElement();
    const style = new LUIStyleFill();
    style.color.setFromHash("#8acf94")
    child.addStyleElement(style);
    LUIFreeStackLayout.layout(child, {left: 10, right: 10, height: 50})
    panel.addChild(child);
}
{
    const child = new LUIElement();
    const style = new LUIStyleFill();
    style.color.setFromHash("#ff6e76") //                                       25%
    child.addStyleElement(style); //                                             v 
    LUIFreeStackLayout.layout(child, {bottom: 10, left: 10, right: 10, height: 0.25})
    panel.addChild(child);
}
{
    const child = new LUIElement();
    const style = new LUIStyleFill();
    style.color.setFromHash("#f2f0ec").setAlpha(0.8);
    child.addStyleElement(style);
    LUIFreeStackLayout.layout(child, {bottom: 20, right: 20, width: 0.5, height: 0.5})
    panel.addChild(child);
}

LUISpaceLayout

LUI

ts
$ui(this.gfx.gui)
    .newChild
        .childrenLayoutSpace
        .styleFill("#515151")
        .newChild
            .styleFill("#ffcb4f") // Yellow left
            .layoutProp(LUISpaceLayout.PropDir, LUISpaceLayout.DirLeft)
            .layoutProp(LUISpaceLayout.PropSize, 50)
        .endChild
        .newChild
            .styleFill("#d595cf") // pink upper half
            .layoutProp(LUISpaceLayout.PropDir, LUISpaceLayout.DirTop)
            .layoutProp(LUISpaceLayout.PropSize, 0.5) // 50%
        .endChild
        .newChild
            .styleFill("#8acf94") // green bottom
            .layoutProp(LUISpaceLayout.PropDir, LUISpaceLayout.DirBottom)
            .layoutProp(LUISpaceLayout.PropSize, 20)
        .endChild
        .newChild
            .styleFill("#ff6e76") // center
            .layoutProp(LUISpaceLayout.PropRest, LUISpaceLayout.AxisX)
        .endChild
        
    .endChild
;
ts
const baseStyle = new LUIStyleFill();
baseStyle.color.setFromHash("#515151")
const base = new LUIElement().addStyleElement(baseStyle);
base.setContainerLayout(new LUISpaceLayout())
this.gfx.gui.addChild(base);

{
    const elemStyle = new LUIStyleFill(); // yellow left
    elemStyle.color.setFromHash("#ffcb4f")
    const elem = new LUIElement();
    elem.addStyleElement(elemStyle)
    elem.setLayoutProperty(LUISpaceLayout.PropDir, LUISpaceLayout.DirLeft)
    elem.setLayoutProperty(LUISpaceLayout.PropSize, 50)
    base.addChild(elem);
}
{
    const elemStyle = new LUIStyleFill(); // pink upper half
    elemStyle.color.setFromHash("#d595cf")
    const elem = new LUIElement();
    elem.addStyleElement(elemStyle)
    elem.setLayoutProperty(LUISpaceLayout.PropDir, LUISpaceLayout.DirTop)
    elem.setLayoutProperty(LUISpaceLayout.PropSize, 0.5) // 50%
    base.addChild(elem);
}
{
    const elemStyle = new LUIStyleFill(); // green bottom
    elemStyle.color.setFromHash("#8acf94")
    const elem = new LUIElement();
    elem.addStyleElement(elemStyle)
    elem.setLayoutProperty(LUISpaceLayout.PropDir, LUISpaceLayout.DirBottom)
    elem.setLayoutProperty(LUISpaceLayout.PropSize, 20)
    base.addChild(elem);
}
{
    const elemStyle = new LUIStyleFill(); // center
    elemStyle.color.setFromHash("#ff6e76")
    const elem = new LUIElement();
    elem.addStyleElement(elemStyle)
    elem.setLayoutProperty(LUISpaceLayout.PropRest, LUISpaceLayout.AxisX)
    base.addChild(elem);
}

LUILineLayout (childrenLayoutXAligned)

LUI

ts
$ui(this.gfx.gui)
    .newChild  
        .styleFill("#313131")
    .endChild
    .newChild
        .styleFill("#515151")
        .layoutFreeStack({height: 40})
        .childrenLayoutXAligned(40, 2)
        .newChild
            .styleFill("#ffcb4f")
        .endChild
        .newChild
            .styleFill("#d595cf")
        .endChild
        .newChild
            .styleFill("#8acf94")
        .endChild
    .endChild
    .newChild
        .styleFill("#515151")
        .layoutFreeStack({left: 5, width: 40})
        .childrenLayoutYAligned(40, 2)
        .newChild
            .styleFill("#ffcb4f")
        .endChild
        .newChild
            .styleFill("#d595cf")
        .endChild
        .newChild
            .styleFill("#8acf94")
        .endChild
    .endChild
;
ts
const bg = new LUIElement();
const bgStyle = new LUIStyleFill();
bgStyle.color.setFromHash("#313131")
bg.addStyleElement(bgStyle);
this.gfx.gui.addChild(bg)
//
// horizontal
const xLine = new LUIElement();
const xLineStyle = new LUIStyleFill();
xLineStyle.color.setFromHash("#515151")
xLine.addStyleElement(xLineStyle);
LUIFreeStackLayout.layout(xLine, {height:40})
this.gfx.gui.addChild(xLine);
xLine.setContainerLayout(LUILineLayout.createXAligned(40, 2))
{
    const c = new LUIElement();
    const s = new LUIStyleFill()
    s.color.setFromHash("#ffcb4f")
    c.addStyleElement(s);
    xLine.addChild(c);
}
{
    const c = new LUIElement();
    const s = new LUIStyleFill()
    s.color.setFromHash("#d595cf")
    c.addStyleElement(s);
    xLine.addChild(c);
}
{
    const c = new LUIElement();
    const s = new LUIStyleFill()
    s.color.setFromHash("#8acf94")
    c.addStyleElement(s);
    xLine.addChild(c);
}
//
// vertical
const yLine = new LUIElement();
const yLineStyle = new LUIStyleFill();
yLineStyle.color.setFromHash("#515151")
yLine.addStyleElement(yLineStyle);
LUIFreeStackLayout.layout(yLine, {left: 5, width:40})
this.gfx.gui.addChild(yLine);
yLine.setContainerLayout(LUILineLayout.createYAligned(40, 2))
{
    const c = new LUIElement();
    const s = new LUIStyleFill()
    s.color.setFromHash("#ffcb4f")
    c.addStyleElement(s);
    yLine.addChild(c);
}
{
    const c = new LUIElement();
    const s = new LUIStyleFill()
    s.color.setFromHash("#d595cf")
    c.addStyleElement(s);
    yLine.addChild(c);
}
{
    const c = new LUIElement();
    const s = new LUIStyleFill()
    s.color.setFromHash("#8acf94")
    c.addStyleElement(s);
    yLine.addChild(c);
}

Custom Layouts

If LUILineLayout, LUISpaceLayout and LUIFreeStackLayout are not enough it's also possible to implement a custom layout.

To do that simply do this:

ts
export class MyLayout implements LUIContainerLayouter {
    perform(parent:LUIElement):void {
        for (let i=0; i<parent.numChildren(); i++) {
            const child = parent.getChildAt(i);
            const someProp = child.getLayoutProperty("yourProp");
            
            // set based on parents position
            child.getPosition()
                .setX(parent.getX())
                .setY(parent.getX())
                .setWidth(parent.getWidth())
                .setHeight(parent.getHeight())

            // TODO: Implement a more interesting layout here!
        }
    }    
}

For every child of the given parent you have to calculate the position based on the parent. It's possible to read properties from the children to finetune the children.