4.8. The Heads Up Display (HUD)

The heads up display (HUD) is a 2D framework for providing a graphical user interface to the player. Like everything else, it can be extensively customised via Lua. The most basic API allows positioning and rotating text / textured rectangles in a scenegraph-like hierarchy on the screen. On top of this, various widgets have been implemented in Lua, via HUD Class definitions. This, you are also free to make your own. The HUD subsystem is designed to handle things like health/ammo displays, mini-maps, menu screens, and general purpose mouse-driven graphical user interfaces.

4.8.1. Basics

Let us first instantiate an existing class. The Rect HUD class is the simplest class it is possible to write, as it has no user-defined behaviours. Its one-line definition is in the common/hud directory, but you can also define your own. Instantiate it as follows:

Basic HUD object example
Use of /common/hud/Rect as shown in the Lua snippet.
obj = gfx_hud_object_add(`/common/hud/Rect`)
obj.position = vec(100, 100)  -- centre of object, from screen bottom left
obj.texture = `/common/hud/textures/Icon064.jpg`
obj.size = vec(50, 50)
obj.colour = vec(0, 1, 1)  -- cyan (masks texture)
obj.zOrder = 2
obj.alpha = 0.5
obj.orientation = 44  -- degrees (clockwise)
...
obj:destroy()
-- Can also initialise members with a table param:
obj = gfx_hud_object_add(`Rect`, {size=vec(50,50), ...})

The HUD object has a number of fields that define how it is rendered. Some examples of these are above, and they are comprehensively documented (TODO: here). If not given, they fall back to defaults first in the HUD class and then the system defaults.

The size defaults to the size of the texture, if one is given. The zOrder field is an integer between 0 and 7, which defaults to 3. Higher values are drawn on top of lower values. The HUD object is garbage collected when it is abandoned, and can be destroyed earlier with :destroy().

Rect can be useful for simple things but usually we will define our own HUD class, which has fields and methods much like a game class. The HUD class can be instantiated into as many HUD objects as we want. The following HUD class implements a spinning icon;

hud_class `SpinningIcon` {
    texture = `/common/hud/textures/Icon064.jpg`;
    speed = 20;
    init = function (self)
        self.needsFrameCallbacks = true
        self.angle = 0
    end;
    destroy = function (self)
        -- nothing to do
    end;
    frameCallback = function(self, elapsed)
        self.angle = (self.angle + elapsed*self.speed) % 360
        self.orientation = self.angle
    end;
}
Spinning Icon
The icon on the right (faster_obj) spins faster than the one on the left (obj).

The 3 defined methods are all callbacks, and there are other possible callbacks as well. You can also define your own methods with your own names, to help structure your code or to provide a public API to the object. The init callback sets the angle field of the object and requests frame callbacks. If you enable a callback, you had better have a method of the right name to receive it. If a callback raises an error, the system will usually disable the callback by setting the needsX field to false again.

Note how giving the speed in the class allows it, optionally, to be overridden on a per-object basis. The following code shows how we can override the speed when we instantiate the object:

obj = gfx_hud_object_add(`SpinningIcon`, {position=vec(200,100)})
faster_obj = gfx_hud_object_add(`SpinningIcon`, {position=vec(300,100), speed=30})

4.8.2. Corner Mapped Textures

By default, when a texture is set on a HUD object, it is filtered (stretched or shrunk) to fit the rectangular size of the object. This can be useful when the size of the object can vary at run time, or if a single texture is to be used for a variety of differently sized HUD objects. However, the filtering artefacts can look ugly for a lot of textures, especially when the texture is magnified a lot.

Corner Mapped Texture
The texture itself is shown to the right. To the left is shown the HUD object mapping this texture with corner mapping.

For certain kinds of textures, there is a technique that avoids the artefacts while allowing arbitrary scaling of the HUD object. This involves mapping the four quarters of the texture 1:1 into the corners of the HUD object and stretching a single row and column of texels across the middle of the HUD object.

This works well in a variety of situations where the centre of the HUD object is a solid colour, but detail is desired around the edge and at the corners. For example, simple rounded corner rectangles, borders, bevels, etc. Some example textures are provided in /common/hud/CornerTextures. See the button (§4.8.6.1) class below for a concrete example.

o = gfx_hud_object_add(`/common/hud/Rect`, {texture=`MyTexture.jpg`})
o.size = vec(100, 100) -- larger than the texture
o.cornered = true -- enable corner mapping

4.8.3. Text

HUD text bodies allow the rendering of text according to a particular font (§4.9). It supports top/bottom colour, ANSI terminal colour codes, hard tabs, new lines, wrapping according to a pixel width, and scrolling.

Basic Text Rendering
A HUD text body using the simple misc.fixed font.

The text bodies behave like HUD objects, but they are not extensible. They do not have customisable fields and methods, and they cannot receive callbacks. They do, however, have all the basic fields of HUD objects.

t = gfx_hud_text_add(`/common/fonts/misc.fixed`)
t.text = "Hello World!"
t.position = vec(300, 300)
t.orientation = 90
t.zOrder = 7
...
t:destroy()

The size field of text is immutable. Upon setting the text, it gives you the number of pixels occupied by this particular text in this font. The code takes account of tabs, new lines, and ANSI terminal colour codes.

4.8.3.1. Text Colour

Colour and alpha are supported via two mechanisms. Firstly, and most simply, one can set the colour/alpha fields, which act as a general mask on the colour/alpha present in the texture. This is the same as regular HUD objects.

Per-Letter Colouring
Text with per-letter colouring.

To apply colour and alpha to individual letters and words, and to use top/bottom colouring (gradients) there is a different mechanism. Instead of setting the text field, one uses several calls to append(), between which the letter colour/alpha can be changed. The clear() call will reset the state. Setting the text field is equivalent to calling clear(), setting the colours to white and the alphas to 1, and then calling append().

t:clear()
t.letterTopColour = vec(1, 1, 0) -- yellow
t.letterBottomColour = vec(1, 0, 0) -- red
t.letterTopAlpha = 0.7
t:append("Some firey text\n")
t.letterTopAlpha = 1
t.letterTopColour = vec(.5, .5, 1)
t.letterBottomColour = vec(.5, .5, 1)
t:append("and some pale blue text.")

4.8.3.2. Text Drop Shadows

Drop Shadows
A drop shadow of 1 pixel towards the bottom right of the screen.

To allow clearer reading of the text on arbitrary backgrounds, it is possible to enable a drop shadow. This works by rendering the text first coloured black, at an offset, and then rendering the actual text in a second draw on top of it. In the second case below, the texel lookup are offset by 0.5 texels, which causes bilinear filtering. This has the same effect as blurring the text with a 2x2 box filter. Note that there must be an additional pixel of space around each letter in the font texture for this to work.

t.shadow = vec(1, -1) -- bottom right by 1 pixel
t.shadow = vec(1.5, -1.5) -- pixel blurring

4.8.3.3. Wrapping Paragraphs

By default, the text body will only break the text at any new-line characters (`\n`) given in the input text. This means the width of the text body will be the length of its longest line. To automatically wrap the text at a given width in pixels, set the textWrap field, which defaults to nil.

t.textWrap = vec(500, 400)

This defines a rectangle. Firstly, text is broken to avoid it rendering wider than the given number of pixels (500 in this case). Secondly, no more lines are drawn than would fit into the given height (400 pixels in this case). The size field will always return the textWrap dimensions when it is non-nil, regardless of how much text is in the text body. Lines are broken at word boundaries unless a word is too long for the line.

It is also possible to scroll the text, i.e. to clip lines at the top of the text, as well as, or instead of, the bottom of the text. This is done by setting the scroll field, which defaults to 0.

t.scroll = 200 

This starts rendering at 200 pixels below the top of the given text, i.e. represents a scrolling down of 200 pixels.

4.8.4. The Hierarchy

Like the 3D scene, the HUD objects exist in a hierarchy, i.e. one can be the child of another. This is achieved by setting the parent field:

The Hierarchy
The child is drawn relative to the parent.
p = gfx_hud_object_add(`/common/hud/Rect`, {
    colour = vec(1,0,0);
    position = vec(400,150);
    size = vec(40,40);
})
c = gfx_hud_object_add(`/common/hud/Rect`, {
    alpha = 0.5;
    position = vec(10,10);
    size = vec(15,15);
})
c.parent = p
p.orientation = 45

The object p has a child object c. This relationship was established by setting its parent field, which can be changed at any time or provided in the constructor's 2nd argument. HUD objects can be children or parents, but HUD texts can only be children. A tree is thus formed, with HUD objects at the nodes, and either HUD objects or HUD text at the leaves.

The position of the child is relative to its parent. It thus displays on top of the parent, in its upper right corner. The child is always drawn on top of the parent, so setting zOrder is only necessary to disambiguate the draw order of siblings. Disabling a parent causes all children to effectively be disabled as well.

Typically, this hierarchy is used to implement more high-level GUI elements in terms of low-level ones. For example, a GUI dialog might consist of buttons and text entry widgets. Disabling the dialog would hide everything, and the dialog can be moved / rotated as a single atomic unit.

As the parent's position (or orientation) is updated, the child's derived position and orientation are recalculated. It is also possible to set inheritOrientation to false, which is useful to avoid icons rotating within a spinning mini-map, or to keep text looking crisp and clear.

Note that the child will be garbage collected if there is no link to it, so it is standard to store a link from the parent to the child in order to keep it alive. The link from child to parent is a weak link, in that it does not in itself prevent the parent from being garbage collected. Typically, however, one does not link to children without also linking to the parent.

When a HUD object is destroyed, its children are automatically destroyed with it. If you don't want this behaviour, you can unlink the children in the destroy callback by setting their parent to nil.

4.8.5. Handling Keyboard/Mouse Input

Handling keyboard/mouse input allows us to create interactive HUD objects, for example buttons, text entry, scroll bars, etc. There are two callbacks that provide this fundamental capability -- one for mouse pointer movements, and one for button events (both mouse and keyboard buttons). They are both enabled by setting needsInputCallbacks:

hud_class `InputDemo` {
    init = function (self)
        self.needsInputCallbacks = true
        self.inside = false
    end;
    destroy = function (self)
        -- nothing to do
    end;
    mouseMoveCallback = function(self, local, screen, inside)
        print(local, screen, inside)
        self.inside = inside
    end;
    buttonCallback = function(self, event)
        if inside then
            print(event)
        end
    end;
}

The mouseMoveCallback gets the cursor position local to the object (with derived orientation and position transformations reversed), the screen cursor position, and a boolean indicating whether the mouse cursor is over the HUD object or not. This takes into account other HUD objects that may be occluding this HUD object. If inside is true, then the local position should be within the rectangle defined by the HUD object's size.

The buttonCallback receives all keyboard and mouse button events whenever needsInputCallbacks is true. So typically, you should implement some code to implement mouse or click focus in order to disregard button presses when they should not be processed. Grit does not prescribe a focus strategy, but it is trivial to implement one, as shown in the above code.

The received events are strings. Each string consists of a prefix character, one of +, -, =, or :, that denotes the action. For example +a means a was pressed, -a means a was released, and =a is a repeat event, issued by the operating system because the key is being held down. For such keys, the suffix of the key is the name of the button, which is always lower case. Buttons like the arrow keys have names that begin with an upper case letter, e.g. "Left" or "Ctrl". The mouse buttons have names beginning with a lower case letter, such as "left", "middle", and "right".

Key events beginning with the ":" character are text events. These incorporate the shift key for capitalisation, internationalisation accent keys, and other keyboard modifiers that affect the way text is typed. So use these events for text input, do not try and capitalise letters yourself by watching for the shift key.

4.8.6. Existing HUD Classes

In /common/hud there are a few existing classes you can use to build GUIs quickly. /common/hud/Rect is an example, and we have already seen several example applications in this chapter (§4.8). The other classes are more sophisticated. In all cases, the code for these classes is available to be read. This is useful to find out how to instantiate them, and also to see examples of HUD code, for those writing their own HUD classes. Some key classes are documented here:

4.8.6.1. Button

The class /common/hud/Button can be used to create a clickable button with text on it. The button changes colour on mouse hover and click, and can also be greyed out. It can be instantiated as follows. Note the pressedCallback, which the code in this class executes when it receives a button press.

Button
The button created by the Lua snippet left.
b = gfx_hud_object_add(`/common/hud/Button`, {
    caption = "Click Me!";
    pressedCallback = function (self)
        print("You clicked me!")
    end;
})
-- By default, size is chosen based on the caption size.
b.parent = my_dialog
b.position = vec(40, 0) -- position within the parent
b:setGreyed(true) -- turn the button off, default false
b.enabled = false -- hide the button, default true

It is also possible to customize some of the attributes of the button. The following rather garish button demonstrates this.

Garish Button
The colours become more interesting upon mouse interaction.
b = gfx_hud_object_add(`/common/hud/Button`, {
    size = vec(100,40);
    font = `/common/fonts/Impact24`;
    caption = "Click Me!";
    baseColour = vec(0, 0, 0);  -- Black
    hoverColour = vec(1, 0, 0);  -- Red
    clickColour = vec(1, 1, 0);  -- Yellow
    borderColour = vec(1, 1, 1);  -- White
    captionColour = vec(1, 1, 1);
    captionColourGreyed = vec(0.5, 0.5, 0.5);  -- Grey
})

4.8.6.2. Label

The /common/hud/Label class encapsulates a HUD text body. It provides a simple way of aligning the text, and has the ability to set the text to grey, which is useful when greying out a whole dialog, including text labels.

label = gfx_hud_object_add(`/common/hud/Label`, {
    size = vec(100, 40);
    alignment = "LEFT";  -- Also, "CENTRE", or "RIGHT".
    value = "Enter number of things here: ";
    position = vec(30, 30);
    greyed = true;
})
label:setValue("Changed value: ")
label:setGreyed(false)

4.8.6.3. Border

The /common/hud/Border class wraps a child object and draws a border around it using corner mapping (§4.8.2). When instantiating the class, the child object is given and automatically has its parent set to the border object.

Border
Putting a border around another HUD object.
o = gfx_hud_object_add(`/common/hud/Border`, {
    position = vec(200, 200);
    padding = 8;  -- Extra space on the inside.
    colour = 0.25 * vec(1, 1, 1);  -- Dark grey
    texture = "/common/hud/CornerTextures/Border04.png";
    -- The child object:
    child = gfx_hud_object_add(`/common/hud/Button`, {
        caption = "Some button.";
    })
})

(TODO: Editbox, scale, controls)

4.8.7. Layout

By making use of the HUD hierarchy, one can position HUD objects using relative co-ordinates. When creating a dialog, this means hardcoding the positions of all the elements of that dialog, which can be tedious and hard to maintain. There are a few HUD objects that control the layout of their children, making this easier.

4.8.7.1. StackX and StackY

The /common/hud/StackX and StackY HUD classes will control the positions of their child objects and arrange them into horizontal or vertical alignment. StackY will place the children vertically, in order, starting from the top. StackX does the same but horizontally, starting from the left.

Ultimately, the size of the stack object will be the bound of all the objects inside it, allowing you to nest stacks, add borders, etc. The stack cannot be directly resized, it also does not react to children whose size change at some time after the stack was created.

Border
Three buttons laid out using StackY. Note the extra gap of 10 pixels.
o = gfx_hud_object_add(`/common/hud/StackY`, {
    position = vec(200, 200);
    padding = 8;  -- Default space between elements (defaults to 0)

    -- The child objects:
    { align="LEFT" };  -- "RIGHT" and "CENTRE" (default) also available.
    gfx_hud_object_add(`/common/hud/Button`, {
        caption = "Shoot Missile.";
    });
    gfx_hud_object_add(`/common/hud/Button`, {
        caption = "Shoot Laser.";
    });
    vec(0, 10);  -- It's also possible to add extra space (or subtract it).
    gfx_hud_object_add(`/common/hud/Button`, {
        caption = "Place Mine.";
    });
})

Each element in the list is either the next child object, a vector2 value giving some padding (which behaves like an invisible Rect of the same size), or a string that specifies a new alignment. The alignment applies to all subsequent objects, unless it is changed again.

The StackX is the same, except obviously they are placed one after each other from left to right. Also, the alignment options are "TOP", "CENTRE", and "BOTTOM", and any additional spacing is given as vec(0, n) instead of vec(0, n).

4.8.7.2. Stretcher

The /common/hud/Stretcher class will position its child object to fill the given space. It does this by changing its own position (the child is at vec(0, 0) relative), and size, and also changing the child's size to fill its own boundaries. If the child has an updateChildrenSize callback, it calls that after changing the child size.

The class is instantiated by overriding the calcRect function, which allows the user to specify how the size of the stretcher should relate to its parent's size. The callback is re-executed every time the parent size changes. The result of calcRect is passed to setRect, i.e. it is the values left, bottom, right, top. In this case, it specifies a 50 pixel padding, and the Stretcher has no parent, so it uses the screen dimensions as input.

Stretcher
Using the /common/hud/Stretcher class to resize a button to fill the screen.
o = gfx_hud_object_add(`/common/hud/Stretcher`, {
    calcRect = function(self, psize)
        return 50, 50, psize.x-50, psize.y-50
    end;
    child = gfx_hud_object_add(`/common/hud/Button`, {
        caption = "Some button.";
    })  
})  

4.8.7.3. Positioner

The /common/hud/Positioner controls its own position according to the size of its parent. It is typically used to place other objects relative to the four corners of the screen, and update these positions as the resolution or window size changes. There are 5 existing positioner objects you can attach a HUD object to (by setting them as parent): hud_center, hud_bottom_left, hud_bottom_right, hud_top_left, and hud_top_right.

o = gfx_hud_object_add(`/common/hud/Rect`, {
    size = vec(100, 100);
    parent = hud_top_left;
    position = vec(50, -50);
})

4.8.8. Bringing it all together