Wrapping my head around 1 node = 1 script

Hey, i have a fair bit of experience with unreal and unity and i’ve been messing around a bit with godot.
I have a bit of experience with python but far less than i do with C++ and C#.

In general, the more i can handle things from code, with as few strings to identify what i want to do and with as few manual connections done from the scene, the better i’d consider a solution to be.

I’m having a bit of a hard time wrapping my head around how to deal with the “1 node = 1 script” choice and i’d love to hear some thoughts.

Ok so, the problem:

In unity/unreal i can have for example a “spin” script, a “levitate” script, a “health” script and a “collider” script.
They respectively make the object spin constantly, levitate up and down, it can be damaged and it has a collider.
The first two scripts would just get the object’s transform and interact with the position and rotation.
Add all together, i now have a little flying enemy.

All is well, my “bullet” can, on collision, get the health script and if present it can deal damage to my enemy.

If i want i can make a “crate” by only having the health and collider scripts, or a “pickup” with just the levitate and collider scripts.

However, in godot i can’t have a node with all those scripts, instead my “flying enemy” node will have 4 nodes, each with 1 of those scripts.

I’m inclined to leave the top node (“flying enemy”, “crate” and “pickup”) without any script as a result.
This means my top node is just used for the transform, allowing me to add and remove components without messing anything up.
(quick side note: it’s unfortunate that i need a child node, thus a transform, for each script… I don’t need the position or scale of the health node for example)

The spin and levitate scripts have to take a reference to a transform, as they now have to affect the parent. Reasonable.
However it’s more complicated with my health and collider scripts, as i’ll be colliding with the latter, but i want to have the former take damage.

I could give the colliders a reference to the health script, but that doesn’t seem very good as there’s no reason for the collider to have the concept of health and damage.
Or i can operate under the assumption that everything is structured in the same way (empty parent, all components as child of it) and have the bullet script get the collider’s parent, and from it check if there’s a health script.
But it seems a bit stinky and error prone, it also means i can’t have colliders parented to each other as then the parent isn’t what i’d expect (eg: a character’s arm may want multiple colliders).

There is .owner, which gives me the root of the “local” scene (so the “flying enemy” and the “crate”), that may be useful here as it can replace using the parent (as long as i can set the owner if needed, in case i’m instantiating nodes at runtime).
I get the owner’s health script, make it take damage and it’ll handle the damage calculations.

In short the question is: what is “the godot way” to approach this kind of architecture?

1 Like

From what I understand is that your used to multiple scripts. Either you can put all the code in one script or you can have a tree of nodes each with one script and put your actual scene/object below those to mimic having multiple scripts (but that would only work to a very shallow limit I believe)

Let me know if I’m misunderstood this and I need to give this another read though!

Yeah, it’s a bit awkward for me right now to have a component based system (all in 1 script sounds terrible) but also having the components be forcefully separated.

In unreal and unity, i can separate based on whether a script needs to have a transform or not (actor vs component in unreal for example).

So if i want something to spin i’d give it the spin component, but in godot i’d have to give it a “spinner” child node that spins the parent instead. This already goes against the “signals going up” rule of thumb.

It’s not lethal, but it makes connecting scripts that belong together a bit tedious, particularly when two scenes are interacting with each other.

A good example is the bullet scene trying to damage the enemy scene through the health node.

There must be a mechanism that leads the bullet to get a reference of the health script, but ideally it should work without knowing how the enemy scene is structured (as using paths or finding nodes by their name is very error prone).

Getting the owner and finding the health script works as long as i can set the owner at runtime, but it also forces me to either place the health node in a specific point of the hierarchy (error prone), or find the node (which is slow).

side note: is really no way to have multiple scripts without having additional transforms? Idk why my health script needs a rotation or scale…

I’m not sure maybe you can find a plugin though!

Plugins can help customize godot to fit your needs and I’m there is a good chance someone has made one to have more than one script on a node

The base “Node” type does not have a transform.


For component style nodes you will have to have a fairly flat scene tree, or make use of metadata to link nodes together.

func _on_bullet_body_entered(body: Node2D) -> void:
    var health_component: Node = body.get_meta("Health", null)
    if health_component:
        health_component.health -= 10

I do not use component based systems in Godot, there is no performance benefit, unlike in Unity and Unreal. Godot is structured around inheritance for scripts and scene structure, while underlying systems like physics and navigation do pack their data tighter in a data oriented design.

5 Likes

@iv2b
I can offer no technical support, but I can offer an opinion on your perspective and development workflow.

Unreal and unity’s multi-script per object system, from my perspective, feels the exact same as having multiple child nodes with scripts supporting a main parent node.

Here’s how I visualize the system you want to incorporate:

Unreal/Unity:

  • Main object (Let’s call this the player for an example)
    - Movement script
    - Jumping script
    - Health script
    - Etc.
  • Use that object in any scene that needs it.

Godot:

  • Main parent node in a scene
    - Movement node with movement script
    - Jump node with jumping script
    - Health node with health script
    - Etc.
  • Use that scene in any other scene that needs it.

It looks and feels the exact same for me. I know we all work and structure our codebases differently, but it seems like you’re just not used to the Godot workflow yet.

Also, it seems pretty cumbersome to have every function be a different script. It seems better to make a script for one purpose, not separate scripts for functions of that purpose.

It’s pretty easy to have a player’s movement, jumping, and health be in one script in my opinion. And it’s not like you need to apply the jump behavior to another separate object. If you do, just make a simple jump function for a separate script in that object.

Like, when I made my server script, I didn’t need to make every function a separate script. As long as it was for the server, it went into the server script. And when I needed a client script, I made a client node and script that had one purpose using multiple functions: manage the client.

Also, if your have trouble keeping up what your functions do and where they are being called from, you can have self-explanatory code and/or add a simple comment. Plus, having scripts with 1 purpose include multiple functions to serve that purpose are easy to locate.

Your logic isn’t not a problem or a jab at you personally. It’s just an observation I had. Maybe your optimizing a little too early.

1 Like

How exactly would you do this?
It would be nice to know how to do it.

Would it be using a lot of “get()” or would it more be like a “head” and “neck” system when doing 3d first person system?

Let me know if the second part is a bit unclear because it sounds a little bit weird re-reading it

@Cha_Cha_Slide_Leader

That’s the thing, I wouldn’t!

If you actually wanted to do this, you’d use a series of interconnected signals that would pass data to other scripts.

So the jump script would send a signal to the main player script that triggers the main script’s jump function.

Line of logic:
connect jump signal to jump script -> press jump input -> emit jump signal from jump script -> pass jump data -> main player script receives signal -> triggers player jump logic.

Instead of doing all that. You could just have the jump function in the main player script.

press jump input -> trigger jump logic

1 Like

I’m very pleased to know this, thank you very much!

I’m not familiar with godot’s metadata, i’ll check it out tomorrow.
In general i try to avoid referencing through strings as one rename will break things, but we’ll see!

Why is there no benefit in godot unlike unity and unreal?
Inheritance is fine and has its uses, but using components forces me to write code that’s more generic, which i value.
It’s not going to be full ecs or anything like that, but architecturally it’s nice i think.

specifically performance benefit. In Godot manipulating the scene tree, even with transformless "Node"s is a fairly expensive operation, so if you tried to operate on components as Unity and Unreal do you would see worse performance than those other engines provide. This dives into the “full ECS” you are avoiding.

Yeah having child nodes with 1 script each is quite similar to having 1 node with multiple scripts in it.

It’s a bit more awkward to get to the siblings though and if your primary script doesn’t do enough by itself then you’re forced to split things.

For example, i can have a player controller. I sit down, code it, i can jump, wall run, slide, etc. dope.
However if i now need something that isn’t quite fitting for it i either have to make a child node+script or cram both things into a single script, so i’d consider it better to have the top node of the scene always be empty.

Of course not every function needs its script, it’s aok to do a movement script that does it all for example.

But using the movement script as example, maybe you notice the movement is jittery when going up stairs and ledges, so you want a 2nd script to shoot some raycasts and smooth the movement out.
Or you notice you get a bit of air time when going down steep ramps, so you may want a script doing some sphere casts for example, keeping you grounded.
You could bundle them up in a single script, but it makes perfect sense to have them separate too.

Similarly you may want your movement script to be its own thing, with a controller script that could either be listening for player input, or it could be a bot emulating that input for things like multiplayer games.

I’d be curious to learn about the overhead of having nodes with many scripts vs a single script.

Perhaps i could try it out in a scene too, instantiating a lot of nodes and seeing how many i get before i dip below 60.

Though i’d imagine that’s a very mild performance hit as long as i’m not trying to find parent/child nodes every time.

Also hey if godot can go full ecs i’m down!

I my opinion, that’s extremely unnecessary. You just don’t need another script for these things. If it’s not already built-in by Godot, you can implement it in the same script easily.

But I won’t stop you. You do you. :+1:

I’ve never used Unity or Unreal, so I’m not sure if this would be an equivalent.
You can create scripts that extend Object or RefCounted and add those to a Node.

For example:

class_name SomeThing extends Node3D

# This would allow you to assign the script via the inspector
@export var spin: Spin
# Or you could just new up
var levitate: Levitate = Levitate.new()

func _physics_process(delta: float) -> void:
    spin.some_function(delta, self)
    levitate.whatever(delta, self)

If your scripts all had a generic method like apply then you could even make this generic.

class_name MyBaseNode extends Node3D

@export var scripts: Array[Object]

func _physics_process(delta: float) -> void:
    for script in scripts:
        script.apply(delta, self)

This would allow you to add whichever scripts you wanted via the inspector and run them all on update.

Now is the where I point out I’ve not tested any of this and hacked those examples together directly in this comment. So they might have errors or issues, or this might not work at all. Hopefully it gives you an idea to try? :man_shrugging:

1 Like

I’m not a Godot (or a gamedev) veteran, but I think I looked for and use a similar approach to what you described (separating reusable logic into different scripts that can be combined - components) and asked a somewhat related question here so I’ll try and add my 2 cents as well.

First, I support what’s already been said by others - you can use the Node type for components that don’t need position and all that stuff. I think that all the Node type adds to the basic Object is the logic for being added to a scene/node hierarchy (which is what you need in Godot, if you want to be able to build complex entities out of components using the “Godot way”).

Now I’ll try and address some of your other concerns…

There’s a couple of ideas a had for this. One is to just search the children of an entity for a node of specific type. This shouldn’t be too slow unless you have many components per entity. I use something like this:

extends Object
class_name CompUtils

static func get_component(node: Node, comp_type: Variant):
	for i in range(node.get_child_count()):
		var child := node.get_child(i)
		if is_instance_of(child, comp_type):
			return child
	return null

Used like this:

var char_health = CompUtils.get_component(_character, HealthComponent)

Another way is to use Godot’s unique node feature which basically lets you assign a name to a node that’s unique in the scene and to get that node using that same name even if you move it somewhere else in the hierarchy (but still as part of the same scene). If it’s of interest to you, under the hood, the lookup is done using a hash map so it should be reasonably fast.

I went with the first option because in my current project I don’t expect my entities to have more than 10 or so components and, at the moment, I don’t have the need for my components not to be direct children of the root node for my entity scenes.

What I’ve also seen people do is have their “damage” component that has the collision shape that gets hit also have a property which is a health component that should receive the damage. That way, when you set up the entities scene, you need to remember to assign the health component to the damage component (you can also have a warning pop up in the editor for this), but there’s no lookup at all and you have the added flexibility of having multiple damage areas with different health.

This might not be that satisfying, but I don’t really see this being that wrong. In ECS, I’d imagine your “spinner” component would operate on the component holding the position which in Godot’s case is the root Node2D (or Node3D) node. If it was anything else (e.g. health), you’d just lookup the appropriate component in the parent. I also don’t think this is so “anti-godot” as you might think. For example, I think CollisionShape2D assumes to be a child of CollisionBody2D and registers itself as a shape. In other words, some nodes are designed to be used in combination with others. I also think a 2D component assuming it’s parent inherits Node2D is a pretty safe assumption. On the other hand, if the component isn’t for 2D specifically, the assumption isn’t needed in the first place.

Another idea that comes to mind is for the “spinner” component to operate on itself, but to put everything that needs to spin (e.g. a sprite) as children of that component. That way, the “spinner” wouldn’t have to go to it’s parent.

Finally, if it helps, I currently have a small (and so far still simple) project where a have different entity scenes with 5-6 components each. I’ve done tests with ~200 entities on screen and this worked without issues and most of CPU time went on physics and navigation anyway.

I hope this helps.

4 Likes

I think your reply is the most complete, so i marked it as solution as well for others who happen to stumble on the thread in the future.

Using Node instead of Node2D or Node3D is something that lessens the cost of going for components, which i like and others who have my same concerns may like too.

Using get_child is fine and probably the only way to do this, but i think the sauce is in having a owner node.

By default from what i could see, when you save a scene, that scene’s root node is the owner and the other nodes have a reference to it, meaning you should find the component from the owner, not from whatever component you happen to be interacting with.

This is useful for the bullet hitting a collider example, if you do collider.get_component(health) or something of that kind, you may not find anything if the health component is a sibling to the collider.

I’ve yet to see if it’s possible to set the owner at runtime, since if you instantiate a node it’ll have no owner by default, but i’ve not needed this yet (if someone knows, please chime in!).

If it’s not possible then it’d be necessary to create some type of entity script to put as the root of all your scenes (so the bullet scene would have entity as root script, then a collider, health and movement components as its children) to bridge everything together (eg: collider.get_entity().get_component(health) to get the health component no matter where it is).

Having references everywhere also works to a degree, so the collider has a health component that you manually set in the scene, but that scales less well than having these pairings done separately (you don’t want the collider script to know what health is, but rather to have events and others bind to it).

If you have different health scripts for different parts, it may be best to extend the health script and have it handle things based on which collider was hit.

Using unique nodes works great for things like managers/singletons, less so for the rest.

2 Likes

In the docs is a description of owner property Node — Godot Engine (stable) documentation in English

Also here is example

2 Likes

That’s perfect, i missed the set_owner method when reading the docs last time.

Thank you for sharing!

1 Like