Optimisez les performances de votre jeu mobile : Conseils sur le profilage, la mémoire et l'architecture du code des meilleurs ingénieurs de Unity

Notre équipe de production Unity Studio connaît le code source sur le bout des doigts et soutient une pléthore de clients Unity afin qu'ils puissent tirer le meilleur parti du moteur. Dans leur travail, ils plongent profondément dans les projets des créateurs pour aider à identifier les points où les performances pourraient être optimisées pour plus de rapidité, de stabilité et d'efficacité. Nous nous sommes assis avec cette équipe, composée des ingénieurs logiciels les plus expérimentés de Unity, et leur avons demandé de partager une partie de leur expertise sur l'optimisation des jeux mobiles.
Alors que nos ingénieurs commençaient à partager leurs idées sur l'optimisation des jeux mobiles, nous avons rapidement réalisé qu'il y avait beaucoup trop d'excellentes informations pour le seul article de blog que nous avions prévu. Au lieu de cela, nous avons décidé de transformer leur montagne de connaissances en un e-book complet (que vous pouvez télécharger ici), ainsi qu'une série d'articles de blog qui mettent en lumière certains de ces 75+ conseils pratiques.
Nous lançons le premier article de cette série en nous concentrant sur la façon dont vous pouvez améliorer les performances de votre jeu grâce au profilage, à la mémoire et à l'architecture du code. Dans les semaines à venir, nous suivrons avec deux autres articles : le premier couvrant la physique de l'interface utilisateur, suivi d'un autre sur l'audio et les ressources, la configuration du projet et les graphiques.
Vous voulez consulter la série complète maintenant ? Téléchargez le e-book complet gratuitement.
Plongeons-y !
Quel meilleur endroit pour commencer que le profilage et le processus de collecte et d'action sur les données de performance mobile ? C'est ici que l'optimisation des performances mobiles commence vraiment.
Profilez tôt, souvent et sur le dispositif cible
Le Profil Unity fournit des informations essentielles sur les performances de votre application, mais il ne peut pas vous aider si vous ne l'utilisez pas. Profilez votre projet tôt dans le développement, pas seulement lorsque vous êtes proche de la livraison. Enquêtez sur les glitches ou les pics dès qu'ils apparaissent. Au fur et à mesure que vous développez une "signature de performance" pour votre projet, vous serez en mesure de repérer plus facilement de nouveaux problèmes.
Bien que le profilage dans l'éditeur puisse vous donner une idée de la performance relative des différents systèmes de votre jeu, le profilage sur chaque appareil vous donne l'occasion d'obtenir des informations plus précises. Profilez une version de développement sur les appareils cibles chaque fois que cela est possible. N'oubliez pas de profiler et d'optimiser pour les appareils les plus puissants et les moins puissants que vous prévoyez de prendre en charge.
Avec le Profiler Unity, vous pouvez tirer parti des outils natifs d'iOS et d'Android pour des tests de performance supplémentaires sur leurs moteurs respectifs :
- Sur iOS, utilisez Xcode et Instruments.
- Sur Android, utilisez Android Studio et Android Profiler.
Certaines configurations matérielles peuvent tirer parti d'outils de profilage supplémentaires (par exemple, Arm Mobile Studio, Intel VTune, et Snapdragon Profiler). Voir Profilage des applications créées avec Unity pour plus d'informations.
Concentrez-vous sur l'optimisation des bonnes zones
Ne devinez pas et ne faites pas d'hypothèses sur ce qui ralentit la performance de votre jeu. Utilisez le Profiler Unity et des outils spécifiques à la plateforme pour localiser la source précise d'un ralentissement.
Bien sûr, toutes les optimisations décrites ici ne s'appliqueront pas à votre application. Quelque chose qui fonctionne bien dans un projet peut ne pas se traduire dans le vôtre. Identifiez les véritables goulets d'étranglement et concentrez vos efforts sur ce qui bénéficie à votre travail.
Comprenez comment fonctionne le profiler Unity
Le Profiler Unity peut vous aider à détecter les causes de tout ralentissement ou gel à l'exécution et à mieux comprendre ce qui se passe à une image spécifique, ou à un moment donné. Activez les pistes CPU et Mémoire par défaut. Vous pouvez surveiller des modules de Profiler supplémentaires comme Rendu, Audio et Physique, selon les besoins de votre jeu (par exemple, un gameplay lourd en physique ou basé sur la musique).

Construisez l'application sur votre appareil en cochant Développement Build et Autoconnect Profiler, ou connectez-vous manuellement pour accélérer le temps de démarrage de l'application.

Choisissez la plateforme cible à profiler. Le bouton Enregistrer suit plusieurs secondes de la lecture de votre application (300 images par défaut). Allez à Unity > Préférences > Analyse > Profiler > Nombre de frames pour augmenter cela jusqu'à 2000 si vous avez besoin de captures plus longues. Bien que cela signifie que l'éditeur Unity doit effectuer plus de travail CPU et occuper plus de mémoire, cela peut être utile selon votre scénario spécifique.
C'est un profileur basé sur l'instrumentation qui profile les temps de code explicitement enveloppés dans ProfileMarkers (comme les méthodes Start ou Update de MonoBehaviour, ou des appels API spécifiques). De plus, lors de l'utilisation du paramètre Profilage approfondi, Unity peut profiler le début et la fin de chaque appel de fonction dans votre code script pour vous indiquer exactement quelle partie de votre application cause un ralentissement.

Lors du profilage de votre jeu, nous vous recommandons de couvrir à la fois les pics et le coût d'une image moyenne dans votre jeu. Comprendre et optimiser les opérations coûteuses qui se produisent à chaque image peut être plus utile pour les applications fonctionnant en dessous du taux de trame cible. Lorsque vous recherchez des pics, explorez d'abord les opérations coûteuses (par exemple, physique, IA, animation) et la collecte des ordures.
Cliquez dans la fenêtre pour analyser une image spécifique. Ensuite, utilisez soit la vue Chronologie soit la vue Hiérarchie pour ce qui suit :
- Chronologie montre la répartition visuelle du temps pour une image spécifique. Cela vous permet de visualiser comment les activités se rapportent les unes aux autres et à travers différents threads. Utilisez cette option pour déterminer si vous êtes limité par le CPU ou le GPU.
- Hiérarchie montre la hiérarchie des ProfileMarkers, regroupés ensemble. Cela vous permet de trier les échantillons en fonction du coût en temps en millisecondes (Temps ms et Auto ms). Vous pouvez également compter le nombre de Appels à une fonction et la mémoire gérée du tas (GC Alloc) sur l'image.

Lisez un aperçu complet du Profiler Unity ici. Ceux qui découvrent le profilage peuvent également regarder cette Introduction au Profilage Unity.
Avant d'optimiser quoi que ce soit dans votre projet, enregistrez le fichier .data du Profiler. Implémentez vos modifications et comparez les données .data sauvegardées avant et après la modification. Faites confiance à ce cycle pour améliorer les performances : profiler, optimiser et comparer. Ensuite, rincez et répétez.
Utilisez l'Analyseur de Profil
Cet outil vous permet d'agréger plusieurs trames de données du Profiler, puis de localiser les trames d'intérêt. Vous voulez voir ce qui arrive au Profiler après avoir apporté une modification à votre projet ? La vue Comparer vous permet de charger et de différencier deux ensembles de données, afin que vous puissiez tester des modifications et améliorer leur résultat. L'Analyseur de Profil est disponible via le Gestionnaire de Paquets Unity.

Travaillez sur un budget temporel spécifique par trame
Chaque trame aura un budget temporel basé sur vos images cibles par seconde (fps). Idéalement, une application fonctionnant à 30 fps permettra environ 33,33 ms par trame (1000 ms / 30 fps). De même, un objectif de 60 fps laisse 16,66 ms par trame.
Les appareils peuvent dépasser ce budget pendant de courtes périodes (par exemple, pour des cinématiques ou des séquences de chargement), mais pas pendant une durée prolongée.
Prendre en compte la température de l'appareil
Cependant, pour les mobiles, nous ne recommandons pas d'utiliser ce temps maximum de manière constante car l'appareil peut surchauffer et le système d'exploitation peut réduire la puissance du CPU et du GPU. Nous recommandons d'utiliser seulement environ 65 % du temps disponible pour permettre un refroidissement entre les images. Un budget d'image typique sera d'environ 22 ms par image à 30 fps et 11 ms par image à 60 fps.
La plupart des appareils mobiles n'ont pas de refroidissement actif comme leurs homologues de bureau. Les niveaux de chaleur physique peuvent avoir un impact direct sur les performances.
Si l'appareil fonctionne à une température élevée, le Profiler pourrait percevoir et signaler de mauvaises performances, même si ce n'est pas une cause de préoccupation à long terme. Pour lutter contre la surchauffe lors du profilage, profilez par courtes périodes. Cela refroidit l'appareil et simule des conditions réelles. Notre recommandation générale est de garder l'appareil au frais pendant 10 à 15 minutes avant de profiler à nouveau.
Déterminer si vous êtes limité par le GPU ou le CPU
Le Profiler peut vous dire si votre CPU prend plus de temps que votre budget d'image alloué, ou si le coupable est votre GPU. Il le fait en émettant des marqueurs préfixés par Gfx comme suit :
- Si vous voyez le marqueur Gfx.WaitForCommands , cela signifie que le thread de rendu est prêt, mais que vous pourriez attendre un goulot d'étranglement sur le thread principal.
- Si vous rencontrez fréquemment Gfx.WaitForPresent, cela signifie que le thread principal était prêt mais attendait que le GPU présente l'image.
Unity utilise une gestion automatique de la mémoire pour votre code et vos scripts générés par l'utilisateur. De petites quantités de données, comme des variables locales de type valeur, sont allouées à la pile. Des morceaux de données plus volumineux et un stockage à long terme sont alloués au tas géré.
Le ramasse-miettes identifie périodiquement et désalloue la mémoire du tas inutilisée. Bien que cela fonctionne automatiquement, le processus d'examen de tous les objets dans le tas peut provoquer des saccades ou un fonctionnement lent du jeu.
Optimiser votre utilisation de la mémoire signifie être conscient du moment où vous allouez et désallouez de la mémoire dans le tas, et comment vous minimisez l'effet de la collecte des déchets. Voir Comprendre le tas géré pour plus d'informations.

Utilisez le Profiler de mémoire
Ce module complémentaire séparé (disponible en tant que package expérimental ou en aperçu dans le Gestionnaire de packages) peut prendre un instantané de votre mémoire de tas géré, pour vous aider à identifier des problèmes tels que la fragmentation et les fuites de mémoire.
Cliquez dans la vue de la carte d'arbre pour tracer une variable vers l'objet natif qui conserve la mémoire. Ici, vous pouvez identifier des problèmes courants de consommation de mémoire, comme des textures excessivement grandes ou des actifs en double.
Apprenez à tirer parti du Profiler de mémoire dans Unity pour une utilisation améliorée de la mémoire. Vous pouvez également consulter notre documentation officielle du Profiler de mémoire.
Réduisez l'impact de la collecte des déchets (GC)
Unity utilise le collecteur de déchets Boehm-Demers-Weiser, qui arrête l'exécution de votre code de programme et ne reprend l'exécution normale qu'une fois son travail terminé.
Soyez conscient de certaines allocations de tas inutiles, qui pourraient provoquer des pics de GC :
- Chaînes : En C#, les chaînes sont des types de référence, pas des types de valeur. Réduisez la création ou la manipulation de chaînes inutiles. Évitez d'analyser des fichiers de données basés sur des chaînes comme JSON et XML ; stockez les données dans des ScriptableObjects ou des formats comme MessagePack ou Protobuf à la place. Utilisez la classe StringBuilder si vous devez construire des chaînes à l'exécution.
- Appels de fonction Unity : Certaines fonctions créent des allocations sur le tas. Conservez des références aux tableaux plutôt que de les allouer au milieu d'une boucle. De plus, profitez de certaines fonctions qui évitent de générer des déchets. Par exemple, utilisez GameObject.CompareTag au lieu de comparer manuellement une chaîne avec GameObject.tag (car retourner une nouvelle chaîne crée des déchets).
- Boxage: Évitez de passer une variable de type valeur à la place d'une variable de type référence. Cela crée un objet temporaire, et les déchets potentiels qui l'accompagnent convertissent implicitement le type valeur en un type objet (par exemple, int i = 123; object o = i). Essayez plutôt de fournir des remplacements concrets avec le type valeur que vous souhaitez passer. Les génériques peuvent également être utilisés pour ces remplacements.
- Coroutines : Bien que yield ne produise pas de déchets, la création d'un nouvel objet WaitForSeconds le fait. Mettez en cache et réutilisez l'objet WaitForSeconds plutôt que de le créer dans la ligne yield.
- LINQ et expressions régulières : Ces deux génèrent des déchets à partir du boxing en arrière-plan. Évitez LINQ et les expressions régulières si la performance est un problème. Écrivez des boucles for et utilisez des listes comme alternative à la création de nouveaux tableaux.
Temps de collecte des déchets si possible
Si vous êtes certain qu'un gel de collecte des déchets n'affectera pas un point spécifique de votre jeu, vous pouvez déclencher la collecte des déchets avec System.GC.Collect.
Voir Comprendre la gestion automatique de la mémoire pour des exemples de la façon d'utiliser cela à votre avantage.
Utilisez le collecteur de déchets incrémental pour diviser la charge de travail du GC
Plutôt que de créer une seule interruption longue pendant l'exécution de votre programme, la collecte de déchets incrémentale utilise plusieurs interruptions beaucoup plus courtes qui répartissent la charge de travail sur de nombreux cadres. Si la collecte de déchets impacte les performances, essayez d'activer cette option pour voir si elle peut réduire le problème des pics de GC. Utilisez l'Analyseur de Profil pour vérifier son avantage pour votre application.

Le PlayerLoop de Unity contient des fonctions pour interagir avec le cœur du moteur de jeu. Cette structure comprend un certain nombre de systèmes qui gèrent l'initialisation et les mises à jour par cadre. Tous vos scripts dépendront de ce PlayerLoop pour créer le gameplay.
Lors du profilage, vous verrez le code utilisateur de votre projet sous le PlayerLoop (avec les composants de l'Éditeur sous l'EditorLoop).


Familiarisez-vous avec le PlayerLoop et le cycle de vie d'un script.
Vous pouvez optimiser vos scripts avec les conseils et astuces suivants.
Comprendre le PlayerLoop de Unity
Assurez-vous de comprendre l'ordre d'exécution de la boucle de cadre de Unity. Chaque script Unity exécute plusieurs fonctions d'événements dans un ordre prédéterminé. Vous devez comprendre la différence entre Awake, Start, Update, et d'autres fonctions qui créent le cycle de vie d'un script.
Référez-vous au Diagramme de Flux du Cycle de Vie du Script pour l'ordre d'exécution spécifique des fonctions d'événements.
Minimiser le code qui s'exécute à chaque image
Considérez si le code doit s'exécuter à chaque image. Déplacez la logique inutile hors de Mise à jour, Mise à jour tardive et Mise à jour fixe. Ces fonctions d'événements sont des endroits pratiques pour mettre du code qui doit se mettre à jour à chaque image, tout en extrayant toute logique qui n'a pas besoin de se mettre à jour avec cette fréquence. Chaque fois que c'est possible, exécutez uniquement la logique lorsque les choses changent.
Si vous devez utiliser Mise à jour, envisagez d'exécuter le code toutes les n images. C'est une façon d'appliquer le découpage temporel, une technique courante pour répartir une charge de travail lourde sur plusieurs images. Dans cet exemple, nous exécutons le ExempleFonctionCoûteuse une fois toutes les trois images :
private int interval = 3;
void Update()
{
if (Time.frameCount % interval == 0)
{
ExampleExpensiveFunction();
}
}Évitez la logique lourde dans Start/Awake
Lorsque votre première scène se charge, ces fonctions sont appelées pour chaque objet :
- Éveiller
- Activer
- Commencer
Évitez la logique coûteuse dans ces fonctions jusqu'à ce que votre application rende sa première image. Sinon, vous pourriez rencontrer des temps de chargement plus longs que nécessaire.
Référez-vous à l'ordre d'exécution des fonctions d'événements pour des détails sur le chargement de la première scène.
Évitez les événements Unity vides
Même les MonoBehaviours vides nécessitent des ressources, donc vous devriez supprimer les méthodes Mise à jour ou Mise à jour tardive vides.
Utilisez des directives de préprocesseur si vous utilisez ces méthodes pour des tests :
#if UNITY_EDITOR
void Update()
{
}
#endifIci, vous pouvez utiliser librement le Mise à jour dans l'éditeur pour des tests sans surcharge inutile dans votre build.
Supprimer les déclarations de journal de débogage
Les déclarations de journal (en particulier dans Mise à jour, Mise à jour tardive, ou Mise à jour fixe) peuvent ralentir les performances. Désactivez vos déclarations de journal avant de créer une version.
Pour faciliter cela, envisagez de créer un attribut conditionnel avec une directive de prétraitement. Par exemple, créez une classe personnalisée comme ceci :
public static class Logging
{
[System.Diagnostics.Conditional("ENABLE_LOG")]
static public void Log(object message)
{
UnityEngine.Debug.Log(message);
}
}
Générez votre message de journal avec votre classe personnalisée. Si vous désactivez le préprocesseur ENABLE_LOG dans les Paramètres du joueur, toutes vos déclarations de journal disparaissent d'un coup.
Utilisez des valeurs de hachage au lieu de paramètres de chaîne
Unity n'utilise pas de noms de chaîne pour adresser les propriétés Animator, Material et Shader en interne. Pour la vitesse, tous les noms de propriétés sont hachés en identifiants de propriété, et ces identifiants sont en fait utilisés pour adresser les propriétés.
Lors de l'utilisation d'une méthode Set ou Get sur un Animator, Material ou Shader, utilisez la méthode à valeur entière au lieu des méthodes à valeur chaîne. Les méthodes de chaîne effectuent simplement un hachage de chaîne et transmettent ensuite l'identifiant haché aux méthodes à valeur entière.
Utilisez Animator.StringToHash pour les noms de propriétés Animator et Shader.PropertyToID pour les noms de propriétés Material et Shader.
Choisissez la bonne structure de données
Votre choix de structure de données impacte l'efficacité alors que vous itérez des milliers de fois par image. Pas sûr d'utiliser une Liste, un Tableau ou un Dictionnaire pour votre collection ? Suivez le guide MSDN sur les structures de données en C# comme guide général pour choisir la bonne structure.
Évitez d'ajouter des composants à l'exécution
L'invocation de AddComponent à l'exécution a un certain coût. Unity doit vérifier les composants en double ou d'autres composants requis chaque fois qu'il ajoute des composants à l'exécution.
Instancier un Prefab avec les composants souhaités déjà configurés est généralement plus performant.
Mettre en cache les GameObjects et les composants
GameObject.Find, GameObject.GetComponent, et Camera.main (dans les versions antérieures à 2020.2) peuvent être coûteux, il est donc préférable d'éviter de les appeler dans les méthodes Update. Au lieu de cela, appelez-les dans Start et mettez en cache les résultats.
Voici un exemple qui démontre une utilisation inefficace d'un appel répété à GetComponent :
void Update()
{
Renderer myRenderer = GetComponent<Renderer>();
ExampleFunction(myRenderer);
}Au lieu de cela, invoquez GetComponent une seule fois, car le résultat de la fonction est mis en cache. Le résultat mis en cache peut être réutilisé dans Update sans aucun autre appel à GetComponent.
private Renderer myRenderer;
void Start()
{
myRenderer = GetComponent<Renderer>();
}
void Update()
{
ExampleFunction(myRenderer);
}Utiliser des pools d'objets
Instancier et Détruire peuvent générer des déchets et des pics de collecte des déchets (GC), et est généralement un processus lent. Plutôt que d'instancier et de détruire régulièrement des GameObjects (par exemple, tirer des balles d'un pistolet), utilisez des pools d'objets préalloués qui peuvent être réutilisés et recyclés.

Créez les instances réutilisables à un moment du jeu (par exemple, pendant un écran de menu) où un pic de CPU est moins perceptible. Suivez ce "pool" d'objets avec une collection. Pendant le jeu, activez simplement la prochaine instance disponible lorsque nécessaire, désactivez les objets au lieu de les détruire, et renvoyez-les au pool.

Cela réduit le nombre d'allocations gérées dans votre projet et peut prévenir les problèmes de collecte des déchets.
Apprenez à créer un système simple de Pooling d'objets dans Unity ici.
Utilisez des ScriptableObjects
Stockez des valeurs ou des paramètres immuables dans un ScriptableObject au lieu d'un MonoBehaviour. Le ScriptableObject est un actif qui vit à l'intérieur du projet et que vous n'avez besoin de configurer qu'une seule fois. Il ne peut pas être directement attaché à un GameObject.
Créez des champs dans le ScriptableObject pour stocker vos valeurs ou paramètres, puis référencez le ScriptableObject dans vos MonoBehaviours.

Utiliser ces champs du ScriptableObject peut éviter la duplication inutile de données chaque fois que vous instanciez un objet avec ce MonoBehaviour.
Regardez ce tutoriel Introduction aux ScriptableObjects pour voir comment les ScriptableObjects peuvent aider votre projet. Vous pouvez également trouver la documentation pertinente ici.
Dans le prochain article de blog, nous examinerons de plus près l'optimisation des graphiques et du GPU. Cependant, si vous souhaitez accéder à la liste complète des conseils et astuces de l'équipe maintenant, notre e-book complet est disponible ici.

Si vous êtes intéressé à en savoir plus sur les services de support intégré et souhaitez donner à votre équipe un accès direct aux ingénieurs, des conseils d'experts et des orientations sur les meilleures pratiques pour vos projets, consultez les plans de succès de Unity ici.
Nous voulons vous aider à rendre vos applications Unity aussi performantes que possible, donc si vous avez des sujets d'optimisation que vous aimeriez connaître davantage, veuillez nous tenir informés dans les commentaires.
