When I’m designing a game, I try to make the nodes and scenes modular. When they are modular, the scenes can be placed in many situations and are easier to debug. This is even recommended by the developers of the engine (see more examples from the developers in the Godot documentation). So please join me in a little article on making nodes and scenes modular.
Note: this is meant for intermediate to advanced designers of the Godot engine. The concepts and code discussed here may be unfamiliar to those who haven’t been programming for a while, or who haven’t made a few games, and have not read up on object oriented programming (OOP). Also note that some of the syntax described here works in Godot v3.4 and v3.5. It has not been tested in Godot v.4.0+.
A node or a scene should rely mainly on what’s in the scene or node. If a node must depend on something outside of it, then it should at least be given to the node (something I think is called “dependency injection”). I usually do it like this:
# Top of the script for the node or scene. extends Node2D # This is the node that will be "injected" into the node or scene. export(NodePath) var foobar_node_path = @"" onready var foobar_node = get_node_or_null(foobar_node_path)
After that is written in the node’s script, I add it to the node via the editor:
Then all I need to do is instance this node in the modular scene:
# You can do this in the `_ready()` function. Or you could do it in other parts of the script. func _ready(): var foo = foobar_node.instance()
Be careful when using
NodePaths. They should not be used for nodes that will move around the scene tree (or will be freed at runtime). It is best to rely on the editor to generate the
NodePath, or have them constructed programmatically. To get the path of a
Node, use the
get_path() method of the
var my_path = foobar_node.get_path()
When you have a node that’s modular, it still has to perform its job: the node has to show the bad guy, the scene has to animate the main character, or the node just needs to keep track of things. So how do you make it usable yet also prevent it from wrecking the other parts of the game with bugs and stuff? Use this motto from the developers: call down, signal up. What does that mean? In the scene tree of your average Godot scene, all of the nodes are arranged in a tree, with one node as the root. The parent of nodes can call methods on their child nodes so as to get them to do something. When they need to call to sibling nodes or other parent nodes, they can use signals to get those other nodes to react. Here’s some examples of calling down and signaling up.
We have here a scene tree:
Mama_node can call methods on the
Baby_node so as to get it do something. Here’s something that the
Baby_node may do:
## In Baby_node # These variables are local to the Baby_node, and should only be used in this node, or its child nodes. var health = 10 var move_distance = 10 func move_player(): # Move the player here with something like move_and_slide().
In this example, the
Baby_node has the
Player_node as a child. It can use its methods, variables, and what-have-you to perform actions on the
Player_node. If the
Mama_node needs to move the
Player_node, it could call down to the
Baby_node. When the
Mama_node needs to have the
Player_node do other things (like take damage in some cutscene), it can access the
health variable on the
Baby_node. All that the
Baby_node needs to worry about is the
Player_node (or any other child nodes in its custody). It doesn’t need to worry about the enemy nodes, or the scenes that need to interact with the
Player_node; that’s something for the
Mama_node (or higher parent nodes) to solve. So what if the
Baby_node needs to tell the
Mama_node (or other important nodes) about the
Player_node? That’s where the node needs to signal up.
Baby_node, a signal may be defined:
# In the script attached to the Baby_node. signal player_died
When another node (such as the
root_node or some “director” node) needs that information, all it needs to do is listen for this signal. It may be hooked up like this:
# In a script attached to some "director" node. func _ready(): connect("player_died", self, "_on_player_death") # Further down the script. func _on_player_death(): # Show player death animation, and fade to black.
Now these two nodes (or scenes) don’t need to rely on each other. The “director” scene can have its optional player death (either by directly animating it, or using an
AnimationPlayer node). This also helps with debugging. When we encounter a bug where the player is suppose to die and doesn’t, we don’t have to go far to figure out where the error is. We could look in the
Mama_node (which has indirect access to the
Player_node), or we could look at the
Baby_node (the most likely culprit) for our bug. When we reduce our nodes and scenes to separate steps in the possible states of what can happen, it makes debugging easier because we know where in the steps how things are suppose to happen.
Hope you enjoy these couple of tips. I’m sure there are many other ways the scenes and the nodes (or various objects) of the Godot engine can be made modular. Just remember to watch out for code or designs which break
NodePaths, mess up function calls, or make it difficult to use signals, and your development journey will be made easier.