397 lines
15 KiB
Text
397 lines
15 KiB
Text
|
.. _doc_scene_organization:
|
||
|
|
||
|
Scene organization
|
||
|
==================
|
||
|
|
||
|
This article covers topics related to the effective organization of
|
||
|
scene content. Which nodes should one use? Where should one place them?
|
||
|
How should they interact?
|
||
|
|
||
|
How to build relationships effectively
|
||
|
--------------------------------------
|
||
|
|
||
|
When Godot 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, one must instantiate the sub-scenes without them
|
||
|
requiring details about their environment. One needs to be able to trust
|
||
|
that the sub-scene will create itself without being picky about how one uses
|
||
|
it.
|
||
|
|
||
|
One of the biggest things to consider in OOP is maintaining
|
||
|
focused, singular-purpose classes with
|
||
|
`loose coupling <https://en.wikipedia.org/wiki/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, one should design scenes to have no dependencies.**
|
||
|
That is, one 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 <https://en.wikipedia.org/wiki/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, one must expose data and then rely on a parent context to
|
||
|
initialize it:
|
||
|
|
||
|
1. Connect to a signal. Extremely safe, but should be used only to "respond" to
|
||
|
behavior, not start it. Note that signal names are usually past-tense verbs
|
||
|
like "entered", "skill_activated", or "item_collected".
|
||
|
|
||
|
.. tabs::
|
||
|
.. code-tab:: gdscript GDScript
|
||
|
|
||
|
# Parent
|
||
|
$Child.connect("signal_name", object_with_method, "method_on_the_object")
|
||
|
|
||
|
# Child
|
||
|
emit_signal("signal_name") # Triggers parent-defined behavior.
|
||
|
|
||
|
.. code-tab:: csharp
|
||
|
|
||
|
// Parent
|
||
|
GetNode("Child").Connect("SignalName", ObjectWithMethod, "MethodOnTheObject");
|
||
|
|
||
|
// Child
|
||
|
EmitSignal("SignalName"); // Triggers parent-defined behavior.
|
||
|
|
||
|
2. Call a method. Used to start behavior.
|
||
|
|
||
|
.. tabs::
|
||
|
.. code-tab:: gdscript GDScript
|
||
|
|
||
|
# 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).
|
||
|
|
||
|
.. code-tab:: csharp
|
||
|
|
||
|
// Parent
|
||
|
GetNode("Child").Set("MethodName", "Do");
|
||
|
|
||
|
// Child
|
||
|
Call(MethodName); // Call parent-defined method (which child must own).
|
||
|
|
||
|
3. Initialize a :ref:`FuncRef <class_FuncRef>` property. Safer than a method
|
||
|
as ownership of the method is unnecessary. Used to start behavior.
|
||
|
|
||
|
.. tabs::
|
||
|
.. code-tab:: gdscript GDScript
|
||
|
|
||
|
# Parent
|
||
|
$Child.func_property = funcref(object_with_method, "method_on_the_object")
|
||
|
|
||
|
# Child
|
||
|
func_property.call_func() # Call parent-defined method (can come from anywhere).
|
||
|
|
||
|
.. code-tab:: csharp
|
||
|
|
||
|
// Parent
|
||
|
GetNode("Child").Set("FuncProperty", GD.FuncRef(ObjectWithMethod, "MethodOnTheObject"));
|
||
|
|
||
|
// Child
|
||
|
FuncProperty.CallFunc(); // Call parent-defined method (can come from anywhere).
|
||
|
|
||
|
4. Initialize a Node or other Object reference.
|
||
|
|
||
|
.. tabs::
|
||
|
.. code-tab:: gdscript GDScript
|
||
|
|
||
|
# Parent
|
||
|
$Child.target = self
|
||
|
|
||
|
# Child
|
||
|
print(target) # Use parent-defined node.
|
||
|
|
||
|
.. code-tab:: csharp
|
||
|
|
||
|
// Parent
|
||
|
GetNode("Child").Set("Target", this);
|
||
|
|
||
|
// Child
|
||
|
GD.Print(Target); // Use parent-defined node.
|
||
|
|
||
|
5. Initialize a NodePath.
|
||
|
|
||
|
.. tabs::
|
||
|
.. code-tab:: gdscript GDScript
|
||
|
|
||
|
# Parent
|
||
|
$Child.target_path = ".."
|
||
|
|
||
|
# Child
|
||
|
get_node(target_path) # Use parent-defined NodePath.
|
||
|
|
||
|
.. code-tab:: csharp
|
||
|
|
||
|
// Parent
|
||
|
GetNode("Child").Set("TargetPath", NodePath(".."));
|
||
|
|
||
|
// Child
|
||
|
GetNode(TargetPath); // 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. One can reuse it
|
||
|
in another context without any extra changes to its API.
|
||
|
|
||
|
.. note::
|
||
|
|
||
|
Although the examples above illustrate parent-child relationships,
|
||
|
the same principles apply towards all object relations. Nodes which
|
||
|
are siblings should only be aware of their hierarchies while an ancestor
|
||
|
mediates their communications and references.
|
||
|
|
||
|
.. tabs::
|
||
|
.. code-tab:: gdscript GDScript
|
||
|
|
||
|
# Parent
|
||
|
$Left.target = $Right.get_node("Receiver")
|
||
|
|
||
|
# Left
|
||
|
var target: Node
|
||
|
func execute():
|
||
|
# Do something with 'target'.
|
||
|
|
||
|
# Right
|
||
|
func _init():
|
||
|
var receiver = Receiver.new()
|
||
|
add_child(receiver)
|
||
|
|
||
|
.. code-tab:: csharp
|
||
|
|
||
|
// Parent
|
||
|
GetNode<Left>("Left").Target = GetNode("Right/Receiver");
|
||
|
|
||
|
public class Left : Node
|
||
|
{
|
||
|
public Node Target = null;
|
||
|
|
||
|
public void Execute()
|
||
|
{
|
||
|
// Do something with 'Target'.
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public class Right : Node
|
||
|
{
|
||
|
public Node Receiver = null;
|
||
|
|
||
|
public Right()
|
||
|
{
|
||
|
Receiver = ResourceLoader.Load<Script>("Receiver.cs").New();
|
||
|
AddChild(Receiver);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
The same principles also apply to non-Node objects that maintain dependencies
|
||
|
on other objects. Whichever object actually owns the objects should manage
|
||
|
the relationships between them.
|
||
|
|
||
|
.. warning::
|
||
|
|
||
|
One should favor keeping data in-house (internal to a scene) though as
|
||
|
placing a dependency on an external context, even a loosely coupled one,
|
||
|
still means that the node will expect something in its environment to be
|
||
|
true. The project's design philosophies should prevent this from happening.
|
||
|
If not, the code's inherent liabilities will force developers to use
|
||
|
documentation to keep track of object relations on a microscopic scale; this
|
||
|
is otherwise known as development hell. Writing code that relies on external
|
||
|
documentation for one to use it safely is error-prone by default.
|
||
|
|
||
|
To avoid creating and maintaining such documentation, one converts the
|
||
|
dependent node ("child" above) into a tool script that implements
|
||
|
:ref:`_get_configuration_warning() <class_Node_method__get_configuration_warning>`.
|
||
|
Returning a non-empty string from it will make the Scene dock generate a
|
||
|
warning icon with the string as a tooltip by the node. This is the same icon
|
||
|
that appears for nodes such as the
|
||
|
:ref:`Area2D <class_Area2D>` node when it has no child
|
||
|
:ref:`CollisionShape2D <class_CollisionShape2D>` nodes defined. The editor
|
||
|
then self-documents the scene through the script code. No content duplication
|
||
|
via documentation is necessary.
|
||
|
|
||
|
A GUI like this can better inform project users of critical information about
|
||
|
a Node. Does it have external dependencies? Have those dependencies been
|
||
|
satisfied? Other programmers, and especially designers and writers, will need
|
||
|
clear instructions in the messages telling them what to do to configure it.
|
||
|
|
||
|
So, why do all this complex switcharoo 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 effecting other classes.
|
||
|
|
||
|
Scripts and scenes, as extensions of engine classes, should abide
|
||
|
by *all* OOP principles. Examples include...
|
||
|
|
||
|
- `SOLID <https://en.wikipedia.org/wiki/SOLID>`_
|
||
|
- `DRY <https://en.wikipedia.org/wiki/Don%27t_repeat_yourself>`_
|
||
|
- `KISS <https://en.wikipedia.org/wiki/KISS_principle>`_
|
||
|
- `YAGNI <https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it>`_
|
||
|
|
||
|
Choosing a node tree structure
|
||
|
------------------------------
|
||
|
|
||
|
So, a developer starts work on a game only to stop at the vast possibilities
|
||
|
before them. They might know what they want to do, what systems they want to
|
||
|
have, but *where* to put them all? Well, how one goes about making their game
|
||
|
is always up to them. One can construct node trees in countless ways.
|
||
|
But, for those who are unsure, this helpful guide can give them a sample of
|
||
|
a decent structure to start with.
|
||
|
|
||
|
A game should always have a sort of "entry point"; somewhere the developer can
|
||
|
definitively track where things begin so that they can follow the logic as it
|
||
|
continues elsewhere. This place also serves as a bird's eye view of all of the
|
||
|
other data and logic in the program. For traditional applications, this would
|
||
|
be the "main" function. In this case, it would be a Main node.
|
||
|
|
||
|
- Node "Main" (main.gd)
|
||
|
|
||
|
The ``main.gd`` script would then serve as the primary controller of one's
|
||
|
game.
|
||
|
|
||
|
Then one has their actual in-game "World" (a 2D or 3D one). This can be a child
|
||
|
of Main. In addition, one will need a primary GUI for their game that manages
|
||
|
the various menus and widgets the project needs.
|
||
|
|
||
|
- Node "Main" (main.gd)
|
||
|
- Node2D/Spatial "World" (game_world.gd)
|
||
|
- Control "GUI" (gui.gd)
|
||
|
|
||
|
When changing levels, one can then swap out the children of the "World" node.
|
||
|
:ref:`Changing scenes manually <doc_change_scenes_manually>` gives users full
|
||
|
control over how their game world transitions.
|
||
|
|
||
|
The next step is to consider what gameplay systems one's project requires.
|
||
|
If one has a system that...
|
||
|
|
||
|
1. tracks all of its data internally
|
||
|
2. should be globally accessible
|
||
|
3. should exist in isolation
|
||
|
|
||
|
... then one should create an :ref:`autoload 'singleton' node <doc_singletons_autoload>`.
|
||
|
|
||
|
.. note::
|
||
|
|
||
|
For smaller games, a simpler alternative with less control would be to have
|
||
|
a "Game" singleton that simply calls the
|
||
|
:ref:`SceneTree.change_scene() <class_SceneTree_method_change_scene>` method
|
||
|
to swap out the main scene's content. This structure more or less keeps
|
||
|
the "World" as the main game node.
|
||
|
|
||
|
Any GUI would need to also be a
|
||
|
singleton; be a transitory part of the "World"; or be manually added as a
|
||
|
direct child of the root. Otherwise, the GUI nodes would also delete
|
||
|
themselves during scene transitions.
|
||
|
|
||
|
If one has systems that modify other systems' data, one should define those as
|
||
|
their own scripts or scenes rather than autoloads. For more information on the
|
||
|
reasons, please see the
|
||
|
:ref:`Autoloads versus regular nodes <doc_autoloads_versus_internal_nodes>`
|
||
|
documentation.
|
||
|
|
||
|
Each subsystem within one's game should have its own section within the
|
||
|
SceneTree. One should use parent-child relationships only in cases where nodes
|
||
|
are effectively elements of their parents. Does removing the parent reasonably
|
||
|
mean that one should also remove the children? If not, then it should have its
|
||
|
own place in the hierarchy as a sibling or some other relation.
|
||
|
|
||
|
.. note::
|
||
|
|
||
|
In some cases, one needs these separated nodes to *also* position themselves
|
||
|
relative to each other. One can use the
|
||
|
:ref:`RemoteTransform <class_RemoteTransform>` /
|
||
|
:ref:`RemoteTransform2D <class_RemoteTransform2D>` nodes for this purpose.
|
||
|
They will allow a target node to conditionally inherit selected transform
|
||
|
elements from the Remote\* node. To assign the ``target``
|
||
|
:ref:`NodePath <class_NodePath>`, use one of the following:
|
||
|
|
||
|
1. A reliable third party, likely a parent node, to mediate the assignment.
|
||
|
2. A group, to easily pull a reference to the desired node (assuming there
|
||
|
will only ever be one of the targets).
|
||
|
|
||
|
When should one do this? Well, this is subjective. The dilemma arises when
|
||
|
one must micro-manage when a node must move around the SceneTree to preserve
|
||
|
itself. For example...
|
||
|
|
||
|
- Add a "player" node to a "room".
|
||
|
- Need to change rooms, so one must delete the current room.
|
||
|
- Before the room can be deleted, one must preserve and/or move the player.
|
||
|
|
||
|
Is memory a concern?
|
||
|
|
||
|
- If not, one can just create the two rooms, move the player
|
||
|
and delete the old one. No problem.
|
||
|
|
||
|
If so, one will need to...
|
||
|
|
||
|
- Move the player somewhere else in the tree.
|
||
|
- Delete the room.
|
||
|
- Instantiate and add the new room.
|
||
|
- Re-add the player.
|
||
|
|
||
|
The issue is that the player here is a "special case"; one where the
|
||
|
developers must *know* that they need to handle the player this way for the
|
||
|
project. As such, the only way to reliably share this information as a team
|
||
|
is to *document* it. Keeping implementation details in documentation however
|
||
|
is dangerous. It's a maintenance burden, strains code readability, and bloats
|
||
|
the intellectual content of a project unnecessarily.
|
||
|
|
||
|
In a more complex game with larger assets, it can be a better idea to simply
|
||
|
keep the player somewhere else in the SceneTree entirely. This results in:
|
||
|
|
||
|
1. More consistency.
|
||
|
2. No "special cases" that must be documented and maintained somewhere.
|
||
|
3. No opportunity for errors to occur because these details are not accounted
|
||
|
for.
|
||
|
|
||
|
In contrast, if one ever needs to have a child node that does *not* inherit
|
||
|
the transform of their parent, one has the following options:
|
||
|
|
||
|
1. The **declarative** solution: place a :ref:`Node <class_Node>` in between
|
||
|
them. As nodes with no transform, Nodes will not pass along such
|
||
|
information to their children.
|
||
|
2. The **imperative** solution: Use the ``set_as_toplevel`` setter for the
|
||
|
:ref:`CanvasItem <class_CanvasItem_method_set_as_toplevel>` or
|
||
|
:ref:`Spatial <class_Spatial_method_set_as_toplevel>` node. This will make
|
||
|
the node ignore its inherited transform.
|
||
|
|
||
|
.. note::
|
||
|
|
||
|
If building a networked game, keep in mind which nodes and gameplay systems
|
||
|
are relevant to all players versus those just pertinent to the authoritative
|
||
|
server. For example, users do not all need to have a copy of every players'
|
||
|
"PlayerController" logic. Instead, they need only their own. As such, keeping
|
||
|
these in a separate branch from the "world" can help simplify the management
|
||
|
of game connections and the like.
|
||
|
|
||
|
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.
|
||
|
Godot's node trees form an aggregation relationship, not one of composition.
|
||
|
But while one still has the flexibility to move nodes around, it is still best
|
||
|
when such moves are unnecessary by default.
|