Scene organization
This article covers topics related to the effective organization of scene content. Which nodes should you use? Where should you place them? How should they interact?
How to build relationships effectively
When Redot users begin crafting their own scenes, they often run into the following problem:
They create their first scene and fill it with content only to eventually end up saving branches of their scene into separate scenes as the nagging feeling that they should split things up starts to accumulate. However, they then notice that the hard references they were able to rely on before are no longer possible. Re-using the scene in multiple places creates issues because the node paths do not find their targets and signal connections established in the editor break.
To fix these problems, you must instantiate the sub-scenes without them requiring details about their environment. You need to be able to trust that the sub-scene will create itself without being picky about how it's used.
One of the biggest things to consider in OOP is maintaining focused, singular-purpose classes with loose coupling to other parts of the codebase. This keeps the size of objects small (for maintainability) and improves their reusability.
These OOP best practices have several implications for best practices in scene structure and script usage.
If at all possible, you should design scenes to have no dependencies. That is, you should create scenes that keep everything they need within themselves.
If a scene must interact with an external context, experienced developers recommend the use of Dependency Injection. This technique involves having a high-level API provide the dependencies of the low-level API. Why do this? Because classes which rely on their external environment can inadvertently trigger bugs and unexpected behavior.
To do this, you must expose data and then rely on a parent context to initialize it:
- Connect to a signal. Extremely safe, but should be used only to "respond" to behavior, not start it. By convention, signal names are usually past-tense verbs like "entered", "skill_activated", or "item_collected".
# Parent
$Child.signal_name.connect(method_on_the_object)
# Child
signal_name.emit() # Triggers parent-defined behavior.
// Parent
GetNode("Child").Connect("SignalName", Callable.From(ObjectWithMethod.MethodOnTheObject));
// Child
EmitSignal("SignalName"); // Triggers parent-defined behavior.
// Parent
Node *node = get_node<Node>("Child");
if (node != nullptr) {
// Note that get_node may return a nullptr, which would make calling the connect method crash the engine if "Child" does not exist!
// So unless you are 1000% sure get_node will never return a nullptr, it's a good idea to always do a nullptr check.
node->connect("signal_name", callable_mp(this, &ObjectWithMethod::method_on_the_object));
}
// Child
emit_signal("signal_name"); // Triggers parent-defined behavior.
- Call a method. Used to start behavior.
# Parent
$Child.method_name = "do"
# Child, assuming it has String property 'method_name' and method 'do'.
call(method_name) # Call parent-defined method (which child must own).
// Parent
GetNode("Child").Set("MethodName", "Do");
// Child
Call(MethodName); // Call parent-defined method (which child must own).
// Parent
Node *node = get_node<Node>("Child");
if (node != nullptr) {
node->set("method_name", "do");
}
// Child
call(method_name); // Call parent-defined method (which child must own).
- Initialize a Callable property. Safer than a method as ownership of the method is unnecessary. Used to start behavior.
# Parent
$Child.func_property = object_with_method.method_on_the_object
# Child
func_property.call() # Call parent-defined method (can come from anywhere).
// Parent
GetNode("Child").Set("FuncProperty", Callable.From(ObjectWithMethod.MethodOnTheObject));
// Child
FuncProperty.Call(); // Call parent-defined method (can come from anywhere).
// Parent
Node *node = get_node<Node>("Child");
if (node != nullptr) {
node->set("func_property", Callable(&ObjectWithMethod::method_on_the_object));
}
// Child
func_property.call(); // Call parent-defined method (can come from anywhere).
- Initialize a Node or other Object reference.
# Parent
$Child.target = self
# Child
print(target) # Use parent-defined node.
// Parent
GetNode("Child").Set("Target", this);
// Child
GD.Print(Target); // Use parent-defined node.
// Parent
Node *node = get_node<Node>("Child");
if (node != nullptr) {
node->set("target", this);
}
// Child
UtilityFunctions::print(target);
- Initialize a NodePath.
# Parent
$Child.target_path = ".."
# Child
get_node(target_path) # Use parent-defined NodePath.
// Parent
GetNode("Child").Set("TargetPath", NodePath(".."));
// Child
GetNode(TargetPath); // Use parent-defined NodePath.
// Parent
Node *node = get_node<Node>("Child");
if (node != nullptr) {
node->set("target_path", NodePath(".."));
}
// Child
get_node<Node>(target_path); // Use parent-defined NodePath.
These options hide the points of access from the child node. This in turn keeps the child loosely coupled to its environment. You can reuse it in another context without any extra changes to its API.
So, why does all this complex switcheroo work? Well, because scenes operate best when they operate alone. If unable to work alone, then working with others anonymously (with minimal hard dependencies, i.e. loose coupling) is the next best thing. Inevitably, changes may need to be made to a class, and if these changes cause it to interact with other scenes in unforeseen ways, then things will start to break down. The whole point of all this indirection is to avoid ending up in a situation where changing one class results in adversely affecting other classes dependent on it.
Scripts and scenes, as extensions of engine classes, should abide by all OOP principles. Examples include...
Choosing a node tree structure
You might start to work on a game but get overwhelmed by the vast possibilities before you. You might know what you want to do, what systems you want to have, but where do you put them all? How you go about making your game is always up to you. You can construct node trees in countless ways. If you are unsure, this guide can give you a sample of a decent structure to start with.
A game should always have an "entry point"; somewhere you can definitively track where things begin so that you can follow the logic as it continues elsewhere. It also serves as a bird's eye view of all other data and logic in the program. For traditional applications, this is normally a "main" function. In Redot, it's a Main node.
- Node "Main" (main.gd)
The main.gd script will serve as the primary controller of your game.
Then you have an in-game "World" (a 2D or 3D one). This can be a child of Main. In addition, you will need a primary GUI for your game that manages the various menus and widgets the project needs.
- Node "Main" (main.gd)
- Node2D/Node3D "World" (game_world.gd)
- Control "GUI" (gui.gd)
When changing levels, you can then swap out the children of the "World" node. Changing scenes manually gives you full control over how your game world transitions.
The next step is to consider what gameplay systems your project requires. If you have a system that...
- tracks all of its data internally
- should be globally accessible
- should exist in isolation
... then you should create an autoload 'singleton' node.
If you have systems that modify other systems' data, you should define those as their own scripts or scenes, rather than autoloads. For more information, see Autoloads versus regular nodes.
Each subsystem within your game should have its own section within the SceneTree. You should use parent-child relationships only in cases where nodes are effectively elements of their parents. Does removing the parent reasonably mean that the children should also be removed? If not, then it should have its own place in the hierarchy as a sibling or some other relation.
The key to scene organization is to consider the SceneTree in relational terms rather than spatial terms. Are the nodes dependent on their parent's existence? If not, then they can thrive all by themselves somewhere else. If they are dependent, then it stands to reason that they should be children of that parent (and likely part of that parent's scene if they aren't already).
Does this mean nodes themselves are components? Not at all. Redot's node trees form an aggregation relationship, not one of composition. But while you still have the flexibility to move nodes around, it is still best when such moves are unnecessary by default.