First Steps with GG-UI
Build a first HUD in a Trinyx project: an asset laid out in the widget designer, a widget class that gives it its data and runs its buttons, and the call that shows it on a player's screen. It takes about fifteen minutes and touches each part you will use again.
The steps use the Playground project. Any project works the same way: its content folder holds its assets, and its src folder its code.
What You Will Make
A HUD in the top left of the screen that shows a score the game sets, the height of the player's body, and a button that resets the score.
| Part | File | What it is |
|---|---|---|
| The asset | content/PlayerHud.tnxwidget | Layout, look, and animation, edited in the designer |
| The class | src/PlayerHud.h, src/PlayerHud.cpp | What the asset reads, is given, and calls |
| The call | the game mode | Shows the HUD when the player's body exists |
1. Write the Widget Class
The class says what the HUD's asset can use. Every member listed in TNX_ is something the asset sees.
// src/PlayerHud.h #pragma once #include "EPlayer.h" #include "TnxWidget.h" class PlayerHud : public TnxWidget<PlayerHud> { public: explicit PlayerHud(TnxContextOf<EPlayer<>> player) : Player(player) { } TnxContextOf<EPlayer<>> Player; // the player's body: "=Player.CTransform.PosY" in the asset TnxInput<int32_t> Score{ 100 }; // a value the game gives it: "=Score" void ResetScore() { Score = 0; } // { "call": "ResetScore" } on a button TNX_WIDGET(PlayerHud, Player, Score, ResetScore) };
// src/PlayerHud.cpp #include "PlayerHud.h" TNX_REGISTER_WIDGET(PlayerHud)
- A context (
TnxContextOf) is game state the asset reads: here an entity of typeEPlayer, whose components' fields it can show. It is a constructor parameter, so the HUD can't be shown without the player it is for. - An input (
TnxInput) is a value the game gives the asset. A number shows its changes with nothing built again. - A method is what the asset's actions call. It runs on the UI's update; to change game state it sends a command (step 6).
TNX_goes in oneREGISTER_ WIDGET .cppfile, once.
Re-run CMake so the new files are found, build, and open the editor.
2. Make the HUD Asset
In the Content Browser, choose New > HUD… and name it PlayerHud. A HUD asset covers the whole screen. It starts with a safe area, which keeps content clear of notches and overscan, and a Notifications region in the top right that the game can add widgets to.
Double-click it to open the Widget Designer. The canvas shows the screen at the size and DPI chosen above it, with the designer's panels docked around it.
3. Give It Its Class
In Inputs and Contexts, set Class to PlayerHud. The panel now lists what the class declares: the context Player (an EPlayer), the input Score, and the method ResetScore().
The canvas makes an instance of the class, so what its code sets shows there (a score of 100), and its methods run when the asset's actions call them. Each input and context can also be given a preview value, which is shown over what the class sets: leave Score's empty for now.
4. Lay It Out
From the Widget Palette, drag into the safe area:
- a Place, anchored to the top left (pick the corner in Details' anchor grid), 24 units in;
- a Column inside it;
- two Labels and a Button inside the column.
Select each in the Widget Hierarchy and set it in Widget Details:
- the first label's text to
=Score: a formula, so it shows the input; - the second label's text to
=Player.CTransform.PosY: the formula editor offers the context's components and fields, so nothing is typed from memory; - the button's text to
Reset, and under What it does, a click that callsResetScore.
Now that the asset reads CTransform.PosY, Inputs and Contexts lists it under Player: give it a preview value, 1.8, so the label shows a height while there is no game running.
The file the designer writes is plain text you can read and diff:
{ "widget": "PlayerHud", "class": "PlayerHud", "kind": "hud", "version": 1, "root": { "name": "Screen", "type": "Stack", "width": "grow", "height": "grow", "children": [ { "name": "Safe", "type": "SafeArea", "children": [ { "name": "Stats", "type": "Place", "anchor": "TopLeft", "x": 24, "y": 24, "children": [ { "name": "Lines", "type": "Column", "gap": 6, "children": [ { "name": "Score", "type": "Label", "text": "=Score" }, { "name": "Height", "type": "Label", "text": "=Player.CTransform.PosY" }, { "name": "Reset", "type": "Button", "text": "Reset", "on": { "click": { "call": "ResetScore" } } } ] } ] }, { "name": "TopRight", "type": "Place", "anchor": "TopRight", "x": -24, "y": 24, "children": [ { "name": "Notifications", "type": "Column", "region": true, "gap": 8, "max": 4 } ] } ] } ] } }
Widget Problems lists anything that is wrong, with the line and what to change: a field the context hasn't got, a method the class doesn't have. Fix them before going on.
5. Try It in the Designer
Press I (Interact) and click Reset. The button reacts as it will in the game, the class's method runs and the score goes from 100 to 0, and the canvas lists what the click did: call ResetScore from Reset (ran). Press I again to go back to editing.
Save. A running game reads the file again at once.
6. Show It on the Player's Screen
A HUD is shown for a player once their body exists. In the Playground, that is where the game mode spawns it, in ArenaMode::SpawnPlayerBody:
#include "PlayerHud.h" // After the player's Construct is made and the Soul has claimed it: TnxUI::SetHud<PlayerHud>(*world, soul, player->Body);
SetHud<PlayerHud> makes the class with the player's body as its Player context and shows the asset named PlayerHud. Play in the editor: the HUD is drawn over that player's view. The game changes what it shows through the HUD it got back:
const TnxHud hud = TnxUI::SetHud<PlayerHud>(*world, soul, player->Body); hud.Widget<PlayerHud>()->Score = 250; // from any thread hud.Add("Notifications", "Toast", { { "Text", "Game.Welcome" } }); // into the region
While the game plays, the designer's contexts can follow it: in Inputs and Contexts, point Player at the live player and the canvas shows the real values as you edit.
7. Change Game State from the HUD
UI never writes simulation state. A button that should change the game, buying something or resetting a level, sends a command, which the simulation runs like a key press: recorded, replicated, and replayed on rollback.
struct ResetLevel { uint8_t Level = 0; TNX_COMMAND(ResetLevel, "Game.ResetLevel", Level) }; TNX_REGISTER_COMMAND(ResetLevel)
The asset can send it directly, { "click": { "command": "Game.ResetLevel", "Level": 1 } }, or code can, with TnxSim::Send(world, ResetLevel{ .Level = 1 }): a method of the class sends it from the world it keeps, given to it when the HUD is set. A Construct that lists the command handles it on the Brain and checks it is allowed.
Where Next
- Everything a HUD asset can do (lists, regions, styles, animation, nested widget classes): Widget Assets
- How GG-UI works, in the engine and on its own: Using GG-UI
- Writing UI in code, for tools and debug screens: Foundations