Salut Hackernoon ! Aujourd'hui, je continue à discuter du développement ECS (Entity-Component-System) dans Unity. Dans le , j'ai expliqué ce qu'est ECS (Entity-Component-System), pourquoi ECS est nécessaire, comment travailler avec ECS, les avantages d'ECS dans Unity et les inconvénients d'ECS dans . guide sur ECS (Entity-Component-System) dans Unity: partie 1 Unity Dans cette partie, je me concentrerai sur les erreurs des débutants et les bonnes pratiques d'utilisation d'ECS dans le jeu Unity. Je couvrirai également légèrement les frameworks pour Unity/C#. Erreurs du débutant lors de l'utilisation d'ECS dans le jeu Unity Dans cette section, je vais vous parler des erreurs que les personnes plus expérimentées ont remarquées dans mon ancien code et qui m'ont empêché de développer. Je couvrirai également les erreurs que font de nombreux débutants lorsqu'ils commencent à maîtriser l'ECS. J'espère que cela vous aidera à éviter certains bugs. Héritage des composants et interfaces L'héritage de composants et l'utilisation d'interfaces sont une erreur répandue chez les débutants. Alors pourquoi est-ce mauvais pour le développement d'Unity ? 4 raisons : L'abstraction au niveau des composants n'offre absolument aucun avantage par rapport à l'abstraction ECS via les données Cela crée des limites pour vous : vous aurez plus de mal à étendre de tels composants et à affiner la logique associée Cela conduit à une situation où un composant a sa propre logique, ce qui, comme nous nous en souvenons, est une violation des principes ECS, ce que vous ne devriez pas faire. Cela conduit à une nécessité déraisonnable d'hériter des systèmes ou de faire toutes sortes de changements de cas par types. Je note tout de suite que l'héritage de systèmes n'est pas toujours une bonne idée, mais cela ne fait rien de terrible, contrairement à l'héritage de composants. Donc, si vous voulez hériter des composants, ne le faites pas. Réfléchissez à nouveau à la façon dont vous pouvez résoudre le problème d'une autre manière. Incapacité à utiliser correctement l'abstraction ECS L'abstraction ECS consiste à placer simplement des données communes (qui doivent être héritées dans la POO) dans un composant séparé. Vous faites un "héritier" d'un tel composant simplement en ajoutant un nouveau composant avec les données dont vous avez besoin et en filtrant les entités avec et . Tout est élémentaire. Si vous avez des données communes entre les composants/entités, vous pouvez presque toujours les mettre dans un composant séparé. Plus tôt vous le faites, mieux c'est. BaseComponent InheritorComponent Activation/désactivation des systèmes pour modifier la logique Dans ECS, le monde et les systèmes qui le traitent sont statiques et existent toujours, mais les entités et leurs données sont très dynamiques. Et si vous avez besoin de désactiver une logique, désactiver un système n'est pas la bonne solution. Il n'y a souvent pas d'accès aux autres systèmes (et c'est une bonne chose). Une option beaucoup plus pratique consiste à créer des composants marqueurs. Il dira que la logique du système ne devrait pas fonctionner pour une entité avec un marqueur. De nombreux nouveaux arrivants affirment : "Mais si je n'ai pas d'entités pour le système, pourquoi le système devrait-il fonctionner ? Ne serait-il pas préférable de le désactiver pour des raisons d'optimisation ?". Non, ce n'est pas mieux. Si vous admettez qu'il peut n'y avoir aucune entité, il est plus simple d'ajouter au tout début de la méthode principale du système. À l'échelle du , appeler une fonction et comparer deux entiers est une goutte d'eau dans l'océan qui n'affectera en rien vos performances. if (entities.Length < 1) return; jeu Unity Les seuls cas légitimes de désactivation de systèmes en cours d'exécution sont les tests A/B et le débogage/test de systèmes spécifiques. Cependant, la plupart des frameworks fournissent des outils pour le faire non pas à partir du code mais à partir de la fenêtre de l'éditeur. Faire d'ECS un absolu Il faut rappeler que la POO n'est pas interdite aux adhérents ECS :D Lorsque vous travaillez avec ECS, vous ne devez pas être complètement obsédé par ECS et tout y transférer car cela peut être contre-productif. De plus, comme je l'ai mentionné dans les : toutes les structures de données ne s'adaptent pas bien à ECS. Il est donc préférable de créer une classe OOP distincte dans de tels cas. inconvénients d'ECS Certains vont plus loin et créent certains éléments du projet (par exemple, l'interface utilisateur) non selon ECS, mais un peu de côté de toute autre manière pratique, puis se connectent à ECS par un pont. En outre, toute logique supplémentaire (chargement de configurations, travail direct avec le réseau, enregistrement dans un fichier) est plus facile à créer et à utiliser directement à partir du système souhaité. développeurs Unity Vous devez choisir quoi faire en fonction du bon sens. L'ECS doit aider au développement et non l'entraver. Essayer de porter le code existant vers ECS sans aucune modification Très souvent, les débutants essaient de transférer textuellement leur code existant vers ECS. Ce n'est pas une bonne idée car les approches d'écriture de code d'ECS diffèrent des modèles architecturaux traditionnels. Le résultat d'un tel portage est généralement un gâchis d'ECS et un code final très médiocre. Si vous avez besoin de porter l'ancien code vers ECS, la meilleure option est d'écrire la même logique à partir de zéro sur ECS. Tout ce que vous avez à faire est d'utiliser vos connaissances et le code existant comme guide. Utilisation de délégués/rappels ou de logique réactive dans les systèmes Dans ECS, il peut être dangereux de récupérer une partie de la logique des systèmes et de la stocker dans un composant pour une utilisation ultérieure ou de réagir instantanément à un changement (par exemple, un système réagissant à l'ajout d'un composant dans un autre système). Cela ajoute non seulement une interconnectivité inutile aux systèmes (ils deviennent fortement dépendants des appels externes). Cela rompt également notre magnifique pipeline de traitement de données en ajoutant une logique que nous avons peu de contrôle sur l'appel. Organisation des fichiers dans des dossiers par type Lorsque vous commencez à travailler avec ECS, vous souhaitez d'abord placer les nouveaux fichiers par type : composants dans le dossier Composants et systèmes dans le dossier Systèmes. Mais avec l'expérience, je me rends compte que ce mode de tri est loin d'être efficace. Il n'est pas facile de naviguer avec pour comprendre quels composants sont liés à quels systèmes. La meilleure option consiste à trier par fonctionnalités lorsque tout ce qui concerne une fonctionnalité particulière se trouve dans un dossier (peut-être avec une hiérarchie interne de composants/systèmes). Autrement dit, tous les composants et systèmes liés à la santé et aux dommages se trouveront dans le dossier Santé. Cela permettra de consulter le dossier pour comprendre le contexte des données de base des systèmes qu'il contient et de faciliter la navigation dans le projet. Meilleures pratiques pour utiliser ECS dans le jeu Unity Ci-dessus, vous avez probablement lu exactement ce qu'il ne faut PAS faire lors du développement de jeux Unity sur ECS. Parlons maintenant des pratiques utiles et des conseils sur ce que vous pouvez faire. Balisage d'entités avec des composants marqueurs Dans ECS, il existe un concept tel qu'un composant de balise. Il s'agit d'un composant sans champ qui ne joue que le rôle de balisage d'entité. Vous pouvez le considérer comme un indicateur booléen dans une classe : il est soit présent (vrai) soit absent (faux). Par exemple, nous avons mille unités, dont une contrôlée par un joueur. Vous pouvez le marquer avec un composant vide. Cela vous permettra d'obtenir uniquement les unités du joueur dans le filtre et de comprendre si vous travaillez avec une unité régulière ou contrôlée par le joueur lorsque vous parcourez toutes les unités à la fois. PlayerMarker Minimiser les endroits où le composant change Moins il y a d'endroits où un composant change, mieux c'est. En général, c'est en quelque sorte simplement suivre le principe bénéfique de ne pas vous répéter (je le recommande à tout le monde). Il y a de nombreux avantages à pratiquer cela : Il vous permet de mieux comprendre le processus de modification des données dans votre projet et simplifie le débogage en cas de problème. Lors de la mise à jour de la logique des changements de données, vous devrez mettre à jour moins de code, idéalement un seul endroit Il y a simplement moins de chances qu'un bogue de données se produise à la volée Par exemple, au lieu de changer le HealthComponent dans chaque système endommagé, il serait préférable de créer un DamageSystem dont le but est d'infliger des dommages aux entités avec un HealthComponent. Oublier le suffixe de composant Le suffixe de composant est très utile pour les débutants car il leur rappelle que "seules les données sont ici". Mais avec le temps, le besoin de rappel disparaît, et le code reste difficilement compréhensible avec le Component omniprésent. C'est pourquoi je voudrais vous conseiller : vous pouvez oublier en toute sécurité le suffixe Component. Il ne fournit rien d'utile sauf, peut-être, une simplification de la recherche dans IntelliSense. Ce n'est qu'un conseil et peut-être même une question de goût, alors c'est à vous de décider comment vous le gérez :) Par exemple, devient simplement et le code devient un peu plus lisible . Dans ce cas, reste inchangé car le suffixe porte des informations utiles dans ce cas, notant qu'il ne s'agit pas d'un simple composant. HealthComponent Health entity.Has<Health>() PlayerMarker Réactivité retardée et composants à cadre unique La réactivité dans ECS peut être nocive. Mais que faire lorsque la réactivité est requise ? La réponse est la réactivité différée. La réactivité retardée se produit lorsqu'au lieu d'appeler la logique directement au moment de l'événement, vous créez des données indiquant que l'événement s'est produit et que tout le monde réagira simplement à l'événement au moment de son choix. Je peux faire une analogie avec le Dirty Flag dans OOP, où n'importe qui peut déclarer un événement , mais la logique réagira à cet événement quand elle le jugera bon. SetDirty(true) Dans ECS, vous créez simplement un composant avec ou sans données (vous pouvez simplement ajouter un indicateur booléen à un composant existant) que le système traitera le moment venu. Il n'est pas rare que de tels composants existent dans le monde pour qu'une seule trame alerte tous les systèmes mais pas pour répéter la logique dans la trame suivante. La suppression peut être gérée soit par le système générant l'événement, soit par un système distinct qui supprimera tous les composants de type X où vous le souhaitez. Par exemple, vous avez un . Pour lui dire combien de dégâts faire, vous déclarez avec le montant des dégâts et l'ajoutez à l'entité qui devrait subir les dégâts. Le parcourt toutes les entités avec et , endommage l'entité, supprime le et crée un qui informe tous les systèmes après le de l'entité endommagée. À la fin de la trame, le système individuel supprime le afin que les systèmes ne traitent plus ce marqueur dans la trame suivante. DamageSystem MakeDamageComponent DamageSystem HealthComponent MakeDamageComponent MakeDamageComponent DamagedEntityMarker DamageSystem DamagedEntityMarker Requests/Events en tant qu'API pour les systèmes En développant l'idée de composants à cadre unique, nous pouvons les utiliser pour exprimer une sorte d'API pour les systèmes. Nous devons distinguer les composants de requête pour les requêtes externes et les composants d'événement pour informer tout le monde de l'événement. Le système lui-même peut contrôler le cycle de vie des deux composants. Il est possible de supprimer les demandes immédiatement après le traitement et de nettoyer les événements avant de lancer de nouveaux événements. C'est à vous de décider comment les nommer exactement et s'il faut ajouter le suffixe Requests/Events. Par exemple, vous avez le du paragraphe précédent. Vous pouvez lui exprimer une demande de dommages à l'aide du composant et utiliser le composant pour notifier d'autres systèmes. La logique à l'intérieur du système est la suivante : effacez tous de la dernière image, endommagez toutes les entités avec une requête, supprimez la requête et ajoutez le composant . DamageSystem MakeDamageRequest DamagedEntityEvent DamagedEntityEvent DamagedEntityEvent Stocker une référence à une autre entité dans un composant Vous vous posez probablement la question "Comment créer des liens entre entités dans ECS ? Est-il nécessaire de marquer les entités avec des composants puis de les rechercher en boucle ?". Bien sûr que non. Tout est beaucoup plus simple et plus banal : on enregistre juste la référence. La seule différence est que la référence n'est pas à un composant d'une autre entité qui nous intéresse mais à l'entité elle-même. Ainsi, vous ajoutez un champ avec l'entité (ou la manière dont votre framework stocke les entités) au composant. Avant de l'utiliser, vous vérifiez que l'entité est active et possède le composant souhaité. Ensuite, vous obtenez ce composant et travaillez avec lui comme vous le souhaitez. Par exemple, vous pouvez faire en sorte que ne soit pas directement sur l'entité, mais l'exécute en tant qu'événement d'entité distinct en faisant référence à l'entité cible. Pour ce faire, vous ajoutez le champ à et retravaillez le . Maintenant, il devrait parcourir toutes les requêtes, vérifier que la cible est une entité vivante avec un , obtenir le et faire des dégâts. MakeDamageRequest Entity target MakeDamageRequest DamageSystem HealthComponent HealthComponent L'exécution sera désormais également différente. Au lieu d'ajouter le composant directement à l'entité, vous créez une nouvelle entité avec et spécifiez la . De cette façon, au détriment de la commodité du filtrage, vous pouvez déclencher plusieurs événements de dégâts différents pour une seule entité . MakeDamageRequest MakeDamageRequest target target Déplacement de la logique répétitive vers StaticUtils/Extensions Au fil du temps, vous commencez à remarquer que vous exécutez la même logique sur différents systèmes. Habituellement, c'est un signe qu'il est temps de faire un nouveau système :D Mais il arrive que la logique répétée soit secondaire, liée à un ou deux composants/entités spécifiques, et son résultat soit utilisé à des fins différentes. Par exemple, une interprétation spéciale des données dans un composant. Certains développeurs permettent de déclarer une telle logique supplémentaire directement dans le composant (sous la forme de getters, par exemple). Mais pour éviter de violer ECS, je suggère une autre option : les utilitaires statiques (ou Extensions en C#) que nous appelons depuis les systèmes. Par exemple, nous avons un . À l'intérieur, une couleur d'équipe se trouve. Vérifier que deux entités appartiennent à la même équipe peut être nécessaire dans plusieurs systèmes. Nous créons donc une classe statique et une méthode dans celle-ci , décrivant la logique récurrente de comparaison des équipes pour deux entités. InTeamComponent TeamUtils IsInSameTeam(Entity, Entity) Regroupement des systèmes par moment d'exécution Comme vous le savez déjà, l'ordre dans lequel les systèmes sont appelés est essentiel dans ECS. Il est donc pratique de regrouper les systèmes au niveau supérieur selon l'ordre dans lequel ils sont appelés dans un cadre. Par exemple, les premiers systèmes appelés dans une trame pourraient être tous les systèmes liés aux entrées. Ils recueilleront les entrées des utilisateurs et les prépareront au format ECS. En second lieu, un groupe de systèmes avec une logique de jeu, qui interprétera les données d'entrée à sa manière et mettra à jour le monde ECS. Et enfin, nous pouvons avoir un groupe de systèmes responsables du rendu ou simplement diverses choses supplémentaires qui devraient être appelées après toute la logique du jeu. Séparation des fonctionnalités principales dans des assemblages séparés Cette approche séparera les fonctionnalités les unes des autres et contrôlera leurs dépendances. Dans un monde idéal, ils ne devraient pas du tout se chevaucher. L'ordre entre les caractéristiques devrait être sans importance. Il devrait également y avoir un assemblage de base où les composants dont toutes les fonctionnalités ont besoin pour fonctionner doivent être situés. Dois-je diviser les composants/systèmes en plus petits morceaux dans ECS ? Cette question est très intéressante car les développeurs Unity ayant des expériences différentes ont des opinions différentes à ce sujet. Mais je vais essayer de couvrir les deux réponses pour vous aider à comprendre vos besoins. Oui, il faut toujours scinder les composants Cette approche dans l'organisation ECS peut être qualifiée d'atomique. Le degré supérieur de cette approche est que chaque composant n'a qu'un seul champ. Cela nous permettra d'atteindre l'apogée de la combinatoire du projet et d'éliminer le besoin de refactorisation au nom de l'abstraction ECS. Nous ne pouvons plus penser à "comment combiner des entités avec la propriété X". Inconvénients de toujours diviser les composants dans ECS : Le nombre de classes et de fichiers augmentera, ce qui peut prêter à confusion sur les grands projets si vous ne faites pas attention à l'organisation du projet Le nombre de composants (en général ou par entité) peut affecter les performances de votre framework Il est plus difficile de comprendre ce qu'est une entité par un tas de propriétés (qui peuvent être résolues par un marqueur avec un nom normal) Passons maintenant au deuxième avis. Non, vous ne devez diviser les composants que lorsque cela est nécessaire Ce principe s'appelle "Ne vous séparez pas avant l'heure". C'est le principe auquel j'adhère personnellement. Lorsque vous avez besoin de fractionner des données, la métrique est simple : ces données sont-elles utilisées/prévues d'être utilisées ailleurs en dehors de ce composant ? Sinon, il n'y a aucune raison de passer du temps dessus. Vous peut adopter une approche similaire pour partitionner la logique en systèmes. Inconvénients du fractionnement des composants dans ECS uniquement lorsque cela est nécessaire : Il faudra encore consacrer du temps à l'abstraction ECS Moins de liberté pour concevoir des entités A vous de choisir votre camp :) Frameworks pour Unity/C# Si vous êtes débutant, je suppose que vous envisagez d'abord d'apprendre Unity DOTS. Mais je veux te prévenir. Unity DOTS n'est pas une bonne option pour les débutants car il est énorme et complexe. Il n'y a pas autant de documentation et de personnes expérimentées que nous le souhaiterions. De plus, il n'est pas bien compatible avec l'ancien code Unity (tout cela peut changer après la publication de cet article). Si vous aimez Unity Editor et que vous êtes déjà habitué à accrocher des composants directement sur GameObjects, alors est votre meilleur choix. Il offre une API simple, une intégration étroite dans Unity Editor (il peut fonctionner en dehors de Unity) et un travail pratique avec MonoBehaviour. C'est simple et pratique. Tout ce dont vous avez besoin pour travailler avec Unity s'y trouve. Vous venez de l'installer et de travailler avec. Son principal inconvénient est qu'il nécessite un rémunéré pour travailler pleinement dans Unity. Morpeh inspecteur Odin Entitas et DOTS (Unity ECS) : faut-il les maîtriser ? Et maintenant, un bref aperçu des frameworks Unity/C# que j'ai rencontrés personnellement et des avantages/inconvénients que j'ai remarqués. Il convient de noter que tout ce qui est décrit ci-dessous peut changer depuis la publication de cet article, il est donc préférable de vérifier vous-même les frameworks. Entités C'est le plus ancien framework ECS pour Unity/C# et il est toujours le plus populaire. C'est celui que l'on voit le plus souvent dans les offres d'emploi avec ECS. Avantages d'Entitas : Great WorldViewer dans l'éditeur Unity Style de code fluide (grâce à la génération de code) Bonne documentation Très grande communauté De nombreux projets réussis dessus Support obligatoire du développeur (merci à AssetStore) Peut être utilisé en dehors de Unity en C# pur Inconvénients : Performances médiocres par rapport aux autres frameworks ECS (mais toujours meilleures que MonoBehaviour) Beaucoup d'allocations, qui affectent négativement le GC Nécessite la génération de code pour chaque modification de la structure des composants Sur les grands projets, l'API devient très gonflée en raison de la génération de code La version Github ne permet pas d'appeler la génération de code sur les erreurs de compilation, alors que la version AssetStore le permet Vous devriez le maîtriser, au moins à un niveau de base. DOTS (Unity ECS) Je pense qu'il n'a pas besoin d'être présenté. DOTS n'est pas un cadre mais une plate-forme à part entière (pile technologique) avec Unity ECS comme cadre à l'intérieur. Je tiens à souligner que ce qui suit est une expérience quelque peu dépassée. J'admets qu'un DOTS à jour pourrait résoudre les problèmes décrits ci-dessous. Avantages DOTS (Unity ECS) : Une plateforme à part entière pour le développement sur ECS Construit par les développeurs de moteurs avec l'intégration la plus étroite possible dans l'éditeur Prend en charge les travaux et les rafales Avec Jobs et Burst, il atteint des performances maximales parmi les frameworks ECS. Possède une excellente bibliothèque réseau avec des prédictions NetCode Mécanisme des sous-scènes DOTS (Unity ECS) contre : Travail en cours, ce qui entraîne la rupture des anciens changements de code et de nouveaux bogues Documentation faible, qui souvent ne suit pas les changements, il faut lire le code source Nécessite d'écrire plus de code technique que les analogues Le code est difficile à lire et loin d'être concis Ne fonctionne pas plus rapidement que les solutions open source sans Burst/Jobs (et dans certains cas plus lent) Ne correspond pas bien à l'ancien code Unity DOTS est essentiellement un environnement d'exécution, et toutes les anciennes fonctionnalités n'ont pas été portées sur DOTS. Cela limite les possibilités d'utilisation. Conclusion sur ECS dans Unity Comme vous l'avez peut-être remarqué dans la longue liste d'inconvénients, ECS n'est pas une solution miracle. Cette solution architecturale, comme toute autre, a ses avantages et ses inconvénients que vous devez accepter si vous choisissez de développer selon ce modèle architectural. Ainsi, le choix d'utiliser ou non ECS dans vos projets vous appartient entièrement. Je vous recommande fortement d'essayer au moins de faire un petit projet sur ECS pour comprendre si vous aimez cette approche ou non. Je vous recommande également de jeter un œil à ce , où il y a des réponses à de nombreuses questions liées à ECS. Vous trouverez des liens vers des rapports, une liste de frameworks pour différents langages, ainsi que des exemples de jeux et de programmes utilisant ECS. référentiel De mon point de vue personnel, ECS ressemble à une excellente option pour créer des jeux Unity. Le développement, pour moi personnellement, est un plaisir : vous développez un jeu, vous n'essayez pas de comprendre comment intégrer un nouveau code dans un ancien système sans rien casser. Il convient de garder à l'esprit que la convivialité du développement ECS est grandement affectée par le choix du framework, alors essayez différentes options et choisissez avec soin. D'après mon expérience, j'ai tendance à penser que l'ECS (ou sa variante) est l'avenir du développement de jeux interactifs. Et pas seulement parce (et peut-être même Epic) l'ont choisi comme objectif principal, mais simplement parce qu'ECS présente des avantages avantageux dans le contexte du développement de jeux. qu'Unity Technologies Et en général, c'est une approche pratique qui semble maladroite au début mais qui s'avère payante à long terme.