Que recherchez-vous ?
Hero background image
Utiliser le pooling d'objets pour améliorer les performances des scripts C# dans Unity

En implémentant des modèles de conception de programmation de jeux courants dans votre projet Unity, vous pouvez créer et maintenir efficacement une base de code propre, organisée et lisible. Les modèles de conception réduisent non seulement le refactoring et le temps passé aux tests, mais ils accélèrent également les processus d'intégration et de développement, contribuant ainsi à une base solide pour la croissance de votre jeu, de votre équipe de développement et de votre entreprise.

Considérez les modèles de conception non pas comme des solutions finies que vous pouvez copier et coller dans votre code, mais comme des outils supplémentaires qui, lorsqu'ils sont utilisés correctement, peuvent vous aider à créer des applications plus volumineuses et évolutives.

Cette page explique le pooling d'objets et comment il peut aider à améliorer les performances de votre jeu. Il comprend un exemple de la façon d'implémenter le système de pool d'objets intégré de Unity dans vos projets.

Le contenu ici est basé sur le livre électronique gratuit, Améliorez votre code avec des modèles de programmation de jeux, qui explique les modèles de conception bien connus et partage des exemples pratiques pour les utiliser dans votre projet Unity.

D'autres articles de la série de modèles de programmation de jeux Unity sont disponibles sur le hub des meilleures pratiques Unity ou cliquez sur les liens suivants :

4-2 Hiérarchie
Comprendre le regroupement d'objets dans Unity

Le pooling d'objets est un modèle de conception qui peut optimiser les performances en réduisant la puissance de traitement requise du processeur pour exécuter des appels de création et de destruction répétitifs. Au lieu de cela, grâce au pooling d’objets, les GameObjects existants peuvent être réutilisés encore et encore.

La fonction clé du pooling d'objets est de créer des objets à l'avance et de les stocker dans un pool, plutôt que de les créer et de les détruire à la demande. Lorsqu'un objet est nécessaire, il est retiré du pool et utilisé, et lorsqu'il n'est plus nécessaire, il est renvoyé dans le pool plutôt que d'être détruit.

L'image ci-dessus illustre un cas d'utilisation courant du pooling d'objets, celui du tir de projectiles depuis une tourelle de canon. Décomposons cet exemple étape par étape.

Au lieu de créer puis de détruire, le modèle de pool d'objets utilise un ensemble d'objets initialisés maintenus prêts et en attente dans un pool désactivé. Le modèle pré-instancie ensuite tous les objets nécessaires à un moment précis avant le jeu. Le pool doit être activé à un moment opportun où le joueur ne remarquera pas le bégaiement, comme lors d'un écran de chargement.

Une fois que les GameObjects du pool ont été utilisés, ils sont désactivés et prêts à être utilisés lorsque le jeu en aura à nouveau besoin. Lorsqu'un objet est nécessaire, votre application n'a pas besoin de l'instancier au préalable. Au lieu de cela, il peut le demander au pool, l’activer et le désactiver, puis le renvoyer au pool au lieu de le détruire.

Ce modèle peut réduire le coût des tâches lourdes nécessaires à la gestion de la mémoire pour exécuter le garbage collection, comme expliqué dans la section suivante.

Ebook de profilage Unity
Allocation de mémoire

Avant de passer aux exemples sur la manière d'exploiter le pooling d'objets, examinons brièvement le problème fondamental qu'il permet de résoudre.

La technique de pooling n’est pas seulement utile pour réduire les cycles CPU consacrés aux opérations d’instanciation et de destruction. Il optimise également la gestion de la mémoire en réduisant les frais de création et de destruction d'objets, qui nécessitent que la mémoire soit allouée et désallouée, et que des constructeurs et des destructeurs soient appelés.

Mémoire gérée dans Unity

L'environnement de script C# d'Unity offre un système de mémoire gérée. Il permet de gérer la libération de mémoire, vous n'avez donc pas besoin de la demander manuellement via votre code. Le système de gestion de la mémoire permet également de protéger l'accès à la mémoire, en garantissant que la mémoire que vous n'utilisez plus est libérée et en empêchant l'accès à la mémoire qui n'est pas valide pour votre code.

Unity utilise un garbage collector pour récupérer la mémoire des objets que votre application et Unity n'utilisent plus. Cependant, cela a également un impact sur les performances d'exécution, car l'allocation de mémoire gérée prend du temps pour le processeur, et le garbage collection (GC) peut empêcher le processeur d'effectuer d'autres tâches jusqu'à ce qu'il termine sa tâche.

Chaque fois que vous créez un nouvel objet ou en détruisez un existant dans Unity, la mémoire est allouée et libérée. C’est là que le pooling d’objets entre en jeu : Il réduit le bégaiement pouvant résulter des pics de collecte des déchets. Les pics GC accompagnent souvent la création ou la destruction d'un grand nombre d'objets en raison de l'allocation de mémoire. Outre les garbage collection prématurés, le processus peut également provoquer une fragmentation de la mémoire qui rend plus difficile la recherche de régions de mémoire contiguës libres.

En recyclant les mêmes objets existants en les désactivant et en les activant, vous pouvez créer un effet, comme tirer des centaines de balles hors écran, alors qu'en réalité, vous les désactivez et les recyclez simplement.

Apprenez-en davantage sur la gestion de la mémoire dans notreguide de profilage avancé.

Exemple de projet de pool d’objets
Using UnityEngine.Pool

Bien que vous puissiez créer votre propre système personnalisé pour implémenter le pooling d'objets, il existe une classe ObjectPool intégrée dans Unity que vous pouvez utiliser pour implémenter ce modèle efficacement dans votre projet (disponible dans Unity 2021 LTS et versions ultérieures).

Voyons comment exploiter le système de pool d'objets intégré à l'aide de l' API UnityEngine.Pool avec cet exemple de projet disponible sur Github. Une fois sur la page Github, accédez à Assets>7 Object Pool >Scripts > SampleUsage2021 pour les fichiers.

Note: Vous pouvez consulter ce didacticiel de Unity Learn pour voir un exemple de regroupement d'objets à partir d'une version antérieure de Unity.

Cet exemple consiste en une tourelle tirant rapidement des projectiles (réglée à 10 projectiles par seconde par défaut) lorsque le bouton de la souris est enfoncé. Chaque projectile traverse l'écran et doit être détruit lorsqu'il quitte l'écran. Sans pool d’objets, cela peut créer un frein considérable à la gestion du processeur et de la mémoire, comme expliqué dans la section précédente.

En utilisant le pooling d'objets, il semble que des centaines de balles soient tirées hors écran alors qu'en réalité, elles sont simplement désactivées et recyclées encore et encore.

Le code de l'exemple de script permet de garantir que la taille du pool est suffisamment grande pour afficher les objets actifs simultanément, camouflant ainsi le fait que les mêmes objets sont constamment réutilisés.

Si vous avez utilisé le système de particules d'Unity, vous avez une expérience directe d'un pool d'objets. Le composant Système de particules contient un paramètre pour le nombre maximum de particules. Cela recycle les particules disponibles, empêchant l'effet de dépasser un nombre maximum. Le pool d'objets fonctionne de la même manière, mais avec n'importe quel GameObject de votre choix.

Déballage de RevisedGun.cs

Jetons un coup d'œil au code dans RevisedGun.cs qui se trouve dans la démo Github via Assets>7 Object Pool >Scripts > SampleUsage2021.

La première chose à remarquer est l'inclusion de l'espace de noms du pool :

using UnityEngine.Pool;

En utilisant l'API UnityEngine.Pool, vous obtenez une classe ObjectPool basée sur la pile pour suivre les objets avec le modèle de pool d'objets. Selon vos besoins, vous pouvez également utiliser une classe CollectionPool (List, HashSet, Dictionary, etc.)

Ensuite, vous appliquez des paramètres spécifiques pour les caractéristiques de tir de votre arme, y compris le préfabriqué à générer (nommé projectilePrefab du type RevisedProjectile).

L'interface ObjectPool est référencée à partir de RevisedProjectile.cs (qui est expliqué dans la section suivante) et initialisée dans la fonction Awake.

vide privé Éveillé()

{
objectPool = new ObjectPool<RevisedProjectile>(CreateProjectile,

OnGetFromPool, OnReleaseToPool,

OnDestroyPooledObject,collectionCheck, defaultCapacity, maxSize);
}

Si vous explorez le constructeur ObjectPool<T0> , vous verrez qu'il inclut la possibilité utile de configurer une certaine logique lorsque :

Créer d'abord un élément regroupé pour remplir le pool

Prendre un objet de la piscine

Remettre un article à la piscine

Détruire un objet regroupé (par exemple, si vous atteignez une limite maximale)

Notez que la classe ObjectPool intégrée inclut également des options pour une taille de pool par défaut et maximale, cette dernière étant le nombre maximum d'éléments stockés dans le pool. Il se déclenche lorsque vous appelez Release et si le pool est plein, il est détruit à la place.

Voyons comment l'exemple de code effectue plusieurs actions qui spécifient comment Unity doit gérer efficacement le pool d'objets en fonction de votre cas d'utilisation spécifique.

Tout d'abord, le createFunc est transmis et est utilisé pour créer une nouvelle instance lorsque le pool est vide, qui dans ce cas est le CreateProjectile() qui instancie un nouveau profil préfabriqué.

privé RevisedProjectile CreateProjectile()

{
RevisedProjectile projectileInstance = Instantiate(projectilePrefab);
projectileInstance.ObjectPool = objectPool;

retourner projectileInstance ;
}

Le OnGetFromPool est appelé lorsque vous demandez une instance du GameObject, vous activez donc le GameObject que vous obtenez du pool par défaut.

vide privé OnGetFromPool (RevisedProjectile pooledObject)

{
pooledObject.gameObject.SetActive(true);
}

Le OnReleaseToPool est utilisé lorsque le GameObject n'est plus nécessaire et est renvoyé dans le pool – dans cet exemple, il s'agit simplement de le désactiver à nouveau.

vide privé OnReleaseToPool (RevisedProjectile pooledObject)

{
pooledObject.gameObject.SetActive(false);
}

OnDestroyPooledObject est appelé lorsque vous dépassez le nombre maximum d'éléments regroupés autorisés. La piscine étant déjà pleine, l’objet sera détruit.

vide privé OnDestroyPooledObject (RevisedProjectile pooledObject)

{
Détruire (pooledObject.gameObject);
}

Le collectionChecks est utilisé pour initialiser le IObjectPool et lèvera une exception lorsque vous essayez de libérer un GameObject qui a déjà été renvoyé au gestionnaire de pool, mais cette vérification n'est effectuée que dans l'éditeur. En le désactivant, vous pouvez cependant économiser certains cycles CPU, avec le risque de récupérer un objet déjà réactivé.

Comme son nom l'indique, defaultCapacity est la taille par défaut de la pile/liste qui contiendra vos éléments, et donc la quantité d'allocation de mémoire que vous souhaitez engager à l'avance. maxPoolSize sera la taille maximale de la pile, et les GameObjects regroupés créés ne doivent jamais dépasser cette taille. Cela signifie que si vous remettez un objet dans une réserve pleine, l'objet sera détruit à la place.

Ensuite, dans FixedUpdate(), vous obtiendrez un objet regroupé au lieu d'instancier un nouveau projectile à chaque fois que vous exécuterez la logique de tir d'une balle.

RevisedProjectile bulletObject = objectPool.Get();

C'est aussi simple que ça.

Déballage de RevisedProjectile.cs

Jetons maintenant un œil au script RevisedProjectile.cs .

Outre la configuration d'une référence à ObjectPool, ce qui rend la libération de l'objet dans le pool plus pratique, il y a quelques détails intéressants.

Le timeoutDelay est utilisé pour garder une trace du moment où le projectile a été « utilisé » et peut être renvoyé dans le pool de jeu – cela se produit par défaut après trois secondes.

La fonction Deactivate() active une coroutine appelée DeactivateRoutine(float delay), qui non seulement libère le projectile dans le pool avec objectPool.Release(this), mais réinitialise également les paramètres de vitesse de déplacement du Rigidbody.

Ce processus résout le problème des « objets sales » : des objets qui ont été utilisés dans le passé et qui doivent être réinitialisés en raison de leur état indésirable.

Comme vous pouvez le voir dans cet exemple, l' API UnityEngine.Pool rend la configuration des pools d'objets efficace, car vous n'avez pas besoin de reconstruire le modèle à partir de zéro, sauf si vous disposez d'un cas d'utilisation spécifique pour le faire.

Vous n'êtes pas limité aux GameObjects uniquement. Le pooling est une technique d'optimisation des performances permettant de réutiliser tout type d'entité C# : un GameObject, un Prefab instancié, un dictionnaire C#, etc. Unity propose des classes de pooling alternatives pour d'autres entités, telles que DictionaryPool<T0,T1> qui prend en charge les dictionnaires et HashSetPool<T0> pour les HashSets. Apprenez-en davantage à ce sujet dans la documentation.

Le LinkedPool utilise une liste chaînée pour contenir une collection d'instances d'objets à réutiliser, ce qui peut conduire à une meilleure gestion de la mémoire (selon votre cas) puisque vous n'utilisez la mémoire que pour les éléments réellement stockés dans le pool.

Comparez cela à ObjectPool, qui utilise simplement une pile C# et un tableau C# en dessous et, en tant que tel, contient une grande partie de la mémoire contiguë. L'inconvénient est que vous dépensez plus de mémoire par élément et plus de cycles CPU pour gérer cette structure de données dans LinkedPool que dans ObjectPool où vous pouvez utiliser defaultSize et maxSize pour configurer vos besoins.

Couverture du blog
Autres façons d’implémenter le pooling d’objets

La manière dont vous utilisez les pools d'objets varie selon l'application, mais le modèle apparaît généralement lorsqu'une arme doit tirer plusieurs projectiles, comme illustré dans l'exemple précédent.

Une bonne règle de base consiste à profiler votre code chaque fois que vous instanciez un grand nombre d'objets, car vous courez le risque de provoquer un pic de garbage collection. Si vous détectez des pics importants qui exposent votre gameplay à un risque de bégaiement, envisagez d'utiliser un pool d'objets. N'oubliez pas que le pooling d'objets peut ajouter plus de complexité à votre base de code en raison de la nécessité de gérer les multiples cycles de vie des pools. De plus, vous risquez également de réserver de la mémoire dont votre gameplay n'a pas nécessairement besoin en créant trop de pools prématurés.

Comme mentionné précédemment, il existe plusieurs autres façons d’implémenter le pooling d’objets en dehors de l’exemple inclus dans cet article. Une solution consiste à créer votre propre implémentation que vous pouvez personnaliser selon vos besoins. Mais vous devrez être conscient des complications liées à la sécurité des types et des threads, ainsi que de la définition de l'allocation/désallocation d'objets personnalisés.

Heureusement, Unity Asset Store propose d'excellentes alternatives pour vous faire gagner du temps.

Ressources plus avancées pour la programmation dans Unity

Le livre électronique, Améliorez votre code avec des modèles de programmation de jeu, fournit un exemple plus détaillé d'un système simple de pool d'objets personnalisé. Unity Learn propose également une introduction au pooling d'objets, que vous pouvez trouver ici, ainsi qu'un didacticiel complet sur l'utilisation du nouveau système de pooling d'objets intégré dans 2021 LTS.

Tous les e-books et articles techniques avancés sont disponibles sur lesmeilleures pratiques Unity.moyeu. Les e-books sont également disponibles surpagede bonnes pratiques avancéesdans la documentation.

Vous avez aimé ce contenu ?