Bonjour, Habr! Je vous présente la traduction du wiki du projet
Svelto.ECS écrit par Sebastiano Mandalà.
Svelto.ECS est le résultat de nombreuses années de recherche et d'application des principes SOLID dans le développement de jeux sur Unity. Il s'agit de l'une des nombreuses implémentations du modèle ECS disponibles pour C # avec diverses fonctionnalités uniques introduites pour remédier aux lacunes du modèle lui-même.
Premier coup d'oeil
La façon la plus simple de voir les fonctionnalités de base de Svelto.ECS est de télécharger l'
exemple Vanilla . Si vous voulez vous assurer de sa facilité d'utilisation, je vais vous montrer un exemple:
Malheureusement, il n'est pas possible de comprendre rapidement la théorie derrière ce code, qui peut sembler simple mais déroutant en même temps. Pour comprendre cela, vous devez passer du temps à lire le «mur de texte» et essayer les exemples ci-dessus.
Présentation
Récemment, j'ai beaucoup discuté de
Svelto.ECS avec plusieurs programmeurs plus ou moins expérimentés. J'ai recueilli beaucoup de commentaires et pris beaucoup de notes que je vais utiliser comme point de départ pour mes prochains articles, où je parlerai davantage de théorie et de bonnes pratiques. Un petit spoiler: j'ai réalisé que lorsque vous commencez à utiliser Svelto.ECS, le plus gros obstacle est de
changer le paradigme de programmation . C'est incroyable de voir combien j'ai à écrire pour expliquer les nouveaux concepts introduits par Svelto.ECS, par rapport à la petite quantité de code écrit pour développer le framework. En fait, alors que le cadre lui-même est très simple et léger, la transition de la POO avec l'utilisation active de l'héritage ou des composants Unity conventionnels au «nouveau» design modulaire et à couplage lâche que Svelto.ECS suggère de l'utiliser empêche les gens de s'adapter au cadre.
Svelto.ECS est activement utilisé à
Freejam (note du traducteur - L'auteur est le directeur technique de cette entreprise). Comme je peux toujours expliquer à mes collègues les concepts de base du cadre, il leur faut moins de temps pour comprendre comment travailler avec lui. Bien que Svelto.ECS soit aussi difficile que possible, les mauvaises habitudes sont difficiles à surmonter, de sorte que les utilisateurs ont tendance à abuser d'une certaine flexibilité qui leur permet d'adapter le cadre aux «anciens» paradigmes avec lesquels ils sont à l'aise. Cela peut conduire à un désastre en raison d'un malentendu ou d'une distorsion des concepts qui sous-tendent la logique du cadre. C'est pourquoi j'ai l'intention d'écrire autant d'articles que possible, d'autant plus que je suis sûr que le paradigme ECS est la meilleure solution à l'heure actuelle pour écrire du code efficace et pris en charge pour les grands projets qui changent et retravaillent plusieurs fois sur plusieurs années.
Robocraft et
Cardlife en sont la preuve.
Je ne vais pas parler beaucoup des théories qui sous-tendent cet article. Je vais seulement vous rappeler pourquoi j'ai refusé d'utiliser le
conteneur IoC et commencé à utiliser exclusivement le cadre ECS: le conteneur IoC est un outil très dangereux s'il est utilisé sans comprendre l'essence même de l'inversion de contrôle. Comme vous pouvez le voir dans mes articles précédents, je fais la distinction entre l'inversion du contrôle de création (Inversion du contrôle de création) et l'inversion du contrôle de flux (inversion du contrôle de flux). Inverser le contrôle de flux est comme le principe d'Hollywood: "Ne nous appelez pas, nous vous appellerons." Cela signifie que les dépendances injectées ne doivent jamais être utilisées directement via des méthodes publiques, car ce faisant, vous utilisez simplement le conteneur IoC comme substitut à toute autre forme d'injection globale, telle que singleton. Cependant, si le conteneur IoC est utilisé sur la base de l'inversion de la gestion (IoC), alors tout se résume à réutiliser le modèle de «méthode de modèle» pour implémenter des gestionnaires qui ne sont utilisés que pour enregistrer les objets qu'ils gèrent. Dans le contexte réel des inversions de contrôle de flux, les managers sont toujours responsables de la gestion des entités. Cela ressemble-t-il à un modèle ECS? Bien sûr. Sur la base de ce raisonnement, j'ai pris le modèle ECS et développé un cadre rigide basé sur celui-ci, et son utilisation équivaut à appliquer le nouveau paradigme de programmation.
Racine de composition et moteurs
La classe principale est la racine de composition de l'application. La racine de la composition est l'endroit où les dépendances sont créées et implémentées (j'en ai beaucoup parlé dans mes articles). Une racine de composition appartient à un contexte, mais un contexte peut avoir plusieurs racines de composition. Par exemple, l'Usine est la racine de la composition. Une application peut avoir plus d'un contexte, mais il s'agit d'un scénario avancé, et dans cet exemple, nous ne le considérerons pas.
Avant de plonger dans le code, familiarisons-nous avec les premières règles du langage Svelto.ECS. ECS est l'abréviation Entity Component System. L'infrastructure ECS a été bien analysée dans les articles de nombreux auteurs, mais bien que les concepts de base soient courants, les implémentations varient considérablement. Tout d'abord, il n'existe aucun moyen standard de résoudre certains problèmes qui surviennent lors de l'utilisation de code orienté ECS. C'est sur cette question que je fais le plus d'efforts, mais j'en parlerai plus tard ou dans les articles suivants. La théorie est basée sur les concepts d'essence, de composants (entités) et de systèmes. Bien que je comprenne pourquoi le mot système a été utilisé historiquement, dès le début, je ne l'ai pas trouvé suffisamment intuitif à cet effet, j'ai donc utilisé le moteur comme synonyme du système, et vous, selon vos préférences, pouvez utiliser l'un de ces termes.
La classe EnginesRoot est le cœur de Svelto.ECS. Avec son aide, vous pouvez enregistrer des moteurs et concevoir toute l'essence du jeu. La création dynamique de moteurs n'a pas beaucoup de sens, ils doivent donc tous être ajoutés à l'instance EnginesRoot à partir de la même racine de la composition où elle a été créée. Pour des raisons similaires, une instance EnginesRoot ne doit jamais être déployée et les moteurs ne doivent pas être supprimés après avoir été ajoutés.
Pour créer et implémenter des dépendances, nous avons besoin d'au moins une racine de la composition. Oui, dans une application, plusieurs moteurs EngineRoot peuvent bien exister, mais nous n'aborderons pas cela dans l'article actuel, que j'essaie de simplifier autant que possible. Voici à quoi ressemble la racine de composition avec la création de moteur et l'injection de dépendances:
void SetupEnginesAndEntities() {
Ce code provient de l'exemple Survival, qui est maintenant commenté et conforme à presque toutes les règles de bonnes pratiques que je propose d'appliquer, y compris l'utilisation d'une logique de moteur indépendante de la plateforme et testée. Les commentaires vous aideront à comprendre la plupart d'entre eux, mais un projet de cette taille peut être difficile à comprendre si vous êtes nouveau sur Svelto.
Entités
La première étape après avoir créé la racine vide de la composition et une instance de la classe EnginesRoot consiste à identifier les objets avec lesquels vous souhaitez travailler en premier. Il est logique de commencer avec Entity Player. L'essence de Svelto.ECS ne doit pas être confondue avec l'objet de jeu Unity (GameObject). Si vous lisez d'autres articles liés à ECS, vous pouvez voir que dans beaucoup d'entre eux, les entités sont souvent décrites comme des index. C'est probablement la pire façon d'introduire le concept ECS. Bien que cela soit vrai pour Svelto.ECS, il y est caché. Je souhaite que l'utilisateur Svelto.ECS représente, décrive et identifie chaque entité en termes de langage de domaine de conception de jeux. L'entité dans le code doit être l'objet décrit dans le document de conception du jeu. Toute autre forme de définition d'entité conduira à une manière farfelue d'adapter vos anciennes vues aux principes Svelto.ECS. Suivez cette règle fondamentale et vous ne vous tromperez pas. La classe d'entité elle-même n'existe pas dans le code, mais vous ne devez toujours pas la définir de manière abstraite.
Les moteurs
L'étape suivante consiste à réfléchir au comportement à demander aux Entités. Chaque comportement est toujours modélisé à l'intérieur du moteur; vous ne pouvez pas ajouter de logique à d'autres classes à l'intérieur de l'application Svelto.ECS. Nous pouvons commencer par déplacer le personnage du joueur et définir la classe
PlayerMovementEngine . Le nom du moteur doit être très précis, car plus il est spécifique, plus il est probable que le moteur suivra la règle de responsabilité unique. Une dénomination de classe appropriée dans Svelto.ECS est fondamentale. Et le but n'est pas seulement de montrer clairement vos intentions, mais aussi de vous aider à les «voir» vous-même.
Pour la même raison, il est important que votre moteur se trouve dans un espace de noms très spécialisé. Si vous définissez des espaces de noms en fonction de la structure des dossiers, adaptez-vous aux concepts Svelto.ECS. L'utilisation d'espaces de noms spécifiques permet de détecter les erreurs de conception lorsque des entités sont utilisées dans des espaces de noms incompatibles. Par exemple, il n’est pas supposé qu’un objet ennemi sera utilisé à l’intérieur de l’espace de noms du joueur, sauf si le but est de briser les règles associées à la modularité et à la faible connexion des objets. L'idée est que les objets d'un espace de noms particulier ne peuvent être utilisés qu'à l'intérieur de celui-ci ou de l'espace de noms parent. L'utilisation de Svelto.ECS est beaucoup plus difficile à transformer votre code en spaghetti, où les dépendances sont injectées à droite et à gauche, et cette règle vous aidera à augmenter le niveau de qualité du code encore plus haut lorsque les dépendances sont correctement abstraites entre les classes.
Dans Svelto.ECS, l'abstraction avance de quelques lignes, mais ECS aide essentiellement à extraire les données de la logique qui devrait traiter les données. Les entités sont déterminées par leurs données et non par leur comportement. Dans ce cas, les moteurs sont un endroit où vous pouvez placer le comportement conjoint d'entités identiques afin que les moteurs puissent toujours fonctionner avec un ensemble d'entités.
Svelto.ECS et le paradigme ECS permettent à l'encodeur d'atteindre l'un des Saint Graal de la programmation pure, qui est l'encapsulation idéale de la logique. Les moteurs ne devraient pas avoir de fonctions publiques. Les seules fonctions publiques qui doivent exister sont celles qui sont nécessaires pour implémenter les interfaces du framework. Cela conduit à oublier l'injection de dépendance et permet d'éviter un mauvais code qui se produit lors de l'utilisation de l'injection de dépendance sans inversion de contrôle. Les moteurs ne doivent JAMAIS être intégrés dans un autre moteur ou tout autre type de classe. Si vous pensez que vous souhaitez implémenter le moteur, vous faites simplement une erreur fondamentale dans la conception du code.
Par rapport à Unity MonoBehaviours, les moteurs présentent déjà le premier énorme avantage, qui est la possibilité d'accéder à tous les états d'entités de ce type à partir de la même zone de code. Cela signifie que le code peut facilement utiliser l'état de tous les objets directement à partir du même endroit où la logique de l'objet commun sera exécutée. De plus, les moteurs individuels peuvent traiter les mêmes objets afin que le moteur puisse changer l'état de l'objet, tandis que l'autre moteur peut le lire, en utilisant efficacement deux moteurs pour la communication via les mêmes données d'entité. Un exemple peut être vu en regardant les
moteurs PlayerGunShootingEngine et
PlayerGunShootingFxsEngine . Dans ce cas, deux moteurs se trouvent dans le même espace de noms, afin qu'ils puissent partager les mêmes données d'entité.
PlayerGunShootingEngine détermine si un joueur (ennemi) a été endommagé et écrit la valeur
lastTargetPosition du composant
IGunAttributesComponent (qui est un composant
PlayerGunEntity ).
PlayerGunShootFxsEngine traite les effets graphiques de l'arme et lit la position de la cible sélectionnée par le joueur. Il s'agit d'un exemple d'interaction entre les moteurs via l'interrogation de données. Plus loin dans cet article, je montrerai comment autoriser un mécanisme à communiquer entre eux en
poussant les données (Data push) ou
la liaison de données (Data binding) . Logiquement, les moteurs ne devraient jamais stocker l'état.
Les moteurs n'ont pas besoin de savoir comment interagir avec d'autres moteurs. La communication externe se fait par abstraction, et Svelto.ECS résout la connexion entre les moteurs de trois manières officielles différentes, mais j'en parlerai plus tard. Les meilleurs moteurs sont ceux qui ne nécessitent aucune communication externe. Ces moteurs reflètent un comportement bien encapsulé et fonctionnent généralement via une boucle logique. Les boucles sont toujours modélisées à l'aide des tâches Svelto.Task dans les applications Svelto.ECS. Étant donné que le mouvement du joueur doit être mis à jour à chaque tick physique, il serait naturel de créer une tâche à effectuer à chaque tick physique. Svelto.Tasks vous permet d'exécuter chaque type d'
IEnumerator sur plusieurs types de planificateurs. Dans ce cas, nous avons décidé de créer une tâche sur
PhysicScheduler , qui vous permet de mettre à jour la position du joueur:
public PlayerMovementEngine(IRayCaster raycaster, ITime time) { _rayCaster = raycaster; _time = time; _taskRoutine = TaskRunner.Instance.AllocateNewTaskRoutine() .SetEnumerator(PhysicsTick()).SetScheduler(StandardSchedulers.physicScheduler); } protected override void Add(PlayerEntityView entityView) { _taskRoutine.Start(); } protected override void Remove(PlayerEntityView entityView) { _taskRoutine.Stop(); } IEnumerator PhysicsTick() {
Les tâches Svelto.Tasks peuvent être effectuées directement ou via des objets
ITaskRoutine . Je ne parlerai pas beaucoup de Svelto.Tasks ici, car j'ai écrit d'autres articles pour cela. La raison pour laquelle j'ai décidé d'utiliser la routine de tâche au lieu de lancer directement l'implémentation IEnumerator est assez discrétionnaire. Je voulais montrer que vous pouvez démarrer un cycle lorsqu'un objet d'un joueur est ajouté au moteur et l'arrêter lorsqu'il est supprimé. , .
Svelto.ECS
, , . Svelto.ECS, . , , , . , .
,
SingleEntityViewEngine ,
MultiEntitiesViewEngine <EntityView1, ..., EntityViewN> . - , , .
IQueryingEntityViewEngine . . , - , , , , , , - . , , . , , .
EnemyMovementEngine , :
public void Ready() { Tick().Run(); } IEnumerator Tick() { while (true) { var enemyTargetEntityViews = entityViewsDB.QueryEntityViews<EnemyTargetEntityView>(); if (enemyTargetEntityViews.Count > 0) { var targetEntityView = enemyTargetEntityViews[0]; var enemies = entityViewsDB.QueryEntityViews<EnemyEntityView>(); for (var i = 0; i < enemies.Count; i++) { var component = enemies[i].movementComponent; component.navMeshDestination = targetEntityView.targetPositionComponent.position; } } yield return null; } }
.
Tick ().Run() IEnumerator Svelto.Tasks. IEnumerator , Enemy. , ( ), . Enemy Target ( !), , - . , Unity Nav Mesh System, , , NavMesh. , Unity NavMesh, , , Survival.
, Navmesh Unity. , , . ,
navMeshDestination Unity Nav Mesh.
, , , , . , , - , , .
, , . , 5 , Svelto.ECS, , , .
(Node) (,
ECS Ash ), , “” . EntityView , ,
(Model View Controller), Svelto.ECS View, EntityView — , . , , EntityMap, EntityView , . Svelto.ECS :

, . EntityViews. EntityViews, EntityViews. , Player, ,
PlayerEntityView . , , . EntityView . , ( . .),
PlayerPhysicEngine PlayerPhysicEntityView ,
PlayerGraphicEngine PlayerGraphicEntityView PlayerAnimationEngine PlayerAnimationEntityView . ,
PlayerPhysicMovementEngine PlayerPhysicJumpEngine ( . .).
, , , , . , EntityView — , (public) . , , :
, — . , Svelto.ECS. . .
« » (Interface Segregation Principle), , , , . ITransformComponent . , , ( , ).
Svelto.ECS , EntityView .
«». , ., . , ref, . , (data oriented), , . , ( !) . , , — , Unity. , Survival, , , Unity.
, . , , . , , EntityView — , . , , . , ECS. , . — , , . , , — . EntityDescriptor , . Player
PlayerEntityDescriptor . , , , - , ,
BuildEntity<PlayerEntityDescriptor>() , .
, EntityDescriptor, — EntityViews!!! EntityViews , , , .
PlayerEntityDescriptor :
using Svelto.ECS.Example.Survive.Camera; using Svelto.ECS.Example.Survive.HUD; using Svelto.ECS.Example.Survive.Enemies; using Svelto.ECS.Example.Survive.Sound; namespace Svelto.ECS.Example.Survive.Player { public class PlayerEntityDescriptor : GenericEntityDescriptor<HUDDamageEntityView, PlayerEntityView, EnemyTargetEntityView, DamageSoundEntityView, HealthEntityView, CameraTargetEntityView> { } }
( ) , .
PlayerEntityDescriptor EntityViews PlayerEntity.
EntityDescriptorHolder
EntityDescriptorHolder Unity . , Unity GameObject. , . , Robocraft , . . , GameObject MonoBehaviour's. , EntityDescriptorHolders , Svelto.ECS, . , :
void BuildEntitiesFromScene(UnityContext contextHolder) {
, ,
BuildEntity . . MonoBehaviour GameObject. . , , . , , MonoBehaviours , !
, Svelto.ECS,
. , , C# . , , «». :
- , .
- , , .
- . , .
- Svelto.ECS (third party) . . Unity, , , Monobehaviour . , Unity, OnTriggerEnter / OnTriggerExit , Unity. , . :
public class EnemyTriggerImplementor : MonoBehaviour, IImplementor, IEnemyTriggerComponent, IEnemyTargetComponent { public event Action<int, int, bool> entityInRange; bool IEnemyTriggerComponent.targetInRange { set { _targetInRange = value; } } bool IEnemyTargetComponent.targetInRange { get { return _targetInRange; } } void OnTriggerEnter(Collider other) { if (entityInRange != null) entityInRange(other.gameObject.GetInstanceID(), gameObject.GetInstanceID(), true); } void OnTriggerExit(Collider other) { if (entityInRange != null) entityInRange(other.gameObject.GetInstanceID(), gameObject.GetInstanceID(), false); } bool _targetInRange; }
, , . , .,
,
EnginesRoot ,
,
,
. .
(Entity Factory), EnginesRoot
GenerateEntityFactory . EnginesRoot IEntityFactory . , IEntityFactory .
IEntityFactory .
PreallocateEntitySlots BuildMetaEntity ,
BuildEntity BuildEntityInGroup .
BuildEntityInGroup , Survival , ,
BuildEntity :
IEnumerator IntervaledTick() {
, Svelto.ECS. -
BuildEntityInGroup , . Robocraft , , . , , , , — . , Svelto.Tasks , .
, , , … ( ):
MonoBehaviour . . , , . , . , , . , , , .
MonoBehaviour, . MonoBehaviour . , , json- , .
Svelto.ECS
, ECS, — . , , Svelto.ECS . — / , .
DispatchOnSet / DispatchOnChange
, (Data polling).
DispatchOnSet DispatchOnChange ( ), , T . , (Push) , , . , , , , .
DispatchOnSet DispatchOnChange , . , , , . Survival , ,
targetHit IGunHitTargetComponent .
DispatchOnSet DispatchOnChange , , , .
, Svelto.Tasks (IEnumerators). , . ,
DispatchOnSet DispatchOnChange , , , “” , . , , , , . , ! . IEnumerator Svelto Tasks «» «» .
/
, Svelto.ECS Svelto.ECS. , , , Svelto.ECS, , , , .