Conception d'un ECS de moteur de jeu

ECS fonctionnel, mise en place des composants/systèmes

L'auteur de ce sujet a trouvé une solution à son problème.
Auteur du sujet

Bonjour,

# Préambule (pas important):

Comme une partie d'entre-vous le sait déjà, je suis en plein développement d'un moteur de jeu, celui-ci est basé sur une architecture modulaire hiérarchisée de façon stricte, chaque module est une bibliothèque donnant accès à un set de fonctionnalités accessibles uniquement au sein du module même et des modules dépendant de celui-ci.
Par exemple, cela signifie que le moteur possède un module gérant l'audio, et un module gérant le rendu. Ces deux modules n'ont aucune connaissance l'un de l'autre.
Afin de permettre la création d'un jeu, il convient de réunir tous les modules ensembles, via une surcouche, une bibliothèque liée avec tous les modules nécessaires, afin de les faire fonctionner ensemble et de faciliter la création d'une application.

# Le problème (plus important):

La bibliothèque en question, appelée NDK (Nazara Development Kit), est basée sur un Entity Component System, celui-ci est je pense assez classique, voici un code d'exemple pour plus de clarté: http://pastebin.com/QJwWuZE3.

Actuellement, ce n'est qu'un squelette d'ECS, tout fonctionne mais le NDK ne fournit aucun composant/système de base d'interaction avec le moteur.
Et c'est précisément pour ça que je me tourne vers vous avant de me lancer dans la création d'une architecture, j'aimerai y réfléchir, et que vous m'aidiez à y réfléchir.

Mes pistes de réflexions:

  • Tout d'abord, j'hésite à faire hériter Ndk::Entity (une entité de l'ECS) par NzNode (venant du moteur).
    Le faire signifierait donner une position (vecteur 3D), une rotation (quaternion) et une échelle (vecteur 3D) à CHAQUE entité, ainsi qu'un système de hiérarchie.
    Cela permettrait de simplifier l'interface:
1
2
3
4
Ndk::EntityHandle entity = world.CreateEntity();
Ndk::EntityHandle entity2 = world.CreateEntity();
entity->SetParent(entity2);
entity->SetPosition({42.f, 666.f, -1337.f});

L'alternative serait:

1
2
3
auto& nodeComponent = entity->GetComponent<Ndk::NodeComponent>();
nodeComponent.SetParent(entity2); // assertion: entity2 doit posséder un NodeComponent
nodeComponent.SetPosition({42.f, 666.f, -1337.f});

Le problème de la première option est d'ajouter une inconsistence:

1
2
entity->SetPosition(NzVector3f::Zero()); // Méthode de l'entité 
entity->GetComponent<Ndk::VelocityComponent>() = NzVector3f::Up() * 10.f; // Méthode/opérateur du composant

Cependant, l'extrême majorité des entités va avoir besoin d'un positionnement, d'une rotation, etc.
Et même si elles n'utilisent que la position (ex: point light), la rotation est transmise aux entités attachées, donc pas inutile.

  • La gestion du rendu, c'est le point qui me pose le plus de difficultés. Actuellement, une classe NzScene dans le module graphique s'occupe à la fois de gérer la scène visuelle (avec hiérarchie) et d'en effectuer le rendu. Le fait que le rendu et la scène soient gérés au même endroit par la même classe pose évidemment problème.
    Ce système va disparaître au profit de l'ECS, contenant la scène et s'occupant de gérer l'audio, le rendu, la physique, etc. grâce à des systèmes.
    L'idée serait que le module graphique ne contienne plus que des algorithmes (culling, technique de rendu, gestion des matériaux) qui seraient utilisés par le NDK.

Et c'est bien sur ce deuxième point, car j'ai du mal avec l'organisation nécessaire autour du rendu.
Sachant qu'il faut gérer la génération des shadow maps, le culling, et bien d'autres.

Bien sûr, il serait possible de faire un gros système s'occupant du rendu de façon automatique, traitant les entités automatiquement selon leurs composants, c'est d'ailleurs une solution que j'explore.
Cependant, je pense aussi à certaines difficultés, par exemple:
Imaginons une scène, celle-ci possède une télévision qui montre au joueur un endroit particulier de la scène. Au niveau du moteur cela signifie qu'il existe une caméra à un endroit de la scène effectuant son rendu vers une texture réutilisée par le matériau de la télévision.
Cependant: 1) Le culling de la télévision doit être appliqué avant toute chose (ça ne sert à rien d'effectuer le second rendu si la télévision n'est pas visible par le joueur). 2) Le rendu secondaire doit ensuite être effectué (cela implique du culling du point de vue de la caméra secondaire et du rendu complet) 3) Gestion des opérations pré-rendu (rendu des réflexions, génération des shadow maps) 4) Rendu de la scène principale.

Et maintenant imaginons que dans le champs de la caméra secondaire apparaisse une deuxième télévision pointant à un autre endroit. Ou pire, que la télévision apparaisse sur la caméra secondaire (donnant un bel effet de tunnel).

L'idéal serait que le moteur gère ça tout seul, mais j'ai vraiment du mal à imaginer la façon dont il faut gérer tout ça pour l'instant.

Vous n'avez peut-être pas de solution miracle à me proposer, mais votre avis peut certainement m'aider, c'est pourquoi je vous le demande.
Si je n'ai pas été assez clair (à mon habitude :D ), n'hésitez pas à demander plus de précisions, il est presque trois heures du matin donc je passe certainement à côté de détails importants.

Merci d'avoir lu !

Nazara Engine (Moteur de jeu amateur en C++) - Groupe #42

+0 -0

Salut, je t'avoue ne pas avoir absolument tout lu mais je pense avoir compris ton problème. Puisque tu connais l'ECS, tu sais normalement que les entités n'ont aucune propriété (si ce n'est un ID pour les différencier). Ton option d'héritage n'est donc pas la bonne. Enfin presque.

J'ai réalisé un ECS dans le cadre d'un projet libre basé sur le Javaquarium. Et nous nous sommes confronté à la même situation. En effet nous voulions créer des caractéristiques de reproduction, alimentation et vieillissement. Or dans ces trois caractéristiques nous avions nécessairement besoin. Une des options, qui est celle que tu propose, est de mettre cette caractéristique commune à (a priori) toute les entités dans la classe (si c'est une classe) Entity. Sauf qu'on viole la définition de l'ECS, et surtout le jour où tu veux rajouter une entité qui n'a pas cette propriété (dans notre cas c'était des décors influant sur l'aquarium) c'est la merde.

Nous avons donc pris une autre option qui s'en rapproche. A chaque type d'entité (celles qui ont de la vie par exemple) nous décorons une Entity (la classe) native. Et si nous avons absolument besoin d'une caractéristique, nous héritons de l'entité qui décore l'originel.

Pour être plus clair, voici ce que ça donne en code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class Entity{
  int id;
}

public class LiveEntity extends Entity{
    int life;
  // une série de méthode et d'attribut
}

public class Age extends LiveEntity {
  // des trucs
}

Voici le lien des trois classes (ça t'aidera à comprendre) : 1, 2, 3

Donc dans ton cas, ton moteur audio et ton moteur de rendu sont des systèmes. Du coup, plus de problème. Et si les entités de ces 2 systèmes ont besoin d'un positionnement, dans ce cas la tu as une entité mère qui hérite directement de Entity et qui rajoute ces caractéristiques. Du coup dans tes systèmes tu gèrent toujours des Entity.

Regarde les trois liens, tu comprendra mieux comment c'est architecturé. Je ne dis pas que c'est comme ça qu'il faut faire, mais c'est comme ça que j'ai fait, c'est maintenable, évolutif et surtout ça marche très bien ^^.

+0 -0
Auteur du sujet

Merci pour ta réponse, malheureusement l'héritage ne me semblait déjà pas une bonne idée, mais le polymorphisme me semble être une idée encore moins applicable (et ça me ferait revenir aux problèmes de base qui ont mené à la création des ECS).

Je vais passer par un composant, c'est je pense la meilleure option (et celle qu'on m'a vivement conseillé, en oubliant le reste du topic la plupart du temps :D ).

Le moteur audio et le moteur de rendu sont plus que des systèmes malheureusement, mais comme je ne vois pas comment mieux définir la chose, je vais partir sur une implémentation de base et la présenter, histoire de savoir si c'est la voie à suivre, ou en quoi elle est améliorable.

En tout cas, merci d'avoir pris le temps de poster ta réponse :)

Nazara Engine (Moteur de jeu amateur en C++) - Groupe #42

+0 -0

Y a peut etre mieux a faire qu'avec l'heritage c'est possible ^^ Mais si tu veux avoir une propriété commune a plusieurs type d'entités, l'heritage couplé a la decoration reste performant. Le seul probleme de cette methode c'est si tu veux plus d'une propriété commune. Alors la fait completement oublié ;)

+0 -0

Dans Unity3D, qui est basé sur ECS, il y a un composant Transform qui est systématiquement attribué à tous les GameObject (Entity). Pour moi ça me parait cohérent d'avoir des informations de position, rotation, scale pour chaque GameObject dans ton moteur.

Cela facilitera beaucoup de choses.

Effectivement je pense qu'il faut éviter l'héritage et je te rejoins là dessus. Cela serait dommage de bloquer la souplesse du ECS à cause de ça. Dans Unity il existe le concept de Prefab : c'est un ensemble de GameObject qui sont organisés sous forme de hiérarchie (genre dossier) et qui sont exporté comme un ensemble unique.

On peut instancier ces Prefabs et exploiter tous les GameObject qu'ils contiennent (et leurs composants). Du coup pour moi c'est clairement comme ça qu'il faut voir les choses. Au lieu d'hériter, on copie un GameObject qui contient les composants minimums et on vient lui ajouter des composants supplémentaires pour spécialiser l'instance. Si besoin, on exporte ce nouvel ensemble sous un autre Prefab pour facilement le réutiliser. Dans tous les cas il n'y a jamais d'héritage, mais plutôt une augmentation des Entités.

Voilà, je sais pas si c'est très clair ce que je dis là, mais j'espère que cet avis pourra te donner des idées :)

+0 -0
Vous devez être connecté pour pouvoir poster un message.
Connexion

Pas encore inscrit ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte