Tips for making scenes and nodes modular in the Godot engine

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

Self containment

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 Node class:

var my_path = foobar_node.get_path()

Loose coupling

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:

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

On the 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.

Conclusion

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.

Jason Anderson

Jason Anderson has been hacking up computers for nearly 20 years and has been using Linux for over 15 years. Among that, he has a BBA in Accounting. Look him up on Twitter at @FakeJasonA and on Mastodon on @ertain@mast.linuxgamecast.com

Leave a Reply

Your email address will not be published. Required fields are marked *