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

Notre équipe Accelerate Solutions connaît parfaitement le code source et prend en charge une multitude de clients Unity afin qu'ils puissent tirer le meilleur parti du moteur. Dans leur travail, ils se plongent profondément dans les projets des créateurs pour aider à identifier les points où les performances pourraient être optimisées pour une plus grande vitesse, stabilité et efficacité. Nous avons rencontré cette équipe, composée des ingénieurs logiciels les plus expérimentés d'Unity, et leur avons demandé de partager une partie de leur expertise en matière d'optimisation des jeux mobiles.
Alors que nos ingénieurs ont commencé à partager leurs idées sur l’optimisation des jeux mobiles, nous avons rapidement réalisé qu’il y avait beaucoup trop d’informations intéressantes 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 livre électronique complet (que vous pouvez télécharger ici), ainsi qu'une série d'articles de blog mettant en lumière certains de ces 75+ conseils pratiques.
Nous commençons le premier article de cette série en examinant comment vous pouvez améliorer les performances de votre jeu grâce au profilage, à la mémoire et à l'architecture du code. Au cours des prochaines semaines, nous publierons deux autres articles : le premier portant sur la physique de l'interface utilisateur, suivi d'un autre sur l'audio et les ressources, la configuration du projet et les graphiques.
Vous souhaitez découvrir la série complète maintenant ? Téléchargez gratuitement le livre électronique complet.
Allons-y !
Quel meilleur endroit pour commencer que le profilage et le processus de collecte et d’exploitation des données de performances mobiles ? C’est ici que commence véritablement l’optimisation des performances mobiles.
Créez un profil tôt, souvent et sur l'appareil cible
Le profileur 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 dès le début du développement, et pas seulement lorsque vous êtes sur le point de le livrer. Enquêtez sur les problèmes ou les pics dès qu’ils apparaissent. À 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.
Alors que le profilage dans l'éditeur peut vous donner une idée des performances relatives des différents systèmes de votre jeu, le profilage sur chaque appareil vous donne la possibilité 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 les appareils aux spécifications les plus élevées et les plus basses que vous prévoyez de prendre en charge.
En plus de Unity Profiler, vous pouvez exploiter les outils natifs d'iOS et d'Android pour effectuer des tests de performances supplémentaires sur leurs moteurs respectifs :
- Sur iOS, utilisez Xcode et Instruments.
- Sur Android, utilisez Android Studio et Android Profiler.
Certains matériels peuvent bénéficier d’outils de profilage supplémentaires (par exemple, Arm Mobile Studio, Intel VTuneet Snapdragon Profiler). Consultez Profilage des applications créées avec Unity pour plus d'informations.
Concentrez-vous sur l'optimisation des bons domaines
Ne faites pas de suppositions ou de suppositions sur ce qui ralentit les performances de votre jeu. Utilisez Unity Profiler et des outils spécifiques à la plateforme pour localiser la source précise d'un décalage.
Bien entendu, toutes les optimisations décrites ici ne s’appliqueront pas à votre application. Ce qui fonctionne bien dans un projet peut ne pas fonctionner dans le vôtre. Identifiez les véritables goulots d’étranglement et concentrez vos efforts sur ce qui profite à votre travail.
Comprendre le fonctionnement du profileur Unity
Le profileur Unity peut vous aider à détecter les causes de tout retard ou blocage lors de l'exécution et à mieux comprendre ce qui se passe à une image ou à un moment précis. Activez les pistes CPU et Mémoire par défaut. Vous pouvez surveiller des modules de profilage supplémentaires tels que le moteur de rendu, l'audio et la physique, selon les besoins de votre jeu (par exemple, un gameplay basé sur la physique ou la musique).

Créez l'application sur votre appareil en cochant Development 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 lecture de votre application (300 images par défaut). Accédez à Unity > Préférences > Analyse > Profiler > Nombre d'images pour augmenter ce nombre jusqu'à 2 000 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 en fonction de votre scénario spécifique.
Il s'agit d'un profileur basé sur l'instrumentation qui profile les timings de code explicitement encapsulés dans des ProfileMarkers (tels que les méthodes Start ou Update de MonoBehaviour, ou des appels API spécifiques). De plus, lorsque vous utilisez le paramètre de profilage approfondi, Unity peut profiler le début et la fin de chaque appel de fonction dans votre code de script pour vous indiquer exactement quelle partie de votre application provoque 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. La compréhension et l’optimisation des opérations coûteuses qui se produisent dans chaque image peuvent être plus utiles pour les applications exécutées en dessous de la fréquence d’images cible. Lorsque vous recherchez des pics, explorez d'abord les opérations coûteuses (par exemple, la physique, l'IA, l'animation) et la collecte des déchets.
Cliquez dans la fenêtre pour analyser une image spécifique. Ensuite, utilisez la vue Timeline ou Hiérarchie pour les opérations suivantes :
- La Timeline montre la répartition visuelle du temps pour une image spécifique. Cela vous permet de visualiser la manière dont les activités sont liées les unes aux autres et à travers différents threads. Utilisez cette option pour déterminer si vous êtes lié au CPU ou au GPU.
- La hiérarchie montre la hiérarchie des ProfileMarkers, regroupés. Cela vous permet de trier les échantillons en fonction du coût temporel en millisecondes (Time ms et Self ms). Vous pouvez également compter le nombre d' appels à une fonction et la mémoire de tas gérée (GC Alloc) sur la trame.

Lisez un aperçu complet du Unity Profiler ici. Les nouveaux venus dans le profilage peuvent également regarder cette Introduction au profilage Unity.
Avant d’optimiser quoi que ce soit dans votre projet, enregistrez le fichier de données du Profiler. Implémentez vos modifications et comparez les données enregistrées avant et après la modification. Appuyez-vous sur ce cycle pour améliorer les performances : profilez, optimiser et comparez. Ensuite, rincez et répétez.
Utiliser l' Profile Analyzer
Cet outil vous permet d'agréger plusieurs trames de données Profiler, puis de localiser les trames qui vous intéressent. 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 de tester les modifications et d'améliorer leur résultat. L' Profile Analyzer est disponible via le gestionnaire de packages d'Unity.

Travailler sur un budget de temps spécifique par image
Chaque image aura un budget de temps basé sur votre nombre d'images par seconde (fps) cible. Idéalement, une application fonctionnant à 30 ips permettra environ 33,33 ms par image (1 000 ms / 30 ips). De même, un objectif de 60 ips laisse 16,66 ms par image.
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.
Tenir compte de la température de l'appareil
Pour les appareils mobiles, cependant, nous ne recommandons pas d'utiliser ce temps maximum de manière cohérente, car l'appareil peut surchauffer et le système d'exploitation peut limiter thermiquement le processeur et le processeur graphique. Nous vous recommandons d'utiliser seulement environ 65 % du temps disponible pour permettre le refroidissement entre les images. Un budget d'image typique sera d'environ 22 ms par image à 30 ips et de 11 ms par image à 60 ips.
La plupart des appareils mobiles ne disposent 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 chauffe, le Profiler peut percevoir et signaler de mauvaises performances, même si cela ne constitue pas un motif d'inquiétude à long terme. Pour lutter contre la surchauffe du profilage, effectuez le profilage par courtes rafales. Cela refroidit l’appareil et simule les conditions du monde réel. Notre recommandation générale est de garder l'appareil au frais pendant 10 à 15 minutes avant de procéder à un nouveau profilage.
Déterminez si vous êtes lié au GPU ou au CPU
Le Profiler peut vous indiquer si votre CPU prend plus de temps que votre budget d'images 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 vous attendez peut-être 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 la trame.
Unity utilise une gestion automatique de la mémoire pour votre code et vos scripts générés par l'utilisateur. De petits morceaux de données, comme des variables locales de type valeur, sont alloués à la pile. Les morceaux de données plus volumineux et le stockage à plus long terme sont alloués au tas géré.
Le récupérateur de mémoire identifie et libère périodiquement la mémoire de tas inutilisée. Bien que cela s'exécute automatiquement, le processus d'examen de tous les objets du tas peut provoquer des ralentissements ou des saccades du jeu.
Optimiser votre utilisation de la mémoire signifie être conscient du moment où vous allouez et désallouez la mémoire du tas, et de la manière dont vous minimisez l'effet du ramasse-miettes. Pour plus d’informations, consultez Comprendre le tas géré .

Utiliser le Memory Profiler
Ce module complémentaire distinct (disponible sous forme de package expérimental ou d'aperçu dans le gestionnaire de packages) peut prendre un instantané de votre mémoire de tas gérée, pour vous aider à identifier les problèmes tels que la fragmentation et les fuites de mémoire.
Cliquez dans la vue Carte arborescente pour tracer une variable jusqu'à l'objet natif contenant la mémoire. Ici, vous pouvez identifier les problèmes courants de consommation de mémoire, comme des textures excessivement volumineuses ou des ressources en double.
Découvrez comment exploiter le Memory Profiler dans Unity pour améliorer l'utilisation de la mémoire. Vous pouvez également consulter notre documentation officielle sur Memory Profiler.
Réduire l'impact de la collecte des déchets (GC)
Unity utilise le récupérateur de mémoire 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 attentif à certaines allocations de tas inutiles, qui pourraient provoquer des pics de GC :
- Cordes: En C#, les chaînes sont des types de référence et non 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 tels que JSON et XML ; stockez plutôt les données dans des ScriptableObjects ou des formats tels que MessagePack ou Protobuf. Utilisez la classe StringBuilder si vous devez créer des chaînes au moment de l'exécution.
- Appels de fonctions Unity : Certaines fonctions créent des allocations de tas. Mettez en cache les références aux tableaux plutôt que de les allouer au milieu d'une boucle. Profitez également 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 le renvoi d'une nouvelle chaîne crée des déchets).
- Boxe: Évitez de transmettre 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 de valeur en un objet de type (par exemple, int i = 123; object o = i). Essayez plutôt de fournir des remplacements concrets avec le type de valeur que vous souhaitez transmettre. 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 de yield .
- LINQ et expressions régulières : Ces deux-là génèrent des déchets provenant de la boxe en coulisses. Évitez LINQ et les expressions régulières si les performances sont un problème. Écrivez des boucles et utilisez des listes comme alternative à la création de nouveaux tableaux.
Chronométrez la collecte des déchets si possible
Si vous êtes certain qu'un gel du ramasse-miettes n'affectera pas un point spécifique de votre jeu, vous pouvez déclencher le ramasse-miettes avec System. GC.Collect.
Consultez Comprendre la gestion automatique de la mémoire pour obtenir des exemples sur la manière d'utiliser cette fonctionnalité à votre avantage.
Utilisez le récupérateur de mémoire incrémentiel pour diviser la charge de travail du GC
Plutôt que de créer une seule et longue interruption pendant l'exécution de votre programme, la collecte incrémentielle des déchets utilise plusieurs interruptions beaucoup plus courtes qui répartissent la charge de travail sur plusieurs trames. Si le ramassage des déchets a un impact sur les performances, essayez d'activer cette option pour voir si elle peut réduire le problème des pics de GC . Utilisez l' Profile Analyzer pour vérifier son utilité pour votre application.

Le Unity PlayerLoop contient des fonctions permettant d'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 image. Tous vos scripts s'appuieront sur ce PlayerLoop pour créer le gameplay.
Lors du profilage, vous verrez le code utilisateur de votre projet sous PlayerLoop (avec les composants Editor sous EditorLoop).


Découvrez le PlayerLoop et le cycle de vie d'un script.
Vous pouvez optimiser vos scripts avec les trucs et astuces suivants.
Comprendre le PlayerLoop Unity
Assurez-vous de bien comprendre l’ ordre d’exécution de la boucle d’image d’Unity. Chaque script Unity exécute plusieurs fonctions d'événement dans un ordre prédéterminé. Vous devez comprendre la différence entre Awake, Start, Updateet d’autres fonctions qui créent le cycle de vie d’un script.
Reportez-vous à l' organigramme du cycle de vie du script pour connaître l'ordre d'exécution spécifique des fonctions d'événement.
Réduire le code qui exécute chaque image
Déterminez si le code doit être exécuté à chaque image. Supprimez la logique inutile de Update, LateUpdateet FixedUpdate. Ces fonctions d'événement sont des endroits pratiques pour placer du code qui doit être mis à jour à chaque image, tout en extrayant toute logique qui n'a pas besoin d'être mise à jour avec cette fréquence. Dans la mesure du possible, n’exécutez la logique que lorsque les choses changent.
Si vous devez utiliser Update, pensez à exécuter le code toutes les n images. Il s’agit d’une façon d’appliquer le découpage temporel, une technique courante de répartition d’une charge de travail importante sur plusieurs images. Dans cet exemple, nous exécutons ExampleExpensiveFunction une fois toutes les trois images :
Type de bloc inconnu « codeBlock », veuillez spécifier un sérialiseur pour celui-ci dans la propriété « serializers.types »
Évitez la logique lourde dans Start/Awake
Lorsque votre première scène se charge, ces fonctions sont appelées pour chaque objet :
- Awake
- Activé
- Commencer
Évitez la logique coûteuse dans ces fonctions jusqu'à ce que votre application rende sa première image. Dans le cas contraire, vous risquez de rencontrer des temps de chargement plus longs que nécessaire.
Reportez-vous à l’ ordre d’exécution des fonctions d’événement pour plus de 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, vous devez donc supprimer les méthodes Update ou LateUpdate vides.
Utilisez les directives du préprocesseur si vous utilisez ces méthodes pour les tests :
Type de bloc inconnu « codeBlock », veuillez spécifier un sérialiseur pour celui-ci dans la propriété « serializers.types »
Ici, vous pouvez utiliser librement la mise à jour dans l'éditeur pour les tests sans que des frais inutiles ne se glissent dans votre build.
Supprimer les instructions du journal de débogage
Les instructions de journal (en particulier dans Update, LateUpdateou FixedUpdate) peuvent réduire les performances. Désactivez vos instructions de journal avant de lancer une build.
Pour faire cela plus facilement, pensez à créer un attribut conditionnel avec une directive de prétraitement. Par exemple, créez une classe personnalisée comme celle-ci :
Type de bloc inconnu « codeBlock », veuillez spécifier un sérialiseur pour celui-ci dans la propriété « serializers.types »

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 lecteur, toutes vos instructions de journal disparaissent d'un seul coup.
Utiliser 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 plus de rapidité, tous les noms de propriété sont hachés en identifiants de propriété, et ces identifiants sont réellement utilisés pour adresser les propriétés.
Lorsque vous utilisez une méthode Set ou Get sur un Animator, un Material ou un Shader, utilisez la méthode à valeur entière au lieu des méthodes à valeur chaîne. Les méthodes de chaîne effectuent simplement le hachage de chaîne, puis transmettent l'ID 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 a un impact sur l'efficacité lorsque vous effectuez des itérations des milliers de fois par image. Vous ne savez pas si vous devez 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 structure appropriée.
Évitez d’ajouter des composants lors de l’exécution
L'appel de AddComponent au moment de l'exécution entraîne un certain coût. Unity doit vérifier les composants en double ou autres composants requis chaque fois qu'il ajoute des composants au moment de l'exécution.
L'instanciation d'un préfab avec les composants souhaités déjà configurés est généralement plus performante.
Cache GameObjects et composants
GameObject.Find, GameObject.GetComponentet 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 de mise à jour . Appelez-les plutôt dans Démarrer et mettez en cache les résultats.
Voici un exemple qui démontre l'utilisation inefficace d'un appel GetComponent répété :
Type de bloc inconnu « codeBlock », veuillez spécifier un sérialiseur pour celui-ci dans la propriété « serializers.types »
Au lieu de cela, appelez 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.
Type de bloc inconnu « codeBlock », veuillez spécifier un sérialiseur pour celui-ci dans la propriété « serializers.types »
Utiliser des pools d'objets
Instantiate and Destroy peut générer des pics de garbage et de garbage collection (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 avec une arme à feu), 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) lorsqu'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 cela est nécessaire, désactivez les objets au lieu de les détruire et remettez-les dans le pool.

Cela réduit le nombre d'allocations gérées dans votre projet et peut éviter les problèmes de récupération de place.
Découvrez comment créer un système de pool d’objets simple dans Unity ici.
Utiliser des objets scriptables
Stockez des valeurs ou des paramètres immuables dans un ScriptableObject au lieu d'un MonoBehaviour. Le ScriptableObject est un élément qui se trouve à 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.

L'utilisation de ces champs à partir de ScriptableObject peut empêcher la duplication inutile de données à chaque fois que vous instanciez un objet avec ce MonoBehaviour.
Regardez ce didacticiel Introduction à ScriptableObjects pour voir comment ScriptableObjects peut aider votre projet. Vous pouvez également trouver la documentation pertinente ici.
Dans le prochain article de blog, nous examinerons de plus près les graphiques et l’optimisation du GPU. Cependant, si vous souhaitez accéder dès maintenant à la liste complète des trucs et astuces de l'équipe, notre livre électronique complet est disponible ici.

Si vous souhaitez 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 conseils sur les meilleures pratiques pour vos projets, consultez les plans de réussite d'Unity ici.
Nous souhaitons vous aider à rendre vos applications Unity aussi performantes que possible. Si vous souhaitez en savoir plus sur des sujets d'optimisation, n'hésitez pas à nous en faire part dans les commentaires.