Histoires des tranchées de l'optimisation : Sauvegarde de la mémoire avec Addressables

L'efficacité du flux d'actifs entrant et sortant de la mémoire est un élément clé de tout jeu de qualité. En tant que consultant au sein de notre équipe de services professionnels, je m'efforce d'améliorer les performances de nombreux projets de clients. C'est pourquoi j'aimerais partager avec vous quelques conseils sur la manière d'exploiter le système d'actifs Addressables d'Unity pour améliorer votre stratégie de chargement de contenu.
La mémoire est une ressource rare qu'il convient de gérer avec soin, en particulier lors du portage d'un projet sur une nouvelle plate-forme. L'utilisation d'Addressables peut améliorer la mémoire d'exécution en introduisant des références faibles pour éviter le chargement d'actifs inutiles. Les références faibles vous permettent de contrôler le moment où l'élément référencé est chargé dans la mémoire et en sort ; le système Addressables trouvera toutes les dépendances nécessaires et les chargera également. Ce blog aborde un certain nombre de scénarios et de problèmes que vous pouvez rencontrer lorsque vous configurez votre projet pour utiliser le système d'actifs adressables Unity - et explique comment les reconnaître et les résoudre rapidement.

Pour cette série de recommandations, nous allons travailler avec un exemple simple qui se présente de la manière suivante :
- Nous avons un script InventoryManager dans la scène avec des références à nos trois biens d'inventaire : Sword, Boss Sword, Shield prefabs.
- Ces actifs ne sont pas nécessaires à tout moment du jeu.
Vous pouvez télécharger les fichiers du projet pour cet exemple sur mon GitHub. Nous utilisons le paquet de prévisualisation Memory Profiler pour visualiser la mémoire au moment de l'exécution. Dans Unity 2020 LTS, vous devez d'abord activer les paquets de prévisualisation dans les paramètres du projet avant d'installer ce paquet depuis le gestionnaire de paquets.
Si vous utilisez Unity 2021.1, sélectionnez l'option Ajouter un paquet par nom dans le menu supplémentaire (+) de la fenêtre Gestionnaire de paquets. Utilisez le nom "com.unity.memoryprofiler".
Commençons par la mise en œuvre la plus élémentaire, puis progressons vers la meilleure approche pour configurer notre contenu Addressables. Nous allons simplement appliquer des références en dur (assignation directe dans l'inspecteur, suivi par GUID) à nos prefabs dans un MonoBehaviour qui existe dans notre scène.

Lorsque la scène est chargée, tous les objets de la scène sont également chargés en mémoire avec leurs dépendances. Cela signifie que chaque préfabriqué répertorié dans notre système d'inventaire résidera en mémoire, ainsi que toutes les dépendances de ces préfabriqués (textures, maillages, audio, etc.).
Lorsque nous créons une version et prenons un instantané avec le Memory Profiler, nous pouvons voir que les textures de nos actifs sont déjà stockées en mémoire, même si aucune d'entre elles n'est instanciée.

Problème : Il existe des actifs en mémoire dont nous n'avons pas besoin actuellement. Dans un projet comportant un grand nombre d'articles d'inventaire, il en résulterait une pression considérable sur la mémoire d'exécution.
Pour éviter de charger des biens non désirés, nous allons modifier notre système d'inventaire pour utiliser des Addressables. L'utilisation de références d'actifs au lieu de références directes permet d'éviter que ces objets ne soient chargés en même temps que notre scène. Déplaçons nos préfabriqués d'inventaire dans un groupe Addressables et modifions InventorySystem pour qu'il instancie et libère des objets à l'aide de l'API Addressables.

Construire le lecteur et prendre un cliché. Remarquez qu'aucun des actifs n'est encore en mémoire, ce qui est très bien car ils n'ont pas été instanciés.

Instanciez tous les éléments pour les voir apparaître correctement avec leurs actifs en mémoire.

Problème : Si nous instancions tous nos objets et que nous désapparaissons l'épée du boss, nous verrons toujours la texture de l'épée du boss "BossSword_E " en mémoire, même si elle n'est pas utilisée. En effet, s'il est possible de charger partiellement des ensembles d'actifs, il est impossible de les décharger partiellement de manière automatique. Ce comportement peut s'avérer particulièrement problématique pour les lots contenant de nombreux actifs, tels qu'un seul AssetBundle comprenant tous nos préfabriqués d'inventaire. Aucun des actifs de l'ensemble ne sera déchargé jusqu'à ce que l'ensemble de l'AssetBundle ne soit plus nécessaire, ou jusqu'à ce que nous appelions l'opération CPU coûteuse Resources.UnloadUnusedAssets().


Pour résoudre ce problème, nous devons modifier la façon dont nous organisons nos AssetBundles. Alors que nous disposons actuellement d'un seul groupe Addressables qui regroupe tous ses actifs dans un seul AssetBundle, nous pouvons créer un AssetBundle pour chaque préfabriqué. Ces AssetBundles plus granulaires atténuent le problème des grands ensembles qui conservent en mémoire des actifs dont nous n'avons plus besoin.
Il est facile de procéder à ce changement. Sélectionnez un groupe Addressables, puis Content Packaging & Loading > Advanced Options > Bundle Mode, et allez à Inspector pour changer le mode de regroupement de Pack Together à Pack Separately.
En utilisant Pack Separately pour construire ce groupe Addressable, vous pouvez créer un AssetBundle pour chaque actif du groupe Addressable.

Les actifs et les lots ressembleront à ce qui suit :

Revenons maintenant à notre test initial : L'apparition de nos trois objets puis la disparition de l'épée du boss ne laissent plus d'actifs inutiles en mémoire. Les textures de l'épée du boss sont maintenant déchargées car le paquet entier n'est plus nécessaire.
Problème : Si nous faisons apparaître nos trois objets et que nous prenons une capture de mémoire, les actifs dupliqués apparaîtront dans la mémoire. Plus précisément, cela conduira à des copies multiples des textures "Sword_N" et "Sword_D". Comment cela pourrait-il se produire si nous ne modifions que le nombre de liasses ?

Pour répondre à cette question, examinons tout ce qui entre dans la composition des trois offres groupées que nous avons créées. Bien que nous n'ayons placé que trois éléments préfabriqués dans des ensembles, d'autres éléments sont implicitement intégrés dans ces ensembles en tant que dépendances des éléments préfabriqués. Par exemple, la ressource préfabriquée de l'épée comporte également des ressources de maillage, de matériau et de texture qui doivent être incluses. Si ces dépendances ne sont pas explicitement incluses ailleurs dans Addressables, elles sont automatiquement ajoutées à chaque paquet qui en a besoin.

Les Addressables comprennent une fenêtre d'analyse pour aider à diagnostiquer la disposition des paquets. Ouvrez Fenêtre > Gestion des actifs > Addressables > Analyser et exécutez la règle Bundle Layout Preview. Ici, nous voyons que le bundle sword inclut explicitement le préfabriqué sword.prefab, mais il y a de nombreuses dépendances implicites qui sont également incluses dans ce bundle.

Dans la même fenêtre, lancez Check Duplicate Bundle Dependencies. Cette règle met en évidence les actifs inclus dans plusieurs ensembles d'actifs sur la base de la présentation actuelle des Addressables.

Nous pouvons empêcher la duplication de ces actifs de deux manières :
1. Placez les préfabriqués Sword, BossSword et Shield dans le même bundle afin qu'ils partagent les dépendances, ou
2. Inclure explicitement les actifs dupliqués quelque part dans Addressables
Nous voulons éviter de placer plusieurs préfabriqués d'inventaire dans le même paquet afin d'empêcher les actifs non désirés de persister en mémoire. Ainsi, nous ajouterons les actifs dupliqués à leurs propres lots (lot 4 et lot 5).

En plus d'analyser nos paquets, les règles d'analyse peuvent automatiquement corriger les actifs incriminés via les règles de correction sélectionnées. Cliquez sur ce bouton pour créer un nouveau groupe Addressables nommé "Duplicate Asset Isolation" (Isolation des biens dupliqués), qui contient les quatre biens dupliqués. Définissez le mode de regroupement de ce groupe sur Empaqueter séparément pour éviter que d'autres actifs devenus inutiles ne soient conservés en mémoire.

L'utilisation de cette stratégie AssetBundle peut entraîner des problèmes à grande échelle. Pour chaque AssetBundle chargé à un moment donné, il y a une surcharge de mémoire pour les métadonnées de l'AssetBundle. Ces métadonnées risquent de consommer une quantité inacceptable de mémoire si nous appliquons cette stratégie actuelle à des centaines ou des milliers d'éléments d'inventaire. Pour en savoir plus sur les métadonnées AssetBundle, consultez la documentation Addressables.
Affichez le coût actuel de la mémoire des métadonnées de l'AssetBundle dans l'outil Unity Profiler. Allez au module de mémoire et prenez un instantané de la mémoire. Voir la catégorie Autres > SerializedFile.

Il existe une entrée SerializedFile en mémoire pour chaque AssetBundle chargé. Cette mémoire est constituée de métadonnées AssetBundle plutôt que des actifs réels contenus dans les lots. Ces métadonnées comprennent
- Deux tampons de lecture de fichiers
- Un arbre des types répertoriant chaque type unique inclus dans l'offre groupée
- Une table des matières indiquant les actifs
De ces trois éléments, ce sont les tampons de lecture de fichiers qui occupent le plus d'espace. Ces tampons sont de 64 Ko chacun sur PS4, Switch et Windows RT, et de 7 Ko sur toutes les autres plateformes. Dans l'exemple ci-dessus, 1 819 paquets * 64 Ko * 2 tampons = 227 Mo uniquement pour les tampons.
Étant donné que le nombre de tampons augmente linéairement avec le nombre d'AssetBundles, la solution simple pour réduire la mémoire consiste à charger moins de bundles au moment de l'exécution. Toutefois, nous avons précédemment évité de charger de gros paquets afin d'éviter que des éléments indésirables ne persistent en mémoire. Alors, comment réduire le nombre de paquets tout en maintenant la granularité ?
Une première étape solide consisterait à regrouper les actifs en fonction de leur utilisation dans l'application. Si vous pouvez faire des hypothèses intelligentes basées sur votre application, vous pouvez alors regrouper les ressources dont vous savez qu'elles seront toujours chargées et déchargées ensemble, comme les ressources regroupées en fonction du niveau de jeu dans lequel elles se trouvent.
D'autre part, vous pouvez vous trouver dans une situation où vous ne pouvez pas faire d'hypothèses sûres sur le moment où vos actifs sont nécessaires ou non. Si vous créez un jeu à monde ouvert, par exemple, vous ne pouvez pas vous contenter de regrouper tous les éléments du biome forestier en un seul ensemble de ressources, car vos joueurs pourraient s'emparer d'un objet de la forêt et le transporter d'un biome à l'autre. L'ensemble de la forêt reste en mémoire car le joueur a toujours besoin d'un bien de la forêt.
Heureusement, il existe un moyen de réduire le nombre de liasses tout en conservant le niveau de granularité souhaité. Soyons plus intelligents dans la manière dont nous dédupliquons nos paquets.
La règle d'analyse de déduplication intégrée que nous avons exécutée détecte tous les actifs qui se trouvent dans des paquets multiples et les déplace efficacement dans un seul groupe Addressables. En définissant ce groupe sur Pack Separately, nous obtenons une ressource par paquet. Toutefois, il existe des actifs dupliqués que l'on peut regrouper sans risque d'introduire des problèmes de mémoire. Examinez le diagramme ci-dessous :

Nous savons que les textures "Sword_N" et "Sword_D" sont des dépendances des mêmes paquets (Paquet 1 et Paquet 2). Comme ces textures ont les mêmes parents, nous pouvons les emballer ensemble en toute sécurité sans causer de problèmes de mémoire. Les deux textures d'épée doivent toujours être chargées ou déchargées. Il n'y a jamais d'inquiétude quant à la persistance de l'une des textures dans la mémoire, car il n'y a jamais de cas où l'on utilise spécifiquement une texture et pas l'autre.
Nous pouvons mettre en œuvre cette logique de déduplication améliorée dans notre propre règle d'analyse des Addressables. Nous travaillerons à partir de la règle existante CheckForDupeDependencies.cs. Vous pouvez voir le code de mise en œuvre complet dans l'exemple du système d'inventaire. Dans ce projet simple, nous avons simplement réduit le nombre total de liasses de sept à cinq. Mais imaginez un scénario dans lequel votre application comporte des centaines, des milliers, voire plus, d'actifs dupliqués dans Addressables. En travaillant avec Unknown Worlds Entertainment sur un engagement de services professionnels pour leur jeu Subnautica, le projet avait initialement un total de 8 718 paquets après avoir utilisé la règle d'analyse de déduplication intégrée. Nous avons réduit ce nombre à 5 199 liasses après avoir appliqué la règle personnalisée pour regrouper les actifs dédupliqués en fonction de leurs parents de liasse. Vous pouvez en savoir plus sur notre travail avec l'équipe en lisant cet article.
Cela représente une réduction de 40 % du nombre d'offres groupées, tout en conservant le même contenu et le même niveau de granularité. Cette réduction de 40 % du nombre de paquets a également permis de réduire de 40 % la taille de SerializedFile au moment de l'exécution (de 311 Mo à 184 Mo).
L'utilisation d'Addressables permet de réduire considérablement la consommation de mémoire. Vous pouvez réduire davantage la mémoire en organisant vos AssetBundles en fonction de votre cas d'utilisation. Après tout, les règles d'analyse intégrées sont conservatrices afin de s'adapter à toutes les applications. L'écriture de vos propres règles d'analyse peut automatiser la mise en page des paquets et l'optimiser pour votre application. Pour détecter les problèmes de mémoire, continuez à établir souvent des profils et consultez la fenêtre Analyzer pour voir quels actifs sont explicitement et implicitement inclus dans vos offres groupées. Consultez la documentation du système d'actifs Addressables pour plus de bonnes pratiques, un guide pour vous aider à démarrer, et une documentation API plus complète.
Si vous souhaitez bénéficier d'une aide plus pratique pour apprendre à améliorer votre gestion de contenu avec le système Addressables Asset System, contactez le service des ventes au sujet d'un cours de formation professionnelle.