Hi,
I am not a player of your game, but I just wanted to reply to the conceptual GUI model. For the past 6 months, I've worked on the Azul GUI framework (
https://azul.rs/). Azul is mostly immediate mode but
it doesn't redraw at 60fps, so it's "retained mode", if you want to call it that. It uses a HTML-like DOM and CSS, while it's not production-ready, it might be worth a look for the conceptual theme. I created this framework to solve my problems in Rust (a competitor to C / C++), a speciality about Rust is that it has no inheritance, so any OOP-like approach like in many retained-mode GUI frameworks doesn't work because they heavily rely on inheritance. And it doesn't allow mutable pointers to be aliased. I set out to solve that problem with azul.
The problem with IMGUI-like frameworks is the layout. One component can't know about the size of the other components, because that would inflict on the purity of "one component = one function" rule. On the other hand, you have OOP-style GUIs where essentially the root window contains and knows about all of its contents. This creates two problems: first, the graphical details and resources are stored inside of the data model, conflating the application data with the GUI data. Second, communicating between two visually seperate components requires advance shenanigans, like child widgets having pointers to their parent widgets or relying on static data - and once you put threads into that mix, you have race conditions galore.
So to solve the layout problem, I disallow users to directly render-to-the-screen, but rather render into a tree-like data structure (a document object model or DOM). This tree structure provides me with enough information about the hierarchy of elements that I can use a constraint solver to position elements relatively to each other.
GUIs (or at least display lists) are tree-like datastructures, from a rendering standpoint. A root window contains some rectangles, which in turn contain other rectangles or text or images and so on. So it isn't too surprising on why I picked a HTML-like tree approach: It fits the problem nicely - even a retained mode GUI data model will eventually become a tree-like data structure if you view inheritance as a tree model (class A deriving from class B is similar to a parent node A having a child node B).
In your GUI, you highly rely on some global mutable renderer state, like the current text color. The reason I don't like this approach is because it's hard to debug which function set what state, it's easy to forget the cleanup (i.e. forgetting to set text_color back to black after you're done rendering the object) and it is generally thread-unsafe, unless you use mutexes to protect your global state, (which would decrease performance).
So for styling I went for an immutable CSS stylesheet. Immutable because it is only parsed and loaded once at startup and then never changed again. If you look at the web today, the major performance bottleneck isn't CSS or HTML. It's the massive amount of JavaScript on each page. CSS in itself is a decent solution because you can hot-reload it from a file for quick changes, you can easily share properties between components without buying into an entire OOP inheritance model and it can withstand API changes very easily - at one point I changed the entire underlying architecture of the CSS rendering with a huge decrease in memory, but without changing one single line of CSS.
Next "immediate mode" often gets conflated with 60fps game-style UIs, which is wrong - you can write things in an immediate mode way, but only redraw the screen when necessary. I do this by requiring the user to give me an `UpdateScreen` enum back so that I can see if the callback that was called changes the UI (so then the framework needs to re-render the screen). At max, azul consumes 1 - 4 % CPU and if the user does nothing, it uses 0% - so just because it's immediate mode doesn't mean it has to redraw itself at 60fps.
Lastly, you have the application data modeling, i.e. how do we store the data, how do we go about modifying and re-rendering it. This is where many frameworks go (in my opinion) the wrong way. Many retained-mode GUI frameworks do this:
Code: Select all
int main() {
auto my_gui = Window();
window.add_button("Hello world");
window.run();
}
The problem with this is that it doesn't scale to more complex UIs, it's what I call "optimizing for your Hello-World line count". Why is that? Well, essentially, these frameworks think that you just set up your UI once (in the main loop) and then display it and boom, done. But that's not the case, you often have dynamic content that changes state based on user interaction, like a user selecting different screens. This approach leads to very static UIs, i.e. UIs where changing the content of a window is a pain in the ass.
So in order to solve that I allow users to pass function pointers in the rendering (the `layout` method) that
have unique mutable access to the entire application state. Meaning, one callback can change the entire app state - so you have the problem of mutable-pointer-to-parent-widget solved and two visual components can communicate with each other because they both know about the same data model, but they don't know about each other (i.e. the button doesn't know that it increases a labels content, it just knows that it increases a number - and the label just knows that it should re-render itself with that number, but it doesn't know about the button itself).
Azul does not have the problem of 1 frame delays, because it's essentially: user input -> callback is run -> callback updates data model (your business logic goes here) -> rerender data model if necessary -> update screen with new rendered UI. Right now, azul has a frame time of 1 - 4 ms and I realize that this is too slow for games. But who knows, maybe someone can come up with a different implementation.
This model has drawbacks, especially regarding default methods (i.e. a text input that updates itself without the application programmer needing to explicitly write the updating code) and positioning elements based on dynamic positioning (something like JavaScripts `div.getComputedStyle()`). But both of these problems are solvable, but this post is already long enough. So yeah, those were my thoughts on the current state of GUI concepts. In practice (I use azul for an 2d rendering application of my own), it works very well. Of course it's in the early stages, but I thought it may be a good inspiration on how to (conceptually) build hybrid UIs.
Cheers