Organiser un système d'entités avec des gestionnaires de composants externes?

13

Je conçois un moteur de jeu pour un jeu de tir 2D multijoueur de haut en bas, que je veux être raisonnablement réutilisable pour d'autres jeux de tir de haut en bas. En ce moment, je réfléchis à la façon de concevoir quelque chose comme un système d'entités. J'ai d'abord pensé à ceci:

J'ai une classe appelée EntityManager. Il devrait implémenter une méthode appelée Update et une autre appelée Draw. La raison pour laquelle je sépare Logic et Rendering est qu'alors je peux omettre la méthode Draw si j'exécute un serveur autonome.

EntityManager possède une liste d'objets de type BaseEntity. Chaque entité possède une liste de composants tels que EntityModel (la représentation graphique d'une entité), EntityNetworkInterface et EntityPhysicalBody.

EntityManager possède également une liste de gestionnaires de composants tels que EntityRenderManager, EntityNetworkManager et EntityPhysicsManager. Chaque gestionnaire de composants conserve des références aux composants d'entité. Il existe plusieurs raisons pour déplacer ce code hors de la propre classe de l'entité et le faire collectivement à la place. Par exemple, j'utilise une bibliothèque de physique externe, Box2D, pour le jeu. Dans Box2D, vous ajoutez d'abord les corps et les formes à un monde (appartenant à EntityPhysicsManager dans ce cas) et ajoutez des rappels de collision (qui seraient envoyés à l'objet entité lui-même dans mon système). Ensuite, vous exécutez une fonction qui simule tout dans le système. J'ai du mal à trouver une meilleure solution pour ce faire que de le faire dans un gestionnaire de composants externe comme celui-ci.

La création d'entité se fait comme ceci: EntityManager implémente la méthode RegisterEntity (entityClass, fabrique) qui enregistre comment créer une entité de cette classe. Il implémente également la méthode CreateEntity (entityClass) qui retournerait un objet de type BaseEntity.

Voici maintenant mon problème: comment la référence à un composant serait-elle enregistrée auprès des gestionnaires de composants? Je ne sais pas comment je référencerais les gestionnaires de composants d'une usine / fermeture.

Gustav
la source
Je ne sais pas si cela est censé être un système hybride, mais il semble que vos «gestionnaires» soient ce que j'ai généralement entendu appeler des «systèmes»; c'est-à-dire que l'entité est un ID abstrait; un composant est un pool de données; et ce que vous appelez un "gestionnaire" est ce que l'on appelle généralement un "système". Suis-je en train d'interpréter correctement le vocabulaire?
BRPocock
Ma question ici peut être intéressante: Composants du jeu, gestionnaires de jeu et propriétés des objets
George Duckett
Jetez un œil sur gamadu.com/artemis et voyez si leurs méthodes répondent à votre question.
Patrick Hughes
1
Il n'y a pas qu'une seule façon de concevoir un système d'entités car il y a peu de consensus sur sa définition. Ce que @BRPocock décrit et ce qu'Artemis utilise est décrit plus en détail sur ce blog: t-machine.org/index.php/category/entity-systems avec un wiki: entity-systems.wikidot.com
user8363

Réponses:

6

Les systèmes doivent stocker une paire clé-valeur Entité-Composant dans une sorte de carte, d'objet de dictionnaire ou de tableau associatif (selon la langue utilisée). En outre, lorsque vous créez votre objet entité, je ne me soucierais pas de le stocker dans un gestionnaire, sauf si vous devez être en mesure de le désinscrire de l'un des systèmes. Une entité est un composite de composants, mais elle ne doit gérer aucune des mises à jour des composants. Cela devrait être géré par les systèmes. Traitez plutôt votre entité comme une clé mappée à tous les composants qu'elle contient dans les systèmes, ainsi que comme un hub de communication permettant à ces composants de communiquer entre eux.

La grande partie des modèles Entity-Component-System est que vous pouvez implémenter la possibilité de passer des messages d'un composant au reste des composants d'une entité assez facilement. Cela permet à un composant de parler à un autre composant sans vraiment savoir qui est ce composant ni comment gérer le composant qu'il change. Au lieu de cela, il transmet un message et laisse le composant se changer lui-même (s'il existe)

Par exemple, un système de positionnement ne contiendrait pas beaucoup de code, ne faisant que suivre les objets d'entité mappés à leurs composants de position. Mais lorsqu'une position change, ils peuvent envoyer un message à l'entité impliquée, qui à son tour est transmis à tous les composants de cette entité. Une position change pour quelle raison que ce soit? Le système de position envoie à l'entité un message disant que la position a changé, et quelque part, le composant de rendu d'image de cette entité reçoit ce message et se met à jour où il se dessinera ensuite.

Inversement, un système de physique doit savoir ce que font tous ses objets; Il doit pouvoir voir tous les objets du monde pour tester les collisions. Lorsqu'une collision se produit, il met à jour le composant de direction de l'entité en envoyant une sorte de "message de changement de direction" à l'entité au lieu de se référer directement au composant de l'entité. Cela dissocie le gestionnaire d'avoir besoin de savoir comment changer de direction en utilisant un message au lieu de compter sur un composant spécifique (ce qui peut ne pas être là du tout, auquel cas le message tomberait dans l'oreille d'un sourd au lieu d'une erreur). survenant parce qu’un objet attendu était absent).

Vous remarquerez un énorme avantage car vous avez mentionné que vous avez une interface réseau. Un composant réseau écouterait tous les messages reçus que tout le monde devrait connaître. Il aime les potins. Ensuite, lorsque le système réseau est mis à jour, les composants réseau envoient ces messages à d'autres systèmes réseau sur d'autres ordinateurs clients, qui renvoient ensuite ces messages à tous les autres composants pour mettre à jour les positions des joueurs, etc. Une logique spéciale peut être nécessaire pour que seules certaines entités puissent envoyer des messages sur le réseau mais c'est la beauté du système, vous pouvez simplement lui faire contrôler cette logique en lui enregistrant les bonnes choses.

En bref:

L'entité est une composition de composants pouvant recevoir des messages. L'entité peut recevoir des messages, en déléguant ces messages à tous leurs composants pour les mettre à jour. (Message de position modifiée, Direction de changement de vitesse, etc.) C'est comme une boîte aux lettres centrale que tous les composants peuvent entendre les uns des autres au lieu de se parler directement.

Le composant est une petite partie d'une entité qui stocke un état de l'entité. Ceux-ci sont capables d'analyser certains messages et de jeter d'autres messages. Par exemple, un "composant de direction" ne se soucierait que des "messages de changement de direction" mais pas des "messages de changement de position". Les composants mettent à jour leur propre état en fonction des messages, puis mettent à jour les états des autres composants en envoyant des messages à partir de leur système.

Le système gère tous les composants d'un certain type et est responsable de la mise à jour desdits composants à chaque trame, ainsi que de l'envoi des messages des composants qu'ils gèrent aux entités auxquelles les composants appartiennent.

Les systèmes pourraient être en mesure de mettre à jour tous leurs composants en parallèle et de stocker tous les messages au fur et à mesure. Ensuite, lorsque l'exécution de toutes les méthodes de mise à jour des systèmes est terminée, vous demandez à chaque système d'envoyer ses messages dans un ordre spécifique. Contrôle d'abord possible, suivi de physique, suivi de direction, position, rendu, etc. Il importe dans quel ordre ils sont envoyés car un changement de direction physique doit toujours peser un changement de direction basé sur le contrôle.

J'espère que cela t'aides. C'est un enfer d'un modèle de conception, mais il est ridiculement puissant s'il est bien fait.

C0M37
la source
0

J'utilise un système similaire dans mon moteur et la façon dont je l'ai fait est que chaque entité contient une liste de composants. Depuis EntityManager, je peux interroger chacune des entités et voir celles qui contiennent un composant donné. Exemple:

class Component
{
    private uint ID;
    // etc...
}

class Entity
{
    List<Component> Components;
    // etc...
    public bool Contains(Type type)
    {
        foreach(Component comp in Components)
        {
            if(typeof(comp) == type)
                return true;
        }
        return false;
    }
}

class EntityManager
{
    List<Entity> Entities;
    // etc...
    public List<Entity> GetEntitiesOfType(Type type)
    {
        List<Entity> results = new List<Entity>();
        foreach(Entity entity in Entities)
        {
            if(entity.Contains(type))
                results.Add(entity);
        }
        return results;
    }
}

Évidemment, ce n'est pas le code exact (vous auriez en fait besoin d'une fonction de modèle pour vérifier les différents types de composants, plutôt que d'utiliser typeof), mais le concept est là. Ensuite, vous pouvez prendre ces résultats, référencer le composant que vous recherchez et l'enregistrer auprès de votre usine. Cela empêche tout couplage direct entre les composants et leurs gestionnaires.

Mike Cluck
la source
3
Caveat emptor: au point où votre entité contient des données, c'est un objet, pas une entité… On perd la plupart des avantages paralysants (sic?) D'ECS dans cette structure. Les systèmes E / C / S «purs» sont relationnels, pas orientés objet… Non pas que ce soit nécessairement «mauvais» pour certains cas, mais c'est certainement «casser le modèle relationnel»
BRPocock
2
Je ne suis pas sûr de te comprendre. Ma compréhension (et corrigez-moi si je me trompe) est que le système Entity-Component-System de base a une classe Entity qui abrite des composants et peut avoir un ID, un nom ou un identifiant. Je pense que nous pouvons avoir un malentendu dans ce que j'entends par «type» d'entité. Quand je dis Entité "type", je fais référence aux types de Composant. C'est-à-dire qu'une entité est de type "Sprite" si elle contient un composant Sprite.
Mike Cluck
1
Dans un système Entité / Composant pur, une Entité est généralement atomique: par exemple typedef long long int Entity; un composant est un enregistrement (il peut être implémenté en tant que classe d'objets, ou simplement a struct) qui a une référence à l'entité à laquelle il est attaché; et un système serait une méthode ou un ensemble de méthodes. Le modèle ECS n'est pas très compatible avec le modèle OOP, bien qu'un composant puisse être un objet (principalement) de données uniquement, et un système un objet singleton uniquement de code dont l'état vit dans des composants ... bien que les systèmes "hybrides" soient plus courants que les «purs», ils perdent de nombreux avantages innés.
BRPocock
2
@BRPocock concernant les systèmes d'entités «purs». Je pense qu'une entité en tant qu'objet est parfaitement bien, il ne doit pas être un simple identifiant. Une chose est la représentation sérialisée, une autre la disposition en mémoire d'un objet / d'un concept / d'une entité. Tant que vous pouvez maintenir la conduite des données, il ne faut pas être lié au code non idiomatique simplement parce que c'est la voie "pure".
Raine
1
@BRPocock c'est un avertissement, mais pour les systèmes d'entités de type "t-machine". Je comprends pourquoi, mais ce ne sont pas les seuls moyens de modéliser des entités basées sur des composants. les acteurs sont une alternative intéressante. J'ai tendance à les apprécier davantage, surtout pour les entités purement logiques.
Raine
0

1) Votre méthode Factory doit recevoir une référence à l'EntityManager qui l'a appelée (je vais utiliser C # comme exemple):

delegate BaseEntity EntityFactory(EntityManager manager);

2) Demandez à CreateEntity de recevoir également un identifiant (par exemple une chaîne, un entier, c'est à vous) en plus de la classe / type de l'entité, et enregistrez automatiquement l'entité créée dans un dictionnaire en utilisant cet identifiant comme clé:

class EntityManager
{
    // Rest of class omitted

    BaseEntity CreateEntity(string id, Type entityClass)
    {
        BaseEntity entity = factories[entityClass](this);
        registry.Add(id, entity);
        return entity;
    }

    Dictionary<Id, BaseEntity> registry;
}

3) Ajoutez un Getter à EntityManager pour obtenir n'importe quelle entité par ID:

class EntityManager
{
    // Rest of class omitted

    BaseEntity GetEntity(string id)
    {
        return registry[id];
    }
}

Et c'est tout ce dont vous avez besoin pour référencer n'importe quel ComponentManager depuis votre méthode Factory. Par exemple:

BaseEntity CreateSomeSortOfEntity(EntityManager manager)
{
    // Create and configure entity
    BaseEntity entity = new BaseEntity();
    RenderComponent renderComponent = new RenderComponent();
    entity.AddComponent(renderComponent);

    // Get a reference to the render manager and register component
    RenderEntityManager renderer = manager.GetEntity("RenderEntityManager") as RenderEntityManager;
    if(renderer != null)
        renderer.Register(renderComponent)

    return entity;
}

Outre Id, vous pouvez également utiliser une sorte de propriété Type (une énumération personnalisée ou simplement compter sur le système de types du langage) et créer un getter qui renvoie toutes les BaseEntities d'un certain type.

David Gouveia
la source
1
Pas pour être pédant, mais encore une fois… dans un système Entité (relationnel) pur, les entités n'ont pas de type, sauf celui qui leur est donné en vertu de leurs composants…
BRPocock
@BRPocock: Pourriez-vous créer un exemple qui suit la pure vertu?
Zolomon
1
@Raine Peut-être, je n'ai pas d'expérience de première main avec cela, mais c'est ce que j'ai lu. Et il existe des optimisations que vous pouvez implémenter pour réduire le temps passé à rechercher des composants par identifiant. En ce qui concerne la cohérence du cache, je pense que cela a du sens puisque vous stockez des données du même type de manière contiguë en mémoire, en particulier lorsque vos composants sont des propriétés légères ou simples. J'ai lu qu'un seul échec de cache sur la PS3 peut coûter aussi cher que mille instructions CPU, et cette approche de stockage contigu de données de type similaire est une technique d'optimisation très courante dans le développement de jeux modernes.
David Gouveia
2
Dans ref: système d'entité «pur»: l'ID d'entité est généralement quelque chose comme typedef unsigned long long int EntityID;:; l'idéal est que chaque système puisse vivre sur un processeur ou un hôte distinct, et ne nécessite que de récupérer les composants pertinents pour / actifs dans ce système. Avec un objet Entity, il peut être nécessaire d'instancier le même objet Entity sur chaque hôte, ce qui rend la mise à l'échelle plus difficile. Un modèle purement entité-composant-système répartit le traitement entre les nœuds (processus, processeurs ou hôtes) par système, plutôt que par entité, généralement.
BRPocock
1
@DavidGouveia a mentionné «optimisations… recherche d'entités par ID». En fait, les (quelques) systèmes que j'ai mis en œuvre de cette façon ont tendance à ne pas le faire. Plus souvent, sélectionnez les composants selon un modèle indiquant qu'ils présentent un intérêt pour un système particulier, en utilisant des entités (ID) uniquement pour les jointures inter-composants.
BRPocock