Bonjour à tous!
Le quatrième volet
«Développeur C ++» commence ici, l'un des cours les plus actifs dans notre pays, à en juger par les réunions réelles, où non seulement les «croisés» viennent parler avec
Dima Shebordaev :) En général, le cours est déjà devenu l'un des plus importants de notre pays, il est resté inchangé que
Dima organise des cours ouverts et nous sélectionnons des matériaux intéressants avant le début du cours.
C'est parti!
Entrée
Entity Component System (ECS, «entity-component-system») est maintenant au sommet de la popularité en tant qu'alternative architecturale qui met l'accent sur le principe de la composition plutôt que sur l'héritage. Dans cet article, je n'entrerai pas dans les détails du concept, car il y a déjà suffisamment de ressources sur ce sujet. Il existe de nombreuses façons de mettre en œuvre ECS, mais, le plus souvent, j'en choisis des plutôt complexes qui peuvent dérouter les débutants et prendre beaucoup de temps.
Dans cet article, je décrirai un moyen très simple de mettre en œuvre ECS, dont la version fonctionnelle ne nécessite presque pas de code, mais suit pleinement le concept.

ECS
En parlant d'ECS, les gens signifient souvent des choses différentes. Quand je parle d'ECS, je veux dire un système qui vous permet de définir des entités qui ont zéro ou plusieurs composants de données purs. Ces composants sont traités sélectivement par des systèmes logiques purs. Par exemple, la position, la vitesse, la hitbox et l'intégrité d'un composant sont liées à l'entité E. Ils stockent simplement des données en eux-mêmes. Par exemple, un composant d'intégrité peut stocker deux entiers: un pour l'intégrité actuelle et un pour le maximum. Un système peut être un système de régénération de santé qui trouve toutes les instances d'un composant de santé et les augmente de 1 toutes les 120 images.
Implémentation C ++ typique
Il existe de nombreuses bibliothèques proposant des implémentations ECS. Habituellement, ils incluent un ou plusieurs éléments de la liste:
- Héritage du composant / système de base de la classe
GravitySystem : public ecs::System
; - Utilisation active des modèles;
- À la fois cela, et un autre dans certains look CRTP ;
- La classe
EntityManager
, qui contrôle la création / stockage d'entités de manière implicite.
Quelques exemples rapides de Google:
Toutes ces méthodes ont droit à la vie, mais elles présentent certains inconvénients. La façon dont ils traitent les données de manière opaque signifie qu'il sera difficile de comprendre ce qui se passe à l'intérieur et si le ralentissement des performances s'est produit. Cela signifie également que vous devez étudier toute la couche d'abstraction et vous assurer qu'elle correspond bien au code existant. N'oubliez pas les bogues cachés, qui sont probablement beaucoup cachés dans la quantité de code que vous devez déboguer.
Une approche basée sur un modèle peut considérablement affecter le temps de compilation et la fréquence à laquelle vous devrez reconstruire la build. Alors que les concepts basés sur l'héritage peuvent dégrader les performances.
La raison principale pour laquelle je pense que ces approches sont excessives est que le problème qu'elles résolvent est trop simple. En fin de compte, ce ne sont que des composants de données supplémentaires associés à l'entité et à leur traitement sélectif. Ci-dessous, je vais montrer un moyen très simple de mettre en œuvre cela.
Mon approche simple
EssenceDans certaines approches, la classe Entity est définie, dans d'autres, elles fonctionnent avec des entités comme ID / handle. Dans une approche par composants, une entité n'est rien d'autre que les composants qui lui sont associés, et pour cela une classe n'est pas nécessaire. Une entité existera explicitement en fonction de ses composants associés. Pour ce faire, définissez:
using EntityID = int64_t;
Composants d'entitéLes composants sont différents types de données associés à des entités existantes. On peut dire que pour chaque entité e, e aura zéro et plus de types de composants accessibles. En substance, il s'agit d'une relation clé-valeur éclatée et, heureusement, il existe des outils de bibliothèque standard sous forme de cartes pour cela.
Donc, je définis les composants comme suit:
struct Position { float x; float y; }; struct Velocity { float x; float y; }; struct Health { int max; int current; }; template <typename Type> using ComponentMap = std::unordered_map<EntityID, Type>; using Positions = ComponentMap<Position>; using Velocities = ComponentMap<Velocity>; using Healths = ComponentMap<Health>; struct Components { Positions positions; Velocities velocities; Healths healths; };
Cela suffit pour indiquer les entités via les composants, comme prévu par ECS. Par exemple, pour créer une entité avec une position et une santé, mais sans vitesse, vous avez besoin de:
Pour détruire une entité avec un ID donné, il suffit de l'
.erase()
de chaque carte.
Les systèmesLe dernier composant dont nous avons besoin est les systèmes. Il s'agit de la logique qui fonctionne avec les composants pour obtenir un comportement spécifique. Comme j'aime simplifier les choses, j'utilise des fonctions normales. Le système de régénération de santé mentionné ci-dessus peut simplement être la fonction suivante.
void updateHealthRegeneration(int64_t currentFrame, Healths& healths) { if(currentFrame % 120 == 0) { for(auto& [id, health] : healths) { if(health.current < health.max) ++health.current; } } }
Nous pouvons placer l'appel à cette fonction à un endroit approprié dans la boucle principale et le transférer vers le stockage du composant de santé. Étant donné que le référentiel d'intégrité contient uniquement des enregistrements pour les entités qui ont l'intégrité, il peut les traiter de manière isolée. Cela signifie également que la fonction ne prend que les données nécessaires et ne touche pas à l'inutile.
Mais que faire si le système fonctionne avec plusieurs composants? Dites un système physique qui change de position en fonction de la vitesse. Pour ce faire, nous devons croiser toutes les clés de tous les types de composants impliqués et itérer sur leurs valeurs. À ce stade, la bibliothèque standard ne suffit plus, mais l'écriture des assistants n'est pas si difficile. Par exemple:
void updatePhysics(Positions& positions, const Velocities& velocities) {
Ou vous pouvez écrire une aide plus compacte qui permet un accès plus efficace via l'itération au lieu de la recherche.
void updatePhysics(Positions& positions, const Velocities& velocities) {
Ainsi, nous nous sommes familiarisés avec les fonctionnalités de base d'un ECS standard.
Les avantages
Cette approche est très efficace, car elle est construite à partir de zéro sans restreindre l'abstraction. Vous n'avez pas besoin d'intégrer des bibliothèques externes ou d'adapter la base de code aux idées prédéfinies de ce que devraient être les entités / composants / systèmes.
Et puisque cette approche est complètement transparente, sur sa base, vous pouvez créer des utilitaires et des assistants. Cette implémentation évolue avec les besoins de votre projet. Très probablement, pour des prototypes ou des jeux simples pour le jeu jam'ov, vous aurez assez des fonctionnalités décrites ci-dessus.
Ainsi, si vous êtes nouveau dans tout ce domaine ECS, une approche aussi simple vous aidera à comprendre les idées principales.
Limitations
Mais, comme pour toute autre méthode, il existe certaines limitations. D'après mon expérience, c'est précisément une telle implémentation utilisant
unordered_map
dans tout jeu non trivial qui entraînera des problèmes de performances.
L'itération des intersections clés sur plusieurs instances de
unordered_map
avec plusieurs entités n'est pas bien mise à l'échelle car vous effectuez en fait des recherches
N*M
, où N est le nombre de composants qui se chevauchent, M est le nombre d'entités correspondantes et
unordered_map
pas très bon pour la mise en cache. Ce problème peut être résolu en utilisant un magasin de valeurs-clés plus approprié pour l'itération au lieu de
unordered_map
.
Une autre limitation est le passe-partout. Selon ce que vous faites, l'identification de nouveaux composants peut devenir fastidieuse. Vous devrez peut-être ajouter une annonce non seulement dans la structure des composants, mais également dans la fonction d'apparition, la sérialisation, l'utilitaire de débogage, etc. Je suis tombé sur ce problème moi-même et j'ai résolu le problème en générant du code - j'ai défini des composants dans des fichiers json externes, puis j'ai généré des composants C ++ et des fonctions d'assistance au stade de la construction. Je suis sûr que vous pouvez trouver d'autres méthodes basées sur des modèles pour résoudre les problèmes que vous rencontrez.
LA FIN
Si vous avez des questions et des commentaires, vous pouvez les laisser ici ou aller à
une leçon ouverte avec
Dima , l'écouter et demander déjà autour.