In this chapter, we’re going to add the User Interface (UI) to the game. Before that, I’d like to talk about how we’re going to build it.
We can follow some guidelines to avoid entangling our user interface’s code with the logic we wrote until now. We want to avoid coupling the interface with any other code, if possible. To do this, we’ll use signals from both the game and the UI widgets. We will try to keep the interface’s signals game-agnostic.
Note that I don’t necessarily recommend a complete separation between the user interface and the rest of the codebase.
You will find all the sprites you need to follow along in the course downloads below every lesson.
Here’s a quick look at the code structure we’ll work on.
Notice how our game will have two node branches and a separation between the bulk of the UI and the turn queue. Also, notice how we favor aggregation over other relationships. Most of our UI-related nodes will work independently of their parents and siblings. You could move them anywhere in the tree and they could still function the same.
This relationship can also make it easier to reuse code across projects.
The main dependencies in our UI are going to be either:
setup()
function on the corresponding element.For example, the CombatDemo will call the setup()
function of the UI systems in the UILayer directly. I didn’t include it in the graph above for readability.
You could have the CombatDemo pass all the data the UI needs to initialize itself to UILayer instead. While it would help to encapsulate everything inside the UILayer in theory, in practice, it doesn’t make that much of a difference because of how nodes work in Godot: either node can get a reference to all three UI widgets the same way.
To avoid coupling the user interface with the game logic, we want to avoid calling functions or accessing properties directly on game objects from the interface. For example, in a menu listing the battlers, we don’t want to get the battler.stats.health
directly. Doing so tightly couples the Battler class with the menu: if we later decide to rename the stats
or health
properties, we must change them in the menu’s script or the game will not work correctly.
With a single property, this can seem like it’s not a big deal, but when you’re months into development, and you have tens of thousands of lines of code, it can backfire.
One way to avoid this coupling is to avoid storing references to game objects in our interface. This way, we’re not tempted to use them. Instead, we can use a feature of the engine’s signal system to bind the reference to signal callbacks, allowing us to know, for example, which item corresponds to a given button without storing it in any script.
You will see a concrete example in the next lesson with the combat actions menu.
User interfaces tend to change a lot, in my experience. I’ve had projects where we had to redesign and reorganize the UI and, due to their code structure, it took ages.
You will want to move buttons around, turn columns into grids, add some margin in one place, or reorder elements. And you don’t want to have to rewrite much code to do so. That’s why I recommend to break down your user interfaces into bite-sized and self-contained scenes. Using small and composable objects generally works well with object-oriented programming languages and with Godot.
What do I mean by small? A UI component could be one button or an editable form field with a label. Another list component could then instantiate buttons and form fields to arrange them vertically.
With code that encapsulates each element’s logic and exposes a minimal programming interface, like a handful of signals and functions, it becomes easy to modify one without breaking anything else.