Godot Version
4.6 (although most of this discussion applies to other versions as well)
What this Post is:
This post can be read as:
- A tutorial on how to structure nodes and classes in Godot
- A question about how good code should look in a best-case scenario
- A discussion about how we should create and document architectural patterns.
TLDR
Writing good code is hard. You can do so using composition and making nodes as independent as possible.
Hoveringskull suggests a design pattern which creates a lot of dependencies in this video. (Dependency injection)
While I am personally not really convinced, I do struggle with the problems he is addressing and wonder what your opinion on it is.
For coding myself I have a few basic rules by which I judge my code quality:
- Nodes should never know about their parents
Importantly impling: - Nodes should never assume that their parent has a specific property or initializes them in any way
- If your code is simple to understand, it is good
- If you need to copy over code, you have bad code architecture.
Would you agree / disagree with me, or have any other golden rules to follow?
Dictionary
If you are new to coding or Godot, some of the words used in this post might not be familiar to you. Therefore, I made a list of helpful links here that explain them. If you need to look up a word, you can do so here. Or look it up yourself. Or even ask a chatbot. (They are usually pretty good at this kind of task) If you have a resource for learning one of these things that you like, feel free to share them with me so I can add it to the list. I promise looking at this will help you a lot in your game dev journey in the long run.
- Dependency
read - Dependency injection
watch
read - Software or Code Architecture
I have learned about this in my cs classes in uni. So I cannot really recommend any videos on it…
read - Composition
watch - Inheritence (In Godot Extending)
read generally
The origin
I recently watched hoveringskulls amazing video about code architecture. Most of it was about patterns I have heard of or have already been using in Godot. But this section presented an idea that went completely against what I had previously thought about what good code structure in Godot is.
Where I came from
I originally switched from Unity to Godot and had the problem everybody has when introduced to Godot’s system. Struggling because of the inability to attach multiple scripts to one node.
While I had tried to understand the new environment with the help of the docs (which did help a lot in many ways), my confusion about this topic specifically only really cleared itself up when I found this amazing video about composition in Godot by Bitlytic.
Starting from this point, I coded by one simple golden rule
Nodes should never know about their parents
Importantly impling:
Nodes should never assume that their parent has a specific property or initializes them in any way
When said like this, it might sound complex. But if you look at practical examples, it becomes very obvious what these rules mean and what benefits come of following them:
Bad Example
Let’s say I have a player, and I want to give them HP and an HP bar:
I could simply write all the code inside the player’s script.
class_name Player
extends CharacterBody2D
## Handles the player movement and the hp and the hp bar
But what if I later also want an enemy that also has HP and an HP bar.
I could not just give the enemy the player script because the enemy lacks some other properties of the player, like, for example, being movable by the user.
class_name Enemy
extends Player
## Now the Enemy has hp and a hp bar
## But it can also be moved by the player
I could fix this problem by extracting all the movement code into a function and then overwriting it in the enemy code like this:
class_name Enemy
extends Player
## Now the Enemy has hp and a hp bar
## But it can also be moved by the player
func _move_player(delta: float)-> void:
pass
But I also judge code by the following:
If your code is simple to understand, it is good
I believe this to be the best way to judge code architecture.
Now is it really intuitive or simple that our enemy is extending the player (which in non-code terms basically means that the enemy is a more specific version of the player)?
I wouldn’t say so.
There are other disadvantages of this approach as well:
A script can only extend one other script. So if you use this pattern, you can adapt code from one class. For example, the enemy can get the HP bar and the HP from the player. But if you also have an obstacle that can deal damage to the player, you cannot also adapt that code to the enemy since you are already extending the player.
class_name Enemy
extends Player
extends Obstacle
## This is not possible You can only extend one script at a time.
This means you will be forced to copy over the damage-dealing code from the obstacle to the enemy. If you now want to refactor (coding term for change) this damage-dealing code, you will always have to do it at least twice. (And potentially a lot more if you also copied it over to other patterns.)
Generally:
If you need to copy over code, you have bad code architecture.
But let’s stop looking at bad ways to do code and start looking at the way I do it now:
Example 2: How I used to structure Code:
Let’s say I have a player, and I want to give them HP and an HP bar:
I can make an individual node for both HP and the HP bar.
This is already a lot better compared to the previous approach because if I now ever want to give another Node (like for example an enemy) an HPBar or HP I can simply attach these nodes. The Power of composition!
But unfortunately, reality makes life a bit harder than that. Because if the HPBar is supposed to display the HP the HP node saves, we somehow have to create a connection in between them.
There are multiple approaches to this. I am going to rank them by how much I like them, starting with the worst and ending with the best. (higher number equals worse):
3) Adding a hp variable to the player:
class_name Player
extends CharacterBody2D
var hp
This by itself will do nothing. This means we have to access it from the HP and HPBar nodes:
class_name HP
extends Node
@onready var player: Player = $".."
func take_damage(damage: int):
player.hp -= damage
This breaks my rule of never letting children know what their parents do. This is an exelent example of why breaking it is not a good Idea:
Now the HPBar once again is only usable on the player specifically.
There are ways to circumvent this issue, but they all feel like workarounds. There has to be something better for something as simple as adding HP!
There is also another problem. The HP bar has to somehow know when the HP variable changed. There are a couple solutions for this, but here is an example with signals and setters:
class_name Player
extends CharacterBody2D
var hp:
set(new_hp):
if hp == new_hp:
return
hp = new_hp
hp_changed.emit(new_hp)
signal hp_changed(new_hp: int)
Now you can simply subscribe to this signal from the HPBar class.
Not that this is quite complex code that we would have to paste over to every single class that uses the HPBar even if we were to use workarounds to make it possible to use with other classes.
2) Adding the HP (or HPBar) object to the Player
class_name Player
extends CharacterBody2D
@onready var hp: HP = $HP #Do this
#@onready var hp_bar: TextureProgressBar = $HPBar - Or this
This still breaks my rule. But I find it better because there is no need to add the hp_changed signal to the parent Node anymore since the hp_bar can directly access the hp node through its parent. This means the hp_changed signal can be defined in the HP script itself, which I find much more intuitive. It also means less copied code.
However, not only is there still some duplicate code, but it is also not that intuitive.
There are 2 theoretical approaches:
- The player has an HP variable, and the HPBar Node is responsible for accessing it and using it to update itself to display the current hp.
- The player has an hp_bar variable, and the HP Node is responsible for accessing it and updating its display to match the current HP.
In this case I happen to prefer the first approach heavily, since an HP bar doesn’t make sense without HP, but HP does make sense on an object without an HP bar.
But there are many other examples in which this is not so clear, and if there is no good intuition to understand why something was done a specific way, it tends to be harder to understand. As my rule states, this is a sign of bad code architecture.
1) Writing getters for the other Node within the child Nodes
In our example, this could look like this:
class_name HPBar
extends TextureProgressBar
var _hp: HP
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
for node in get_parent().get_children():
if node is HP:
if _hp:
push_error("Multiple HP object found by one hpbar")
_hp = node
if !_hp:
push_warning("Hp bar attached to an object without hp. Disabling bar")
hide()
This approach has two crucial advantages, which make it my favorite so far:
- This is fully independent of the parent. It means we can attach this to ANY node, and it will just “magically” have HP/an HP bar.
-
- also means no duplicate code
Unfortunately it still has disadvantages:
I don’t like to use get_children() like that because it iterates to all possible children of the parent. This means it is an unlimited performance dump.
Luckily we have @export or @onready as an alternative:
class_name HPBar
extends TextureProgressBar
@export var _hp: HP
@onready var _hp: HP = $HP
# @onready var _hp: HP = %HP you can use access as unique name
# But I dont find it necessary here.
func _ready() -> void:
if !_hp:
push_warning("Hp bar attached to an object without hp. Disabling bar")
hide()
If you use get_children() @onready or @export comes down to personal preferences.
But as I said previously:
get_children()comes with (most likely) minor performance problems@exportwill annoy you (I promise) if you use it a lot because you have to assign variables in the editor manually.@onreadymakes the code depend on your node’s name, which can create unpleasant surprises when you are refactoring.
hoveringskulls problem
Hoveringskull does not directly speak of the things I have so far mentioned. But what he does is seemingly completely discard the main advantage of the third method of composition I mentioned. (Writing getters for the other Node within the child Nodes)
He intentionally makes the children depend on their parents to solve another issue, which I had originally solved with Singletons.
I will demonstrate the issue with an example:
Lets say we want to have a mechanic in our game like the f1 key in minecraft.
A key that hides all UI elements of the game.
Let’s say I have decided that the HP bars should be such a UI Element.
How do I do that?
I have a couple of Options.
- use another Script a static variable.
class_name UIState
extends Node
static var ui_toggled: bool = true
func _input(event: InputEvent) -> void:
if event.is_action("ui_toggled"):
ui_toggled = !ui_toggled
You could now just use ''UIState.ui_toggled" from your hp_bar script. (Or any other)
This does sound tempting, but the issue is that there appears to be no good way of finding out when this variable changed due to the lack of static signals in Godot. There are workarounds to this, but once again I would prefer something that feels like it was intended.
- Use a static var inside of hp_bar (and all other ui)
Since you cannot call non-static functions from within static ones this has the same issue as 1. - Use a Singleton.
extends Node
## This is a autoload / Singleton named UIState
signal ui_toggled(toggled_on: bool)
class_name HPBar
extends TextureProgressBar
var _hp: HP
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
UIState.ui_toggled.connect(_update_visibility)
#(...)
func _update_visibility(to_visible: bool)-> void:
if to_visible:
show()
else:
hide()
- Hoveringsculls approach
More regarding that later.
So far I have been using approach 3. It has the great advantage of keeping my nodes the property of being completely independent from the parent.
But it does come with disadvantages, which have cost me many, many hours so far:
- Global accessabilty
Every script everywhere can access a singleton at any time. This isn’t particularly popular with programmers because we usually try to make it so as few scripts as possible have access. The more you program, the more you will notice how much easier this can make your life. - In this particular case, the high coupling of different nodes (in contrast to the desirable decoupling) that comes with global accessibility creates racing conditions. In my experience this has been the biggest problem I have had with Singletons. I will provide an example:
Let’s say we decide that for whatever reason we want to have a level in which the UI is toggled off by default. In theory this is easy with our singleton. We just do this:
func _ready() -> void:
UIState.ui_toggled.emit(false)
But we now introduced a racing condition to our game.
If this _ready() is ran before the one in the hpBar:
func _ready() -> void:
UIState.ui_toggled.connect(_update_visibility)
The HPBar will be left visible!
And this is just one example of many suchlike bugs.
One problem with these Singleton race-condition induced bugs is that they are very hard to debug. From your perspective, you will just see that the UIState has been successfully toggled off, but for seemingly no reason at all, (some) HPBars stay visible.
I don’t know how much time I, personally, spend trying to fix issues that turned out to be another instance of this.
Hoveringsculls approach
Hoveringskull himself does a pretty good job explaining this himself. But for those who prefer reading over watching, I am going to summarize:
Hoveringskull tries to bring the dependency injection pattern into godot.
In order to do so, he splits his project into multiple “contexts.” These contexts are essentially nodes sitting at the top of the node tree that manage the game in different states. (For example, one context for being in the menu and one for being in the game.)
Since the children of contexts are naturally build for very different purposes, they will rarely share common functionality with other contexts child nodes but a lot of functionality with their sibling nodes.
In order to handle this shared functionality. (Like, say, a toggle for all UI elements in a context.) The context has so-called “services” as child nodes. Every (and I mean every) script has at least a bind_services() method, which accepts all the services this particular node needs.
Say, for example, my HPBar class needs the UIToggleService.
In then defines its bind_services() to require the UIToggleService. It is then its parent’s responsibility to call this bind_services() method when the HPBar is initialized. Say this parent is the player. The player itself does not need the UIToggleService. But since it has to get the UIToggleService from somewhere before passing, the player itself has to ask for the UIToggleService as well.
class_name HPBar
extends TextureProgressBar
var _hp: HP
var _ui_toggle_service: UIToggleService
func bind_services(new_ui_toggle_service: UIToggleService):
_ui_toggle_service = new_ui_toggle_service
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
_ui_toggle_service.ui_toggled.connect(_update_visibility)
class_name Player
extends CharacterBody2D
#@onready var hp: HP = $HP #Do this
@onready var hp_bar: HPBar = $HPBar
func bind_services(new_ui_toggle_service: UIToggleService):
hp_bar.bind_services(new_ui_toggle_service)
I must say the more I look at this approach, the less appealing it seems to be.
It comes with the theoretical advantage of no longer requiring singletons, meaning that we can expose services that act like it to only a select amount of classes. To be exact, every class has to specifically ask for the permission to access a service.
This does sound nice, because it will make us as coders aware of which dependencies we create and reduce the chance of circling dependencies or creating racing conditions.
But:
- Racing conditions are still possible. So we haven’t solved this issue.
- Now EVERY child depends on its parent. Meaning if we ever want any child to do anything, we will have to modify the parent and all of its parents to pass on all dependencies all the way to the bottom. This sounds incredibly annoying. It also goes completely against my idea of simply attaching an HP node to any other node and it suddenly having HP.
To be honest, it is fair that not every node needs the ability to have HP. Say a node is responsible for playing music. It is rather unlikely that I will ever want it to have an HP bar. So perhaps it is even desirable for it not to have this ability because it also means it will be completely independent from all scripts that manage things such as HP bars. In practice, my services will probably also be more generalized. Meaning that they have more functionality than just toggling UI. So perhaps the player will already have most of the services that its children could possibly need, and the refactoring necessary for any node will usually be pretty minimal when compared to literally going up the entire scene tree.
Maybe Hoveringskull is also right when he says that a long scene tree itself is a sign of bad scene architecture (it does introduce lag to have too many nodes after all).
In this case, perhaps there are usually not that many layers of passing on services to go through.
But still the idea seems a bit strange to me. The advantages that it provides are definitely desirable, but the cost seems very high.
Final thoughts, questions, and suggestions
I would be very interested in what kind of solutions you have come up with. Do you have set structural patterns you follow, or do you just eyeball it?
Do you think Hoveringskulls dependency injection is a good idea?
Finally, no matter if you are new to Godot or programming in general or an expert already:
Have you encountered some of the problems I described here while learning Godot?
Have you done something a specific way and thought to yourself, “This cannot be the best way to do this.” Or: “There must be an intended solution for this.”
How have you addressed these thoughts?
Have you just ignored them?
Looked at existing codebases from others? (If you did this, I would be very interested in where you found these codebases.)
Have you watched YouTube tutorials to see how people do it?
My ideas for improving documentation
Personally, I found myself often going back to YouTube tutorials for things I already knew how to do. Just to see how someone else solves this and if I architecturally prefer their solution.
Although I must admit that I was never really satisfied with the learning resources I had.
The Godot docs barely speak about architectural choices aside from naming conventions, as far as I have seen. YouTube is always just a small part of a big project and usually doesn’t really show you what you would need for planning a bigger project. On the opposite. If you piece together code from various tutorials, you often end up with a mess of different architectural ideas and patterns.
I wish there was an extensive page in the docs helping us with the choices that we make.
I am thinking of something like the amazing Flutter app architecture documentation.
It introduces programmers to the MVVM architecture and explains why using it with Flutter is beneficial. It does this in small parts that explain why and when the shown concepts are useful.
It doesn’t show all architectural patterns possible, but it does introduce a health standard that new devs can go of of. I think something like it could greatly help with unifying people’s code, making Godot better for companies (since it is easier to hire programmers familiar with your code structure), and also better for solo indie devs looking for help. It could help make big games without creating too much of the infamous game spaghetti code.
I do realize that Flutter is a Google project and cannot directly be compared to an independent project like Godot. But with all the amazing things we have already achieved in the documentation, I feel like we could do it.
I would start writing something like this myself. (And I have with this post), but I don’t quite feel like I know Godot’s intended way of coding well enough. And I can’t help wondering if the lack of such a page in the Docs is because of a lack of time, a lack of demand, or simply because no one really knows what architectural patterns are best to use in Godot.
(By the way, if I write “intended”, I mean things like using the Path2D in order to determine random enemy spawn locations.) The docs do show this off in the Build your first 2D game section. While there are other ways to do this, this one is clearly the best. Unless you have a specific reason not to, you should probably use it. Obviously the Godot devs don’t intend to force all users to do things the same way, but they do create easy solutions for a lot of problems, and right now it is all too easy to ignore them and take the hard way of doing it yourself.)
I think this last section might sound a lot more negative than I intend because I point out things I think could and should be improved.
But I do want to say that I, personally, find Godot, and especially GDScript to be absolutely amazing. I remember learning Java in uni right after learning GDScript myself and oftentimes wondering why things have to be so unclear, poorly documented, or hard to understand.
By now I have learned several other programming languages (including Python, C, C++, C#)
And I still remain a bit of a Java hater. (Why would I do something in Java if it is easier, more readable, more performant, more cross-compatible, and doesn’t require the user to juggle JDK versions in other languages?) But I have realized that a lot of my original dislike for it did not come from Java being bad but from GDScript being good. It is simple, performant, well documented, and beginner-friendly.
If I have a conclusion for this post, it is that I didn’t write it because GDScript or Godot or its documentation is bad, but because it is too good to not try to keep improving it.








