Quelles conceptions existe-t-il pour un système d'entités basé sur des composants qui soit convivial mais toujours flexible?

23

Je m'intéresse au système d'entités basé sur les composants depuis un certain temps, et j'ai lu d'innombrables articles dessus (Les jeux Insomiac , le joli standard Evolve Your Hierarchy , la T-Machine , Chronoclast ... pour n'en nommer que quelques-uns).

Ils semblent tous avoir une structure à l'extérieur de quelque chose comme:

Entity e = Entity.Create();
e.AddComponent(RenderComponent, ...);
//do lots of stuff
e.GetComponent<PositionComponent>(...).SetPos(4, 5, 6);

Et si vous apportez l'idée de données partagées (c'est le meilleur design que j'ai vu jusqu'à présent, en termes de ne pas dupliquer les données partout)

e.GetProperty<string>("Name").Value = "blah";

Oui, c'est très efficace. Cependant, ce n'est pas exactement le plus facile à lire ou à écrire; il se sent très maladroit et travaille contre vous.

J'aimerais personnellement faire quelque chose comme:

e.SetPosition(4, 5, 6);
e.Name = "Blah";

Bien que, bien sûr, le seul moyen d'accéder à ce type de conception soit de retour dans le type de hiérarchie Entity-> NPC-> Enemy-> FlyingEnemy-> FlyingEnemyWithAHatOn que cette conception essaie d'éviter.

Quelqu'un a-t-il vu une conception pour ce type de système de composants qui est toujours flexible, tout en conservant un niveau de convivialité? Et d'ailleurs, parvient-il à contourner le stockage de données (probablement le problème le plus difficile) dans le bon sens?

Quelles conceptions existe-t-il pour un système d'entités basé sur des composants qui soit convivial mais toujours flexible?

Le canard communiste
la source

Réponses:

13

Une des choses que fait Unity est de fournir des accesseurs d'assistance sur l'objet de jeu parent pour fournir un accès plus convivial aux composants communs.

Par exemple, vous pouvez avoir votre position stockée dans un composant Transform. En utilisant votre exemple, vous devrez écrire quelque chose comme

e.GetComponent<Transform>().position = new Vector3( whatever );

Mais dans Unity, cela se simplifie pour

e.transform.position = ....;

transformest littéralement juste une méthode auxiliaire simple dans la GameObjectclasse de base ( Entityclasse dans votre cas) qui ne

Transform transform
{ 
    get { return this.GetComponent<Transform>(); }
}

Unity fait également quelques autres choses, comme définir une propriété "Nom" sur l'objet de jeu lui-même plutôt que dans ses composants enfants.

Personnellement, je n'aime pas l'idée de votre conception de données partagées par nom. L' accès aux propriétés par nom plutôt que par une variable et l'utilisateur ayant également besoin de connaître quel type il est juste semble vraiment l' erreur sujette à moi. Ce que fait Unity, c'est qu'ils utilisent des méthodes similaires comme GameObject transformpropriété dans les Componentclasses pour accéder aux composants frères. Ainsi, tout composant arbitraire que vous écrivez peut simplement faire ceci:

var myPos = this.transform.position;

Pour accéder au poste. Où cette transformpropriété fait quelque chose comme ça

Transform transform
{
    get { return this.gameObject.GetComponent<Transform>(); }
}

Bien sûr, c'est un peu plus verbeux que de simplement le dire e.position = whatever, mais on s'y habitue, et ça n'a pas l'air aussi méchant que les propriétés génériques. Et oui, vous devriez le faire de manière détournée pour vos composants clients, mais l'idée est que tous vos composants "moteurs" courants (moteurs de rendu, collisionneurs, sources audio, transformations, etc.) ont les accesseurs faciles.

Tetrad
la source
Je peux voir pourquoi le système de données partagées par nom peut être un problème, mais comment pourrais-je éviter le problème de plusieurs composants ayant besoin de données (c'est-à-dire dans lequel dois-je stocker quelque chose)? Être très prudent au stade de la conception?
The Communist Duck
De plus, en ce qui concerne «le fait que l'utilisateur doit également savoir de quel type il s'agit», ne pourrais-je pas simplement le convertir en propriété.GetType () dans la méthode get ()?
The Communist Duck
1
Quel type de données globales (dans la portée de l'entité) transférez-vous vers ces référentiels de données partagés? Tout type de données d'objet de jeu doit être facilement accessible via vos classes "moteur" (transformation, collisionneurs, moteurs de rendu, etc.). Si vous effectuez des changements de logique de jeu sur la base de ces "données partagées", il est beaucoup plus sûr de posséder une classe et de vous assurer que ce composant se trouve sur cet objet de jeu que de demander aveuglément de voir si une variable nommée existe. Alors oui, vous devez gérer cela dans la phase de conception. Vous pouvez toujours créer un composant qui stocke simplement des données si vous en avez vraiment besoin.
Tetrad
@Tetrad - Pouvez-vous expliquer brièvement comment votre méthode GetComponent <Transform> proposée fonctionnerait? Serait-il en boucle à travers tous les composants de GameObject et retournerait-il le premier composant de type T? Ou est-ce que quelque chose d'autre se passe là-bas?
Michael
@Michael qui fonctionnerait. Vous pouvez simplement faire en sorte que l'appelant ait la responsabilité de mettre cela en cache localement en tant qu'amélioration des performances.
Tetrad
3

Je suggérerais qu'une sorte de classe Interface pour vos objets Entity serait bien. Il pourrait faire la gestion des erreurs avec la vérification pour vous assurer qu'une entité contient également un composant de la valeur appropriée dans un seul emplacement afin que vous n'ayez pas à le faire partout où vous accédez aux valeurs. Alternativement, la plupart des conceptions que j'ai jamais faites avec un système basé sur des composants, je traite directement les composants, en demandant, par exemple, leur composant positionnel, puis en accédant / mettant à jour les propriétés de ce composant directement.

Très basique à un niveau élevé, la classe prend l'entité en question et fournit une interface facile à utiliser pour les composants sous-jacents comme suit:

public class EntityInterface
{
    private Entity entity { get; set };
    public EntityInterface(Entity e)
    {
        entity = e;
    }

    public Vector3 Position
    {
        get
        {
            return entity.GetProperty<Vector3>("Position");
        }
        set
        {
            entity.GetProperty<Vector3>("Position") = value;
        }
    }
}
James
la source
Si je suppose que je devrais simplement gérer NoPositionComponentException ou quelque chose comme ça, quel est l'avantage de cela sur le simple fait de mettre tout cela dans la classe Entity?
The Communist Duck
N'étant pas sûr de ce que contient réellement votre classe d'entité, je ne peux pas dire qu'il y ait un avantage ou non. Personnellement, je préconise des systèmes de composants où il n'y a pas d'entité de base à proprement parler simplement pour éviter la question de «bien ce qui en fait partie». Cependant, je considérerais une classe d'interface comme celle-ci comme un bon argument pour qu'elle existe.
James
Je peux voir comment cela est plus facile (je n'ai pas à interroger la position via GetProperty), mais je ne vois pas l'avantage de cette façon au lieu de la placer dans la classe Entity elle-même.
The Communist Duck
2

En Python, vous pouvez intercepter la partie 'setPosition' de e.SetPosition(4,5,6)via en déclarant une __getattr__fonction sur Entity. Cette fonction peut parcourir les composants et trouver la méthode ou la propriété appropriée et la renvoyer, de sorte que l'appel ou l'affectation de fonction se déroule au bon endroit. Peut-être que C # a un système similaire, mais il pourrait ne pas être possible car il est typé statiquement - il ne peut probablement pas compiler à e.setPositionmoins que e n'ait défini quelque part la position dans l'interface.

Vous pouvez également faire en sorte que toutes vos entités implémentent les interfaces pertinentes pour tous les composants que vous pourriez y ajouter, avec des méthodes de stub qui déclenchent une exception. Ensuite, lorsque vous appelez addComponent, vous redirigez simplement les fonctions de l'entité pour l'interface de ce composant vers le composant. Un peu compliqué cependant.

Mais le plus simple serait peut-être pas surcharger l'opérateur [] de votre classe d' entité pour rechercher les composants attachés à la présence d'une propriété valide et retourner que, donc il peut être attribué à comme ceci: e["Name"] = "blah". Chaque composant devra implémenter son propre GetPropertyByName et l'entité les appellera tour à tour jusqu'à ce qu'elle trouve le composant responsable de la propriété en question.

Kylotan
la source
J'avais pensé à utiliser des indexeurs ... c'est vraiment sympa. J'attendrai un peu pour voir ce qui apparaît d'autre, mais je vire vers un mélange de cela, le GetComponent () habituel, et certaines propriétés comme @James suggérées pour les composants communs.
The Communist Duck
Je manque peut-être ce qui est dit ici, mais cela semble plutôt remplacer le e.GetProperty <type> ("NameOfProperty"). Quoi que ce soit avec e ["NameOfProperty"]. Quoi que ce soit ??
James
@James C'est un wrapper pour cela (un peu comme votre conception) .. sauf qu'il est plus dynamique - il le fait automatiquement et plus propre que la GetPropertyméthode .. Seul inconvénient est la manipulation et la comparaison des chaînes. Mais c'est un point discutable.
The Communist Duck
Ah, mon malentendu là-bas. J'ai supposé que GetProperty faisait partie de l'entité (et ferait ainsi ce qui a été décrit ci-dessus) et non la méthode C #.
James
2

Pour développer la réponse de Kylotan, si vous utilisez C # 4.0, vous pouvez taper statiquement une variable pour qu'elle soit dynamique .

Si vous héritez de System.Dynamic.DynamicObject, vous pouvez remplacer TryGetMember et TrySetMember (parmi les nombreuses méthodes virtuelles) pour intercepter les noms de composants et renvoyer le composant demandé.

dynamic entity = EntityRegistry.Entities[1000];
entity.MyComponent.MyValue = 100;

J'ai écrit un peu sur les systèmes d'entités au fil des ans, mais je suppose que je ne peux pas rivaliser avec les géants. Quelques notes ES (quelque peu dépassées et ne reflètent pas nécessairement ma compréhension actuelle des systèmes d'entités, mais cela vaut la peine d'être lu, à mon humble avis).

Raine
la source
Merci pour cela, cela aidera :) La méthode dynamique n'aurait-elle pas le même type d'effet que le simple cast sur property.GetType ()?
The Communist Duck
Il y aura un cast à un moment donné, car TryGetMember a un objectparamètre out - mais tout cela sera résolu au moment de l'exécution. Je pense que c'est une manière glorifiée de garder une interface fluide sur ie entity.TryGetComponent (compName, out component). Je ne suis pas sûr d'avoir compris la question, cependant :)
Raine
La question est que je pourrais utiliser des types dynamiques partout, ou (AFAICS) renvoyer la propriété (property.GetType ()).
The Communist Duck
1

Je vois beaucoup d'intérêt ici dans ES sur C #. Découvrez mon port de l'excellente mise en œuvre ES Artemis:

https://github.com/thelinuxlich/artemis_CSharp

En outre, un exemple de jeu pour vous familiariser avec son fonctionnement (en utilisant XNA 4): https://github.com/thelinuxlich/starwarrior_CSharp

Les suggestions sont les bienvenues!

thelinuxlich
la source
Cela a certainement l'air bien. Personnellement, je ne suis pas fan de l'idée de tant d'EntityProcessingSystems - dans le code, comment cela fonctionne-t-il en termes d'utilisation?
The Communist Duck
Chaque système est un aspect qui traite ses composants dépendants. Voir ceci pour avoir l'idée: github.com/thelinuxlich/starwarrior_CSharp/tree/master/…
thelinuxlich
0

J'ai décidé d'utiliser l'excellent système de réflexion de C #. Je l'ai basé sur le système de composants général, plus un mélange des réponses ici.

Les composants sont ajoutés très facilement:

Entity e = new Entity(); //No templates/archetypes yet.
e.AddComponent(new HealthComponent(100));

Et peut être interrogé soit par nom, type, ou (si communs comme Santé et Position) par propriétés:

e["Health"] //This, unfortunately, is a bit slower since it requires a dict search
e.GetComponent<HealthComponent>()
e.Health

Et supprimé:

e.RemoveComponent<HealthComponent>();

Les données sont, encore une fois, communément accessibles par les propriétés:

e.MaxHP

Qui est stocké côté composant; moins de frais généraux s'il n'a pas HP:

public int? MaxHP
{
get
{

    return this.Health.MaxHP; //Error handling returns null if health isn't here. Excluded for clarity
}
set
{
this.Health.MaxHP = value;
}

La grande quantité de frappe est facilitée par Intellisense dans VS, mais pour la plupart, il y a peu de frappe - la chose que je cherchais.

Dans les coulisses, je stocke un Dictionary<Type, Component>et je Componentsremplace la ToStringméthode de vérification du nom. Mon design original utilisait principalement des chaînes pour les noms et les choses, mais c'est devenu un cochon avec lequel travailler (oh regardez, je dois lancer partout aussi le manque de propriétés faciles d'accès).

Le canard communiste
la source