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.

Préférences de logique

Vous êtes-vous déjà demandé si vous deviez approcher un problème X avec une stratégie Y ou Z ? Cet article couvre une grande variété de sujets relatifs à ce dilemme.

Ajouter des nœuds et changer ses propriétés : que faire en premier ?

Lors de l'initialisation des nœuds d'un script en cours, vous pourriez avoir besoin de modifier des propriétés telles que son nom ou sa position. Un dilemme commun est : quand devriez-vous changer ces valeurs ?

Les bonnes pratiques sont de changer les valeurs du nœud avant de l'ajouter à l'arborescence de la scène. Certains setters de propriété ont du code pour mettre à jour d'autres valeurs correspondantes, et ce code peut être lent ! Dans la plupart des cas, ce code n'a aucun impact sur les performances de votre jeu, cependant dans des cas d'utilisations plus poussées comme la génération procédurale, cela peut mettre votre jeu à genoux.

Pour ces raisons, il recommandé de définir les valeurs d'un nœud avant de l'ajouter à l’arborescence de scène. Il y a quelques exceptions où les valeurs ne peuvent pas être définies avant d'être ajouté à l'arborescence de scène, comme la définition de la position globale.

Chargement vs préchargement

Dans GDScript, il existe la méthode globale preload. elle charge les ressources le plus tôt possible pour charger en amont les opérations de "chargement" et éviter de charger les ressources au milieu d'un code sensible aux performances.

Son homologue, la méthode load, ne charge une ressource que lorsqu'elle atteint l'instruction load. C'est-à-dire qu'elle chargera immédiatement une ressource, ce qui peut provoquer des ralentissements lorsque ceci se produit au milieu de processus sensibles. La fonction load() est également un alias pour ResourceLoader.load(path) qui est accessible à tous les langages de script.

Donc, quand exactement y a-t-il un préchargement plutôt qu'un chargement, et quand utiliser l'un ou l'autre ? Voyons un exemple :

# my_buildings.gd
extends Node

# Note how constant scripts/scenes have a different naming scheme than
# their property variants.

# This value is a constant, so it spawns when the Script object loads.
# The script is preloading the value. The advantage here is that the editor
# can offer autocompletion since it must be a static path.
const BuildingScn = preload("res://building.tscn")

# 1. The script preloads the value, so it will load as a dependency
#    of the 'my_buildings.gd' script file. But, because this is a
#    property rather than a constant, the object won't copy the preloaded
#    PackedScene resource into the property until the script instantiates
#    with .new().
#
# 2. The preloaded value is inaccessible from the Script object alone. As
#    such, preloading the value here actually does not provide any benefit.
#
# 3. Because the user exports the value, if this script stored on
#    a node in a scene file, the scene instantiation code will overwrite the
#    preloaded initial value anyway (wasting it). It's usually better to
#    provide `null`, empty, or otherwise invalid default values for exports.
#
# 4. Instantiating the script on its own with .new() triggers
#    `load("office.tscn")`, ignoring any value set through the export.
@export var a_building : PackedScene = preload("office.tscn")

# Uh oh! This results in an error!
# One must assign constant values to constants. Because `load` performs a
# runtime lookup by its very nature, one cannot use it to initialize a
# constant.
const OfficeScn = load("res://office.tscn")

# Successfully loads and only when one instantiates the script! Yay!
var office_scn = load("res://office.tscn")

Le préchargement permet au script de gérer tout le chargement dès le chargement du script lui-même. Le préchargement est utile, mais il y a aussi des moments où on ne le souhaite pas. Pour distinguer ces situations, il y a quelques points à considérer :

  1. S'il est impossible de déterminer le moment du chargement du script, alors le préchargement d'une ressource (en particulier d'une scène ou d'un script) pourrait entraîner des charges supplémentaires auxquelles on ne s'attend pas. Cela pourrait mener à des temps de chargement non intentionnels de longueur variable en plus des opérations de chargement du script d'origine.

  2. Si quelque chose d'autre pourrait remplacer la valeur (comme l'initialisation d'une scène exportée), alors le préchargement de la valeur n'a aucune intérêt. Ce point n'est pas un facteur important si l'on a l'intention de toujours créer le script tout seul.

  3. Si l'on souhaite seulement 'importer' une autre ressource de classe (script ou scène), alors l'utilisation d'une constante préchargée est souvent la meilleure solution. Cependant, dans des cas exceptionnels, il n'est pas souhaitable de le faire :

    1. Si la classe 'importée' est susceptible de changer, alors elle devrait être une propriété, initialisée soit à l'aide d'un @export ou d'un load() (et peut-être même pas initialisée tout de suite).

    2. Si le script requiert beaucoup de dépendances, et que vous ne souhaitez pas utiliser autant de mémoire, alors vous pourriez vouloir charger et décharger ces ressources durant l'exécution selon l'évolution des circonstances. Si l'on précharge des ressources dans des constantes, alors la seule manière de les décharger serait de décharger le script entier. Si elles sont en revanche chargées dans des propriétés, alors il est possible de les définir à null et retirer toutes les références à la ressource (ce qui, en tant que type héritant de RefCounted, causera la suppression des ressources de la mémoire).

Grands niveaux : statique vs dynamique

Dans le cadre de la création d'un niveau de grande taille, quelle solution est la plus appropriée ? Créer le niveau d'un seul morceau statique ? Ou il vaut mieux scinder ce niveau en morceaux et changer le contenu du monde au besoin ?

La réponse simple est, "quand les performances le nécessitent". Le dilemme dont il est question ici est l'un des plus vieux dans le choix du type de programmation : doit-on privilégier l'optimisation de la mémoire sur celle de la vitesse, ou inversement ?

La réponse naïve est d'utiliser un seul niveau et charger toutes les ressources en une fois. Toutefois, dans le cadre de certains projets, cette solution peut mener à l'utilisation d'une importante quantité de mémoire. Gaspiller la mémoire de l'utilisateur risque de ralentir, voire de faire planter les autres programmes en cours d’exécution sur son système.

Il est vivement recommandé de séparer les scènes de grande taille en plus petites scènes (aidant ainsi à la ré utilisabilité des assets). Les développeurs peuvent ensuite concevoir un nœud dont la tâche est de gérer la création/le chargement et la suppression/le déchargement des ressources et nœuds correspondants en temps-réel. Les jeux disposant d'un environnement étendu et varié, ou générés de façon procédurale implémentent souvent ces stratégies dans un soucis d'optimisation de la mémoire.

Néanmoins, la conception d'un système dynamique de ce type est plus complexe car il nécessite plus de programmation, ce qui favorise l'apparition de bugs et d'erreurs. Il faudra donc prendre garde à ne pas développer un système qui augmente la dette technique de l'application.

Ainsi, les meilleures options seraient....

  1. Utiliser des niveaux statiques pour les petits jeux.

  2. En fonction du temps et des ressources disponibles pour la création d'un moyen/grand jeu, créer une bibliothèque ou un plugin qui peut gérer les nœuds et les ressources avec du code. Cette bibliothèque/plugin pourrait être améliorée au niveau de son utilisabilité et sa stabilité, pour évoluer en un outil fiable au fil des projets.

  3. Use dynamic logic for a medium/large game because one has the coding skills, but not the time or resources to refine the code (game's gotta get done). Could potentially refactor later to outsource the code into a plugin.

Pour trouver des exemples des différentes façons existantes pour charger les scènes de façon dynamique à l’exécution, on peut se référer à la documentation suivante : "Change scenes manually".