Let’s start with a brief overview of the final demo’s code structure.
You will have two main node branches:
Splitting the UI and the game world into two node branches helps to remind ourselves that we should try to keep it decoupled from the characters. Also, it ensures the UI always appears in front of the game area. Any damage label or the turn bar will always sit above the characters and visual effects. Note that some UI elements spawn inside the game world: the combat action menu on player turns and the target selection arrow.
Now, let’s start with a simplified diagram of the code you’ll write in this first chapter, that is, the game systems without the user interface.
The graph above shows the combat system’s main classes and their relationships. The diamonds represent a relationship of association, while the arrows represent a dependency or inheritance. In particular, the black diamonds represent composition, and the white ones are for aggregation, a specific kind of composition.
The graph shows that the demo uses composition more often than inheritance, an approach we generally recommend when working in Godot. This means that we break down our objects, like the battler, into multiple components that work together over implementing all the functionality in one parent class.
Godot pushes you to use composition by default with its node system where, where a scene is composed of many independent nodes. This allows you to design all sorts of entities without extending classes many times over, allowing you to create new, more elaborate components. That’s generally the point of using composition over class inheritance: it helps you reuse code more easily.
Note that I don’t plan the classes I’ll need in advance when prototyping a game. In the team, we may only write a graph like the above after finishing a prototype to help our teammates understand the code’s structure and the dependencies.
When writing a game, you never know what you’re coding exactly at the beginning. Even if you have ideas for the design and rules, they tend to change so much during pre-production that most of your time spent planning the code’s architecture may be lost.
It’s more productive, in my experience, to write simple code initially to test ideas and, if the players like the prototypes, to refactor and rethink the code structure as you go, creating strong foundations for more systems.
Here’s how the code will generally flow and articulate at runtime. This section is a birds-eye view of the demo’s behavior.
Let’s start with CombatDemo, the game scene’s root node. Its role is to connect the battlers to the interface and to check for victory conditions. This node represents the combat system conceptually; it is there to start and end the combat.
At the start of the encounter, CombatDemo initializes the user interface by passing it the list of battlers. Each component in the user interface has a setup()
function that takes some object as an argument to initialize itself. For example, the characters’ Heads-Up Display receives a reference to a battler and connects to some of its signals. This allows it to update the health and energy bars when the battler takes a hit or uses a special attack.
The actual combat, that is to say, the battlers attacking their oppponents, relies on the interaction between the battlers and the turn queue. As a child of CombatDemo, you have an ActiveTurnQueue node that contains a list of Battler nodes.
Each battler has a readiness
property that increments each frame. When it reaches 100.0
, the battler emits a signal indicating it is ready to act. The turn queue receives the signal and lets the character play a turn.
The ActiveTurnQueue allows us to coordinate the turns and keep the battlers disconnected from one another. It also makes it easy to change the way the timeline and turns work. For instance, stopping time when a character plays their turn is a matter of changing one variable on the turn queue.
We will see later that a lot is happening in the ActiveTurnQueue’s script. The clock always keeps ticking, making it more complex to schedule the battlers’ actions compared to turn-based systems. That’s because when you don’t stop time during a character’s turn, multiple characters can become ready to act at the same time.
A turn starts with the turn queue allowing a battler to act. Either the player chooses an action and target using menus, or if the battler has an AI node, the AI makes those decisions. Having AI as a component allows you to use any character or monster as a playable or AI-controlled one.
This part of the turn is where the code gets a little trickier. The turn queue creates an action object that it passes to the battler, and the battler consumes it through an act()
function. We will see how all of that works in an upcoming lesson. After the battler finished acting, it emits a signal, ending their turn.
We’re going to use coroutines, that is to say, functions that pause in the middle of their code and resume execution later. In Godot, you can use signals to coordinate two systems, pausing a method until another object emits a signal.
For the user interface, I’ve chosen to define a setup()
function on each system you must call from a parent node. At the start of the game, CombatDemo
calls setup()
on each UI system, passing an array of battlers. To limit coupling between the interface and the Battler
class, UI systems do all their initialization work in their setup function. They connect to the battlers’ signals to react to future changes. They do not keep references to the battlers directly.
Using signals limits coupling while keeping the UI code manageable.
And that’s a brief overview of the code structure towards which we’ll be working. I’ve kept most details out as we will look at each class and system in detail in the next lessons.