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
![]()
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.

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.






