Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
Organisation de la scène
Cet article traite de sujets liés à l'organisation efficace du contenu des scènes. Quels nœuds utiliser ? Où doit-on les placer ? Comment devraient-ils interagir ?
Comment établir des relations efficaces
Lorsque les utilisateurs de Godot commencent à créer leurs propres scènes, ils se heurtent souvent au problème suivant :
Ils créent leur première scène et la remplissent de contenu, pour finir par sauvegarder des branches de leur scène dans des scènes séparées au fur et à mesure que s'accumule le sentiment tenace qu'ils doivent séparer les choses. Cependant, ils constatent alors que les références dures, sur lesquelles ils pouvaient compter auparavant, ne sont plus fonctionnelles. La réutilisation de la scène à plusieurs endroits crée des problèmes car les chemins de nœuds ne trouvent plus leurs cibles et les connexions de signal établies dans l'éditeur sont rompues.
Pour résoudre ces problèmes, il faut instancier les sous-scènes sans qu'elles ne nécessitent de détail sur leur environnement. Il faut pouvoir être sûr que la sous-scène se créera d'elle-même, sans être pointilleuse sur la façon dont on l'utilise.
One of the biggest things to consider in Object-Oriented Programming (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.
Ces bonnes pratiques de la POO ont plusieurs implications sur les bonnes pratiques en matière de structure des scènes et d'utilisation de script.
Il faut, quand cela est possible, concevoir des scènes sans dépendance. C'est-à-dire qu'il faut créer des scènes qui gardent tout ce dont elles ont besoin à l'intérieur d'elles-mêmes.
Si une scène doit interagir avec un contexte externe, les développeurs expérimentés recommandent l'utilisation d'Injection de Dépendances. Cette technique nécessite qu'une API de haut niveau fournisse les dépendances à l'API de bas niveau. Pourquoi faire cela ? Parce que les classes qui dépendent de leur environnement externe peuvent déclencher par inadvertance des bogues et des comportements inattendus.
Pour ce faire, il faut exposer les données, puis s'appuyer sur un contexte parent pour les initialiser :
Connecter à un signal. Extrêmement sûr, mais ne doit être utilisé que pour "répondre" au comportement, pas pour le démarrer. Par convention, les noms de signaux sont généralement des verbes au passé comme "entered", "skill_activated", ou "item_collected".
# Parent $Child.signal_name.connect(method_on_the_object) # Child signal_name.emit() # Triggers parent-specified behavior.
// Parent GetNode("Child").Connect("SignalName", Callable.From(ObjectWithMethod.MethodOnTheObject)); // Child EmitSignal("SignalName"); // Triggers parent-specified 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-specified behavior.
Appeler une méthode. Utilisé pour démarrer le comportement.
# Parent $Child.method_name = "do" # Child, assuming it has String property 'method_name' and method 'do'. call(method_name) # Call parent-specified method (which child must own).
// Parent GetNode("Child").Set("MethodName", "Do"); // Child Call(MethodName); // Call parent-specified 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-specified method (which child must own).
Initialiser une propriété Callable. Plus sûr qu'une méthode car il n'est pas nécessaire d'être propriétaire de la méthode. Utilisé pour démarrer le comportement.
# Parent $Child.func_property = object_with_method.method_on_the_object # Child func_property.call() # Call parent-specified method (can come from anywhere).
// Parent GetNode("Child").Set("FuncProperty", Callable.From(ObjectWithMethod.MethodOnTheObject)); // Child FuncProperty.Call(); // Call parent-specified 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-specified method (can come from anywhere).
Initialiser un nœud ou une autre référence d'objet.
# Parent $Child.target = self # Child print(target) # Use parent-specified node.
// Parent GetNode("Child").Set("Target", this); // Child GD.Print(Target); // Use parent-specified node.
// Parent Node *node = get_node<Node>("Child"); if (node != nullptr) { node->set("target", this); } // Child UtilityFunctions::print(target);
Initialiser un Chemin de Nœud (NodePath).
# Parent $Child.target_path = ".." # Child get_node(target_path) # Use parent-specified NodePath.
// Parent GetNode("Child").Set("TargetPath", NodePath("..")); // Child GetNode(TargetPath); // Use parent-specified NodePath.
// Parent Node *node = get_node<Node>("Child"); if (node != nullptr) { node->set("target_path", NodePath("..")); } // Child get_node<Node>(target_path); // Use parent-specified NodePath.
Ces options cachent les points d'accès au nœud enfant. Cela permet à l'enfant d'être couplé de façon lâche à son environnement. On peut le réutiliser dans un autre contexte sans aucune modification supplémentaire de son API.
Note
Bien que les exemples ci-dessus illustrent les relations parent-enfant, les mêmes principes s'appliquent à toutes les relations objet. Les nœuds qui sont des frères et sœurs ne devraient connaître que leur hiérarchie pendant qu'un ancêtre commun assure la médiation de leurs communications et de leurs références.
# 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)
// Parent
GetNode<Left>("Left").Target = GetNode("Right/Receiver");
public partial class Left : Node
{
public Node Target = null;
public void Execute()
{
// Do something with 'Target'.
}
}
public partial class Right : Node
{
public Node Receiver = null;
public Right()
{
Receiver = ResourceLoader.Load<Script>("Receiver.cs").New();
AddChild(Receiver);
}
}
// Parent
get_node<Left>("Left")->target = get_node<Node>("Right/Receiver");
class Left : public Node {
GDCLASS(Left, Node)
protected:
static void _bind_methods() {}
public:
Node *target = nullptr;
Left() {}
void execute() {
// Do something with 'target'.
}
};
class Right : public Node {
GDCLASS(Right, Node)
protected:
static void _bind_methods() {}
public:
Node *receiver = nullptr;
Right() {
receiver = memnew(Node);
add_child(receiver);
}
};
Les mêmes principes s'appliquent également aux objets non-Node qui dépendent d'autres objets. Quel que soit le propriétaire réel des objets dépendants, il doit gérer les relations entre eux.
Avertissement
Il faut privilégier la conservation des données en interne (interne à une scène) puisque placer une dépendance à un contexte externe, même faiblement couplé, signifie que le nœud s'attendra à ce que quelque chose arrive dans son environnement. Les philosophies de conception du projet devraient empêcher que cela ne se produise. Sinon, les contraintes inhérentes au code forceront les développeurs à utiliser la documentation pour suivre les relations entre objets à une échelle microscopique ; c'est ce qu'on appelle l'enfer du développement. L'écriture d'un code qui nécessite une documentation externe pour pouvoir être utilisé en toute sécurité est par définition sujette à des erreurs.
Pour éviter de créer et de maintenir une telle documentation, on convertit le nœud dépendant ("enfant" ci-dessus) en un script outil (tool) qui implémente _get_configuration_warnings(). Renvoyer une chaîne PackedStringArray non vide avec fera que la barre d'outils Scène génèrera un icône d'avertissement avec la chaîne sous forme d'info-bulle. C'est le même icône qui apparaît pour les nœuds tels que Area2D quand il n'a pas d'enfant CollisionShape2D de défini. L'éditeur auto-documente la scène grâce au code du script lui-même. Aucune duplication de contenu dans la documentation n'est nécessaire.
A Graphical User Interface (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.
Alors, pourquoi tout ce schmilblick complexe fonctionne-t-il ? Parce que les scènes fonctionnent mieux lorsqu'elles fonctionnent seules. Si une scène est incapable de travailler seule, travailler avec d'autres de façon anonyme (avec un minimum de dépendances dures, c.-à-d. un couplage lâche) est le mieux. Inévitablement, il peut être nécessaire d'apporter des changements à une classe, et si ces changements l'amènent à interagir avec d'autres scènes d'une manière imprévue, alors tout s'écroule. L'objectif de cette approche indirecte est d'éviter de se retrouver dans une situation où le changement d'une classe a des effets négatifs sur les autres classes qui en dépendent.
Les scripts et les scènes, en tant qu'extensions de classe du moteur, doivent respecter tous les principes de la POO. En voici quelques exemples...
Choisir une arborescence de nœuds
Ainsi, un développeur commence à travailler sur un jeu pour s'arrêter devant les vastes possibilités qui s'offrent à lui. Il sait peut-être ce qu'il veut faire, quels systèmes il veut avoir, mais où les mettre ? Eh bien, la façon dont on s'y prend pour créer son jeu ne dépend que de soi. On peut construire des arbres de nœuds d'innombrables façons. Mais, pour ceux qui ne sont pas sûrs, ce guide utile peut leur donner un échantillon d'une structure décente pour commencer.
Un jeu devrait toujours avoir une sorte de "point d'entrée" ; un endroit où le développeur peut repérer de façon précise où les événements commencent afin de pouvoir suivre la logique qui se prolonge ailleurs. Cet endroit sert également de vue d'ensemble pour toutes les autres données et logiques du programme. Pour les applications traditionnelles, ce serait la fonction "main". Dans Godot, il s'agirait d'un nœud Main.
Node "Main" (main.gd)
Le script main.gd agira alors comme le contrôleur principal du jeu.
Ensuite, on a le "World" du jeu (en 2D ou 3D). Il peut s'agir d'un enfant de Main. En outre, certains auront besoin pour leur jeu d'une interface graphique (GUI) principale gérant les différents menus et widgets requis par le projet.
- Node "Main" (main.gd)
Node2D/Node3D "World" (game_world.gd)
Control "GUI" (gui.gd)
Lors d'un changement de niveau, on peut alors échanger les enfants du nœud "World". Changer manuellement de scène donne aux utilisateurs un contrôle total sur la façon dont les transitions entre les mondes de votre jeu se déroulent.
L'étape suivante consiste à déterminer quels sont les systèmes de gameplay nécessaires à la réalisation de son projet. Si on a un système qui...
suit l'ensemble de ses données en interne
doit être accessible de façon globale
doit exister de manière isolée
... il convient alors de créer un nœud autoload 'singleton'.
Note
Pour les petits jeux, une alternative plus simple avec moins de contrôle serait d'avoir un singleton "Game" qui appelle simplement la méthode SceneTree.change_scene_to_file() pour remplacer le contenu de la scène principale. Cette structure maintient plus ou moins "World" comme nœud principal du jeu.
Tout GUI doit également être un singleton, faire partie du monde "World" de manière temporaire ou être ajoutée manuellement en tant qu'enfant direct de la racine. Dans le cas contraire, les nœuds GUI se supprimeraient également lors des transitions de scènes.
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.
Chaque sous-système d'un jeu doit avoir sa propre section dans le SceneTree. On ne devrait utiliser les relations parent-enfant que dans les cas où les nœuds sont effectivement des composantes de leurs parents. Le retrait du parent signifie-t-il que le retrait des enfants doit également être opéré ? Si ce n'est pas le cas, il devrait avoir sa propre place dans hiérarchie en tant que frère ou autre relation.
Note
Dans certains cas, il faut que ces nœuds séparés se positionnent aussi les uns par rapport aux autres. Pour cela, on peut utiliser les nœuds RemoteTransform / RemoteTransform2D. Ils permettent à un nœud cible d'hériter sous conditions des éléments de transformation sélectionnés du nœud Remote*. Pour assigner la target NodePath, utilisez l'une des options suivantes :
Une tierce partie fiable, probablement un nœud parent, pour assurer la coordination de la réaffectation.
Un groupe, pour obtenir facilement une référence au nœud désiré (en supposant qu'il y aura toujours une seule cible).
Quand faut-il faire cela est subjectif. Le dilemme se pose lorsqu'il faut micro-gérer quand un nœud doit se déplacer dans l'arbre des scènes pour se préserver. Par exemple...
Ajouter un nœud "Joueur" à une "Salle".
Besoin de changer de salle, vous devez donc supprimer la salle actuelle.
Avant de pouvoir supprimer la salle, il faut sauvegarder et/ou déplacer le joueur.
Si la mémoire n'est pas une préoccupation, vous pouvez...
Créer la nouvelle salle.
Déplacer le joueur dans la nouvelle salle.
Supprimer l'ancienne salle.
Si la mémoire fait partie de vos préoccupations, à la place, vous devrez...
Déplacer le joueur ailleurs dans l'arbre de scène.
Supprimer la salle.
Instancier et ajouter la nouvelle salle.
Rajouter le joueur dans la nouvelle salle.
Le problème ici est que le joueur est un "cas spécial", un cas où les développeurs ont besoin de savoir qu'ils doivent manipuler le joueur de cette façon pour le projet. Dans ce contexte, la seule façon de partager ces informations de manière fiable en équipe est de la documenter. Cependant, il est dangereux de conserver les détails de l'implémentation dans la documentation. C'est une charge de maintenance, qui pèse sur la lisibilité du code et gonfle inutilement le contenu intellectuel d'un projet.
Dans un jeu plus complexe avec des ressources plus volumineuses, il peut être préférable de simplement garder le joueur ailleurs dans l'arbre de scène. Cela implique :
Plus de constance.
Pas de "cas spéciaux" qui doivent être documentés et maintenus quelque part.
Aucune possibilité d'erreur car ces particularités ne sont pas prises en compte.
En revanche, si l'on a besoin d'avoir un nœud enfant qui n'hérite pas des transformations de son parent, on dispose des options suivantes :
La solution déclarative : placer un Node entre eux. Comme il n'a pas de transformation, il ne transmettra pas cette information à ses enfants.
La solution impérative : Utiliser la propriété
top_levelpour les nœuds CanvasItem ou Node3D. Cela permettra au nœud d'ignorer sa transformation héritée.
Note
Si vous construisez un jeu en réseau, gardez à l'esprit quels nœuds et systèmes de jeu sont pertinents pour tous les joueurs par rapport à ceux qui ne le sont que pour le serveur faisant autorité. Par exemple, les joueurs n'ont pas besoin d'avoir une copie de la logique "PlayerController" de chaque joueur. Ils n'ont besoin que de la leur. Ainsi, le fait de les garder dans une branche distincte du monde "World" peut contribuer à simplifier la gestion des connexions de jeu et systèmes similaires.
La clé de l'organisation d'une scène est de considérer l'arbre de scène en termes relationnels plutôt qu'en termes spatiaux. Les nœuds doivent-ils être dépendants de l'existence de leurs parents ? Si ce n'est pas le cas, ils peuvent prospérer tous seuls ailleurs. Si c'est le cas, il va de soi qu'ils devraient être les enfants de ce parent (et probablement faire partie de la scène de ce parent si ce n'est pas déjà le cas).
Cela signifie-t-il que les nœuds sont eux-mêmes des composants ? Pas du tout. Les arbres de nœuds de Godot forment une relation d'agrégation, et non une relation de composition. Mais bien qu'on a toujours la possibilité de déplacer les nœuds, il est toujours préférable que ces déplacements soient évités par défaut.