Discussing Architectural Patterns in Godot after 2 years of development

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:

  1. A tutorial on how to structure nodes and classes in Godot
  2. A question about how good code should look in a best-case scenario
  3. 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:

  1. 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.
  2. 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:

  1. 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.
    1. 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:

  1. get_children() comes with (most likely) minor performance problems
  2. @export will annoy you (I promise) if you use it a lot because you have to assign variables in the editor manually.
  3. @onready makes 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.

  1. 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.

  1. 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.
  2. 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()

  1. 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:

  1. 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.
  2. 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:

  1. Racing conditions are still possible. So we haven’t solved this issue.
  2. 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.

7 Likes

Will just say this about loose typed languages, they may make u type less code, but u pay the price later when u are unable to refactor when your project gets big.

U change something and clean up and did not know that something else still referenced it like in GDScript when u refactor signals if u change the approach.

Then when u run the scene u need it goes boom boom boom.

Basically u pay the price by ermhrm having a one night stand with no type safety not knowing if u got a free gift as a result.

I do agree. Which is why I turned on the warnings for not typing statically and therefore statically type everything.

1 Like

don’t @export work automatically? while @onready only when node loads into the scene?

1 Like

That is, as far as I know, correct. But if you have to assign literally hundreds of nodes manually to your @export vars you seriously consider just taking the slight performance hit. (at least it has been like that for me)
Actually thinking about this I am not so sure anymore. Looking at the docs

It has this warning for Resources:

Using @export variables for Resource objects makes them a dependency of the instance, meaning that all the resources referenced by @export variables are loaded when the scene containing the script is loaded. If you want to reference a Resource object but load it manually when you need it (which, for example, is often the case for PackedScenes containing a whole level), use @export_file or @export_file_path instead.

I don’t know to which degree this applies to regular nodes. But logically thinking, these nodes are only added to the tree at runtime, meaning that a reference to them can also only be obtained at runtime. But maybe someone more knowledgeable than me about the inner workings of Godot can answer this question.

Then just go make some games with it. Don’t ponder on abstractions of abstractions and systemization of systems.

Games are not libraries. They are kinda disposable. They can tolerate a bit of architectural imperfection.

7 Likes

Hm, that is definitely a valid point that I also have lived by for quite a while. I have always just kind of started creating.
But I also often suffered from this approach since things got very complex very fast, and I ended up spending a lot of time fixing bugs compared to actually adding the features I wanted to add.

1 Like

I actually agree with this. It’s not that you don’t try to write legible code, but having programmed in godot and used their system.

They have a different way of doing things, so u can’t do it the same way u would code a web app.

If u keep enough of an open mind, u will find u can be right that godot has its flaws but also it’s strengths.

Personally, I do feel a little snubbed though cause the type declaration occurs right at the end. Java’s is in front so yeah, code wrong sometimes.

Also lack of accessibility modifiers means I cannot lock variables and force the to be accessed through methods, sometimes u can forget u do special stuff like check if someone sets an elephant’s weight to -10 XD

Complexifying the structure by adding more abstraction won’t help with that. It may in fact make things worse.

2 Likes

I do (at least partially) agree and find it very interessting to see that point made by you. This is why one of my rules is:
If your code is simple to understand, it is good

But I assume what you are talking about is not this. I assume you rather mean that I should not even be thinking that much about if my code is good or not.
For me personally, it is definitely also partially that I enjoy structural thinking. I want my code to be clean and expandable even if it is not really necessary for the game I make.
That being said, the suggestions I am making for the documentation are less intended to make people think more about code architecture but actually less. At least my theory would be, that if you have a best practice to follow, you don’t have to spend as much time thinking if the way you do things is actually right. But perhaps that is also just a me problem. Anyways, complexifying structure does indeed sound pretty horrifying. Ideally, I would like to know the simplest way to do things rather than one of the very complex ones. Of course, what is the simplest depends on the individual case.

Thx a lot for your take on this. May I ask if you have worked on long term / complex games with your approach? It is a worry for me that the longer I develop the more structural weaknesses could hinder my progress…

1 Like

Games are complex by nature, one of the most complex types of software. No way around it. You can’t ever fully escape that complexity, no matter how much abstraction you throw at it. If you’re not used to deal with complexity you’ll always stumble. So develop tolerance to some levels of complexity and “mess”. A good approach is to split into as many independent systems as you can, and develop each separately, not thinking about others.

3 Likes

I have also many times over experienced that Godot does tend to have very unique problem solutions. This is actually part of the reason why I worry about best practices so much. I want to use the tools Godot gives me and not forcibly implement structures I know from other languages. I must say that I didn’t have very many issues with type declaration specifically. It was just never really a cause of bugs. (Perhaps that is because of my extensive use of Godot’s typing system idk.)

Hm, ok. So for me that really sounds like you are not a fan of patterns like the one Hoveringskull proposes since they would definitely have you thinking about them a lot.

there are 2 types of people of course

  1. They think “clean code? what is that?” and after month of 10 hours daily work they wonder “hmm why is my game impossible to make?”

  2. They think that simple cleaning of code is good, so they make abstraction system rivaling scientists from 22th century in both complexity and philosophical abstraction, later wondering “hmmm, that’s not a game, that’s an art” and they leave godot cuz of burnout :\

Right… ig it’s best to go with 1.

2 Likes

xD funny way to put it

Yeah, that totally makes sense. To be fair, that is kind of the point of game engines.
Theoretically, I could throw Vulkan API calls at my graphics card until it cries out a game, and if I do it right, that would be more performant than making a game with Godot. But it would also take half an eternity, and I would have little to no chance of ever getting something publishable or even playable. This is why game engine and, specifically, Godot devs do a lot of the hard, complex work for me. So why not extend this to code architecture and help me and everyone else with the complexity of that?

I feel u kinda got no problem to solve but to wax lyrical about good code. But answer is chess manuals basically teach u principles,but they are not laws of the universe.

With experience u realize that just sometimes, in some special cases, u can bend the rules a little.

Godot does have a tendency to have many ways of doing things, and to sometimes encourage abit of minor code duplication rather than trying to cut your scenes too thin.

Anyhow the principle is always, if the clean way doesn’t work, code the dirty way that does work first if u need to ship.

If u notice u duplicate similar blocks of code and this code is a large enough block to be a reusable function, abstract it out.

And the rule of Yagni, like for my rpg. If u don’t need it soon like in the next three days, don’t code it or optimize it yet. This was learnt from coding my rpg.

2 Likes

I agree . . . mostly. All rules have exceptions.

For example, I have a StateMachine class that is the child of whatever object it is manipulating. This is a choice based on the fact that it is a pull machine, not a push machine, and so architecturally it is like an engine inside the object, not a manager outside of it.

Likewise all its States also know about the object they are manipulating, which is in effect their “grandparent”. Again, they are part of the engine and they drive whether they are in control or not. However, I keep each State from knowing or caring about any other states that exist, which means they are atomic and adding or removing one can be done anytime, even runtime, and the rest of the machine is not affected.

Likewise, I have started using a Health component. While everything about health is contained in it, sometimes states have to know about it because the Hurt and Death states should play, for example.

I agree with the second part. However, again, it depends on what that node is doing. Basically, sometimes you need to have things that use a protected accessor - of which GDScript has no concept of. A node that is abstracted and encapsulated for the sake of re-use and simplification of the main node can and should know and expect certain things. For example that a CharacterBody2D has a velocity value. Or that it will take the responsibility of calling move_and_slide().

I would say it’s generally better. But working code is ultimately the most important thing.

I would rephrase this: If you need to copy over code more than once, it’s time to refactor. Architecture changes a lot. Good architecture is one that works.

Have you read our comments, rebuttals, and discussion about that very video in this thread here? Godot Architecture: 7 steps for more flexible, extensible, testable code (video)

Because while there were some good things there, there were also some things that we felt showed a lack of experience applying standard development paradigms to Godot and GDScript specifically.

This is an interesting complaint. Is that because you’re used to multiple inheritance in scripts instead of composition?

Interesting, but they glossed over another solution (or didn’t know about it). Both CharacterBody2D and StaticBody2D inherit from PhysicsBody2D. So, you could inherit from that. However, I don’t think you should be doing that, because Entity is too generic and object to inherit from.

The video also doesn’t discuss Scene Inheritance which is different from Object Inheritance through scripting. Looking at my scene from above:

It inherits from another scene:

Which inherits from yet another:

This allows me to use Scene Inheritance, Object Inheritance, and Composition through Nodes.

Having said all that, I do think it’s an excellent video describing the benefits of composition, and it has got me thinking about turning my HitBox into a component as well.

I think everything you said in this section is true, but oversimplified. Creating a character is messy. A good architecture abstracts out things that similar objects share and creates one place for that code to live. In my above example, the Character is also the base for the Player:

And the NPC:

Notably the NPC has a Health Component because the character code looks for one. But I have plans to take it out because an NPC can never be damaged. Which is why I have removed it from the base Character scene.

What you think is simple, and what I think is simple are likely not going to be the same. This is not an objective measurement, and so is useless IMO.

I would do none of the three things you suggested. Here’s the full code for my Health Component:

@tool
@icon("res://assets/textures/icons/heart.svg")
## A Health Component to add to players and enemies.
class_name Health extends Node

signal damaged
signal healed
signal zeroed


## Maximum Health
@export var maximum: float = 1.0: set = _set_maximum
## Current Health
@export var current: float = 1.0: set = _set_current


func damage(amount: float) -> void:
	current -= amount


func heal(amount: float) -> void:
	current += amount


func full_heal() -> void:
	current = maximum


func increase_max(amount: float) -> void:
	maximum += amount


func _set_maximum(value: float) -> void:
	maximum = value
	current = maximum


func _set_current(value: float) -> void:
	if value <= 0:
		zeroed.emit()
		value = 0
	elif current > value:
		damaged.emit()
	elif current < value:
		healed.emit()
	current = value

image

It tells anyone who wants to listen, what’s going on with it. For example, the Hurt and Death states connect to its signals. Anything that wants to interact with it has functions to do so. However there are not getters. Instead, there are signals. If you are not listening, you do not need to know what’s going on in the Health Node.

You’ll notice that my setters are private, and also separate functions. This is so I can extend them using inheritance. that’s because when the player’s health gets low, I want to play a sound. I also want to save the player’s health whenever I save the game. I also want to send a Signal through the Game object, which is my signal bus. The HUD can then pick that information up and display the number of hearts on the screen.

image

The HUD and Health component do not need to know about one another at all.

@tool
class_name PlayerHealth extends Health

@export var low_health_audio_stream_player: AudioStreamPlayer
@export var low_health_sound: AudioStream
@export var really_low_health_sound: AudioStream


func _ready() -> void:
	get_parent().ready.connect(_on_ready)


# Makes the hearts in the HUD appear.
func _on_ready() -> void:
	if not Engine.is_editor_hint():
		Game.player_max_health_changed.emit(maximum)
		Game.player_health_changed.emit(current)


# Save player health.
func save_node() -> Dictionary:
	var save_data: Dictionary = {
		"current": current,
		"maximum": maximum,
	}
	
	return save_data


# Load player health.
func load_node(save_data: Dictionary) -> void:
	if save_data:
		var loaded_maximum: float = save_data["maximum"]
		if loaded_maximum:
			maximum = loaded_maximum
		var loaded_current: float = save_data["current"]
		if loaded_current:
			current = loaded_current


func _set_maximum(value: float) -> void:
	super(value)
	#var temp = maximum
	if not Engine.is_editor_hint():
		Game.player_max_health_changed.emit(maximum)


func _set_current(value: float) -> void:
	super(value)
	#var temp = current
	if not Engine.is_editor_hint():
		Game.player_health_changed.emit(current)
	
	if low_health_audio_stream_player:
		if current < maximum * 0.166:
			low_health_audio_stream_player.set_stream(really_low_health_sound)
			low_health_audio_stream_player.play()
		elif current < maximum * 0.33:
			if not (low_health_audio_stream_player.stream == low_health_sound and low_health_audio_stream_player.playing == true):
				low_health_audio_stream_player.set_stream(low_health_sound)
				low_health_audio_stream_player.play()
		elif current <= 0:
			low_health_audio_stream_player.stop()
		else:
			low_health_audio_stream_player.stop()

The only thing that needs to know anything about interacting with a health component is weapons and healing potions.

Yes there are. They are just difficult to implement because you have to declare a static signal, then a non-static signal to kick off the static signal. It’s just easier to use an Autoload whose signals are easily available.

Use an Autoload. If you want to use a singleton, you should make an Autoload. Then you should try to refactor your code later once you got things working to see if you can remove it.

This is not the Godot way. You’re complicating things for yourself, and making your code messier than it needs to be. Use an Autoload.

Race conditions happen in Godot. They usually happen when you don’t understand the order in which things are created, added to the tree, and made ready.

Again, using Autoloads solves this problem, because they are guaranteed to be loaded - in order - before the rest of your game. But emitting signals from an Autoload is useless because there’s nothing there to listen to it.

There are solutions to this, but this post is so long that I’m kinda getting burnt out on replying.

Learn how things are loaded, and how to listen for the ready signal, and this will happen much less frequently - and when it does, you’ll know how to fix it.

I think I covered this.

No. It’s ECS-lite and it goes against the architecture of the Godot engine itself. It can work, but it fights the engine to make it work.

You should go look at my GitHub repositories.

I think you would have learned a lot faster if you had posted questions here as you went along instead of dumping two years of experience in one post. Because frankly, we would have helped you avoid a lot of wrong turns years ago.

One ring to rule them all is not the point of an engine like this. I like MVC for web development, but I’ve spent years of my life fighting people who didn’t want to implement it even though it made their lives so much easier in the end. Their web pages still worked before I showed up.

I love Java.

Meh. You just don’t understand how Duck typing works. It’s great for certain scenarios.

No.

No. You can access anything anytime. If you want it in the editor, use a @tool script. But I don’t really know what the question is here.

There are some tricks to this. For example, one I often give is:

extends Area2D

var damage: float = 1.0


func _ready() -> void:
	body_entered.connect(_on_body_entered)


func _on_body_entered(player: Player) -> void:
	player.damage(amount)

I don’t check to see if I got a player. I do that with the collision layers. No need for extra code, and if I do set the layers wrong - I immediately know because I’m told the ground does not have a damage() function. And I know what I need to fix. The bug never makes it far enough into the game for the end user to see it.

If I were going to create say a HitBox component, it would something look like this:

class_name HitBox extends Area2D

@export var damage: float = 1.0


func _ready() -> void:
	body_entered.connect(_on_body_entered)


func _on_body_entered(character: Character) -> void:
	var target_health: Health = character.get_node_or_null("Health")
	if target_health:
		target_health.damage(damage)

Do some game jams. That will help you temper this approach. Your game development speed will skyrocket.

3 Likes

That video was already discussed here some time ago. It just looks like forcing onto Godot some inadequate paradigms the author was used to from their previous experience. To make it worse, all of it is showcased on a very simple game that doesn’t really require any of it, without showing how this would scale to conquer some actual complexity.

All “architecture” discussions/tutorials on Godot games I’ve seen so far approach it in this very naive way by people who are just fresh out of reading some design pattern tutorials but don’t have much implementation experience with large codebases. And it never goes much farther than abstracting node arrangements under a character body and globalizing some signals, all showcased on a trivial mario clone.

The thing is, an average game of a medium size will have so many sub-systems of all kinds that there cannot be a single unifying architecture or an overarching pattern that’d universally reduce complexity. There’ll be so many different architectural problems to solve that you’d need to utilize all of the design patterns you know, and many you don’t yet know, including custom tailored improvizations.

So jumping into making a game armed with some premeditated set of “patterns” is almost always guaranteed to paint you into a corner. You’ll need much more flexibility and breadth than that.

Never predict abstractions, always extract repetition from the actual implementation in the problem domain. That way you’ll minimize your chances to get stuck. Because “getting stuck” is nothing more than realizing you’ve implemented a completely inadequate constellation of abstractions. Which can hardly happen if you extract instead of premeditate.

As for design patterns in general, never use them as a solution in search of a problem. Learn to recognize patterns in a problem domain and adapt them to that. One of the sure signs you’re abusing a pattern is your implementation classes having verbatim pattern names instead of names from your problem domain, exception being the generics.

If you end up having many bugs relatively early on, it’s not a problem of lacking the “right architecture”. It’s more likely just a matter of not knowing well enough how the engine works or simply not having enough general programming experinece.

7 Likes

I mostly agree with what dragonforge said here. I was about to say that I don’t love finding a component by its name. As names are easy to change and Strings can be error prone. But, I realized that you can set the names of nodes in code. And if you make the script a @tool it’ll even show up in the editor. Then you can just define constants for the name of each component.

So in short, I also agree with this. lol

1 Like