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.

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.

Sa contrepartie, la méthode load, charge une ressource uniquement lorsqu’elle atteint l’instruction load. C’est-à-dire qu’elle chargera une ressource à l’instant t ce qui peut causer des ralentissements si elle se produit au milieu de processus sensibles. La fonction load est aussi un alias pour ResourceLoader.load(path) qui est accessible à tous les langages scripts.

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 benefit anyone.
#
# 3. Because the user exports the value, if this script stored on
#    a node in a scene file, the scene instantation 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. It is when one instantiates this script on its own with .new() that
#    one will load "office.tscn" rather than the exported value.
export(PackedScene) var a_building = 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")
using System;
using Godot;

// C# and other languages have no concept of "preloading".
public class MyBuildings : Node
{
    //This is a read-only field, it can only be assigned when it's declared or during a constructor.
    public readonly PackedScene Building = ResourceLoader.Load<PackedScene>("res://building.tscn");

    public PackedScene ABuilding;

    public override void _Ready()
    {
        // Can assign the value during initialization.
        ABuilding = GD.Load<PackedScene>("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, le préchargement d’une ressource, en particulier d’une scène ou d’un script, peut entraîner des charges supplémentaires auxquelles on ne s’attend pas. Cela pourrait entraîner 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 nécessite de nombreuses dépendances et qu’on souhaite limiter l’utilisation de mémoire, il peut arriver qu’on veuille charger et décharger ces diverses dépendances lors de l’exécution, en fonction de l’évolution des circonstances. Si ces ressources sont pré-chargées dans des constantes, la seule façon de les décharger serait de décharger le script entier. Si par contre elles sont chargées comme propriétés, on pourra alors leur attribuer la valeur null et supprimer toute référence à ces ressources (ce qui aura pour effet de supprimer les ressources de la mémoire, celles-ci étant de type à étendre Reference).

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’une seule pièce, ou bien scinder ce niveau en fragments qui seront chargés au fur et à mesure des besoins ?

La réponse est simple, « 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, et 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 à les rendre réutilisables). 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. Il faudra donc prendre garde à ne pas développer un système dont la complexité pourrait mener à l’opposé du résultat recherché.

Ainsi, les meilleures options seraient….

  1. Utiliser un niveau unique pour les petits jeux.
  2. En fonction du temps et des ressources disponibles pour la création d’un jeu de taille moyenne à étendue, la création d’une librairie ou d’un plugin pour simplifier la gestion des nœuds et ressources. Cette librairie/plugin pourrait être amélioré et évoluer en un outil fiable au fil des projets.
  3. Implémenter la logique de chargement dynamique directement dans le jeu dans le cadre d’un projet de taille moyenne/étendue dont les délais ne permettent pas l’élaboration d’une solution plus élégante. Cette implémentation pourrait par la suite être externalisée sous forme de plugin, lorsque le temps nécessaire est disponible.

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 ».