Unity script order execution

Ordre d’Exécution sur Unity

Un des pièges les plus courants dans le développement de jeux Unity est la redoutée « NullReferenceException ».

Un facteur essentiel pour éviter cette erreur est de maintenir un ordre d’exécution des scripts bien organisé.

Si vous recherchez une explication complète de l’ordre d’exécution des événements dans Unity, la documentation officielle fournit des informations détaillées.

Table des matières

Modification de l’ordre d’exécution des scripts

Changer l’ordre d’exécution de vos scripts se fait principalement dans l’éditeur de Unity.

Accédez à « Edit » -> « Project Settings » -> « Script Execution Order ».
Ici, vous pouvez cliquer sur le bouton « + » pour ajouter votre script et définir combien de millisecondes votre script doit attendre avant de s’exécuter. (Les valeurs négatives sont autorisées dans ce champ)

Ordre d'exécution des scripts Unity

Modification de l’ordre d’exécution dans le code

Il est également possible d’ajuster l’ordre d’exécution directement dans votre code à l’aide de l’attribut « DefaultExecutionOrder ».

Il convient de noter que cet attribut n’est pas officiellement documenté.
Comme l’a suggéré un message posté sur le forum de Unity, la raison derrière cela pourrait être que les changements de l’attribut « DefaultExecutionOrder » n’apparaissent pas dans la fenêtre de l’ordre d’exécution, ce qui pourrait potentiellement entraîner de la confusion.

using UnityEngine;

[DefaultExecutionOrder(-50)]
public class Script : MonoBehaviour
{

}

Automatisation de l’ordre d’exécution

Modifier l’ordre d’exécution peut être répétitif et fastidieux, en particulier si vous avez un nombre substantiel de scripts à gérer.

Pour résoudre ce problème, vous pouvez définir un répertoire où tous les scripts nouvellement créés ont automatiquement un ordre d’exécution prédéfini.

Pour ce faire, vous devez créer un fichier « CSAssetPostprocessor » dans un dossier « Editor », qui hérite de « AssetPostprocessor« .

Attention, sur Unity 2021.2+, « OnPostprocessAllAssets » a un paramètre bool supplémentaire: « didDomainReload« .

using System.IO;
using UnityEditor;

public class CSAssetPostprocessor : AssetPostprocessor
{
	// Attention, sur Unity 2021.2+, il y a un paramètre supplémentaire : bool didDomainReload
    private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
    {
        foreach (string assetPath in importedAssets)
        {
            HandleImport(assetPath);
        }
    }

    private static void HandleImport(string assetPath)
    {
        // Seuls les fichiers CS
        if (Path.GetExtension(assetPath) != ".cs")
        {
            return;
        }

        // Seuls les fichiers dans le répertoire "BeforeDefaultTime"
        if (Directory.GetParent(assetPath).Name != "BeforeDefaultTime")
        {
            return;
        }

        var monoImporter = AssetImporter.GetAtPath(assetPath) as MonoImporter;

        // Évitez la boucle infinie
        if (MonoImporter.GetExecutionOrder(monoImporter.GetScript()) == -100)
        {
            return;
        }

        // Définir l'ordre d'exécution à -100 ms
        MonoImporter.SetExecutionOrder(monoImporter.GetScript(), -100);
    }
}

Considérations et alternatives

Si ajuster l’ordre d’exécution des scripts fonctionne bien pour certains scénarios, cela peut ne pas être la meilleure approche lorsque vous devez séquencer les scripts sans connaissance préalable de leur durée d’exécution (par exemple, pour la gestion des requêtes HTTP, le chargement de sous-scènes, …).

Dans de tels cas, il est souvent plus simple de gérer directement l’ordre d’exécution des scripts dans votre code.

Prenons un exemple d’un jeu qui doit charger des données, puis charger un niveau, et enfin charger des ennemis.

Une approche consiste à avoir un script principal, tel que « GameBootstrap », responsable de l’état général du jeu. Il est essentiel de l’implémenter en tant que singleton pour plus de cohérence.

Pour simplifier le processus, vous pouvez créer une classe abstraite, « AbstractLoader », servant de point de référence pour chaque état.
N’hésitez pas à ignorer l’erreur sur « GameBoostrap », nous allons le créer tout de suite.

using UnityEngine;

public abstract class AbstractLoader : MonoBehaviour
{
    // System.Serializable -> nous allons l'utiliser dans l'inspecteur
    [System.Serializable]
    public enum StateLoad
    {
        NONE,
        DATA,
        LEVEL,
        ENEMIES,
        FULLY_LOAD
    }

    public StateLoad stateLoad;

    public StateLoad stateRequired;

    public bool IsLoad { get; private set; } = false;

    abstract public void Load();

    // Doit être appelé lorsque le chargement est terminé !!!!!!
    protected void LoadDone()
    {
        IsLoad = true;
        GameBoostrap.Instance.OnStateLoad?.Invoke(stateLoad);
    }
}

Pour clarifier, nous utilisons l’énumération « StateLoad » pour identifier un loader. Le champ « stateLoad » est l’identifiant, tandis que le champ « stateRequired » permet d’inclure une dépendance.

Ensuite, créez un gameObject avec le composant “GameBootstrap” dans votre scène.

using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Events;
using static AbstractLoader;

public class GameBoostrap : MonoBehaviour
{
    public static GameBoostrap Instance { get; private set; }

    public List abstractLoaders = new List();

    public UnityAction OnStateLoad { get; set; }

    private void Awake()
    {
        if (Instance != null)
        {
            throw new UnityException("Une seule instance autorisée");
        }

        Instance = this;
        // Vous pouvez ajouter DontDestroyOnLoad si vous voulez qu'il soit persistant dans toutes les scènes.
    }

    private void Start()
    {
        ApplyLoaders();
    }

    public void ApplyLoaders()
    {
        // Préparer le chargement
        OnStateLoad += OnStateLoadAction;

        // Charger uniquement les chargeurs sans stateRequired
        foreach (var loader in abstractLoaders)
        {
            if (loader.stateRequired == StateLoad.NONE)
            {
                loader.Load();
                continue;
            }
        }
    }

    // Lorsque tout est chargé, c'est vrai
    private bool _isFullyLoad => abstractLoaders.All(x => x.IsLoad);

    // Cache de _isFullyLoad, pour les appels externes
    public bool IsFullyLoad { get; private set; } = false;

    private void OnStateLoadAction(StateLoad stateLoad)
    {
        // Tout est chargé, nous allons fermer l'événement
        if (_isFullyLoad)
        {
            FullyLoadAction();
            return;
        }

        // Charger lorsque stateRequired est terminé
        foreach (var loader in abstractLoaders)
        {
            if (!loader.IsLoad && loader.stateRequired == stateLoad)
            {
                loader.Load();
            }
        }
    }

    private void FullyLoadAction()
    {
        OnStateLoad -= OnStateLoadAction;
        IsFullyLoad = true;
        OnStateLoad?.Invoke(StateLoad.FULLY_LOAD);
    }
}

Pour maintenir la cohérence, il est conseillé de déclarer systématiquement vos instances dans la méthode « Awake ».

Cela garantit qu’une instance est établie avant d’être accessible depuis d’autres scripts, et elle doit toujours être invoquée pendant ou après le cycle de vie de la méthode « Start ».

Dans cet exemple de code, tous les loaders d’état « NONE » sont initialement chargés. Ensuite, une fois qu’un loader a terminé, les loaders qui en dépendent sont déclenchés jusqu’à ce que tout le chargement soit terminé.

Maintenant, pour tester le processus, créez un script qui simule le chargement pour chacun des trois états, par exemple « ExampleLoader ».

using System.Collections;
using UnityEngine;

public class ExempleLoader : AbstractLoader
{
    public override void Load()
    {
        Debug.Log(stateLoad + " commence");
        StartCoroutine(SimuleDelay());
    }

    // Simuler une requête HTTP ...
    private IEnumerator SimuleDelay()
    {
        yield return new WaitForSeconds(Random.Range(1f, 3f));
        Debug.Log(stateLoad + " est terminé !");
        LoadDone();
    }
}

Créez trois GameObjects avec le composant « ExampleLoader » et associez-les à « GameBootstrap ».

Ordre d'exécution Unity personnalisé

Résultat

Console d'ordre d'exécution Unity

Exécuter des actions en dehors du workflow

Enfin, supposez que vous ayez besoin d’effectuer des actions spécifiques après le chargement d’un état sans être dans le flux de travail.

Dans ce cas, vous pouvez ajouter les lignes de code suivantes à « GameBootstrap ».

    // Vérifiez si un chargeur est chargé
    public bool IsStateLoad(StateLoad stateLoad)
    {
        // S'il n'y a pas de chargeur avec l'état FULLY_LOAD, IsFullyLoad fait ce travail.
        if (stateLoad == StateLoad.FULLY_LOAD)
        {
            return IsFullyLoad;
        }

        return abstractLoaders.Single(x => x.stateLoad == stateLoad).IsLoad;
    }

    public void ExecuteActionAfterStateLoad(StateLoad stateLoad, UnityAction unityAction)
    {
        // L'état est chargé, nous invoquons immédiatement
        if (IsStateLoad(stateLoad))
        {
            unityAction.Invoke();
            return;
        }

        // L'état n'est pas chargé, nous nous abonnons à l'événement (j'aime utiliser une méthode interne pour annuler l'événement)
        void WaitStateLoad(StateLoad state)
        {
            if (state == stateLoad)
            {
                // Doit être désabonné
                OnStateLoad -= WaitStateLoad;
                unityAction.Invoke();
            }
        }

        OnStateLoad += WaitStateLoad;
    }

Voici un exemple de comment l’utiliser :

using UnityEngine;
using static AbstractLoader;

public class TestWaitDataLoad : MonoBehaviour
{
    void Start()
    {
        GameBoostrap.Instance.ExecuteActionAfterStateLoad(StateLoad.DATA, () =>
        {
            Debug.Log("Exécuter après le chargement des données");
        });
    }
}

Console d'ordre d'exécution Unity hors du flux de travail

Ce dernier point est un peu hors sujet, mais sert principalement à illustrer le fait que vous ne devriez pas utiliser « l’ordre d’exécution » dans tous les cas.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *