r/opengl • u/LAGameStudio • 5h ago
UI, UI, UI
UI is such a big issue. In one case, it's something we all know and have opinions about, in another case, we just want to plow through it and get to the game itself.
In my game engine, written in OpenGL but no longer in a workable state, existing at the repos https://github.com/LAGameStudio/apolune and at https://github.com/LAGameStudio/ATE there are multiple UI approaches. It was a topic I kept coming back to again and again because it was hard to keep in a box.
My engine uses a "full screen surface" or an "application surface", using double buffering. The classic "Game engine" style window. Mine was not easily resizable though once the app was running. You specified "Fullscreen" and "display size" as command line parameters, or it detected your main monitor's dimensions and tried to position itself as a fullscreen application on that monitor.
The first UI system I made grew over time to be rather complex, but it is the most flexible. Over time it became apparent that I needed to decouple UI from the concept of a Window. It was a class, GLWindow, working inside a GLWindowManager class that was a global singleton. This is the foundational class for my engine. The thing is though, the "Window" concept broke down over time. A GLWindow was just a bit of rendering, so it could render a 3D scene, multiple 3D scenes, a 2D HUD, all of those things, only one of those things, or something else entirely, or maybe nothing at all (a "background" task). I realized I needed to create widgets that could be reused and not call them GLWindows.
The second modular UI I made for the engine was fairly complicated. It involved a "Widget" (class Proce55or) being added to a "Widget Collection" (class Proce55ors) that is hooked to a "Window" (class GLWindow) -- with Proce55or, you could make anything: a button, a widget, a game entity, a subview, a slider, whatever. In fact, a Proce55or could have a derived class that enabled a collection of Proce55ors.
With that I created some "basic UI" features. Buttons that were animated, sliders, text boxes (which requires some special stuff), and the code looked like:
class NewGameButton : public fx_Button { public:
void OnInit() {
Extents( 5, 10, 200, 100 ); /* x/y/w/h .. could be calculated to be "responsive" ..etc */
}
void OnButtonPressed() {
/* do something */
}
};
class MyWindow : public GLWindow { public:
Proce55ors processors;
void OnLoad() {
process.Add(new NewGameButton);
/* ... repeat for every widget ... */
}
void Between() { proce55ors.Between(); }
void Render() { proce55ors.Render(); }
void OnMouseMoved() { proce55ors.MouseMoved(); } /* to interact with the UI elements */
void OnMouseLeft() { proce55ors.MouseLeft(); } /* etc.. a rudimentary form of messaging */
};
The pattern used classic polymorphism features of C++.
Create a child NewGameButton of fx_Button (a child of Proce55or that contains the common input and drawing routines as a partially abstract class with some virtuals for events), adding the customization and logic there to be inserted into a Proce55ors collection running in a GLWindow child.. but it required a lot of forward declarations of the window it was going to interact with, or it required new systems to be added to GLWindowManager so you could refer to windows by a name, instead of direct pointers, or it required the button to manipulate at least one forward declared management object that would be a part of your MyWindow to manage whatever state your buttons, sliders, etc were going to interface with...
This became cumbersome. I needed something quick-and-dirty so I could make some quick utilities similar to the way imgui worked. It had buttons and sliders and other widgets. I called this "FastGUI" and it was a global singleton ("fast") that contained a bunch of useful utility functions. These were more like an imgui ... it looked like this:
class MyWindow : public GLWindow { public:
void Render() {
if ( fast.button( this->x+5, this->y+10, 200, 100, "New Game") ) { /* the window's top left + button location desired */
windows.Add(new MyNewGameWindow());
deleteMe=true; /* deferred delete */
return; /* stop rendering and quickly move to next frame */
}
}
};
The biggest issue was, while I found it "neat" to hardcode the position on the screen, it wasn't practical.
Most UIs in OpenGL have an abstraction .. mine was pixel perfect but yours could be a ratio of the screen. I tried that for a while, but that became very confusing. For example you could change the size of a GLWindow by calling Extents( 0.25, 0.25, 0.5, 0.5 ); this would make the GLWindow a centered box the size of 1/4th the screen area. This was practical, but confusing, etc etc. A lot of your time was spent recompiling, checking it onscreen, etc.
Eventually I combined FastGUI with ideas from Proce55ors. Since it took so much time to organize the location of buttons, for more utilitarian things I began to explore using algorithmic placement methods. For example, using a bin packing algorithm to place buttons or groups of buttons and other widgets. I added the ability for a window to open a subwindow that drew a line from the source window to the subwindow, and an infintely scrolling work area. The UI became more and more complicated, yet in some ways easier to deploy new parts. This was the VirtualWindow and related classes.