Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

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.

L'une des choses les plus importantes à prendre en compte dans la POO est le maintien de classes ciblées et à but unique avec un couplage lâche à d'autres parties du code de base. La taille des objets reste ainsi réduite (pour des raisons de maintenabilité) et leur réutilisabilité est améliorée.

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 :

  1. 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.
    
  2. 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-defined method (which child must own).
    
  3. 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).
    
  4. Initialiser un nœud ou une autre référence d'objet.

    # Parent
    $Child.target = self
    
    # Child
    print(target) # Use parent-defined node.
    
  5. Initialiser un Chemin de Nœud (NodePath).

    # Parent
    $Child.target_path = ".."
    
    # Child
    get_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. One can reuse it in another context without any extra changes to its 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)

Les mêmes principes s'appliquent également aux objets non nœuds 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.

To avoid creating and maintaining such documentation, one converts the dependent node ("child" above) into a tool script that implements _get_configuration_warnings(). Returning a non-empty PackedStringArray from it will make the Scene dock generate a warning icon with the string(s) as a tooltip by the node. This is the same icon that appears for nodes such as the Area2D node when it has no child CollisionShape2D nodes defined. The editor then self-documents the scene through the script code. No content duplication via documentation is necessary.

Une intégration à l'interface graphique comme celle-ci permet de mieux renseigner les utilisateurs sur les informations essentielles concernant un nœud. A-t-il des dépendances externes ? Ces dépendances ont-elles été satisfaites ? D'autres développeurs, en particulier les concepteurs et les rédacteurs, auront besoin d'instructions claires par des messages leur indiquant ce qu'ils doivent faire pour configurer ce nœud.

So, why does 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 dependent on it.

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 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". Ici, 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 primaire 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 du 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...

  1. suit l'ensemble de ses données en interne

  2. doit être accessible de façon globale

  3. doit exister de manière isolée

Il convient alors de créer un nœud autoload 'singleton'.

Note

For smaller games, a simpler alternative with less control would be to have a "Game" singleton that simply calls the SceneTree.change_scene_to_file() method to swap out the main scene's content. This structure more or less keeps the "World" as the main game node.

Toute interface graphique 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 de l'interface graphique se supprimeraient également lors des transitions de scènes.

Si l'on dispose de systèmes qui modifient les données d'autres systèmes, il faut les définir comme leurs propres scripts ou scènes plutôt que comme des autoloads. Pour plus d'informations, veuillez consulter la documentation 'autoloads contre nœuds normaux'.

Chaque sous-système d'un jeu doit avoir sa propre section dans l'arbre de scène. 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, sœur ou autre parent.

Note

In some cases, one needs these separated nodes to also position themselves relative to each other. One can use the RemoteTransform / 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 NodePath, use one of the following:

  1. Une tierce partie fiable, probablement un nœud parent, pour assurer la coordination de la réaffectation.

  2. Un groupe, pour obtenir facilement une référence au nœud désiré (en supposant qu'il n'y aura jamais qu'une seule des cibles).

Quand faut-il le faire ? Eh bien, c'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, il faut donc supprimer la salle courante.

  • Avant de pouvoir supprimer la salle, il faut sauvegarder et/ou déplacer le joueur.

    La mémoire est-elle une préoccupation ?

    • Si ce n'est pas le cas, il suffit de créer les deux salles, de déplacer le joueur et d'effacer l'ancienne. Pas de problème.

    Si c'est le cas, il faudra...

    • Déplacer le joueur ailleurs dans l'arbre de scène.

    • Supprimer la salle.

    • Instancier et ajouter la nouvelle salle.

    • Replacer le joueur.

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 assets plus volumineux, il peut être préférable de simplement garder le joueur ailleurs dans l'arbre de scène. Cela implique :

  1. Plus de constance.

  2. Pas de "cas spéciaux" qui doivent être documentés et maintenus quelque part.

  3. 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 :

  1. La solution déclarative : placer un Node entre eux. En tant que nœuds sans transformation, les nœuds Node ne transmettront pas ces paramètres à leurs enfants.

  2. The imperative solution: Use the top_level property for the CanvasItem or Node3D node. This will make the node ignore its inherited transform.

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 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 tout-à-fait. 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.