diff --git a/blog/2025/2025-10-19-uis-are-hard/assure-you.webp b/blog/2025/2025-10-19-uis-are-hard/assure-you.webp new file mode 100644 index 0000000..eb3102d Binary files /dev/null and b/blog/2025/2025-10-19-uis-are-hard/assure-you.webp differ diff --git a/blog/2025/2025-10-19-uis-are-hard/how-it-started.webp b/blog/2025/2025-10-19-uis-are-hard/how-it-started.webp new file mode 100644 index 0000000..055c8c9 Binary files /dev/null and b/blog/2025/2025-10-19-uis-are-hard/how-it-started.webp differ diff --git a/blog/2025/2025-10-19-uis-are-hard/index.md b/blog/2025/2025-10-19-uis-are-hard/index.md new file mode 100644 index 0000000..34170e1 --- /dev/null +++ b/blog/2025/2025-10-19-uis-are-hard/index.md @@ -0,0 +1,401 @@ +--- +title: 'UIs Are Hard' +slug: 'uis-are-hard' +description: 'An explanation of the custom UI in the Twin Gods engine' +date: '2025-10-19' +authors: ['domiran'] +tags: ['ui', 'opengl', 'article'] +image: 'ui-overview.webp' +--- +If you're like me, you've thought to yourself at least once: "Hey, making a UI can't be that hard! I wouldn't need to add any more headers or libraries to my project!" Because not having to deal with C++ is a good reason to write your own UI. + +Well, I did it for my game, Twin Gods. I can't remember why anymore beyond "hey, that sounds like fun". Am I glad I have it? Yes! You very much learn a lot about how a UI works. Would I do it again? Probably not. + +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. + + + +## There's a Lot to Cover + +In this article, we'll cover: + +1. Defining a UI element with a simple example +2. How materials are defined +3. How text and color styles are defined +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. + +![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. + +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. + +## Let's Begin + +Let's see how it all started. + +![How It Began](how-it-started.webp) + +Rough, eh? That screenshot is dated 2011. Nevermind the hilarious artwork, how did TGUI go from a fever dream to something someone might not actually downvote on Steam out of sheer anger? + +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. + +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. + +A consequence of this setup is that every UI element contains all the attributes used to make up everything seen in the UI. `DialogItem` is reported as being 1,480 bytes in release mode. It's quite large. This is one reason why `DialogItem`s are stored as a vector on its parent and not, say, a bunch of pointers. (We'll ignore the possible bikeshedding in this article. The data side of things changed quite a few times.) + +And, okay, I lied. There is one specific control: lists. For a later article. + +But of course, the definition of a TGUI `UIFrame` is just as important as the engine code behind it. TGUI's XML might as well be its own language and is one of its primary strengths, I think. + +## Defining a UI + +All screenshots in this article were captured from the running game. + +Here's a simple example UIFrame. + +```xml + + + +``` + +That XML produces this UI. + +![Simple Example](simple-example.webp) + +Hideous! Much of the XML should be self-explanatory but let's cover a few less-obvious points. + +* `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. + +* `Name` is how the frame is referred to in code and via any scripts (in Lua). + +From the beginning, TGUI used XML ([\*4](#note-id-4)). Much has been added but the basic file format has not changed much in the past 13 years. The entire UI is defined in a single file, "UI Frames.xml". + +## High-Level Storage Concept + +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". + +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. + +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()`). + +## Materials + +Here's the definition of the material used above ([\*5](#note-id-5)): + +```xml title="materials\unit speech frame.mat" + + + + + + + +``` + +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. + +More on how this helped a little 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". + +It can define colors: + +```xml + + +``` +Why both ways? A decade of legacy cruft (and, importantly, laziness). + +It can also define fonts. + +```xml + + +``` + +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. + +```xml + +``` + +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. + +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. + +```xml + + + + + + + + +``` + +The above XML produces this: + +![Templated Fields](template-stuff.webp) + +I said earlier the engine treats the UI tree as essentially read-only, right? So what happened? The template system happened, that's what. + +Before that, quickly, `DownBox` flows 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.) + +## How Templates Expand + +When the UI loader encounters a `Template` node, it is put aside into a separate "templates" list, noting the name, and moves on to the next top-level node. When it encounters an XML node of the name format `ValidNodeType:TemplateName`, the template system kicks in, and it's rather simple (in theory). + +The previously-set-aside node now acts like it was copied and pasted in-place. + +This XML: + +```xml + +``` + +Is expanded to: + +```xml + + + + +``` + +`AcrossBox:test-field` pulls the XML for `test-field`, copying and pasting it and all its child nodes on top of the `AcrossBox` node, as if `AcrossBox` is the same node as the `Template` top-level node. To make this last point more clear: `BackColor="Red"` (which visually does nothing here), overwrites `BackColor="White"` (also does nothing). + +### Templated Attribute Resolution + +The template parser sees the same attribute (`BackColor`, in this case) on the same node, post-parse, as the top-level `Template` node and overwrites the template's attribute with the incoming real node. + +On the second `AcrossBox`, the template's original `BackColor="White"` stays because the incoming real node has no such attribute. + +### Template Attribute Expansion Assignment + +What about `label:Text` and `value:Text`? The syntax may be cursed but I could not live without this feature. It searches down the tree (depth only) until it finds a dialog with the given name and then sets the given attribute. + +This is part of the "grand success" of how templates work. Data binding abuses this whole-heartedly and quite literally makes some features possible. + +### Improve the Look With Templates + +Getting back to our example, it looks weird. The label is not a fixed size. We can make this a little better without putting a size everywhere -- since that is, after all, the entire point of TGUI's templates. + +```xml +