Unity Optimisez votre jeu à l'aide de Profiler

La performance est un aspect clé de tout jeu et pas de surprise, peu importe la qualité du jeu, s'il fonctionne mal sur la machine de l'utilisateur, il ne sera pas aussi agréable.

Étant donné que tout le monde ne possède pas un PC ou un appareil haut de gamme (si vous ciblez le mobile), il est important de garder à l'esprit les performances tout au long du développement.

Il y a plusieurs raisons pour lesquelles le jeu pourrait fonctionner lentement:

  • Rendu (trop de maillages high-poly, de shaders complexes ou d'effets d'image)
  • Audio (principalement causé par des paramètres d'importation audio incorrects)
  • Code non optimisé (scripts contenant des fonctions exigeantes en termes de performances aux mauvais endroits)

Dans ce didacticiel, je vais montrer comment optimiser votre code à l'aide de Unity Profiler.

Profileur

Historiquement, le débogage des performances dans Unity était une tâche fastidieuse, mais depuis lors, une nouvelle fonctionnalité a été ajoutée, appelée Profiler.

Profiler est un outil dans Unity qui vous permet d'identifier rapidement les goulots d'étranglement de votre jeu en surveillant la consommation de mémoire, ce qui simplifie grandement le processus d'optimisation.

Fenêtre du profileur Unity

Mauvaise performance

De mauvaises performances peuvent survenir à tout moment: disons que vous travaillez sur l'instance ennemie et que lorsque vous la placez dans la scène, cela fonctionne bien sans aucun problème, mais à mesure que vous générez plus d'ennemis, vous remarquerez peut-être des fps (images par seconde ) commencent à baisser.

Vérifiez l'exemple ci-dessous:

Dans la scène, j'ai un cube auquel est attaché un script, qui déplace le cube d'un côté à l'autre et affiche le nom de l'objet:

SC_ShowName.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SC_ShowName : MonoBehaviour
{
    bool moveLeft = true;
    float movedDistance = 0;

    // Start is called before the first frame update
    void Start()
    {
        moveLeft = Random.Range(0, 10) > 5;
    }

    // Update is called once per frame
    void Update()
    {
        //Move left and right in ping-pong fashion
        if (moveLeft)
        {
            if(movedDistance > -2)
            {
                movedDistance -= Time.deltaTime;
                Vector3 currentPosition = transform.position;
                currentPosition.x -= Time.deltaTime;
                transform.position = currentPosition;
            }
            else
            {
                moveLeft = false;
            }
        }
        else
        {
            if (movedDistance < 2)
            {
                movedDistance += Time.deltaTime;
                Vector3 currentPosition = transform.position;
                currentPosition.x += Time.deltaTime;
                transform.position = currentPosition;
            }
            else
            {
                moveLeft = true;
            }
        }
    }

    void OnGUI()
    {
        //Show object name on screen
        Camera mainCamera = Camera.main;
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
    }
}

En regardant les statistiques, nous pouvons voir que le jeu tourne à plus de 800 ips, donc il n'a pratiquement aucun impact sur les performances.

Mais voyons ce qui se passera si nous dupliquons le Cube 100 fois:

Fps chuté de plus de 700 points !

REMARQUE: Tous les tests ont été effectués avec Vsync désactivé

En règle générale, c'est une bonne idée de commencer à optimiser lorsque le jeu commence à présenter des saccades, des blocages ou que le fps tombe en dessous de 120.

Comment utiliser le profileur ?

Pour commencer à utiliser Profiler, vous aurez besoin de:

  • Démarrez votre jeu en appuyant sur Play
  • Ouvrez Profiler en allant dans Fenêtre -> Analyse -> Profiler (ou appuyez sur Ctrl + 7)

  • Une nouvelle fenêtre apparaîtra et ressemblera à ceci:

Fenêtre du profileur Unity 3D

  • Cela peut sembler intimidant au début (surtout avec tous ces graphiques, etc.), mais ce n'est pas la partie que nous allons examiner.
  • Cliquez sur l'onglet Chronologie et changez-le en Hiérarchie:

  • Vous remarquerez 3 sections (EditorLoop, PlayerLoop et Profiler.CollectEditorStats):

  • Développez PlayerLoop pour voir toutes les parties où la puissance de calcul est dépensée (REMARQUE: si les valeurs de PlayerLoop ne sont pas mises à jour, cliquez sur le bouton "Clear" en haut de la fenêtre du profileur).

Pour de meilleurs résultats, dirigez votre personnage de jeu vers la situation (ou l'endroit) où le jeu est le plus en retard et attendez quelques secondes.

  • Après avoir attendu un peu, Arrêtez le jeu et observez la liste PlayerLoop

Vous devez examiner la valeur GC Alloc, qui signifie Garbage Collection Allocation. Il s'agit d'un type de mémoire qui a été alloué par le composant mais qui n'est plus nécessaire et attend d'être libéré par le Garbage Collection. Idéalement, le code ne devrait pas générer de déchets (ou être aussi proche que possible de 0).

Le temps ms est également une valeur importante, il montre combien de temps le code a pris pour s'exécuter en millisecondes, donc idéalement, vous devriez également viser à réduire cette valeur (en mettant en cache les valeurs, en évitant d'appeler des fonctions exigeantes en performances chaque mise à jour, etc.).

Pour localiser plus rapidement les pièces gênantes, cliquez sur la colonne GC Alloc pour trier les valeurs de haut en bas)

  • Dans le graphique d'utilisation du processeur, cliquez n'importe où pour passer à ce cadre. Plus précisément, nous devons examiner les pics, où le fps était le plus bas:

Graphique d'utilisation du processeur Unity

Voici ce que le profileur a révélé:

GUI.Repaint alloue 45,4 Ko, ce qui est beaucoup, l'étendre a révélé plus d'informations:

  • Il montre que la plupart des allocations proviennent des méthodes GUIUtility.BeginGUI() et OnGUI() dans le script SC_ShowName, sachant que nous pouvons commencer à optimiser.

GUIUtility.BeginGUI() représente une méthode OnGUI() vide (Oui, même la méthode OnGUI() vide alloue beaucoup de mémoire).

Utilisez Google (ou un autre moteur de recherche) pour trouver les noms que vous ne reconnaissez pas.

Voici la partie OnGUI() qu'il faut optimiser:

    void OnGUI()
    {
        //Show object name on screen
        Camera mainCamera = Camera.main;
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
    }

Optimisation

Commençons l'optimisation.

Chaque script SC_ShowName appelle sa propre méthode OnGUI(), ce qui n'est pas bon étant donné que nous avons 100 instances. Alors, que peut-on faire? La réponse est: Avoir un seul script avec la méthode OnGUI() qui appelle la méthode GUI pour chaque Cube.

  • Tout d'abord, j'ai remplacé le OnGUI() par défaut dans le script SC_ShowName par public void GUIMethod() qui sera appelé depuis un autre script:
    public void GUIMethod()
    {
        //Show object name on screen
        Camera mainCamera = Camera.main;
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
    }
  • Ensuite, j'ai créé un nouveau script et l'ai appelé SC_GUIMethod:

SC_GUIMethod.cs

using UnityEngine;

public class SC_GUIMethod : MonoBehaviour
{
    SC_ShowName[] instances; //All instances where GUI method will be called

    void Start()
    {
        //Find all instances
        instances = FindObjectsOfType<SC_ShowName>();
    }

    void OnGUI()
    {
        for(int i = 0; i < instances.Length; i++)
        {
            instances[i].GUIMethod();
        }
    }
}

SC_GUIMethod sera attaché à un objet aléatoire dans la scène et appellera toutes les méthodes GUI.

  • Nous sommes passés de 100 méthodes OnGUI() individuelles à une seule, appuyons sur play et voyons le résultat:

  • GUIUtility.BeginGUI() n'alloue plus que 368 B au lieu de 36,7 Ko, une grosse réduction !

Cependant, la méthode OnGUI() alloue toujours de la mémoire, mais comme nous savons qu'elle n'appelle que GUIMethod() à partir du script SC_ShowName, nous allons directement déboguer cette méthode.

Mais le Profiler ne montre que des informations globales, comment voyons-nous exactement ce qui se passe à l'intérieur de la méthode ?

Pour déboguer à l'intérieur de la méthode, Unity dispose d'une API pratique appelée Profiler.BeginSample

Profiler.BeginSample vous permet de capturer une section spécifique du script, indiquant le temps qu'il a fallu pour terminer et la quantité de mémoire allouée.

  • Avant d'utiliser la classe Profiler dans le code, nous devons importer l'espace de noms UnityEngine.Profiling au début du script:
using UnityEngine.Profiling;
  • L'exemple de profileur est capturé en ajoutant Profiler.BeginSample("SOME_NAME"); au début de la capture et en ajoutant Profiler.EndSample(); à la fin de la capture, comme ceci:
        Profiler.BeginSample("SOME_CODE");
        //...your code goes here
        Profiler.EndSample();

Comme je ne sais pas quelle partie de GUIMethod() provoque des allocations de mémoire, j'ai inclus chaque ligne dans Profiler.BeginSample et Profiler.EndSample (Mais si votre méthode a beaucoup de lignes, vous n'avez certainement pas besoin de joindre chaque ligne, divisez-la simplement en morceaux égaux, puis travaillez à partir de là).

Voici une dernière méthode avec Profiler Samples implémenté:

    public void GUIMethod()
    {
        //Show object name on screen
        Profiler.BeginSample("sc_show_name part 1");
        Camera mainCamera = Camera.main;
        Profiler.EndSample();

        Profiler.BeginSample("sc_show_name part 2");
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        Profiler.EndSample();

        Profiler.BeginSample("sc_show_name part 3");
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
        Profiler.EndSample();
    }
  • Maintenant, j'appuie sur Play et je vois ce qu'il affiche dans le Profiler:
  • Pour plus de commodité, j'ai recherché "sc_show_" dans le profileur, car tous les échantillons commencent par ce nom.

  • Intéressant... Beaucoup de mémoire est allouée dans sc_show_names part 3, qui correspond à cette partie du code:
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);

Après quelques recherches sur Google, j'ai découvert que l'obtention du nom de l'objet alloue beaucoup de mémoire. La solution consiste à attribuer le nom d'un objet à une variable de chaîne dans void Start(), de cette façon, il ne sera appelé qu'une seule fois.

Voici le code optimisé:

SC_ShowName.cs

using UnityEngine;
using UnityEngine.Profiling;

public class SC_ShowName : MonoBehaviour
{
    bool moveLeft = true;
    float movedDistance = 0;

    string objectName = "";

    // Start is called before the first frame update
    void Start()
    {
        moveLeft = Random.Range(0, 10) > 5;
        objectName = gameObject.name; //Store Object name to a variable
    }

    // Update is called once per frame
    void Update()
    {
        //Move left and right in ping-pong fashion
        if (moveLeft)
        {
            if(movedDistance > -2)
            {
                movedDistance -= Time.deltaTime;
                Vector3 currentPosition = transform.position;
                currentPosition.x -= Time.deltaTime;
                transform.position = currentPosition;
            }
            else
            {
                moveLeft = false;
            }
        }
        else
        {
            if (movedDistance < 2)
            {
                movedDistance += Time.deltaTime;
                Vector3 currentPosition = transform.position;
                currentPosition.x += Time.deltaTime;
                transform.position = currentPosition;
            }
            else
            {
                moveLeft = true;
            }
        }
    }

    public void GUIMethod()
    {
        //Show object name on screen
        Profiler.BeginSample("sc_show_name part 1");
        Camera mainCamera = Camera.main;
        Profiler.EndSample();

        Profiler.BeginSample("sc_show_name part 2");
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        Profiler.EndSample();

        Profiler.BeginSample("sc_show_name part 3");
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), objectName);
        Profiler.EndSample();
    }
}
  • Voyons ce que le profileur affiche:

Tous les échantillons allouent 0B, donc plus aucune mémoire n'est allouée.

Articles suggérés
Conseils d'optimisation pour Unity
Améliorer les performances d'un jeu mobile dans Unity
Comment utiliser la mise à jour dans Unity
Paramètres d'importation de clips audio Unity pour les meilleures performances
Le générateur de panneaux d'affichage pour Unity
Comment devenir un meilleur programmeur dans Unity
Comment créer un jeu inspiré de la FNAF dans Unity