From 408339e388b362599d4494310463d7cf7039d9d0 Mon Sep 17 00:00:00 2001 From: Ross Lombardi Date: Mon, 27 Oct 2025 20:20:19 -0400 Subject: [PATCH] =edits for tone and clarity --- blog/2025/2025-10-19-uis-are-hard/index.md | 92 ++++++++++++++-------- 1 file changed, 60 insertions(+), 32 deletions(-) diff --git a/blog/2025/2025-10-19-uis-are-hard/index.md b/blog/2025/2025-10-19-uis-are-hard/index.md index 34170e1..b444e5f 100644 --- a/blog/2025/2025-10-19-uis-are-hard/index.md +++ b/blog/2025/2025-10-19-uis-are-hard/index.md @@ -13,7 +13,7 @@ Well, I did it for my game, Twin Gods. I can't remember why anymore beyond "hey, What came out the other end is a confusing mixture of knowledge, a lot of head-bashing, years of code that went sideways more than once, and a working UI for a functioning game. Let's see how it works, shall we? -This is not an article to sell you on using my UI ([\*0](#note-id-0)). This is only intended as a showcase of how it works, *why* it works that way, what went into making it, and how difficult it is to make a UI from scratch. And I cannot understate how difficult it is -- *and how much time it takes* -- to make a UI from scratch. +This is not an article to sell you on using my UI ([\*0](#note-id-0)). This is only intended as a showcase of how it works, *why* it works that way, what went into making it, and how difficult it is to make a UI from scratch. And I cannot overstate how difficult it is -- *and how much time it takes* -- to make a UI from scratch. @@ -27,16 +27,18 @@ In this article, we'll cover: 4. The template system 5. How this all helped shape the look of the game's UI -Other topics, such as data binding (don't be fooled by this only being two words, this is *type-checked* on game startup), callbacks, the click feedback system, the dialog transition system, text rendering (which, [hates you](https://faultlore.com/blah/text-hates-you/)), the UI loader (a rather vital part), the lame UI editor, the script wait system, the UI flow and layout system, how the UI is rendered, the "vis stack" (again, don't be fooled by it only being two words), mouse vs gamepad navigation, the UI event system, lists, the placeholder text system, and how the data is actually organized engine-side will be left to future articles. +Other topics, such as data binding (don't be fooled by this only being two words, this is *type-checked* on game startup), callbacks, the click feedback system, the dialog transition system, text rendering (which, [hates you](https://faultlore.com/blah/text-hates-you/)), the UI loader (a rather vital part), building a UI editor, the script wait system, the UI flow and layout system, how the UI is rendered, the "vis stack" (again, don't be fooled by it only being two words), mouse vs gamepad navigation, the UI event system, lists, the placeholder text system, and how the data is actually organized engine-side will be left to future articles. ![Menu Overview](ui-overview.webp) ## But First -I will be blunt: I want to neither encourage nor discourage you from using the general paradigm used by Twin Gods' UI. This is only one man's journey from an empty code file to a fully-featured UI that could actually be used in a professional video game. +I will be blunt: I want to neither encourage nor discourage you from using the general paradigm used by Twin Gods' UI. This is only one man's journey from an empty code file to a fully-featured UI used in a video game. A few names. Twin Gods' engine is dubbed **Hauntlet**. The UI library has no name so we'll refer to it as "Twin Gods UI", or "TGUI" (because I think HUI sounds silly). Like all the rest of Hauntlet, it is in C++ and is rendered with OpenGL 4.6. +Due to the class names on the engine side, I tend to refer to any "UI element" as a "dialog", which corresponds to the `DialogItem` class we'll see later. + ## Let's Begin Let's see how it all started. @@ -47,7 +49,7 @@ Rough, eh? That screenshot is dated 2011. Nevermind the hilarious artwork, how d The basics of TGUI have not changed since 2011 ([\*1](#note-id-1)). The main workhorse is a class called `DialogItem`. TGUI has no "controls" as you'd expect of a UI library, which is a relic from its early days as a "I didn't know what I was doing" sort of thing ([\*2](#note-id-2)). Every UI element is a `DialogItem`. `DialogItem` contains a `std::vector` member. -If I remember right, the original version contained only the very basics: support for a background image, on-click event, text, child elements, and the layout type (row, column, pure x/y). It also used to support drag and drop. Implement *that* at your own peril. +If I remember right, the original version contained only the very basics: support for a background image, on-click event, text, child elements, and the layout type (row, column, pure x/y). It also used to support drag and drop. (Implement *that* at your own peril.) It's all gotten more complicated since then. Additionally, the concept of controls *did* emerge, though not through the C++ side of things. We'll get to that a little later. @@ -75,11 +77,17 @@ That XML produces this UI. Hideous! Much of the XML should be self-explanatory but let's cover a few less-obvious points. +* The top-level node must be a `UIFrame`. The valid child nodes we'll see here: `AcrossBox`, `DownBox`, and `Node`. + +* `DownBox` and `AcrossBox` act like layout containers in other UI systems. `DownBox` "flows" (positions) all child dialogs as rows and `AcrossBox` flows all child dialogs as columns. `Node` is expected to have no children, applies no flow, and is basically undefined behavior if you try it. (`Node` is mostly symbolic; you can absolutely have `DownBox` as a leaf.) + +* Because the `DialogItem` class is used for everything, every XML node has every property with some exceptions based on context. For example, the top-level `UIFrame` node is the only one that can have the `Transition-Type` attribute. + * `BackColor` is applied on top of any materials. If it was "Red", the resulting shader color is then multiplied by red. * The astute reader may note the `;` hanging out in front of the `Background` attribute. For better or worse TGUI's XML reflects its history ([\*3](#note-id-3)). The semicolon denotes a material file. `Background="plain texture.webp"` would specify a texture directly. We'll see a material soon. -* `Anchor` is a note to the layout and flow code. It determines the initial position of the dialog. "Center" simply means it is laid out in the middle of its parent. Other options are "TopLeft", "Top", "TopRight", etc. +* `Anchor` is a note to the layout and flow code. It determines the initial position of the dialog. "Center" simply means it is laid out in the middle of its parent. Other options are "TopLeft", "Top", "TopRight", etc. * `Name` is how the frame is referred to in code and via any scripts (in Lua). @@ -89,20 +97,28 @@ From the beginning, TGUI used XML ([\*4](#note-id-4)). Much has been added but t An aside on data. -Due to the class names on the engine side, I tend to refer to any "UI element" as a "dialog", which corresponds to the `DialogItem` class. The above XML produces 2 dialogs (or `DialogItem`s): the top-level "UIFrame" node and the "Node" node. The entire `DialogItem` tree is then stuffed into a `UIFrame` object and stored in a list: `std::vector UIFrames`. I may also refer to a "UI frame" more casually in-game (say, to non-technical players) as a "window" or "screen". +Storage of the entire UI boils down to a single `vector`. The above XML produces 2 dialogs (or `DialogItem`s): the top-level "UIFrame" node and the "Node" node. The entire `DialogItem` tree is then stuffed into its own `UIFrame` object. That `UIFrame` is stored in the master frames list: `std::vector UIFrames`. This `UIFrames` (note the "s") list has the same lifetime as the running game. -It's important to note that a `UIFrame` is single-instance. Once loaded, the engine does not duplicate or create a new instance of any of these objects when they are displayed in-game. The tree is never touched after load. The whole tree of a `UIFrame` is displayed at once, barring any dialogs that are set invisible. This `UIFrames` (note the "s") list has the same lifetime as the running game. At a basic level, this `UIFrame` object is a fancy container for a single UI tree with additional properties left for future articles. +At a basic level, this `UIFrame` object is a fancy container for a single UI tree with additional properties left for future articles. One could more casually refer to a "UI frame" as a "window", "screen", "prompt", etc. + +The whole tree inside a `UIFrame` is displayed at once, barring any `DialogItem`s that are set invisible. + +It's important to note that a `UIFrame` is single-instance. Once loaded, the engine does not duplicate or create a new instance of any of these objects when they are displayed in-game. The tree is never touched after load. Obviously, even though the *tree* is essentially read-only, the actual `DialogItem`s are written to quite frequently, especially during data binding. -Note that each `UIFrame` in the XML file *can* be stored in a variable for use in code (data binding, showing windows to the player, etc.). All `UIFrame`s *do* exist in the UIFrames *list* (or they could not be found otherwise) but many important `UIFrame`s also have a hard variable in the engine for direct reference in code (eg, `UIFrames().simple_example.show()`). +While all `UIFrame`s are stored in the aforementioned `UIFrames` list, a `UIFrame` can optionally be stored in a variable, referred to as a "named frame". After parsing `UI Frames.xml` on startup, the engine looks for all named frames specified in code (eg, `UIFrame SimpleExample("simple_example")`) and shows an error for any that it can't find. + +Many important `UIFrame`s are "named frames" for direct reference in code (eg, `UIFrames().SimpleExample.show()`). This is commonly used for data binding. + +Hauntlet's Lua scripting system can access and write to `DialogItem`s. This means a script can maintain and update its own UI, though it cannot create new `DialogItem`s. ## Materials Here's the definition of the material used above ([\*5](#note-id-5)): ```xml title="materials\unit speech frame.mat" - + @@ -113,13 +129,13 @@ Here's the definition of the material used above ([\*5](#note-id-5)): I won't show the shader but it is a basic ["9-slice"](https://en.wikipedia.org/wiki/9-slice_scaling) shader with support for coloring at the 4 corners and a border color, hence the 5 uniforms. -**It cannot be overstated how important materials were to the look of Twin Gods' UI, and the ease of development.** The screenshot shown in the top of the article represents the third major "rewrite" of the UI. +**It cannot be overstated how important materials were to the look and ease of development of Twin Gods' UI.** The screenshot shown in the top of the article represents the third major "rewrite" of the UI. -More on how this helped a little later. +More on how this helped later. ## UI Styles -Note the value of `value` in the material. It can be set to either a hex color (starting with a "#") or a *named* color defined in the "UI Styles" file. "UI Styles.xml" is a lovely companion piece to "UI Frames.xml". +Note the value of `value` in the material. It can be set to either a hex color (starting with a "#") or a *named* color defined in the "UI Styles" file. "UI Styles.xml" is a companion piece to "UI Frames.xml". It can define colors: @@ -136,25 +152,27 @@ It can also define fonts. ``` -It can even define the prefix folder used to determine where to pull fonts from because I'm lazy and haven't updated the `Font` attribute to work like literally every other in-game asset after the last `FileSystem` refactor. +It can even define the prefix folder from which all pull fonts ([\*6](#note-id-6)). ```xml ``` +This means that when a `Font` specifies "short\16-outline", the full folder is "fonts\nope\short\16-outline". + You'll also notice a `FontStyle` attribute, which makes it look like each dialog only supports a single font style. ![I Assure You](assure-you.webp) Text styling will be covered in a later article. -In any case, the "UI Styles" file is a vague CSS-like system, and by CSS I mean I can specify color, font face, font size, shader, outline, and a few other things and that's basically it. +In any case, the "UI Styles" file is a vague CSS-like system. I can specify color, font face, font size, shader, outline, and a few other things but that's it. It does not support things like animation. That is left to shaders. I should note before anyone gets mad that if `DropShadow` appears in a `FontStyle`, the `Outline` color becomes the drop shadow color. 13 years of *cruft*! ## Templates, In My UI? -One of the more interesting features, I think, is TGUI's template capability. This is why TGUI will probably never get official support for controls. Brace yourself, the XML only gets uglier from here. +One of the more interesting features, I think, is TGUI's template capability. This is why TGUI will probably never get official support for controls. ```xml