Cherchez Unity

Travaillez efficacement avec les structures de données C# et les API Unity

Last updated: December 2018

What you will get from this page: solutions for working optimally with Unity APIs and data structures. Challenges covered include recursive iteration with Particle System APIs; frequent memory allocation; APIs that return Arrays; choosing incorrect data structures, and too much overhead from the misuse of dictionaries.

A lot of these tips will introduce additional complexity, which results in higher maintenance overhead and the possibility for more bugs. Therefore, profile your code first before applying some of these solutions.

All of these tips are from the Unite talk, Squeezing Unity: Tips for raising performance. For updated tips from Ian that take into account the evolution of Unity’s architecture to data-oriented design, see Évolution des bonnes pratiques.

Unity APIs

Problème : l'itération récursive avec les API du système de particules :

Lorsque vous appelez l'une des principales API du système de particules, comme Start, Stop, IsAlive(), elle itère de manière récursive par défaut via tous les enfants dans la hiérarchie d'un système de particules :

  • L'API trouve tous les enfants dans le Transform d'un système de particules, appelle GetComponent pour chaque Transform et, si un système de particules est présent, invoque la méthode interne appropriée.
  • Si ces enfants Transforms ont leurs propres enfants, elle descendra jusqu'au bas de la hiérarchie Transform. Cela peut être un problème si vous avez une hiérarchie Tranform complexe.

Solution :

Ces API ont un paramètre withChildren dont la valeur est par défaut True. Pour éliminer ce comportement récursif, passez cette valeur en False. Lorsque withChildren est défini sur False, seul le système de particules que vous appelez directement sera modifié.

Problème : des systèmes de particules multiples qui commencent et s'arrêtent au même moment

Les artistes créent souvent des effets visuels avec des systèmes de particules qui s'étendent sur un certain nombre de Transforms enfants, que vous voulez peut-être lancer et arrêter au même moment.

Solution :

Créez un MonoBehaviour qui met en cache la liste des systèmes de particules en appelant la liste GetComponent au moment de l'initialisation. Puis, lorsque les systèmes de particules doivent être modifiés, appelez Start, Stop, etc., pour chacun d'entre eux, tour à tour, et assurez-vous de paramétrer withChildren sur False.

Problème : allocation de mémoire fréquente

Quand une fermeture C# se ferme sur une variable locale, l'exécution C# doit allouer une référence pour garder une trace de cette variable. Dans les versions 5.4 à 2017.1 de Unity, plusieurs PI de systèmes de particules utilisent des fermetures de manière interne lorsqu'elles sont appelées. Dans ces versions de Unity, toutes les requêtes Stop et Simulate alloueront de la mémoire, même si le système de particules est déjà arrêté.

Solution :

Toutes les API de systèmes de particules, comme la plupart des API publiques de Unity, sont simplement des fonctions de wrapper C# autour de fonctions internes dédiées au fonctionnement de l'API. Certaines de ces fonctions internes sont du C# pur, mais d'autres font appel au cœur C++ du moteur Unity. Dans le cas des API des systèmes de particules, toutes les API internes sont appelées “Internal_” suivi du nom de la fonction publique, par exemple :

work-optimally-with-Unity-APIs-table-graph

...and so on.

Au lieu de laisser les API publiques de Unity appeler ses supports internes via les fermetures, vous pouvez employer une méthode d'extension ou adapter la méthode interne via Reflection et mettre la référence en cache pour une utilisation ultérieure. Voici les signatures des fonctions concernées :

work-optimally-with-Unity-APIs-functions-signatures

Tous les arguments ont la même signification que l'API publique, sauf le premier, qui est une référence pour le système de particules que vous voulez arrêter ou simuler.

Problème : les API renvoient des matrices

Chaque fois que vous accédez à une API Unity qui renvoie une matrice, elle alloue une copie récente de cette matrice. Cela arrive lorsque vous utilisez des fonctions ou des propriétés qui renvoient des matrices. Des exemples fréquents sont Mesh.vertices : accédez-y et vous génèrerez une copie récente des vertex du maillage. Ainsi que Input.touches, qui vous fournira une copie de toutes les touches utilisées par l'utilisateur pendant le frame actuel.

Solution :

De nombreuses API Unity sont désormais disponibles en versions sans allocation, nous vous conseillons de les utiliser. Par exemple, vous pouvez utiliser Input.GetTouch et Input.touchCount au lieu de Input.touches. À partir de la version 5.3, les versions sans allocation de toutes les API de requêtes de physiques sont accessibles. Pour les applications 2D, il existe également des versions sans allocation de toutes els API de requêtes de physiques 2D. Découvrez plus d'infos sur les versions sans allocation des API de physiques Unity ici.

Enfin, si vous utilisez GetComponents ou GetComponentsInChildren, des versions plus récentes acceptent désormais une listes générique basée sur un modèle. Ces API rempliront la liste avec les résultats d'une requête GetComponents. Il ne s'agit pas purement d'un processus sans allocation, car si les résultats d'une requête GetComponents excèdent la capacité de la liste, cette dernière sera redimensionnée. Mais si vous réutilisez la liste, la fréquence des allocations diminuera au fur et à mesure du cycle de vie de votre application.

Utilisation optimale des structures des données

Problème : sélection des mauvaises structures de données

Évitez de choisir des structures de données seulement parce qu'elles sont simples à utiliser. Sélectionnez plutôt celle dont les caractéristiques de performances correspondent le mieux à l'algorithme ou au système de jeu que vous programmez.

Solution :

Indexer sous forme de matrice ou de liste est très judicieux car cela nécessite peu d'efforts. Indexer de manière aléatoire ou itérer dans une matrice ou une liste représente une faible surcharge. Si vous itérez via une liste d'éléments pour chaque frame, préférez les matrices ou les listes.

Si vous avez besoin d'ajouts ou de suppressions de manière constante, utilisez un dictionnaire ou un HashSet (plus d'informations ci-après).

Si vous utilisez des données en vous basant sur des valeurs clés lorsque des données sont liées à d'autres données dans un sens unique, utilisez un dictionnaire.

Plus d'informations sur le HashSet et les dictionnaires : ces deux structures de données sont basées sur une table de hachage. Une table de hachage comprend un certain nombre de catégories, chacune d'entre elles étant composée d'une liste regroupant toutes les valeurs avec un code de hachage spécifique. En C#, ce code de hachage est basé sur la méthode GetHashCode. Le nombre de valeurs dans une catégorie donnée est généralement bien inférieur à la taille totale du HashSet ou du dictionnaire, ajouter et supprimer des éléments d'un HashSet est plus constant qu'ajouter ou supprimer des éléments de manière aléatoire dans une liste ou une matrice. La différence précise dépend de la capacité de votre tableau de hachage et du nombre d'éléments qu'il comprend.

Vérifier la présence (ou l'absence) d'une valeur donnée dans un HashSet ou un dictionnaire est judicieux pour les mêmes raisons : la vérification parmi le (relativement) peu de valeurs de la catégorie qui représente le code de hachage de la valeur est rapide.

Problème : trop de charge supplémentaire à cause de la mauvaise utilisation des dictionnaires

Ceux qui souhaitent itérer via des éléments de données à chaque frame utilisent généralement un dictionnaire pour son efficacité. Le problème est que vous itérez via une table de hachage. Cela signifie que chaque catégorie de cette table est impliquée, qu'elle contienne les valeurs concernées ou non. C'est donc un processus bien plus lourd, surtout pour une itération via des tables de hachage contenant peu de valeurs.

Solution :

Mieux vaut créer une structure ou un tuple puis stocker une liste ou une matrice de ces structures ou tuples qui contiennent vos relations de données. Itérez via cette liste/matrice au lieu d'itérer via le dictionnaire.

À environ 14 min. 25 sec. du début de l'intervention d'Ian, celui-cidonne un conseil sur la manière et le moment auxquels utilisez les dictionnaires InstanceID-keyed pour réduire la charge du processus.

Problème : que faire si vous rencontrez plusieurs difficultés ?

Dans la vraie vie, bien entendu, les problèmes se chevauchent souvent et il n’existe pas de structure de données unique qui les résoudra tous.

Un exemple fréquent est un Update Manager. Il s'agit d'un schéma architectural dans lequel un objet (généralement un MonoBehaviour) distribue des requêtes Update à différents systèmes dans votre jeu. Lorsque les systèmes veulent recevoir des mises à jour, ils s'y abonnent via l'objet Update Manager. Pour un tel système, vous auriez besoin d'une itération légère à insertion constante et à vérification d'éléments dupliqués permanente.

Solution : utilisez deux structures de données

  • Maintenez une liste ou une matrice pour l'itération.
  • Avant de changer la liste, utilisez un HashSet (ou tout autre type d'ensemble d'indexation) pour vous assurer que l'élément que vous ajoutez ou supprimez est réellement présent.
  • Si la suppression pose problème, pensez à implémenter une liste liée ou intrusivement liée (la mémoire est plus sollicitée et la charge du processus d'itération légèrement plus lourde).
Plus de ressources

Dites-nous si vous avez aimé ce contenu !

Oui, continuez comme ça Ça pourrait être mieux
Compris

Ce site utilise des cookies dans le but de vous offrir la meilleure expérience possible. Consultez la page de politique des cookies pour en savoir plus.