UI Editor
Overview
I made this editor for our team’s engine while working on Spite: Curse of Tzalozel. During previous game projects in the school engine, UI element placements were often hardcoded and could not be changed in the editor. My goal with this UI editor was to allow our artists to set the layout for our UI.
UI Elements
All UI elements have properties for position, scale and pivot.

Every element also has a name that can be edited in an input feild, and can be anchored to a part of the screen.

To view how the anchors affect the elements, it is possible to change the reference resolution.

All elements exist in layers, allowing control over draw order of elements.

Image & Text
Images have properties for sprite material and tint color

Text elements have properties for display text, font, font size, alignment and tint color. The rendered text will automatically wrap to fit the width of the text element.

Button
Buttons have properties for both image and text. Additionally, there is an extra color tint for when the button is hovered, a click event property dictating what the button does and a property for what sfx to play.

Slider & Checkbox
Sliders have image properties for both the backdrop and the handle. The handle can also be scaled independently from the backdrop.

Checkboxes have properties for the backdrop material, default color and hover color. There is also properties for the checkmark sprite.

Editing in code
When programming the UI, I often needed access to the element in code. In the begining, this was done by simply looping through all layers in a specific canvas, looking for an element with a hardcoded name, and saving the layer index and ID, like this:
1const Tga::StringId sliderNameToCheck = "Slider"_tgaid;
2Goose::UIID sliderId{};
3uint32_t sliderLayerIndex{};
4
5for (uint32_t layerIndex = 0; layerIndex < canvas.layers.size(); ++layerIndex)
6{
7 auto& layer = canvas.layers[layerIndex];
8 for (uint32_t i = 0; i < layer.sliders.size(); ++i)
9 {
10 if (layer.sliders.data()[i].baseData.name == sliderNameToCheck)
11 {
12 sliderId = layer.sliders.dataKeys()[i];
13 sliderLayerIndex = layerIndex;
14 }
15 }
16}
This method quickly became messy once I needed more references to elements. I improved this by creating a Reference class and from the editor generating constant references of elements in a header file. This would be done for all UI elements marked as exposed

This made everything more readable and also had the benefit of giving compile errors when elements were renamed or removed, making it easier to find and fix.
1namespace UI_VARIABLES
2{
3 namespace UI_KJ
4 {
5 constexpr Goose::UIReference Slider_To_Expose{ 7,1,3,218103821 };
6
7 }
8}
9
10// Example of how to access
11void PrintSliderValue()
12{
13 std::cout << UI_VARIABLES::UI_KJ::Slider_To_Expose.AccessSlider().value << "\n";
14}
The idea of generating constants in a header file is something I got from working with the Wwise audio engine, which similarly creates constants for every audio event. How I generate the file is by continuously adding text to a string. I wrap every canvas in it’s own namespace and creating variables for every exposed element based on their names.
1void Goose::GenerateCanvasHeader()
2{
3 const std::string tabString = " ";
4 const std::string doubleTabString = " ";
5 std::string fileContent = "#pragma once\n#include \"UIReference.h\"\nnamespace UI_VARIABLES\n{\n" + tabString;
6
7 auto& allCanvases = GetCanvasRegistry().AccessAssetMap();
8
9 for (uint32_t canvasIndex = 0; canvasIndex < allCanvases.size(); ++canvasIndex)
10 {
11 const UICanvas& canvas = allCanvases.data()[canvasIndex];
12
13 const CanvasID canvasId = allCanvases.dataKeys()[canvasIndex];
14
15 FilePath canvasName = GetCanvasRegistry().GetStringFromID(canvasId);
16 canvasName.replace_extension("");
17
18 fileContent += "namespace " + canvasName.string() + "\n" + tabString + "{\n" + doubleTabString;
19
20 int layerIndex = 0;
21 for (auto& layer : canvas.layers)
22 {
23 auto exposeToFile = [&](const UIBaseData& aUIObject, const UniversalInstanceID aId, UIType aType)
24 {
25 String64 objectName = aUIObject.name.GetString();
26 objectName.replace(' ', '_');
27
28 fileContent += "constexpr Goose::UIReference " + std::string(objectName.c_str()) + "{"
29 + std::to_string(aId) + "," + std::to_string(layerIndex) + "," + std::to_string(static_cast<int>(aType))
30 + "," + std::to_string(canvasId) + "};\n" + doubleTabString;
31 };
32
33 for (int i = 0; i < static_cast<int>(layer.images.size()); ++i)
34 {
35 const UIBaseData& uiObject = layer.images.data()[i].baseData;
36
37 if (uiObject.exposeToHeader == false)
38 {
39 continue;
40 }
41
42 const UniversalInstanceID id = layer.images.dataKeys()[i];
43
44 exposeToFile(uiObject, id, UIType::Image);
45 }
46 for (int i = 0; i < static_cast<int>(layer.texts.size()); ++i)
47 {
48 const UIBaseData& uiObject = layer.texts.data()[i].baseData;
49 const UniversalInstanceID id = layer.texts.dataKeys()[i];
50
51 if (uiObject.exposeToHeader == false)
52 {
53 continue;
54 }
55
56 exposeToFile(uiObject, id, UIType::Text);
57 }
58
59 for (int i = 0; i < static_cast<int>(layer.buttons.size()); ++i)
60 {
61 const UIBaseData& uiObject = layer.buttons.data()[i].baseData;
62 const UniversalInstanceID id = layer.buttons.dataKeys()[i];
63
64 if (uiObject.exposeToHeader == false)
65 {
66 continue;
67 }
68
69 exposeToFile(uiObject, id, UIType::Button);
70 }
71
72 for (int i = 0; i < static_cast<int>(layer.sliders.size()); ++i)
73 {
74 const UIBaseData& uiObject = layer.sliders.data()[i].baseData;
75 const UniversalInstanceID id = layer.sliders.dataKeys()[i];
76
77 if (uiObject.exposeToHeader == false)
78 {
79 continue;
80 }
81
82 exposeToFile(uiObject, id, UIType::Slider);
83 }
84
85 for (int i = 0; i < static_cast<int>(layer.checkboxes.size()); ++i)
86 {
87 const UIBaseData& uiObject = layer.checkboxes.data()[i].baseData;
88 const UniversalInstanceID id = layer.checkboxes.dataKeys()[i];
89
90 if (uiObject.exposeToHeader == false)
91 {
92 continue;
93 }
94
95 exposeToFile(uiObject, id, UIType::Checkbox);
96 }
97
98 layerIndex++;
99 }
100
101 fileContent += "\n" + tabString + "}\n" + tabString;
102 }
103
104 fileContent += "\n}";
105
106 const FilePath headerPath = Tga::Settings::SourceRoot() / "Goose" / "UI" / "CanvasReferences.h";
107 std::ofstream outPut(headerPath, std::ios::out | std::ios::trunc);
108 outPut << fileContent.c_str();
109 outPut.close();
110}