Est-il raisonnable de créer des applications (pas des jeux) en utilisant une architecture système-entité-composant?

24

Je sais que lors de la création d'applications (natives ou Web) telles que celles de l'AppStore Apple ou de l'App Store Google Play, il est très courant d'utiliser une architecture Model-View-Controller.

Cependant, est-il raisonnable de créer également des applications en utilisant l'architecture Component-Entity-System commune aux moteurs de jeu?

Andrew De Andrade
la source
1
Découvrez l'architecture de la table lumineuse: chris-granger.com/2013/01/24/the-ide-as-data
Hakan Deryal

Réponses:

39

Cependant, est-il raisonnable de créer également des applications en utilisant l'architecture Component-Entity-System commune aux moteurs de jeu?

Pour moi, absolument. Je travaille dans les effets visuels et j'ai étudié une grande variété de systèmes dans ce domaine, leurs architectures (y compris CAD / CAM), avide de SDK et de tous les papiers qui me donneraient une idée des avantages et des inconvénients des décisions architecturales apparemment infinies qui pourrait être fait, même les plus subtils n’ont pas toujours un impact subtil.

Les effets visuels sont assez similaires aux jeux dans la mesure où il existe un concept central de "scène", avec des fenêtres qui affichent les résultats rendus. Il y a aussi souvent beaucoup de traitement en boucle centrale qui tourne constamment autour de cette scène dans des contextes d'animation, où il peut y avoir de la physique, des émetteurs de particules engendrant des particules, des mailles animées et rendues, des animations de mouvement, etc., et finalement pour les rendre tout à l'utilisateur à la fin.

Un autre concept similaire à des moteurs de jeu au moins très complexes était la nécessité d'un aspect "concepteur" où les concepteurs pouvaient concevoir des scènes de manière flexible, y compris la possibilité de faire leur propre programmation légère (scripts et nœuds).

Au fil des ans, j'ai trouvé qu'ECS était le mieux adapté. Bien sûr, cela n'est jamais complètement dissocié de la subjectivité, mais je dirais que cela semble fortement poser le moins de problèmes. Cela a résolu beaucoup plus de problèmes majeurs avec lesquels nous luttions toujours, tout en ne nous donnant que quelques nouveaux problèmes mineurs en retour.

POO traditionnel

Des approches POO plus traditionnelles peuvent être très efficaces lorsque vous avez une bonne compréhension des exigences de conception dès le départ, mais pas des exigences de mise en œuvre. Que ce soit par une approche à interfaces multiples plus plate ou une approche ABC hiérarchique plus imbriquée, elle tend à cimenter la conception et à la rendre plus difficile à changer tout en rendant l'implémentation plus facile et plus sûre à changer. Il y a toujours un besoin d'instabilité dans tout produit qui dépasse une seule version, donc les approches OOP ont tendance à biaiser la stabilité (difficulté de changement et manque de raisons de changement) vers le niveau de conception, et l'instabilité (facilité de changement et raisons de changement) au niveau de la mise en œuvre.

Cependant, face à l'évolution des besoins des utilisateurs, la conception et la mise en œuvre peuvent devoir changer fréquemment. Vous pourriez trouver quelque chose de bizarre comme un fort besoin de l'utilisateur final pour la créature analogique qui doit être à la fois végétale et animale en même temps, invalidant complètement le modèle conceptuel entier que vous avez construit. Les approches orientées objet normales ne vous protègent pas ici, et peuvent parfois rendre ces changements imprévus et révolutionnaires encore plus difficiles. Lorsque des domaines très critiques sont impliqués, les raisons des modifications de conception se multiplient encore.

La combinaison de plusieurs interfaces granulaires pour former l'interface conforme d'un objet peut beaucoup aider à stabiliser le code client, mais cela n'aide pas à stabiliser les sous-types qui pourraient parfois éclipser le nombre de dépendances client. Vous pouvez avoir une interface utilisée par seulement une partie de votre système, par exemple, mais avec mille sous-types différents implémentant cette interface. Dans ce cas, le maintien des sous-types complexes (complexes parce qu'ils ont de nombreuses responsabilités d'interface disparates à remplir) peut devenir le cauchemar plutôt que le code les utilisant via une interface. La POO tend à transférer la complexité au niveau objet, tandis qu'ECS la transfère au niveau client ("systèmes"), et cela peut être idéal lorsqu'il y a très peu de systèmes mais tout un tas "d'objets" ("entités") conformes.

entrez la description de l'image ici

Une classe possède également ses données de manière privée et peut donc maintenir elle-même des invariants. Néanmoins, il existe des invariants "grossiers" qui peuvent en fait encore être difficiles à maintenir lorsque les objets interagissent les uns avec les autres. Pour qu'un système complexe dans son ensemble soit dans un état valide, il faut souvent considérer un graphe complexe d'objets, même si leurs invariants individuels sont correctement maintenus. Les approches traditionnelles de style POO peuvent aider à maintenir des invariants granulaires, mais peuvent en fait rendre difficile le maintien d'invariants larges et grossiers si les objets se concentrent sur de minuscules facettes du système.

C'est là que ces types d'approches ou de variantes ECS de construction de blocs lego peuvent être si utiles. De plus, les systèmes étant de conception plus grossière que l'objet habituel, il devient plus facile de maintenir ces types d'invariants grossiers à la vue plongeante du système. De nombreuses interactions d'objets minuscules se transforment en un grand système se concentrant sur une tâche large au lieu de petits objets minuscules se concentrant sur de petites tâches minuscules avec un graphique de dépendance qui couvrirait un kilomètre de papier.

Pourtant, je devais regarder en dehors de mon domaine, dans l'industrie du jeu, pour en savoir plus sur ECS, bien que j'aie toujours été d'un état d'esprit orienté données. Aussi, assez drôle, j'ai presque fait mon chemin vers ECS tout simplement en itérant et en essayant de trouver de meilleurs designs. Je n'ai cependant pas fait tout le chemin et j'ai raté un détail très crucial, à savoir la formalisation de la partie "systèmes", et l'écrasement des composants jusqu'aux données brutes.

J'essaierai de passer en revue comment j'ai fini par m'installer sur ECS, et comment cela a fini par résoudre tous les problèmes avec les itérations de conception précédentes. Je pense que cela aidera à souligner exactement pourquoi la réponse ici pourrait être un «oui» très fort, que ECS est potentiellement applicable bien au-delà de l'industrie du jeu.

Architecture de la force brute des années 80

La première architecture sur laquelle j'ai travaillé dans l'industrie des effets visuels avait un long héritage qui dépassait déjà une décennie depuis que j'ai rejoint l'entreprise. C'était du codage brut C brut par force (pas une inclinaison sur C, comme j'adore C, mais la façon dont il était utilisé ici était vraiment grossière). Une tranche miniature et simpliste ressemblait à des dépendances comme celle-ci:

entrez la description de l'image ici

Et ceci est un diagramme extrêmement simplifié d'une toute petite partie du système. Chacun de ces clients dans le diagramme ("Rendu", "Physique", "Mouvement") obtiendrait un objet "générique" à travers lequel ils vérifieraient un champ de type, comme ceci:

void transform(struct Object* obj, const float mat[16])
{
    switch (obj->type)
    {
        case camera:
            // cast to camera and do something with camera fields
            break;
        case light:
            // cast to light and do something with light fields
            break;
        ...
    }
}

Bien sûr, avec un code beaucoup plus laid et plus complexe que cela. Souvent, des fonctions supplémentaires sont appelées à partir de ces boîtiers de commutateurs, ce qui permet de commuter récursivement le commutateur encore et encore et encore. Ce diagramme et ce code pourraient presque ressembler à ECS-lite, mais il n'y avait pas de forte distinction entité-composant (" cet objet est- il une caméra?", Pas "cet objet fournit-il du mouvement?"), Et aucune formalisation du "système" ( juste un tas de fonctions imbriquées allant partout et mélangeant les responsabilités). Dans ce cas, à peu près tout était compliqué, toute fonction était un potentiel de catastrophe en attente de se produire.

Notre procédure de test ici devait souvent vérifier des choses comme des maillages séparés des autres types d'éléments, même si la même chose arrivait aux deux, car la nature de la force brute du codage ici (souvent accompagnée de beaucoup de copier-coller) était souvent faite il est très probable que ce qui est autrement la même logique exacte pourrait échouer d'un type d'élément à l'autre. Essayer d'étendre le système pour gérer de nouveaux types d'articles était assez désespéré, même s'il y avait un besoin fortement exprimé de la part de l'utilisateur, car c'était trop difficile lorsque nous nous débattions autant pour gérer les types d'articles existants.

Quelques pros:

  • Euh ... ne prend aucune expérience en ingénierie, je suppose? Ce système ne nécessite aucune connaissance, même des concepts de base comme le polymorphisme, il est totalement brutal, donc je suppose que même un débutant pourrait être en mesure de comprendre une partie du code même si un professionnel du débogage peut à peine le maintenir.

Quelques inconvénients:

  • Cauchemar d'entretien. Notre équipe marketing a réellement ressenti le besoin de se vanter d'avoir corrigé plus de 2000 bogues uniques en un cycle de 3 ans. Pour moi, c'est quelque chose d'embarrassant à propos du fait que nous avons eu tellement de bogues en premier lieu, et ce processus n'a probablement encore corrigé qu'environ 10% du total des bogues qui augmentaient en nombre tout le temps.
  • À propos de la solution la plus rigide possible.

Architecture COM des années 1990

La plupart de l'industrie des effets visuels utilise ce style d'architecture d'après ce que j'ai rassemblé, lisant des documents sur leurs décisions de conception et jetant un œil à leurs kits de développement logiciel.

Ce n'est peut-être pas exactement COM au niveau ABI (certaines de ces architectures ne peuvent avoir que des plugins écrits en utilisant le même compilateur), mais partage beaucoup de caractéristiques similaires avec les requêtes d'interface effectuées sur les objets pour voir quelles interfaces leurs composants prennent en charge.

entrez la description de l'image ici

Avec ce type d'approche, la transformfonction analogique ci-dessus est venue ressembler à cette forme:

void transform(Object obj, const Matrix& mat)
{
    // Wrapper that performs an interface query to see if the 
    // object implements the IMotion interface.
    MotionRef motion(obj);

    // If the object supported the IMotion interface:
    if (motion.valid())
    {
        // Transform the item through the IMotion interface.
        motion->transform(mat);
        ...
    }
}

C'est l'approche sur laquelle la nouvelle équipe de cette ancienne base de code a opté, pour éventuellement refactoriser. Et ce fut une amélioration spectaculaire par rapport à l'original en termes de flexibilité et de maintenabilité, mais il y avait encore quelques problèmes que je couvrirai dans la section suivante.

Quelques pros:

  • Beaucoup plus flexible / extensible / maintenable que la précédente solution de force brute.
  • Favorise une forte conformité à de nombreux principes de SOLID en rendant chaque interface complètement abstraite (sans état, sans implémentation, uniquement des interfaces pures).

Quelques inconvénients:

  • Beaucoup de passe-partout. Nos composants devaient être publiés via un registre afin d'instancier des objets, les interfaces qu'ils supportaient nécessitaient à la fois l'héritage ("implémentation" en Java) de l'interface et la fourniture de code pour indiquer quelles interfaces étaient disponibles dans une requête.
  • Promotion de la logique dupliquée partout grâce aux interfaces pures. Par exemple, tous les composants qui ont implémenté IMotionauront toujours exactement le même état et exactement la même implémentation pour toutes les fonctions. Pour atténuer cela, nous commencerions à centraliser les classes de base et les fonctionnalités d'assistance dans tout le système pour les choses qui auraient tendance à être implémentées de manière redondante de la même manière pour la même interface, et éventuellement avec un héritage multiple en cours derrière le capot, mais c'était assez malpropre sous le capot même si le code client le rendait facile.
  • Inefficacité: les sessions vtune montraient souvent que la QueryInterfacefonction de base apparaissait presque toujours comme un point chaud moyen à supérieur, et parfois même le point chaud n ° 1. Pour atténuer cela, nous ferions des choses comme le rendu des parties du cache de la base de code une liste d'objets déjà connus pour prendre en chargeIRenderable, mais cela a considérablement accru la complexité et les coûts de maintenance. De même, cela a été plus difficile à mesurer, mais nous avons remarqué des ralentissements certains par rapport au codage de style C que nous faisions auparavant lorsque chaque interface nécessitait une répartition dynamique. Des choses comme les erreurs de prédiction des branches et les obstacles à l'optimisation sont difficiles à mesurer en dehors d'une petite facette du code, mais les utilisateurs ont généralement remarqué la réactivité de l'interface utilisateur et des choses de ce genre qui empirent en comparant côte à côte les versions précédentes et plus récentes du logiciel. côté pour les zones où la complexité algorithmique n'a pas changé, seules les constantes.
  • Il était encore difficile de raisonner sur l'exactitude à un niveau plus large du système. Même si c'était beaucoup plus facile que l'approche précédente, il était encore difficile de saisir les interactions complexes entre les objets dans tout ce système, en particulier avec certaines des optimisations qui commençaient à devenir nécessaires contre lui.
  • Nous avons eu du mal à corriger nos interfaces. Même s'il ne peut y avoir qu'un seul endroit large dans le système qui utilise une interface, les exigences de l'utilisateur changeraient au fil des versions, et nous finirions par devoir apporter des modifications en cascade à toutes les classes qui implémentent l'interface pour accueillir une nouvelle fonction ajoutée à l'interface, par exemple, à moins qu'il n'y ait une classe de base abstraite qui centralisait déjà la logique sous le capot (certaines d'entre elles se manifesteraient au milieu de ces changements en cascade dans l'espoir de ne pas répéter cela encore et encore).

entrez la description de l'image ici

Réponse pragmatique: composition

L'une des choses que nous remarquions avant (ou du moins j'étais) qui causait des problèmes était qu'elle IMotionpouvait être implémentée par 100 classes différentes mais avec exactement la même implémentation et le même état associés. De plus, il ne serait utilisé que par une poignée de systèmes comme le rendu, le mouvement d'images clés et la physique.

Donc, dans un tel cas, nous pourrions avoir comme une relation 3 à 1 entre les systèmes utilisant l'interface à l'interface, et une relation 100 à 1 entre les sous-types implémentant l'interface à l'interface.

La complexité et la maintenance seraient alors considérablement biaisées par la mise en œuvre et la maintenance de 100 sous-types, au lieu de 3 systèmes clients qui en dépendent IMotion. Cela a déplacé toutes nos difficultés de maintenance vers la maintenance de ces 100 sous-types, pas les 3 endroits utilisant l'interface. Mise à jour de 3 emplacements dans le code avec peu ou pas de "couplages efférents indirects" (comme dans les dépendances mais indirectement via une interface, pas une dépendance directe), pas de problème: mise à jour de 100 emplacements de sous-types avec une cargaison de "couplages efférents indirects" , assez gros problème *.

* Je me rends compte qu'il est étrange et erroné de viser la définition de "couplages efférents" dans ce sens du point de vue de la mise en œuvre, je n'ai tout simplement pas trouvé de meilleure façon de décrire la complexité de la maintenance associée lorsque l'interface et les mises en œuvre correspondantes d'une centaine de sous-types doit changer.

J'ai donc dû pousser fort mais j'ai proposé d'essayer de devenir un peu plus pragmatique et de relâcher toute l'idée de "pure interface". Cela n'avait aucun sens pour moi de faire quelque chose de IMotioncomplètement abstrait et d'apatride à moins que nous ne voyions un avantage à avoir une riche variété d'implémentations. Dans notre cas, IMotionavoir une riche variété d'implémentations se transformerait en fait en un véritable cauchemar de maintenance, car nous ne voulions pas de variété. Au lieu de cela, nous essayions de créer une implémentation en un seul mouvement qui soit vraiment bonne contre les exigences changeantes du client, et nous travaillions souvent autour de l'idée d'interface pure en essayant de forcer chaque implémenteur IMotionà utiliser la même implémentation et le même état associé afin que nous ne le fassions pas. t des objectifs en double.

Les interfaces sont ainsi devenues plus comme de larges Behaviorsassociés à une entité. IMotiondeviendrait simplement un Motion"composant" (j'ai changé la façon dont nous avons défini le "composant" loin de COM à un qui est plus proche de la définition habituelle, d'une pièce constituant une entité "complète").

Au lieu de cela:

class IMotion
{
public:
    virtual ~IMotion() {}
    virtual void transform(const Matrix& mat) = 0;
    ...
};

Nous l'avons transformé en quelque chose de plus comme ceci:

class Motion
{
public:
    void transform(const Matrix& mat)
    {
        ...
    }
    ...

private:
    Matrix transformation;
    ...
};

Il s'agit d'une violation flagrante du principe d'inversion de dépendance pour commencer à passer de l'abstrait au concret, mais pour moi, un tel niveau d'abstraction n'est utile que si nous pouvons prévoir un réel besoin dans un avenir futur, hors de tout doute raisonnable et non exercer des scénarios ridicules «et si» complètement détachés de l'expérience utilisateur (ce qui nécessiterait probablement un changement de conception de toute façon), pour une telle flexibilité.

Nous avons donc commencé à évoluer vers cette conception. QueryInterfaceest devenu plus comme QueryBehavior. De plus, il a commencé à sembler inutile d'utiliser l'héritage ici. Nous avons plutôt utilisé la composition. Les objets se sont transformés en une collection de composants dont la disponibilité pouvait être interrogée et injectée lors de l'exécution.

entrez la description de l'image ici

Quelques pros:

  • Était beaucoup plus facile à maintenir dans notre cas que le précédent système de style COM à interface pure. Des surprises imprévues comme un changement dans les exigences ou des plaintes de flux de travail pourraient être prises en compte plus facilement avec une Motionimplémentation très centrale et évidente , par exemple, et non réparties sur une centaine de sous-types.
  • A donné un tout nouveau niveau de flexibilité du type dont nous avions réellement besoin. Dans notre système précédent, puisque l'héritage modélise une relation statique, nous ne pouvions définir efficacement de nouvelles entités qu'au moment de la compilation en C ++. Nous ne pouvions pas le faire à partir du langage de script, par exemple avec l'approche de composition, nous pouvions enchaîner de nouvelles entités à la volée au moment de l'exécution en y attachant simplement des composants et en les ajoutant à une liste. Une «entité» transformée en une toile vierge sur laquelle nous pourrions simplement assembler un collage de tout ce dont nous avions besoin à la volée, avec des systèmes pertinents reconnaissant et traitant automatiquement ces entités en conséquence.

Quelques inconvénients:

  • Nous éprouvions toujours des difficultés dans le département de l'efficacité et la maintenabilité dans les domaines critiques pour la performance. Chaque système finirait toujours par vouloir mettre en cache les composants des entités qui fournissaient ces comportements pour éviter de les parcourir tous à plusieurs reprises et de vérifier ce qui était disponible. Chaque système exigeant des performances le ferait très légèrement différemment, et était sujet à un ensemble différent de bogues en échouant à mettre à jour cette liste mise en cache et éventuellement une structure de données (si une certaine forme de recherche était impliquée comme l'abattage tronconique ou le raytracing) sur certains événement de changement de scène obscur, p.ex.
  • Il y avait encore quelque chose de maladroit et de complexe sur lequel je ne pouvais pas mettre le doigt sur tous ces petits objets comportementaux et simples. Nous avons encore généré beaucoup d'événements pour gérer les interactions entre ces objets "comportementaux" qui étaient parfois nécessaires, et le résultat a été un code très décentralisé. Chaque petit objet était facile à tester pour l'exactitude et, pris individuellement, était souvent parfaitement correct. Pourtant, il nous semblait que nous essayions de maintenir un écosystème massif composé de petits villages et d'essayer de raisonner sur ce qu'ils font tous individuellement et de faire ensemble. La base de code des années 80 de style C ressemblait à une mégalopole épique et surpeuplée qui était définitivement un cauchemar de maintenance,
  • Perte de flexibilité avec le manque d'abstraction mais dans un domaine où nous n'en avons jamais réellement rencontré un réel besoin, donc pas vraiment un inconvénient pratique (mais certainement au moins théorique).
  • La préservation de la compatibilité ABI a toujours été difficile, ce qui l'a rendu plus difficile en exigeant des données stables et pas seulement une interface stable associée à un "comportement". Cependant, nous pourrions facilement ajouter de nouveaux comportements et simplement déprécier les comportements existants si un changement d'état était nécessaire, et c'était sans doute plus facile que de faire des backflips sous les interfaces au niveau du sous-type pour gérer les problèmes de version.

Un phénomène qui s'est produit est que, puisque nous avons perdu l'abstraction sur ces composants comportementaux, nous en avons eu plus. Par exemple, au lieu d'un IRenderablecomposant abstrait , nous attacherions un objet avec un béton Meshou un PointSpritescomposant. Le système de rendu saurait comment effectuer le rendu Meshet les PointSpritescomposants et trouverait les entités qui fournissent ces composants et les dessinent. À d'autres moments, nous avions divers rendus tels SceneLabelque nous avons découvert que nous avions besoin avec le recul, et nous avons donc attaché un SceneLabeldans ces cas à des entités pertinentes (éventuellement en plus d'un Mesh). L'implémentation du système de rendu serait ensuite mise à jour pour savoir comment rendre les entités qui les fournissaient, et c'était une modification assez facile à effectuer.

Dans ce cas, une entité composée de composants pourrait également être utilisée comme composant d'une autre entité. Nous construirions les choses de cette façon en connectant des blocs lego.

ECS: Systèmes et composants de données brutes

Ce dernier système était pour autant que je l'ai fait moi-même, et nous le bâtissions encore avec COM. J'avais l'impression de vouloir devenir un système à composants d'entité, mais je ne le connaissais pas à l'époque. Je regardais autour d'exemples de style COM qui saturaient mon domaine, alors que j'aurais dû regarder les moteurs de jeu AAA pour une inspiration architecturale. J'ai finalement commencé à faire ça.

Ce qui me manquait, c'était plusieurs idées clés:

  1. La formalisation des "systèmes" pour traiter les "composants".
  2. Les «composants» sont des données brutes plutôt que des objets comportementaux composés ensemble en un objet plus grand.
  3. Les entités comme rien de plus qu'un ID strict associé à une collection de composants.

J'ai finalement quitté cette entreprise et commencé à travailler sur un ECS en tant qu'indy (toujours en travaillant dessus tout en drainant mes économies), et c'est de loin le système le plus facile à gérer.

Ce que j'ai remarqué avec l'approche ECS, c'est qu'elle a résolu les problèmes avec lesquels je luttais encore ci-dessus. Plus important pour moi, c'était comme si nous gérions des «villes» de taille saine au lieu de petits villages avec des interactions complexes. Ce n'était pas aussi difficile à entretenir qu'une "mégalopole" monolithique, trop grande dans sa population pour être gérée efficacement, mais n'était pas aussi chaotique qu'un monde rempli de minuscules petits villages interagissant les uns avec les autres où il suffit de penser aux routes commerciales entre eux formait un graphique cauchemardesque. ECS a distillé toute la complexité vers des "systèmes" volumineux, comme un système de rendu, une "ville" de taille saine mais pas une "mégalopole surpeuplée".

Les composants devenant des données brutes me semblaient vraiment étranges au début, car cela brise même le principe de masquage des informations de base de la POO. C'était une sorte de remise en question de l'une des plus grandes valeurs que je chérissais au sujet de la POO, qui était sa capacité à maintenir les invariants qui nécessitaient l'encapsulation et la dissimulation d'informations. Mais cela a commencé à devenir un problème, car il est rapidement devenu évident ce qui se passait avec une douzaine de systèmes larges transformant ces données au lieu d'une telle logique dispersée entre des centaines et des milliers de sous-types mettant en œuvre une combinaison d'interfaces. J'ai tendance à y penser comme toujours dans un style OOP, sauf si les systèmes fournissent la fonctionnalité et la mise en œuvre qui accèdent aux données, les composants fournissent les données et les entités fournissent des composants.

Il est devenu encore plus facile , contre-intuitif, de raisonner sur les effets secondaires causés par le système alors qu'il n'y avait qu'une poignée de systèmes volumineux transformant les données en larges passes. Le système est devenu beaucoup plus plat, mes piles d'appels sont devenues moins profondes que jamais pour chaque thread. Je pourrais penser au système à ce niveau de surveillant et ne pas rencontrer de surprises étranges.

De même, il a simplifié même les domaines critiques en termes de performances en ce qui concerne l'élimination de ces requêtes. L'idée de "système" étant devenue très formalisée, un système pouvait souscrire aux composants qui l'intéressaient et se contenter de lui remettre une liste en cache d'entités répondant à ces critères. Chaque individu n'a pas eu à gérer cette optimisation de la mise en cache, elle est devenue centralisée dans un seul endroit.

Quelques pros:

  • Semble juste résoudre presque tous les problèmes architecturaux majeurs que je rencontrais dans ma carrière sans jamais me sentir pris au piège dans un coin de conception face à des besoins imprévus.

Quelques inconvénients:

  • J'ai parfois du mal à m'en occuper parfois, et ce n'est pas le paradigme le plus mature ou le mieux établi, même dans l'industrie du jeu vidéo, où les gens discutent exactement de ce que cela signifie et comment faire les choses. Ce n'est certainement pas quelque chose que j'aurais pu faire avec l'ancienne équipe avec laquelle je travaillais, qui était composée de membres profondément attachés à la mentalité de style COM ou à la mentalité de style C des années 1980 de la base de code d'origine. Là où je suis parfois confus, c'est comme la façon de modéliser les relations de style graphique entre les composants, mais j'ai toujours trouvé une solution qui ne s'est pas avérée horrible plus tard, où je peux simplement rendre un composant dépendant d'un autre ("cette motion dépend de cet autre en tant que parent et le système utilisera la mémorisation pour éviter de répéter les mêmes calculs de mouvement récursifs ", par exemple)
  • ABI est toujours difficile, mais jusqu'à présent, j'ose même dire que c'est plus facile qu'une approche d'interface pure. C'est un changement de mentalité: la stabilité des données devient le seul objectif d'ABI, plutôt que la stabilité de l'interface, et à certains égards, il est plus facile d'atteindre la stabilité des données que la stabilité de l'interface (ex: pas de tentation de changer une fonction simplement parce qu'elle a besoin d'un nouveau paramètre. Ce genre de choses se produit dans les implémentations de systèmes grossiers qui ne cassent pas ABI).

entrez la description de l'image ici

Cependant, est-il raisonnable de créer également des applications en utilisant l'architecture Component-Entity-System commune aux moteurs de jeu?

Donc, de toute façon, je dirais absolument "oui", avec mon exemple VFX personnel étant un bon candidat. Mais cela reste assez similaire aux besoins du jeu.

Je ne l'ai pas mis en pratique dans des zones plus éloignées complètement déconnectées des préoccupations des moteurs de jeu (les effets visuels sont assez similaires), mais il me semble que beaucoup plus de zones sont de bons candidats pour une approche ECS. Peut-être même qu'un système GUI conviendrait à un, mais j'utilise toujours une approche plus OOP là-bas (mais sans héritage profond contrairement à Qt, par exemple).

C'est un territoire largement inexploré, mais il me semble approprié chaque fois que vos entités peuvent être composées d'une riche combinaison de «traits» (et exactement quel combo de traits qu'ils fournissent étant toujours sujet à changement), et où vous avez une poignée de généralisés systèmes qui traitent des entités qui ont les traits nécessaires.

Il devient une alternative très pratique dans ces cas à tout scénario où vous pourriez être tenté d'utiliser quelque chose comme l'héritage multiple ou une émulation du concept (mixins, par exemple) uniquement pour produire des centaines ou plus de combos dans une hiérarchie d'héritage profonde ou des centaines de combos de classes dans une hiérarchie plate implémentant un combo spécifique d'interfaces, mais où vos systèmes sont peu nombreux (des dizaines, par exemple).

Dans ces cas, la complexité de la base de code commence à se sentir plus proportionnelle au nombre de systèmes au lieu du nombre de combinaisons de types, car chaque type n'est plus qu'une entité composant des composants qui ne sont rien de plus que des données brutes. Les systèmes GUI s'adaptent naturellement à ces types de spécifications où ils peuvent avoir des centaines de types de widgets possibles combinés à partir d'autres types de base ou interfaces, mais seulement une poignée de systèmes pour les traiter (système de disposition, système de rendu, etc.). Si un système GUI utilisait ECS, il serait probablement beaucoup plus facile de raisonner sur l'exactitude du système lorsque toutes les fonctionnalités sont fournies par une poignée de ces systèmes au lieu de centaines de types d'objets différents avec des interfaces héritées ou des classes de base. Si un système GUI utilisait ECS, les widgets n'auraient aucune fonctionnalité, seulement des données. Seule la poignée de systèmes qui traitent des entités de widget aurait une fonctionnalité. La façon dont les événements remplaçables pour un widget seraient gérés me dépasse, mais sur la base de mon expérience limitée jusqu'à présent, je n'ai pas trouvé de cas où ce type de logique ne pourrait pas être transféré de manière centralisée vers un système donné d'une manière qui, dans avec le recul, a produit une solution beaucoup plus élégante que je ne m'attendais jamais.

J'adorerais le voir employé dans plus de domaines, car il m'a sauvé la vie. Bien sûr, cela ne convient pas si votre conception ne se décompose pas de cette façon, des entités agrégeant des composants aux systèmes grossiers qui traitent ces composants, mais s'ils correspondent naturellement à ce type de modèle, c'est la chose la plus merveilleuse que j'ai jamais rencontrée. .

Thomas Owens
la source
1) Qu'a fait votre exemple de programme VFX du point de vue d'un utilisateur? 2) Sur quel projet ECS travaillez-vous actuellement? ♥ Merci d'avoir écrit ceci! ♥
chiot
1
Explication très approfondie - merci. Je sens que j'arrive à plusieurs des mêmes conclusions que vous en ce qui concerne la façon dont ECS est applicable au-delà des jeux; dans mon cas, des interfaces graphiques particulièrement complexes. Au début, cela semble vraiment bizarre d'aller à contre-courant de ce qui est habituellement fait (les hiérarchies d'héritage profondes sont particulièrement importantes dans les cadres d'interface utilisateur), mais il est réconfortant de voir d'autres qui trouvent cette approche plus efficace.
Danny Yaroslavski
1
Merci pour cet article excellent! Pour une interface graphique basée sur les composants, je recommanderais de regarder l'UGUI d'Unity3d. Il est incroyablement flexible et extensible par rapport à ceux basés sur l'héritage comme CocoaTouch.
Ivan Mir
16

L'architecture Component-Entity-System pour les moteurs de jeu fonctionne pour les jeux en raison de la nature du logiciel de jeu, de ses caractéristiques uniques et de ses exigences de qualité. Par exemple, les entités fournissent un moyen uniforme d'adresser et de travailler avec les éléments du jeu, qui peuvent être radicalement différents dans leur objectif et leur utilisation, mais qui doivent être rendus, mis à jour ou sérialisés / désérialisés par le système de manière uniforme. En incorporant un modèle de composant dans cette architecture, vous leur permettez de conserver une structure de base simple, tout en ajoutant plus de fonctionnalités et de fonctionnalités selon les besoins, avec un faible couplage de code. Il existe un certain nombre de systèmes logiciels différents qui pourraient bénéficier des caractéristiques de cette conception, tels que les applications de CAO, les codecs A / V,

TL; DR - Les modèles de conception ne fonctionnent bien que lorsque le domaine problématique est suffisamment adapté aux fonctionnalités et aux inconvénients qu'ils imposent à la conception.

Shotgun Ninja
la source
8

Si le domaine problématique lui convient bien, certainement.

Mon travail actuel implique une application qui doit prendre en charge une variété de capacités en fonction d'un tas de facteurs d'exécution. L'utilisation d'entités basées sur des composants pour découpler toutes ces capacités et permettre l'extensibilité et la testabilité de manière isolée a été idyllique pour nous.

edit: Mon travail consiste à fournir une connectivité à du matériel propriétaire (en C #). Selon le facteur de forme du matériel, le micrologiciel installé, le niveau de service que le client a acheté, etc., etc., nous devons fournir différents niveaux de fonctionnalités à l'appareil. Même certaines fonctionnalités qui ont la même interface ont des implémentations différentes selon la version de l'appareil.

Les bases de code précédentes ici ont eu des interfaces très larges avec beaucoup non implémentées. Certains ont eu de nombreuses interfaces minces qui ont ensuite été composées statiquement dans une classe bestiale. Certains ont simplement utilisé des dictionnaires string -> string pour le modéliser. (nous avons de nombreux départements qui pensent tous pouvoir faire mieux)

Tout cela a ses défauts. Les interfaces larges sont une douleur et demie pour se moquer / tester efficacement. Ajouter de nouvelles fonctionnalités signifie changer l'interface publique (et toutes les implémentations existantes). De nombreuses interfaces minces ont conduit à un code très moche, mais depuis que nous avons fini par contourner un gros objet gras, les tests ont encore souffert. De plus, les interfaces fines ne géraient pas bien leurs dépendances. Les dictionnaires de chaînes ont les problèmes habituels d'analyse et d'existence ainsi que les trous infernaux de performance, de lisibilité et de maintenabilité.

Ce que nous utilisons maintenant est une entité très mince dont les composants ont été découverts et composés en fonction des informations d'exécution. Les dépendances sont effectuées de manière déclarative et auto-résolues par le framework de composants de base. Les composants eux-mêmes peuvent être testés de manière isolée car ils fonctionnent directement avec leurs dépendances et les problèmes avec les dépendances manquantes sont détectés tôt - et dans un seul endroit plutôt que lors de la première utilisation de la dépendance. De nouveaux composants (ou tests) peuvent être déposés et aucun code existant n'est affecté par celui-ci. Les consommateurs demandent à l'entité une interface avec le composant, nous sommes donc libres de voir avec les différentes implémentations (et comment les implémentations sont mappées aux données d'exécution) avec une relative liberté.

Pour une situation comme celle-ci où la composition de l'objet et ses interfaces peuvent inclure un sous-ensemble (très varié) de composants communs, cela fonctionne très bien.

Telastyn
la source
1
En supposant que vous y êtes autorisé, pouvez-vous fournir plus de détails sur votre travail actuel? Je suis curieux de savoir en quoi le CES a été idyllique pour ce que vous construisez.
Andrew De Andrade
Y a-t-il un article, un article ou un blog sur votre expérience? j'aimerais aussi avoir plus de détails techniques à ce sujet :)
user1778770
@ user1778770 - non accessible au public, non. Quel genre de questions aviez-vous?
Telastyn
Eh bien, commençons par quelque chose de simple, votre concept couvre-t-il toute la pile des applications (par exemple, de l'entreprise à l'interface)? ou seulement une seule couche d'un cas d'utilisation unique?
user1778770
@ user1778770 - dans mon implémentation, les entités / composants existent dans une couche. Différentes entités peuvent exister dans différentes couches, mais elles ne sont souvent pas 1: 1 (sinon les couches n'offrent aucun avantage).
Telastyn