I figured out how to create components without nodes (and it is much better!)

The Problem

__________________________

(Note: this is a very long read so bare with me. I think it will be useful!)

Amidst my 4 years of learning Godot, one of the things I desperately wanted was an easy, smooth way to create component-based systems in Godot. I’ve always loved components and used the typical node-based approach for it. While it works well, I have a big problem with this approach: it’s not good when paired with scene inheritance.

Having made lots of games, some published while others never saw the light of day, I realized that you just only use components in your game, you need some inheritance as well. Imagine you have a class Enemy, and midway through development you wanted to add a new component to your enemies. Say, you wanted to add an XP component, so enemies drop XP on death. It should be easy enough, just create an XP component and attach it to your enemies. The problem is, you have already created 100 enemies so you need to go through all 100 of them to add that component… Ouch. You may try to circumvent this by making use of Godot’s scene inheritance, but that comes with its own problems. What if you wanted to change something in the original scene or script? Well, get ready for the component to be completely reset on all 100 enemies from that one small change, because Godot’s scene inheritance is literally Russian roulette. Really hope they improve scene inheritance in the future, but I think it’s high time I move on from them since this happened to me 3 times now on 4 different projects and I would have been livid if it weren’t for Git.

There are some other smaller problems too. Nodes are just not easy to manipulate in the Remote view and sometimes I just want to be easily able to access everything without having to go through the huge list of nodes. Even in the new Live Editing feature, if you select a node using the List Node option, all of its child nodes will be displayed including your components. I would much rather have the actually useful nodes in that list.

Additionally, setting up an Actor variable just to connect to the parent node of whatever the component is already a child of is redundant and involves unnecessary mouse clicking. Using nodes also clutters up the SceneTree, and I find that it is better to utilize the SceneTree for genuinely important nodes like Area2Ds that needs to physically exist in the scene. There is just no need for abstract components like Health, Input and Velocity to take up scene space. I know that nodes don’t have too much of an overhead but it would be better that these components are more abstract and let classes like Areas, CollisionShapes, Particles, Sounds, VisibilityNotifiers, etc. actually exist in the scene.

I want to be more involved in the code than the editor. I want to the components to be easy to setup. I want them to be more lightweight. I want them to be easy for testing. And most importantly, I want to be able to use inheritance AND composition smoothly.

Luckily, I found a much nicer, smoother, more lightweight approach for creating components using Resources, Classes and the _init() function!

Basic Implementation

__________________________

Let me demonstrate a basic example of how to create a new component in Godot without nodes. Note that this test is done in Godot 4.4.1. I haven’t tested this in older or later version but we aren’t using any new features so I believe this should work across other versions too.

Let’s think of a problem and try to solve it. Say that you want to create a Player class. You want to attach a bunch of components to this class, but for now let’s focus on one. We will try to attach a HealthComponent to the player so it can have some health.

We can define a HealthComponent class by using class_name. Unlike Nodes, the HealthComponent doesn’t actually need to exist in the scene, so we can extend Resource (or even RefCounted), since it is more lightweight than Node. I will give it a variable value which is going to hold the actual health value.

# health_component.gd
extends Resource
class_name HealthComponent

var value: int = 100

Then in my Player class, I will create a new instance of this HealthComponent by simply writing HealthComponent.new(). Assume that the player is a CharacterBody2D, since it will need physics to interact with the physics environment. I will also add the _ready() function so that I can print the health value once the player node is ready.

# player.gd
extends CharacterBody2D
class_name Player

var health: HealthComponent = HealthComponent.new()


func _ready() -> void:
  print(health.value)

Once you run the game, you should see the output:

100

Perfect! We created a new instance of the HealthComponent class and it is printing the value in that class. Now, the question is… what if we wanted to initialize the HealthComponent with a different value? Instead of 100 what if we wanted to assign, say, 150? or 200? We can’t simply pass an integer into the new() method since that method doesn’t accept arguments. AT LEAST, not yet! This is where the _init() function comes into play!

The _init() function

__________________________

I always wondered how the _init() function worked, and what the difference between _ready() and _init() was. After fiddling around, I think I figured it out. The _ready() function is called when a node is a ready, ie. when a node has been physically instantiated into the scene. On the other hand, the _init() function is called when a class is ready. Or when a class script is instantiated. That is why the Resource class doesn’t have the _ready() function nor the _process() or _physics_process() functions, but every class has the _init() function.

Well that’s cool and all, but how do we initialize a class instance with different values? Very simple! In the HealthComponent script, we need to add in the _init() function. In here, we can pass in any argument we want to. In this case, I want to initialize a HealthComponent with a different health value so I will create hp as an argument and assign hp to value .

# health_component.gd
extends Resource
class_name HealthComponent

var value: int = 100


func _init(hp: int) -> void:
  value = hp

Now, in the player script, I can pass in any integer value I want to into the new() method when creating a new HealthComponent instance. You should even see autocompletion showing the arguments inside _init()!

# player.gd
extends CharacterBody2D
class_name Player

var health: HealthComponent = HealthComponent.new(150)


func _ready() -> void:
  print(health.value)

If you play the game, you should now see the new value of 150 as the value of the health.

150

If you want to, you can even set up a default value for hp in _init() so you don’t have to always pass an argument to new() when creating a new HealthComponent class, like so:

# health_component.gd
...
func _init(hp: int = 100) -> void:
  value = hp
...

And with that, we now have a working, independent component for health that we can attach to any other class we want. We could attach this to an Enemy class or Destructable class, and it is as easy as calling new()!

Extending Further

__________________________

You can do similar things you would do in node-based components with resource-based components as well. As an example, let’s implement some signals in the HealthComponent. Whenever the health value changes, I want to emit a signal called health_changed which takes in the old health and the new health. I will use a setter function in value that will emit the old and new health value and then manually update the old health value to the new one. I will also create a method called take_damage() which we can use to subtract health from.

# health_component.gd
extends Resource
class_name HealthComponent

signal health_changed(old: int, new: int)

var value: int:
  set(new_value):
    health_changed.emit(value, new_value)
    value = new_value


func _init(hp: int) -> void:
  value = hp


func take_damage(amount: int) -> void:
  value -= amount

In Player, I will connect to the signal and print the change between the old health and new health. In _ready() I will use take_damage() to subtract the health by 60 so the setter function in HealthComponent will be called, and the signal will be emitted.

# player.gd
extends CharacterBody2D
class_name Player

var health: HealthComponent = HealthComponent.new(150)


func _ready() -> void:
  health.health_changed.connect(on_health_change)

  print(health.value)
  health.take_damage(60)
  print(health.value)


func on_health_change(old: int, new: int) -> void:
  print("Health has changed | %s -> %s" % [old, new])

The output should print the initial health, followed by the change, followed by the new health.

150
Health has changed | 150 -> 90
90

As demonstrated, we can emit signals from the HealthComponent instance and also call methods from it as well.

More Components

__________________________

Let’s add a new component to our player. I will add an InputComponent which will take in inputs from the keyboard. Let’s assume we are making a top-down game and that our player can move in eight directions. We will create a method called get_input() which will return a Vector2 with x-axis and y-axis, which contains the resultant of the four directional keys.

# input_component.gd
extends Resource
class_name InputComponent


func get_input() -> Vector2:
  var input_dir: Vector2 = Vector2.ZERO
  input_dir.x = Input.get_axis("ui_left", "ui_right")
  input_dir.y = Input.get_axis("ui_up", "ui_down")
  return input_dir.normalized()

One thing to note: since Resources do not have _process() nor _physics_process() nor even _input() and _unhandled_input(), we cannot do the check within the class itself. One solution would be to simply make the InputComponent extend Node instead. Note that this will NOT create a scene/node instance of the object in the SceneTree when you callnew(). Again, new() is only for instantiating a class, ie. its script. So it will not physically exist in the scene, but it does exist in memory. You can treat it exactly like a Resource but with the additional features inside the Node class.

However, if you want to continue extending Resource (or RefCounted), you can instead call get_input() from within the Player instead. I personally think this approach is better as well, because you may sometimes want to call get_input() while other times you may not want to (like during a cutscene), so calling it within the Player would give you more control of when that method is called, compared to having the InputComponent as a Node and having it run the method in _process() every tick.

Let’s implement this inside the player. I will create a new instance of InputComponent and then inside _process() I will call the get_input() method and print the results.

# player.gd
extends CharacterBody2D
class_name Player

var health: HealthComponent = HealthComponent.new(150)
var input: InputComponent = InputComponent.new()


func _ready() -> void:
  health.health_changed.connect(on_health_change)


func on_health_change(old: int, new: int) -> void:
  print("Health has changed | %s -> %s" % [old, new])


func _process(_delta: float) -> void:
  print(input.get_input())

If you run the game and hold down the arrow keys, you should now see the input result of pressing them.

Fun Fact: in this situation, you don’t even need to create an instance at all! We are not trying to save any value here (like we did with health), we are simply calling get_input() to get the immediate state of the arrow keys. We are not storing any value. So in this case, we can simply define the get_input() method as a static function, like so:

# input_component.gd
...
static func get_input() -> Vector2:
  var input_dir: Vector2 = Vector2.ZERO
  input_dir.x = Input.get_axis("ui_left", "ui_right")
  input_dir.y = Input.get_axis("ui_up", "ui_down")
  return input_dir.normalized()
...

This will allow us to call the function directly within Player, without creating an instance. Just write the name of the class and call the function (you should even see autocompletion once you define it as static).

# player.gd
...
func _process(_delta: float) -> void:
  print(InputComponent.get_input())
...

Notice in _process() we directly reference the class then call the get_input() method.

Benefits of this System

__________________________

The benefits of a resource-based composition system over a node-based system are:

  • (Best Advantage) it allows you to seamlessly use both inheritance and composition since everything is handled via code. You don’t have to rely on the scene inheritance which, from my experience of doing lots of refactors, requires a stronger foundation for it to be reliable.
  • Quick and easy to implement and also reuse in other projects.
  • It declutters the SceneTree of your nodes and only the most compulsory nodes have scene instances inside of them.
  • It makes testing easier as both the Remote tree view and the Live Editing view will be much cleaner.
  • You can export these class instances so they can be set in the properties panel (if you really need the editor) or even manipulated during debugging (for testing purposes).
  • Less fiddling with the mouse and more time in the code editor (especially useful if you use an external IDE like VScode or Rider).
  • Slightly better performance since Resources and RefCounteds are more lightweight than Nodes.
  • You can create multiple instances of the same component. For example, you can create two HealthComponents like so:… var health: HealthComponent = HealthComponent.new(150) var health_2: HealthComponent = HealthComponent.new(50) …player.gd

This can be useful if you want to introduce a unique mechanic, or you want to test something. If you were using a node-based approach, you’d need to create a second duplicate of the component again which would clutter your SceneTree even more.

  • Extremely versatile! You don’t have to stop at just components; you can use these for all sorts of things, since they are just abstract objects. Say you want to create an inventory system for the player. You could create an Item class which contains some useful information, like its name, then create instances inside of an inventory array. The items would be treated as objects and can be initialized by passing in arguments to the _init() function. If we assume that name is an argument and we want to initialize an Item with a name, then the inventory array could look like this:… var inventory: Array = [ Item.new(“key”), Item.new(“wood”), Item.new(“stick”), Item.new(“sword”), Item.new(“potion”), ] …player.gd

Drawbacks

__________________________

  • Unlike Python, we can’t set the argument of a specific value inside new(). If we have a huge list of arguments in _init() and they have default values, but we only want to change one argument, then we need to write the whole line of arguments before you can change what you need. So if you have a StatsComponent with Health, Attack and Defense, and you want to change Defense only, you can’t do StatsComponent.new(defense=def_value). You have to write the value for Health and Attack first before you can change the Defense: StatsComponent.new(hp_value, att_value, def_value). You can also change the value by using the dot notation, so stats.defense = def_value also works. This is another thing that I hope gets implemented in GDscript soon.
  • If the node needs to exists within the SceneTree, like Area2Ds and Audio, then you’d probably need to write a bit more code, since you need to instantiate the node and add it as a child to the scene you want. In this case, a node approach would work better. Perhaps you could use a node-based component system for these objects or simply code them in, but this would depend on what the developer is comfortable with.
  • Like highlighted above, Resources do not have access to functions like _ready(), _process(), _input(), etc. This can be circumvented by simply calling whatever method you want in the entity class your components are a part of inside of that entity’s _ready(), _process(), _input(), etc.

Conclusion

__________________________

This has gotten very long but I think you get the drill now. Personally, I will be using this method from now on for my projects because I find it more comfy than the node-based approach. I believe there is a proposal for Traits in Godot, so when that get added I might explore that too. I wanted to let others know so that we can start a good discussion of this here. Perhaps there are more advantages or disadvantages that I haven’t found out, so I’d like to know what others think. If anyone has any more knowledge, or if I had made a mistake, do put it in the comments because I’d love to learn more. Thanks for reading!

- Awf Ibrahim (Awfy)

19 Likes

Thanks for taking the time to write this, I found it very interesting. :+1:
I’m still new to Godot myself, but in the sense of this being a new game engine for me compared to many others I’ve worked in during past game projects.

You mentioned how it might be tedious or time consuming to set specific values in the new() component you create because it might have too many arguments. My question is, can you pass an array of these arguments that would contain all the information needed to the new component to be setup? If so, that might create a way to pass a single argument that the new component can later figure out what needs to be set versus the defaults?

If it is an array, you’d still need to know which value is what. In the StatsComponent example, if the stats included Health, Attack and Defense, in the array, you’d need a way to figure out which element is which. So I’m not sure how that would help.

I guess you could use a Dictionary instead? So you could have a dict like:

{
   "health": 120,
   "attack": 20,
   "defense": 5,
}

I suppose you could pass this to new(), so your init function would take this in and set each variable like:

health = dict.health
attack = dict.attack
defense = dict.defense

But then you’d need to individually check if a key exists in the dictionary. If you try to access a key that doesn’t exist you’d get an error, so if you pass in a dictionary with only dict.defense, you’d get an error because dict.health and dict.attack would not exist.

Honestly, best thing to do would be to declare defaults inside _init(), then later change the specific value you need by doing component.foo = bar in _ready().

Someone else on the thread could also let me know if there is another way to do this. I’m also a bit clueless on this too.

1 Like

Oh, I think you found the solution then with the dictionary because then you can leave things out that you don’t need to set from the default.

Example, I want to only pass the “health” and not worry about attack or defense values, my function that is always expecting the health, attack, defense can be simplified to have built-in defaults so that no matter which parts are missing, the function can still work. If I’m understanding how you have this setup, the new() can just wait for a dictionary, one argument entry should be enough to contain all possible keys that one may or may not use? My example should work, even if you send an empty like {} because it should all run in _init() if I am understanding everything correctly? I was just using the my_dictionary variable as a stand-in for what is passed to _init()

	var my_dictionary = {"health": 120}

	var health
	if "health" in my_dictionary:
		health = my_dictionary["health"]
	else:
		health = 120
	print(health)

	var attack
	if "attack" in my_dictionary:
		attack = my_dictionary["attack"]
	else:
		attack = 20
	print(attack)

	var defense
	if "defense" in my_dictionary:
		defense = my_dictionary["defense"]
	else:
		defense = 5
	print(defense)
2 Likes

Good read.

I’m not sold on the system you’re selling yet, because I don’t like things like health being components. It doesn’t make sense to me. I’ve been playing with resources for stats for characters, and I go back and forth on it. I’ve used this in the past to trigger whether or not a player has gotten an ability or not.

I do think all your arguments are valid, I just don’t think most of those things are things I would make with nodes. Still, really thoughtfully written.

I do like using _init() in certain cases. It’s just a constructor, but it is really useful at times. (It becomes more complex to use in nodes because of the way nodes get constructed.)

@knightmb if you want to pass variable arguments I’d go with passing a Dictionary. Also, you can very easily check if a dictionary has a key and operate on it that way.

In a component based system, using nodes for separating things like Health is quite common. There are a lot of tutorials on both Unity and Godot on YouTube showing how to do things like this.

This write-up is similar to that but in a more code based approach.

Yes I know. I’ve followed a few just to try out the idea.

I completely understand your approach from a technical point of view. The most compelling part is that scene inheritance is wonky. My question is more just I prefer inheritance over composition for things like stats, so I’m not sure I see the benefits. What I’m thinking is that perhaps it’s good because your code for handling health is self contained.

My state machine is node based. I have a hurt state that listens for the player to say it is hurt. I guess ultimately I’d just be tying either a signal to a signal, or I’d be tying directly to the health component signal. It seems overly complicated though.

Other than encapsulated code, what do I get having a health component?

If you are making a game like BOTW, imagine every entity that has health. It’s not just the player or the enemy, but the trees, the grass, the items, the boxes, the animals, etc. they all have health or hitpoints. They’ll all have HP, but they will use it in different ways. You enemies may have health, attack and defense, but your trees may only have health.

If you use inheritance for your stats, how would you separate the health part of your stats to your trees? Do you just create a separate health variable for trees? What about animals, just another health specific to the animal class? Wouldn’t you then have to reimplement the same health related methods, like taking damage or displaying a health bar, to your new classes? If you have to do this, it means you’d need to overthink about your inheritance structure. You need to think about which classes in your game will have health as a stat otherwise you’d have to go through the trouble of refactoring your inheritance again.

By making health a component, you can easily attach it to any class as it’s own object. You can implement health to any class without worrying about inheritance or reimplement the same health functionalities again in another class. It’s generally better to keep your inheritance structure small. If you have a base class for enemies then you make enemies from that, that’s fine. But if your stats class is being inherited by every other object, you’ll be in trouble midway through development when you need to refactor your code or add in a new feature.

Another advantage is that it’s easy to reuse that component for other projects. Maybe one of your projects use multiple stats like HP, attack, defence, etc. but another project only requires HP. So it’s easy to copy-paste your HP component from your current project to your new project with very little friction. You may need to make small changes but it saves a lot of time and make prototyping very easy too.

4 Likes

Good points. I’ll keep them in mind. Thanks.

I read the article with intrest because i find most entity component systems clunky when managing many object types, for example a Pickup object might have a collision component, a mesh instance, and an ‘item’ with name, properties, inventory image etc. Then once picked up, the collision component is no longer useful, the ‘item’ can go into the inventory, and the mech instance isnt needed because the inventory only displays an image. Then, for example, if the item must be equipped then the corresponding scene with a mesh instance must be created, and the inventory item removed. I have seen this pattern several times and cant help thinking that the pickup, the item, and the equipped object should be unified into a single object.
When i tried to unify them the problem i encountered is that the collision object would still emit pickup signals and would still detect player collisions when the item was equipped, even when i tried to disable the feature in code.
And where should the collision shape /3d node go when the item exists (or doesnt exist) in the player inventory ? Perhaps to (0.0, -999999.9, 0.0).
How do you create these objects anyway - they should just be created with a mesh instance, the name, and other data, not multiple sets, and perhaps an image, or maybe there could be a 3d scene that renders the image automatically.
Ideally items could be created in script based on mesh resources.
Then of course you might get loads of junk objects hiding in player inventories or lurking in the world map, never being used, just wasting memory …

And there is often a problem with packed scenes in the scene tree with components that require a bunch of @export var’s to be set up - they cant be configured easily from the root node because of when the _ready() gets called, so it would seem like a good idea to make the packed scenes configurable and to inject the data from the root using _init() functions.
The might seem to violate the theory that centralized systems are bad, however.

I’m still very new to godot but isn’t that one of the main selling points about components? Like if you don’t need one anymore you could just set it to null when you’re done with it

Here especially with the example of doing it in code it would be simple to handle the event of once picked up, set to null. Idk how it would work in editor version but I imagine it would be just as easy

Yeah, thats what i did have in mind. Then when the item is dropped the component gets recreated. Then if entities are not nodes they cant be parented to other nodes, i.e. right hand bone attachment. So the mesh component is parented and when drops it needs to know the item/entity (i.e. they both contain a reference to the other). I was theorizing that human language describes inventory items in the order item->model, then when dropped it becomes model->item. For example when thrown the model needs a rigid body component and it flies off into the scene.

Of course the item could be equipped into a special inventory slot representing the right hand instead.

I suppose that was a bad example of why entity component systems are ‘clunky’.
A better example is the packed scenes with internal components that have to be modified inside the scene. Thats why i thought the original post was a good read.

I think you should contribute this to the godot documentation as comment to the scripting topic. It’s a must read for newbie scripters. Scripting in godot docs is not covered well enough. You could do more powerful things with RefCounted and internal class scripts. Btw. I liked your approach, because I’m doing the same thing form the day one with gdscript :+1: .

1 Like

@awfyboy

I think you might want to try writing your _init something like this:

func _init(settings: Dictionary) -> void:
	# set default values
	value = 100
	# override the defaults with values from the dictionary
	for setting : String in settings.keys():
		if setting in self:
			set(setting, settings.get(setting))
		else:
			print("Invalid setting: ", setting)

It then shouldn’t really matter what you pass in the dictionary, it’ll just process the variables the resource actually has and “ignore” the rest.

4 Likes

Heyo, I made an account just so I could join the conversation. Sorry, it’s kind of a long read :confused:

What are the methods I could use when linking components together or more specifically, using components like HurtboxComponent or HealthBarComponent?

I assume the obvious way of going about that while avoiding coupling and children grabbing references from their parents or siblings, would be to use the parent class to tie everything together. Components hold data and functions but they shouldn’t do anything on their own, that’s what makes them truly modular.

So the parent class, in this case, the Player class, would instance a HealthComponent (Resource) and then a HealthBarComponent (Node) and a HurtboxComponent (Node) would be attached via the Player’s scene tree.
Then after creating references to those node-based components, the Player class would then have to get the data from the HealthComponent (probably via the health_changed() signal) and update the node components accordingly. Though I suppose the HurtboxComponent would be attempting to update the HealthComponent rather than the other way around.
So to sum, func on_health_changed() → void: healthbar_component.value = health_component.value and same with max_value. Then probably another signal that connects to the Hurtbox_Component in the same way as the HeathComponent so player_harmed() which would probably be sending us the damage values that the enemy’s attack contained so we want to use that data on take_damage() function on the HealthComponent so in func on_player_harmed(incoming_damage: float) → void: health_component.take_damage(incoming_damage).

All that is fine but I was wondering if there is an alternative way in which I could simply remove and add components as I like with minimal code alteration. If I wanted to remove the HealthBarComponent, I would first have to delete the node and then go into the Player class, delete the reference and then delete the corresponding code in the on_health_changed() function (unless there is an easier way?), if I wanted to delete the HealthComponent, I remove the reference/instance and then the signal connector and then finally the line in on_player_harmed().

All this just has me thinking, is it not way simpler to break the conventional “good coding practices” and instead have the node components do the logic on their own? The HealthBarComponent will ALWAYS need a HealthComponent, no? So why not just make it have a dependency for it?
Unless of course you want the flexibility to feed the HealthBarComponent whatever information you want.

Backing out of the topic of Resource components for a second. If the HealthComponent existed in the scene tree. I could add a HealthBarComponent and a Health Component, then simply drag the HealthComponent into an @export on the HealthBarComponent. Done. No messing around with spaghetti in the parent class. Say I want to add a HurtboxComponent, simply drag the HealthComponent into another @export variable and boom! Now the HurtboxComponent can directly call the take_damage() function.

Obviously there are many problems with this method, one being, these dependent components can’t exist without a HealthComponent, the game would break otherwise. It expects a specific class and it’s specific parameters, functions, etc. The issue is that the dependent components require consistent namings.
We can’t swap out the HealthComponent with another of equal functionality because the parameters, functions, signals, etc. would be named differently.

In the original scenario, if I swapped the HealthComponent with a EntityStatsComponent, everything I would have to change would be on the parent class and not on the other components since they only contain data or functions and require a parent class to utilise them via sending them data of only the requested type.
I would only have to swap out “HealthComponent” with “EntityStatsComponent” and then maybe " .value" with " .health" or whatever since the namings are still different but the point is everything is controlled in the one place, on the parent class.

This a pretty enticing benefit but so is the speed and ease of use that coupled components have. Would you prefer safe code with longer setup and packup but more flexibility or would you prefer a less safe yet quick and iterative process that is less flexible and prevents you from mix and matching components however you like?

I should clarify, I’m pretty much talking to myself here.

Tbh, I think I’m gonna opt for the safer route. As much as I like the idea of being able to quickly iterate and potentially develop things like enemies and objects 3x faster, the safer flexible route could potentially save me a lot time as well from preventing stupid dependency related issues or having more flexible components that I could easily fit into whatever game or scenario.
Also cleaner code will save my future collaborates a headache.

I’m in the stage of developing my programming style but once I do, I just need to commit to it, that will hopefully mean that over time, I will hit less and less of these types of crossroads because I can simply just opt for the option that suits my overall style the best. Safer code means less time procrastinating and overthinking about whether x or y is gonna cause problems down the road or whatever but at the short-term cost of me having to actually learn how to write safer code.
I am a chronic overthinker (I know right, how surprising?) so I end up wasting a lot of time learning and not using that time actually getting practice or working on my games.

So back on topic. I demonstrated two methods of utilising a HurtboxComponent and a HealthbarComponent but only one of them utilise the Resource components that the original post was demonstrating.
Are there other methods to go about this, they don’t necessarily have to be good methods?

Also with the quicker node dependency approach I mentioned, is there a way to achieve that using a Resource HealthComponent? I can’t imagine how the Node components could get references to the specific instance of it without just searching for whether or not their parent has such a component via code which is even more problematic than the @export var health_component: HealthComponent option I suggested.

I suppose you could make it so that the components would throw an error if they couldn’t find a HealthComponent on their parent class and you could print a warning to the console saying there is a HealthbarComponent but it’s failing to find a HealthComponent on it’s parent class.

Hmmm, tbh that option doesn’t seem too bad, that would work, right? Once you see the error, you can check the seen and make sure the scene hierarchy is correct and confirm that the parent class has a health component. Seems like the best of both worlds… so what is the big downside here?

The children would be gathering data from the parent whereas it should be the other way around but could it cause a problem? If so, what kinds of problems? If the children are accounting for the scenario in which they couldn’t retrieve the data they were looking for, is there actually an issue? Is it with execution order of nodes? I genuinely don’t know… is it a bad idea to entrust child with accounting for errors in this way? Can it even reliably be done?

Don’t get me wrong, I’m absolutely gonna opt for parent classes doing all the work, I’m only curious. I still prefer the flexibility of independent components and it’s controlled converging space for all connected components that the parent class serves as.
It also lets you get a nice look at a manageable sized web, you can clearly see how the parent is functioning, what data it is using and what it’s doing with it instead of having it all in hidden across a whole assortment of coupled nodes.

It seems like an easy choice for large projects or team projects to go for the parent mastermind method and you could probably opt for the quick iterative method if you’re trying to make a tonne of similar content under a short deadline.

I just want y’all thoughts and ideas on the methods I mentioned or ones I didn’t mention, doesn’t matter if they’re better or worse.
It’s important to know what makes a game good but it’s also important to know what makes a game bad, that logic applies here too.

You’re overthinking it. Try it. Then try it the other way.

What makes you think that’s not a good coding practice? Components should do their own logic in an OOP system. It’s called Encapsulation. An object is responsible for its own stuff.

If you have a health component, it should have a damage() function, and everyone should call it if they want to damage the thing it’s attached to. It takes the information given and decides what happens. For example let’s say you’re making an RPG and you have damage types like fire, cold, and electricity. So when you call damage you would tell it what damage type it was as well. Then the health component would decide how much of that damage was applied based on resistances.

This was quite the brain dump. In the future, I’d recommend going back over your post and clarifying things once you’ve got all your thoughts down.

You said something that I can’t find to quote now, but it was basically you’re trying to figure out your coding style so you can stick with it. I want to caution you against this type of thinking. Basically, your perfectionism is showing. Perfectionism is procrastination with a positive spin. Perfect is the enemy of done. Your coding style will change based on the needs of the project. One size does not fit all. I prefer OOP because I learned it as a kid. But I still use Functional programming when it is warranted.

Try it out. See what happens. Learn for next time. You will end up refactoring (redoing) code. That’s ok. It’s how we learn.

4 Likes

Haha yeah overthinking and perfectionism are traits I’m constantly battling, I do my best work when I successfully ignore them and just let myself write bad code. No idea why I’m afraid of refactoring, it’s not like I haven’t done it before, it’s not easy but it’s also nothing unreasonable and embrancing it has been where I’ve made my most rapid growth in skill. Considering my learning efficiency in previous years, this year, where I’ve finally became aware of my perfectionist problem and how to avoid it, has been my best year for game development so far. Though still far off from my ideal work efficiency goal since knowing how to avoid something and then following through are two separate things. I’m in this for the long-term though so I’m not in a rush. Trying to rush it would just make me insecure ngl.

As far Encapsulation (thanks for lmk the name). I expressed my preference for the alternative. I prefer to minimise coupling and maximise flexibility. The speed and ease of encapsulation is great and all and I heavily used it in my last project for a factory-pattern spell-combination system and a status effect system. I was just messing around, seeing what I could do. I very much enjoyed using encapsulation but I feel the only purpose I will realistically desire it’s use for, is when I want to add an effect that can be applied to any node of a class and I want it all to exist via resources and spawn in dynamically. Meaning in the only case that I want to use encapsulation, I don’t even get to reap the benefits I mentioned before since I’m not using it to build scenes (though obviously there are still many benefits since I opt for it in this scenario). I have also tried using it in the scene tree way since that seems to be the common method taught on youtube in the godot community, it’s just not something I found myself to really care for. I haven’t made a game or project yet that needed a tonne of enemies or weapons or whatever and writing code like that hasn’t translated well to my other projects since with every project, I try to do something vastly different. If I had used the decoupled method with the parent mastermind, I’d probably still be using the same components several projects later. Don’t get me wrong, I absolutely do just cut out the parts of the code I want to reuse to make new components but not before my overthinking idiot brain decides to waste time contemplating.

We all got our preferences which is why I was asking for thoughts and ideas and not really asking for advice even if I may have worded it like I was, I’m not very bright, okay? haha.
While any response would contribute to my continued procrastination, I still think it is time well spent since I enjoy learning how other people accomplish the same tasks.
Generally, I was just going to disregard most of it but as I said at the end of my original comment “It’s important to know what makes a game good but it’s also important to know what makes a game bad, that logic applies here too.”
I just want to know all the ways composition can be tackled, the good and the bad, not for the purpose of me trying to find an all around best option because as you said and I agreed with, different methods serve different purposes, no one size fits all, I just to improve my understanding of programming all together and further grasp concepts, who knows, I might accidentally learn a new way to go about solving an unrelated problem.

However, that’s enough of that, this topic has consumed enough of my time for today. I need to yap less and develop more!!! I appreciate the response and absolutely agree with most of it!

1 Like

I think we have all been there at some point. Perfectionism IMHO is mostly a good thing.

Just remember that code that works is good enough. So just code it and don’t worry about it too much. Ugly code that works is just the same as super slick clever code that also works. The only difference is the technical debt you build for yourself if you ever want to update, optimise, maintain, develop or share your code in the future. It is only then that the technical debt becomes repayable, so something that should take a day or two to implement takes two weeks (sometimes longer) of deciphering spaghetti code and bug tracing and fixing.

Oh, and the rearing of the ‘what the hell is going on here’ disaster when some untraceable knock on side effect from all the ugly code manifests in your game in completely unexpected ways until you end up refactoring so much that you may as well start again!

6 Likes

@awfyboy big thanks for explaining that approach. I will defiantly try it out with new projects. :flexed_biceps:

BTW. are there any other high level design patterns / architecture design guidelines that you find especially useful for Godot?

I don’t know if someone mentioned this before but this is how I would do it (I haven’t written in GDScript for some time so excuse me if it has some errors, I hope you will get the general logic):

var stats: Dictionary = {
“health”: 100,
“etc”: 50,
}

func _init(dict: Dictionary) → void:
for i in dict:
stats[i] = dict[i]

You would send a dictionary with new() in the same format as stats. This way, you can change whatever you want without having to write every single stat.