Je m’initie à l'Entity Component System depuis quelques temps avec ce cours. Mais dans la conception de la version "dynamique", je suis tombé sur un os: les composants peuvent-ils installer leurs dépendances à leur construction, les supprimer à leur destruction, et savoir quand ils doivent être détruits (car rien ne dépend d'eux et ils ont été installés automatiquement par d'autres composants) sans violer le principe de responsabilité unique ou dois-je implémenter une autre classe pour cela?
Ta question n'est pas très claire, car la notion même de classe (au sens POO du terme) ne devrait pas intervenir dans un ECS. Au mieux, pourrait-on avoir une classe générique pour représenter la notion de "collection de composants" (pour contenir tous les composant d'un type particulier).
Si bien qu'il n'y a que le LSP qui peut plus ou moins être ignoré, vu que, si la notion même de classes (au sens POO du terme) n'est pas utilisée, il y a peu de chances pour que la notion d'héritage publique (au sens POO du terme) n'ai le moindre intérêt(*).
Les autres principes SOLID doivent être scrupuleusement respectés, en particulier le SRP et l'OCP
(*) Il se peut cependant que certains services (comportements) -- par exemple, la notion de concept utilisée pour le rendu -- puissent être représentés par des abstractions qui utiliseront -- du coup -- la notion de classe au sens POO du terme. Si tel est le cas, LSP est rétabli dans tous ses droits et devoir en termes de conception
Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
Ta question n'est pas très claire, car la notion même de classe (au sens POO du terme) ne devrait pas intervenir dans un ECS.
Je crois qu'on en avait deja discuté et qu'on n'etait pas d'accord. Du coup, n'est pas surprend que dans mon tuto, je ne suive pas ton idée
Pour moi, ECS (data driven) est une architecture logicielle, donc transverse au paradigme utilisé (la POO). Il n'y a donc aucune raison que les principes de POO ne s'appliquent pas.
Et dans les différentes implementations possible d'un ECS, celle que je détaille le plus se base sur les 2 règles :
- pas de hiérarchie profond de classes (donc effectivement, le LSP n'a pas de sens)
- separation stricte entre données (entity et component) et algos (system)
Cette approche limite donc très fortement les principes de POO applicables, mais sans les rejeter.
@BorisD
Dans la version "simple" de l'implementation, c'est de toute façon pas le role des components de détruire ou créer quoi que ce soit. (Ce ne sont que des agrégats de données dans la version la plus simple). C'est le role des "component collection" de faire ca.
Mais on voit vite que cette approche peut etre limitée, par exemple si tu as beaucoup de composants, beaucoup de relations entre composants ou beaucoup de creation/destruction de composants. Dans ce cas, il est possible de complexifié les entités et composants pour leurs ajouter des infos ou des responsabilités supplémentaires, pour optimiser les taches.
Mais c'est a voir au cas par cas, en fonction de ton type de jeu, de tes types de composants, ce que tu en fait, etc. Fais une version simple, puis du profiling pour trouver les pertes de perfs. Tu verras ce qu'il faut optimiser a ce moment la.
...la notion même de classe (au sens POO du terme) ne devrait pas intervenir dans un ECS...
Pourquoi ça?
(*) Il se peut cependant que certains services (comportements) -- par exemple, la notion de concept utilisée pour le rendu -- puissent être représentés par des abstractions qui utiliseront -- du coup -- la notion de classe au sens POO du terme. Si tel est le cas, LSP est rétabli dans tous ses droits et devoir en termes de conception
Je ne comprends pas trop ce passage. Qu'appelles-tu:
"des abstractions"
"la notion de classe au sens POO du terme"
gbdivers a écrit:
Dans la version "simple" de l'implementation, c'est de toute façon pas le role des components de détruire ou créer quoi que ce soit. (Ce ne sont que des agrégats de données dans la version la plus simple). C'est le role des "component collection" de faire ca.
Mais on voit vite que cette approche peut etre limitée, par exemple si tu as beaucoup de composants, beaucoup de relations entre composants ou beaucoup de creation/destruction de composants. Dans ce cas, il est possible de complexifié les entités et composants pour leurs ajouter des infos ou des responsabilités supplémentaires, pour optimiser les taches.
Donc, si j'ai bien compris, ça peutéventuellement être le cas dans le but d'optimiser. Étant donné que mon objectif est d'écrire un code relativement propre, je pense que je vais respecter ce principe.
Tous les langages de programmation permettent de créer ce qu'il convient des agrégats de données. C'est à dire des structures qui regroupent plusieurs données de type potentiellement différents comme
struct Point{
int x;
int y;
};
ou comme
struct Adresse{
std::string rue;
int numero;
size_t codePostal;
std::string commune
};
Mais ces agrégats sont principalement "orientés données" dans le sens où l'on considère la notion de point ou d'adresse comme... l'ensemble des données auxquelles ils donnent accès.
On se bat très lourdement pour inciter les gens à penser à leurs classes en termes de services lorsque l'on utilise le paradigme orienté objet; c'est à dire à réfléchir en priorité aux ordres que l'on souhaite pouvoir donner à nos classes et aux questions que l'on souhaite pouvoir leur poser:
Au lieu de se dire que "une personne est l'agrégat représenté par son nom, son prénom et son adresse", on conseille au gens de réfléchir à quelles questions on souhaite que notre classe puisse répondre, et aux ordres que l'on souhaite pouvoir lui donner, ce qui correspond à la liste:
on veut pouvoir connaitre son nom
on veut pouvoir connaitre son prénom
on veut pouvoir connaitre son adresse
on veut pouvoir lui ordonner de déménager (à une nouvelle adresse qui sera fournie)
Tu me diras qu'au final, cela revient sensiblement au même, vu que, pour pouvoir fournir les services attendus de la part de la classe, il faudra bien qu'elle dispose de données représentant... le nom, le prénom et l'adresse de la personne.
Si ce n'est que ces données pourront alors être considérées comme de "vulgaires détails d'implémentation", parce que l'utilisateur de la classe Personne n'en a en réalité rien à foutre de la manière dont ces différentes données sont représentées au niveau de la personne.
De plus, le paradigme orienté objet vient avec une notion essentielle qui ne se trouve dans aucun autre paradigme: la notion substituabilité, qui permet à une fonction s'attendant à recevoir un paramètre du type de la classe de base de travailler avec une donnée dont le type réel est celui d'une classe dérivée.
Pour que cette notion fonctionne, il faut impérativement que le LSP (Liskov Substitution Principle ou principe de Substitution de Liskov) soit respecté à la lettre. Mais en plus, on se rend compte que les classes qui autorise la substituabilité peuvent être désignées sous le terme général de classe ayant sémantique d'entité.
Maintenant, lorsque réfléchit un tout petit peu à la notion de ECS, on se rend compte que ce qui correspond au C (les "composants") ne sont -- au pire -- que des agrégats de données: On n'en attend aucun service particulier si ce n'est le fait qu'ils nous permettent d'accéder (le plus souvent en lecture et en modification)... au différentes valeurs qui les composent (*).
Nous pourrions dire que tous les composants que l'on va créer seront ce qu'il convient d'appeler de POD et que les structures qui les décrivent ont forcément sémantique de valeur. Ce sont donc des types de données qui n'ont -- a priori -- absolument aucune chance d'intervenir dans une hiérarchie de classes. Et, du coup, LSP est purement et simplement inapplicable, vu qu'il n'y aura -- théoriquement -- aucun héritage possible.
C'est la raison pour laquelle je te dis que le seul principe SOLID auquel tu ne sois pas soumis lorsque tu pars sur le principe d'un ECS c'est le LSP et que la notion de classe au sens POO du terme (avec cette notion d'héritage et de substituabilité) ne devrait pas intervenir (à moins bien sur d'être bloqué avec un langage qui ne supporte que le paradigme OO).
(*) note que tous les composants devraient sans doute permettre d'accéder à l'identifiant de l'entité à laquelle ils sont rattaché, et que cet identifiant est a priori non modifiable
BorisD a écrit:
Je ne comprends pas trop ce passage. Qu'appelles-tu:
(1) "des abstractions"
(2)"la notion de classe au sens POO du terme"
(1)En informatique, une abstraction est tout ce qui te permet de "t'éloigner un peu plus" des 1 et des 0 utilisés pour représenter les différentes données (et les différentes instructions).
A ce titre, n'importe quel langage de programmation, n'importe quel type de donnée que tu pourras utiliser peut être considéré comme "une abstraction". Mais, dans le cas présent, cette définition pourrait être quelque peu restreinte pour prendre la forme de "toute notion dont tu pourrais avoir besoin qui soit suffisamment générique que pour pouvoir être réutilisée pour autre chose".
Par exemple, la notion de "sortie client" représente une abstraction, car, l'un dans l'autre, on peut se foutre pas mal de savoir si l'affichage se fera en mode "console" ou en mode "graphique", ni même s'il y aura vraiment un affichage, ou si le concept se "contentera" de créer un fichier qui pourra être "utilisé ailleurs" : à un moment ou à un autre, nous voudrons effectuer une "sortie client", et le système devra faire en sorte que, quel que soit la manière dont les information seront traitées, nous puissions avoir un code proche de
Context c;
c.show();
et show() provoque la "sortie client" telle que nous voulions l'obtenir ;).
Finalement, cette notion d'"abstraction" se rapporte très fort à la notion de même nom auquel a trait le DIP
(2) j'ai répondu au début de mon intervention
Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
Donc si j'ai bien compris, tu veut dire dans ton premier post que la notion objet (avec ses méthodes) est inutile dans un ECS et de simples structures "C style" pourraient suffire, donc les autres principes OO introduits par le C++ ne sont pas de mise (y compris le LSP du principe SOLID). Pour moi, cela dépend de l'implémentation de l'ECS. En effet, dans un ECS dont les compositions sont définies à la compilation de cette façon
class Button : private Render, private Hitbox
{
//...
};
alors Render et Hitbox peuvent être de simples structures "C style" non-OO. Mais dans un ECS qui ajoute des composants à ses compositions à l'exécution ne sait pas comment utiliser ses structures "C style" qui doivent être mises à jour en même temps que l'entité, ce qui nécessite une fonction update() ou autre. De plus, le stockage des composants nécessite un héritage (pour les stocker dans la même collection et utiliser le polymorphisme).
Donc si j'ai bien compris, tu veut dire dans ton premier post que (1) la notion objet (avec ses méthodes) est inutile dans un ECS et de simples structures "C style" pourraient suffire, (2) donc les autres principes OO introduits par le C++ ne sont pas de mise (y compris le LSP du principe SOLID).
(1) oui
(2) non: le seul principe SOLID qui soit spécifique à la programmation OO est le LSP (le L de SOLID). Si pas de programmation OO, pas de LSP à respecter, vu qu'il n'a simplement jamais lieu d'être invoqué
TOUS LES AUTRES PRINCIPES (SOID) SONT DE STRICTE APPLICATION
BorisD a écrit:
Pour moi, cela dépend de l'implémentation de l'ECS. En effet, dans un ECS dont les compositions sont définies à la compilation de cette façon
class Button : private Render, private Hitbox
{
//...
};
deux choses:
Tu effectue un héritage privé qui n'a absolument rien à voir avec le LSP, vu qu'il ne parle que de l'héritage public
L'héritage privé est une relation "est implémenté en termes de" qui peut très bien être remplacée par une relation de simple agrégation
exemple pour le (2)
struct Button {
Render render;
Hitbox hit;
};
Note que, a priori, chaque élément de ton application sera essentiellement représenté par un numéro d'entité (une "entity id") et que l'on s'en fout pas mal si la partie "Render" et la partie "Hitbox" d'une entité donnée (par exemple de ton bouton) ne sont pas "planquées" au même endroit
BorisD a écrit:
alors Render et Hitbox peuvent être de simples structures "C style" non-OO. Mais dans un ECS qui ajoute des composants à ses compositions à l'exécution ne sait pas comment utiliser ses structures "C style" qui doivent être mises à jour en même temps que l'entité, ce qui nécessite une fonction update() ou autre.
Ce sont tes services qui doivent savoir comment traiter le tout, pas tes données
L'une des solutions envisageables serait que chaque entité se voie associer un tableau de booléens, dont chaque bit correspondrait à la présence d'un composant particulier : ils seraient mis à 1 s'il existe un composant du type désigné qui est associé à l'entité et ils seraient mis à 0 si ce n'est pas le cas.
Les différents services pourrons donc décider de faire appels à d'autres services ("spécialisé) en fonction de la valeur du bit correspondant au composant envisagé
BorisD a écrit:
De plus, le stockage des composants nécessite un héritage (pour les stocker dans la même collection et utiliser le polymorphisme).
D'abord, pourquoi voudrais tu stocker tous tes composants ensembles? Pourquoi ne voudrais tu pas créer une collection pour chaque type de composant particulier ?
Ensuite, tu pourrais peut-être souhaiter toutes les collections (qui seraient à chaque fois spécifiques à un type de composant particulier) dans une "méga collection", car si X, Y et Z sont trois composants différents, tu pourrais avoir une table de X, une table de Y et une table de Z, et ces trois tables correspondraient alors à ta "base de données des composants".
Tu n'as aucun besoin d'avoir du polymorphisme dans l'histoire, car, hormis quelques services particuliers (erase(id), empty(), clear() et size() ), tous les autres services portent effectivement le même nom, mais prennent en paramètre et / ou renvoyent des valeurs de type fondamentalement différents (X Vs Y Vs Z pour les paramètres, typename std::vector<X>::iterator Vs typename std::vector<Y>::iterator typename std::vector<Z>::iterator pour les retours de fonctions).
Le "duck typing" permet de gérer ce genre de choses... Intéresse-toi peut-être à std::any ou std::variant pour ce genre de problème
Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
L'une des solutions envisageables serait que chaque entité se voie associer un tableau de booléens, dont chaque bit correspondrait à la présence d'un composant particulier : ils seraient mis à 1 s'il existe un composant du type désigné qui est associé à l'entité et ils seraient mis à 0 si ce n'est pas le cas.
Les différents services pourrons donc décider de faire appels à d'autres services ("spécialisé) en fonction de la valeur du bit correspondant au composant envisagé
A mon avis, il y a une confusion avec la concept d'ECS et certains implementations spécifiques des ECS.
Pour commencer, quand koala parle de "la notion de classe au sens POO du terme", il parle en fait de la sémantique d'entité (a ne surtout pas confondre avec "entity" dans "ECS, c'est autre chose). Tout les elements qu'il donne sont lié a ca : substituabilité, SOLID, etc. Meme la notion de service est très fortement lié a cette sémantique. Dans le cas de la sémantique de valeur, un objet représente avant tout une valeur sur laquelle on applique des opérations.
Il n'y a pas d'implementation de référence d'un ECS, juste des définitions un peu vague et très variables selon les auteurs. Il y a des implémentations où les entités sont propriétaires des composants, ou des implémentations où les composants sont des classes avec héritage public. Le cas particulier d'implémentation où les entités et les composants ne représentent que des données et aucun service n'est qu'un cas particulier.
Pour le moment, vous allez avoir du mal a vous comprendre, puisque vous parler de choses différentes. Cf mon premier message, l'architecture ECS et le paradigme POO sont transverses.
@BorisD
Pour commencer, suis le règle que j'ai donné dans mon premier message : "separation stricte entre données (entity et component) et algos (system)", ce qui correspond a ce dont parle koala. Ca sera plus simple pour discuter.
Par contre, pas de variant ou any, il n'y en a pas besoin.
Ce sont tes services qui doivent savoir comment traiter le tout, pas tes données
L'une des solutions envisageables serait que chaque entité se voie associer un tableau de booléens, dont chaque bit correspondrait à la présence d'un composant particulier : ils seraient mis à 1 s'il existe un composant du type désigné qui est associé à l'entité et ils seraient mis à 0 si ce n'est pas le cas.
Ce qui implique que:
Mes entités connaissent mes composants.
Peut-être (je ne suis pas sûr d'avoir compris (comme d'habitude)) que le système connaisse les composants.
gbdivers a écrit:
Pour commencer, suis le règle que j'ai donné dans mon premier message : "separation stricte entre données (entity et component) et algos (system)", ce qui correspond a ce dont parle koala. Ca sera plus simple pour discuter.
Dois-je comprendre que les algos sont gérés par le(s) système(s)? Donc le système doit connaître les composants dès la compilation? Serait-ce lui qui fournirait les services? Pour être sûr qu'on parle bien de la même chose, je précise que l'extensivité est ma priorité. Puisqu'un exemple vaut mille mots, voici le code idéal pour ajouter un composant:
Pour Illustrer le détail d'implémentation (pas forcément un exemple très judicieux mais bon...)
class Person
{
public:
Person(std::string const & name,SqlDatabase db)
:db_{db}
{
auto result = db.exec("INSERT INTO T_PERSON (C_NAME) VALUES(%1)",name);
id_ = result.lastInserted();
}
Person(int id,SqlDatabase db):
:id_{id}
,db_{db}
{}
std::string name() const {
auto result = db_.exec("SELECT C_NAME FROM T_PERSON WHERE C_ID=%1",id_);
if (result.next()){
return result(0).toString();
}
throw std::runtime_error("Invalid Person!");
}
void changeName(std::string const & n){
db_.exec("UPDATE T_PERSON SET C_NAME=%1 WHERE C_ID=%2",n,id_);
}
private:
int id_{};
SqlDatabase db_;
};
Ici ma classe Person ne possède qu'une référence sur une base de données et une clé primaire sur une table, on se rend compte qu'elle va rendre les services qu'on attend d'elle en exécutant des requêtes sur une base de données. Cette classe n'encapsule pas les données proprement dites, mais le nécessaire et les méthodes pour les traiter. Cette classe entre en plein dans une orientation orientée service, elle permet de manipuler une personne dont les données se trouvent dans une base de données...
Peut-être (je ne suis pas sûr d'avoir compris (comme d'habitude)) que le système connaisse les composants.
Les entités n'ont rien à savoir du tout: ce sont juste des identifiants (un size_t)
Les composants, à la base, ce sont juste des type (alias de type,énumération, structure POD ) auquel nous adjoignons "simplement" une donnée supplémentaire: l'identifiant de l'entité auquel chaque composant est rattaché
Le services, ce sont des comportement. Dans leur version la plus simple, ca peut n'être que... des fonctions libres
Après, il faut un peu de "glu" pour mettre tout cela en place:
1- On doit pouvoir déterminer rapidement si un composant particulier est associé avec une entité particulière. Le plus facile est sans doute d'associer un bitset à chaque entité, dont chaque bit correspond à la présence (ou à l'absence) d'un composant particulier pour cette entité particulière.
Il faudra donc mettre un "mapping" en place qui nous permettra, en connaissant le type du composant qui nous intéresse, de déterminer quel bit lui correspond dans le bitset.
2- nous devrons maintenir en place une liste des entités qui existent. Un simple tableau devrait pouvoir suffire. Il peut s'agir d'une variable globale, si on s'arrange pour qu'il n'y ai qu'un nombre très limités de fonctions qui puissent y accéder.
3- pour chaque type de composant, nous devons maintenir en place une liste de l'ensemble des composant de ce type qui existent. Nous serons cependant sans doute confrontés à trois problèmes de performances distincts:
certains services voudront pouvoir parcourir l'ensemble des éléments qui existent, sans trop s'inquiéter de l'entité à laquelle chaque composant est assigné et nous voudrons éviter autant que possible le cache miss lors de ce parcours "intégral" de la liste
d'autre services voudront en revanche accéder au composant qui est spécifiquement associé à une entité particulière. La recherche de ce composant devant alors se faire avec une complexité la plus faible possible; idéalement en O(log(N)).
la suppression d'un élément doit se faire le plus rapidement possible
L'idéal, pour éviter les problèmes de caches, est sans doute de maintenir la liste des éléments sous la forme d'un tableau contigu en mémoire. Ce n'est pas forcément incompatible avec une suppression la plus rapide possible, car il suffit d'intervertir l'élément que l'on souhaite supprimer avec le dernier élément valide, puis de redimensionner le tableau pour qu'elle puisse se faire en temps constant.
Mais cette manière de travailler sera incompatible avec une recherche en O(log(N)), car, s'il est possible d'avoir une recherche dichotomique dans un tableau, cela implique que le tableau soit trié. Or, chaque suppression d'un composant va "mélanger" l'ordre dans lequel les composants sont placés dans le tableau.
Il y aura donc une "petite astuce à trouver" à ce niveau là pour garantir que la recherche puisse se faire avec une complexité en O(log(N))
Encore une fois, rien n'empêche que les listes de composants soient des variables globales, pour autant que l'on s'assure que les seules possibilités de manipuler ces listes passent par un nombre très restreint et particulièrement bien surveillé de fonctions.
4- Il faut impérativement veiller à ce que la "clé" (le bitset) reflète en permanence exactement les composants qui sont associés à une entité particulière: quand on crée ou que l'on détruit un composant particulier pour une entité particulière, la clé associée à cette entité particulière doit être mise à jour immédiatement.
Un système de signaux et de slot pourrait venir en aide ici
5- Quand une entité n'est plus nécessaire, tous les composants qui y sont rattachés devraient être supprimés des listes. Encore une fois, un système de signaux et de slots peut venir en aide ici.
BorisD a écrit:
Dois-je comprendre que les algos sont gérés par le(s) système(s)? Donc le système doit connaître les composants dès la compilation? Serait-ce lui qui fournirait les services?
Les algorithmes, ce sont des comportements.
Les comportements sont représentés par les systèmes
Donc, oui, les systèmes doivent connaître les composants sur lesquels ils devront travailler à la compilation, et ils devront -- pour la plupart -- en plus pouvoir s'assurer rapidement (à l'aide de la clé que j'ai introduite plus haut) s'ils doivent ou non s'effectuer pour une entité particulière (il ne sert à rien qu'ils "perdent du temps" à chercher un composant particulier s'il n'existe pas pour une entité particulière)
BorisD a écrit:
Pour être sûr qu'on parle bien de la même chose, je précise que l'extensivité est ma priorité. Puisqu'un exemple vaut mille mots, voici le code idéal pour ajouter un composant:
aussi, je ne peut pas me permettre que les entités et le système connaissent les composants.
Les composants n'ont aucune dépendance les uns vis à vis des autres: ils vivent chacun leur vie sans s'inquiéter des autres!
Ce sont les services qui dépendent de la présence (pour une entité particulière) de un ou de plusieurs composants ou d'une liste de composants (par exemple : la liste des ages pour le service "vieillir tout le monde").
En outre, il faut tout faire pour que la vérification soit la plus rapide possible pour ne pas se trouver dans une situation dans laquelle un service mettrait cinq minute à s'assurer qu'il peut effectivement travailler avec sur une entité donnée alors que le traitement en lui même ne prendrait que trente secondes.
Dans cette optique, les chaînes de caractères sont à proscrire pour tout besoin de recherche / vérification : les tests effectués sur des chaînes de caractères se font typiquement avec une complexité en O(N) : ils prennent d'autant plus de temps que la chaîne de caractères est longue. On ne peut pas se permettre une telle perte de temps
Le seul cas où une chaine de caractères sera utilisée, c'est lors de l'affichage
BorisD a écrit:
aussi, je ne peut pas me permettre que les entités et le système connaissent les composants.
Il n'y a que les systèmes qui doivent connaître les composants... Et encore : pas tous... Ils ne doivent connaître que... les composants dont ils auront besoin pour travailler (ce qui est logique)
- Edité par koala01 4 mai 2018 à 23:29:21
Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
Dans ce cas, le code est beaucoup moins facilement extensible! Il faudrait faire hériter (ou reprogrammer) un nouveau système dès que l'on veut ajouter des composants au code source! Je ne vois qu'une utilité à cette implémentation de l'ECS (et encore, est-ce réellement utile?): les objets peuvent gagner (ou perdre) de nouvelles fonctionnalités. Dit comme ça, c'est tout de suite moins intéressant. Le seul usage pratique vraiment utile serait dans le cas d'un logiciel permettant de "créer" un nouvel objet pour un autre programme (ou le même), mais à quel prix! Pertes de performances, sécurité moins élevée (puisqu'un objet peut changer de rôle, et passer d'un bouton à un moteur de particules), etc...
Enfin, l'utilité de cette implémentation reste réelle, mais il vaut mieux avoir une bonne raison de le faire, et ne pas la tirer au hasard.
Donc, je récapitule: cette implémentation de l'ECS est composée
des systèmes: les algorithmes chargés de traiter les données
des entités: de simples liens entre les composant, histoire de dire lesquels vont ensemble
des composants: des données utiles aux systèmes
ainsi, pour dire autrement, les systèmes sont les fonctions, les entités les id, et les composants les variables.
Dans ce cas, le code est beaucoup moins facilement extensible! Il faudrait faire hériter (ou reprogrammer) un nouveau système dès que l'on veut ajouter des composants au code source!
Tu n'as pas à faire hériter quoi que ce soit!!!(*)
Alors, oui, bien sur que tu dois ajouter un nouveau système à chaque fois que tu veux ajouter une nouvelle caractéristique à (certaines de) tes entités, mais tu n'as pas l'air de de te rendre compte que c'est de toutes manières ce que tu ferais en appliquant une approche orientée objet classique
Mais le problème est beaucoup moins ce qu'il faut faire quand tu veux "simplement" rajouter une caractéristique à certaines de tes entités que ce qu'il faut faire lorsque tu te dis,justement, qu'un nouveau type d'entité n'a pas besoin de certaines caractéristiques que tu retrouve communément dans les autres
je m'explique:
Imaginons que tu veuilles créer une bibliothèque graphique, et que, pour ce faire, tu veuille créer ta notion de "widgets". Tous tes widgets vont exposer un certain nombre de caractéristiques communes comme:
la position dans ce qui servira de "référentiel" (une fenêtre),
l'espace dont il ont besoin pour pouvoir être représenté dans ce référentiel
la capacité de rendre un widget visible /invisible
la capacité d'activer ou de désactiver un widget
Tous??? à voir!!!! Un layout, par exemple, n'a aucun besoin d'être activé / désactivé, et n'a aucun besoin d'être rendu visible ou invisible: il prend un espace donné dans le référentiel et s'occupe finalement de fournir (aux widgets qu'il va contenir) l'espace dont ils peuvent disposer pour pouvoir être affichés.
Ensuite, certains widgets devront pouvoir afficher des images, d'autres devront afficher du texte, d'autres encore devront afficher soit un élément de sélection multiple soit un élément de sélection unique, et on voudra même sans doute vouloir cliquer sur certains d'entre eux ou modifier le texte qu'ils contiennent!
Si tu t'y prend bien, tu créeras une interface particulière pour chacune de ces caractéristiques, et tu pourras, par exemple, créer ton label en le faisant hériter de l'interface "TextHolder" et ton LineEdit de l'interface "TextModifier" (qui hériteras sans doute de l'interface TextHolder).
Les choses seront un peu plus compliquées avec ta notion de bouton, car un bouton "générique" (comprends : qui pourra s'adapter à n'importe quelle usage auquel l'utilisateur pourra penser)
peut (ou non) afficher une image
peut (ou non) afficher du texte
est forcément cliquable
peut (ou non) afficher un élément de sélection multiple
peut (ou non) afficher un élément de sélection unique
ne doit pas permettre à l'utiisateur de modifier le texte, s'il y en a un qui est affiché
Tu me diras qu'il n'y a aucun problème: vu que j'ai des interfaces séparées pour chacune de ces caractéristiques, je peux créer ma notion de bouton ("générique") en lui faisant hériter de chacune des interfaces dont il a besoin. Chouette!!!
Mais le problème va survenir lorsque je vais vouloir spécialiser ce bouton... Car l'ISP nous dit que l'on doit veiller à n'exposer que les interfaces les plus simples possibles, en n'exposant finalement que les fonctions dont on a vraiment besoin.
Or, si je peux choisir de faire en sorte que mon bouton affiche une image OU du texte OU un élément de sélection multiple OU un élément de sélection unique si bien que pour respecter l'ISP, mes différents bouton "concrets" ne devraient pas exposer l'interface relatives aux éléments qu'ils ne sont pas sensés contenir.
Et, pas de bol, je ne peux pas faire passer ces interfaces "inutiles" de l'accessibilité publique (dans ma notion de bouton "générique") à l'accessibilité privée (dans ma notion de bouton concrète), par exemple, parce que cela contreviendrait au LSP. Et je suis donc bloqué, parce qu'il y aura toujours un des principes SOLID que je ne vais pas respecter.
Avec l'approche ECS, je n'aurais pas ce problème, car, si je veux un bouton qui affiche une image, le système qui s'occupera de l'affichage du bouton ne choisira que ... le comportement relatif à la présence d'une image qui doit être affichée. Et il en va de même pour la présence (ou l'absence) des autres caractéristiques dont j'ai parlé
De plus, lorsque je vais vouloir afficher mon bouton (ou n'importe quel autre widget d'ailleurs), je me fous pas mal du fait qu'il soit actif ou non, je me fous pas mal que l'élément soit cliquable ou non (même je pourrai utiliser cette caractéristique pour décider de créer une ombre), je me fous pas mal que le texte soit modifiable ou non (même si je pourrai utiliser cette caractéristique pour changer la couleur de fond): si j'ai du texte, j'affiche du texte; si j'ai une image, j'affiche une image, si j'ai "autre chose" qui doit être affiché, j'affiche cette "autre chose", et je suis tranquille
BorisD a écrit:
Je ne vois qu'une utilité à cette implémentation de l'ECS (et encore, est-ce réellement utile?): les objets peuvent gagner (ou perdre) de nouvelles fonctionnalités. Dit comme ça, c'est tout de suite moins intéressant.
Peut-être n'as tu -- simplement -- pas encore assez de bouteille pour en voir tout l'intérêt... Car c'est justement là que réside tout l'intérêt de la chose
BorisD a écrit:
Le seul usage pratique vraiment utile serait dans le cas d'un logiciel permettant de "créer" un nouvel objet pour un autre programme (ou le même), mais à quel prix! Pertes de performances, sécurité moins élevée (puisqu'un objet peut changer de rôle, et passer d'un bouton à un moteur de particules), etc...
Parce que tu confond les composants (les données) et les comportements (services)
Un bouton et un moteur à particules peuvent tous les deux utiliser des particules pour être représentés visuellement.
Mais la notion de moteur à particule représente un comportement (le fait d'afficher des particules), alors que la notion de bouton représente une représentation des données, et donc, d'avantage un composant
Si bien que, non, décidément, il n'y a aucun moyen pour que le bouton devienne un moteur à particule
Au pire, le bouton pourrait devenir un LineEdit, un Label ou un "ImageView", mais ca s'arrête là
BorisD a écrit:
Enfin, l'utilité de cette implémentation reste réelle, mais il vaut mieux avoir une bonne raison de le faire, et ne pas la tirer au hasard.
N'importe quelle technique de développement n'est utile que si elle est utilisée à bon escient, mais aura des conséquences catastrophique si elle est utilisée en dépit du bon sens...
Il en va comme cela pour les paradigmes : le paradigme OO est génial, mais le paradigme purement impératif ou le paradigme générique (vu que l'on dispose des trois en C++) présentent tous les deux des intérêts majeurs
Il en va comme cela de certaines technique, comme la récursivité, qui permet de simplifier énormément la logique dans certaines circonstances, mais qui augmente le risque de saturer la pile d'appel
Il en va de même de tous les choix que tu pourrais envisager de faire : si tu fais le bon choix au bon moment, tu seras un développeur heureux, si tu fais le mauvais choix (même si c'est pour de "bonnes raisons"), tu passera plus de temps à essayer de te dépêtrer des choix qui ont été faits qu'autre chose
BorisD a écrit:
Donc, je récapitule: cette implémentation de l'ECS est composée
des systèmes: les algorithmes chargés de traiter les données
des entités: de simples liens entre les composant, histoire de dire lesquels vont ensemble
des composants: des données utiles aux systèmes
ainsi, pour dire autrement, les systèmes sont les fonctions, les entités les id, et les composants les variables.
C'est exactement cela
Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
Tu n'as pas à faire hériter quoi que ce soit!!!(*)
Dans ce cas je reprogramme un nouveau système?
Alors, oui, bien sur que tu dois ajouter un nouveau système à chaque fois que tu veux ajouter une nouvelle caractéristique à (certaines de) tes entités, mais tu n'as pas l'air de de te rendre compte que c'est de toutes manières ce que tu ferais en appliquant une approche orientée objet classique
Je m'en rend compte mais j'espérais que l'ECS pourrait contourner cette pratique pour rendre le code plus souple.
Mais le problème est beaucoup moins ce qu'il faut faire quand tu veux "simplement" rajouter une caractéristique à certaines de tes entités que ce qu'il faut faire lorsque tu te dis,justement, qu'un nouveau type d'entité n'a pasbesoin de certaines caractéristiques que tu retrouve communément dans les autres
[...]
De plus, lorsque je vais vouloir afficher mon bouton (ou n'importe quel autre widget d'ailleurs), je me fous pas mal du fait qu'il soit actif ou non, je me fous pas mal que l'élément soit cliquable ou non (même je pourrai utiliser cette caractéristique pour décider de créer une ombre), je me fous pas mal que le texte soit modifiable ou non (même si je pourrai utiliser cette caractéristique pour changer la couleur de fond): si j'ai du texte, j'affiche du texte; si j'ai une image, j'affiche une image, si j'ai "autre chose" qui doit être affiché, j'affiche cette "autre chose", et je suis tranquille
C'est vrai, dans ce cas, je ne vois pas d'autre solution. Excellent exemple. Si encore il ne s'agissait que de texte / image, j'aurais pu faire le malin en disant que pour de nombreuses bibliothèques graphiques texte + police + coordonnées = image, mais avec les éléments de sélection, je sèche.
Parce que tu confond les composants (les données) et les comportements (services)
Un bouton et un moteur à particules peuvent tous les deux utiliser des particules pour être représentés visuellement.
Mais la notion de moteur à particule représente un comportement (le fait d'afficher des particules), alors que la notion de bouton représente une représentation des données, et donc, d'avantage un composant
Si bien que, non, décidément, il n'y a aucun moyen pour que le bouton devienne un moteur à particule
Au pire, le bouton pourrait devenir un LineEdit, un Label ou un "ImageView", mais ca s'arrête là
L'exemple est mauvais mais l'idée est là: on ne sait pas toujours ce que va devenir l'objet à la compilation. Bon, évidemment, plus la hiérarchie est profonde, plus les risques sont élevés, à quoi tu pourrais me répondre "Et bien n'utilise pas de hiérarchie profonde!", mais cela impliquerait de reprogrammer un nouvel ECS. Il faut donc trouver un bon compromis entre "sécurité" et "extensibilité". Mon avis personnel: quand les ECS ont des choses vraiment importantes à se partager (ou une même "cible"), ce sont les mêmes (ou bien il y a une situation d'héritage ou autre), et à l'inverse, quand ce qu'ils ont en commun, c'est leur nature d'ECS, ils sont différents et n'ont rien en commun.
N'importe quelle technique de développement n'est utile que si elle est utilisée à bon escient, mais aura des conséquences catastrophique si elle est utilisée en dépit du bon sens...
[...]
Il en va de même de tous les choix que tu pourrais envisager de faire : si tu fais le bon choix au bon moment, tu seras un développeur heureux, si tu fais le mauvais choix (même si c'est pour de "bonnes raisons"), tu passera plus de temps à essayer de te dépêtrer des choix qui ont été faits qu'autre chose
Cela va de soi. Simplement, je soulignais le fait qu'il pouvait être VRAIMENT dangereux de l'utiliser à mauvais escient la ou une autre implémentation peut être aussi efficace.
Non, ce n'est pas VRAIMENT dangereux, comme tu l'écris si bien... C'est juste une approche différente, qui a ses avantages et ses inconvénients.
Les avantages sont nombreux:
tu ne perds pas (si tu t'y prend bien) la double indirection due à l'utilisation de comportements polymorphes
tu gagnes énormément en souplesse, entre autres, en termes de "scripting": si tu veux qu'une entité particulière dispose d'une caractéristique particulière, tu l'ajoute, et tu es tranquille
tu gagnes énormément en souplesse en termes de développement, car tu n'as pas besoin de créer "une classe de plus" pour permettre à un magicien d'éventuellement apprendre quelques compétences de guerrier
tu gagnes énormément en souplesse à l'exécution, car tu n'as pas besoin de changer le type de ton entité au fur et à mesure qu'il apprend (ou qu'il abandonne) certaines de ces compétences
j'en passe, et sans doute de meilleures
L'inconvénient majeur est sans doute le fait qu'il est sommes toutes assez difficile de mettre l'approche ECS au point de manière à être efficace à tous les points de vue (en termes de recherche et de parcours des données, pour que le développeur de l'ECS ait "facile" à rajouter des composants, etc)
Mais tous les avantages que j'ai cités pourraient être repris comme des inconvénients de l'approche OO!!! Et, il faut dire ce qui est: dans certaines situations, l'approche ECS ressemble furieusement au fait de sortir le bazooka pour assommer une mouche ;).
C'est pour cela qu'il est particulièrement important de faire les bons choix au bon moment: si tu attends d'avoir quinze classes différentes pour ton personnage avant de te dire que "tiens, finalement, l'approche ECS pourrait être pas mal", ca va décidément pas le faire
Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
Évidemment, il ne vaut mieux pas utiliser l'ECS là où une simple relation d'héritage pourrait suffire tout en restant propre sans avoir 50 classes sur les bras. En revanche, si on finit effectivement avec une cinquantaine de classes (dont le nombre risque encore d'augmenter), alors l'ECS peut être "pas trop mal" (enfin, peut-être) à condition que ces classes aient effectivement des composants et comportements (ou systèmes) à partager. Par exemple, reprenons l'exemple du magicien qui veut apprendre des compétences de guerrier (donc qui a besoin de composants de guerrier pour utiliser les systèmes correspondants). Si il est le seul à pouvoir lancer des sorts et que les guerriers finissent au chômage (comprendre inutilisés ou supprimés), alors il sera le seul à utiliser ces composants / systèmes. Aussi, si chaque composant / système n'est utilisé que dans un seul type de personnages (ou classe, au sens rpg du terme), alors il n'y aura plus d'intérêt à les détacher des différentes classes et on pourra donc utiliser une relation d'héritage classique.
C'est vraiment à utiliser au cas par cas, et à adapter selon les besoins.
à condition que ces classes aient effectivement des composants et comportements (ou systèmes) à partager.
Ca, c'est pas ton problème!!!!
Tu te fous pas mal de savoir si les différents composants ne soient utilisés que par une seule catégorie d'entité ou s'ils seront utilisés par une dizaine de ces catégories.
Ton seul et unique problème, c'est de faire en sorte de pouvoir rajouter / supprimer des composants au niveau de tes entités "à demande", et éventuellementde pouvoir placer des dépendances et / ou des incompatibilités entre ces composants
BorisD a écrit:
Par exemple, reprenons l'exemple du magicien qui veut apprendre des compétences de guerrier (donc qui a besoin de composants de guerrier pour utiliser les systèmes correspondants). Si il est le seul à pouvoir lancer des sorts et que les guerriers finissent au chômage (comprendre inutilisés ou supprimés), alors il sera le seul à utiliser ces composants / systèmes.
Ca, tu t'en fous royalement parce que
Il y a une règle tacite qui implique que tout ce qui est disponible sera forcément utilisé
parce que tu peux placer certaines incompatibilités qui feront que ton magicien ne pourra pas apprendre toutes les compétences de ton guerrier, ou que le guerrier sera plus intéressant ne serait-ce que parce qu'il aura d'avantage de force ou de point de vie que n'importe quel magicien
Mais ca, c'est un problème d'équilibre au niveau de ton jeu, qui n'a absolument aucun besoin de rentrer en ligne de compte dans ta conception.
BorisD a écrit:
Aussi, si chaque composant / système n'est utilisé que dans un seul type de personnages (ou classe, au sens rpg du terme), alors il n'y aura plus d'intérêt à les détacher des différentes classes et on pourra donc utiliser une relation d'héritage classique.
Eh non, car, même si on considère qu'il n'y a plus que le magicien qui est créé (ce qui ne devrait pas être le cas, mais bon... faisons comme si ), il faut tenir compte
du fait que le magicien n'est pas obligé d'apprendre certaines des aptitudes du guerrier
du fait qu'il ne pourra pas apprendre ces aptitudes directement
du fait que ces aptitudes nécessiteront sans doute une interface particulière dans une approche purement objet (ne serait-ce que une ou deux fonctions particulière) dont le magicien "de base" (n'ayant pas les compétences de guerrier) n'aura aucun besoin
que l'ISP devrait t'empêcher de fournir cette interface tant que le magicien n'a pas acquis les compétences en question
BorisD a écrit:
C'est vraiment à utiliser au cas par cas, et à adapter selon les besoins.
Bien sur! On n'a jamais prétendu que l'approche ECS était la solution à tous les maux non plus!!! On dit juste qu'une approche essentiellement orientées vers l'utilisation de données peut apporter de nombreux avantages par rapport à une approche orientée objets... Si les circonstances s'y prêtent... Et que le cas d'un RPG, d'un jeu de la vie et bien d'autres encore s'y prêtent particulièrement bien
Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
Ton seul et unique problème, c'est de faire en sorte de pouvoir rajouter / supprimer des composants au niveau de tes entités "à demande", et éventuellement de pouvoir placer des dépendances et / ou des incompatibilités entre ces composants
Ah oui, j'avais oublié qu'il fallait pouvoir les supprimer...
Par contre, une dernière question: les systèmes, sont ils membres des compositions ou est-ce des fonctions externes style
Faire en sorte qu'un système soit une partie de la composition n'aurait pas beacoup de sens, vu que cela créerait un lien fort (une dépendance) entre les données et les comportements, alors que le but de l'ECS est -- justement -- de faire en sorte que ce lien soit le plus faible possible
des fonctions libres ou -- éventuellement -- des classes qui serviraient à définir les systèmes seraient sans doute bien plus adaptés, car il ne faut pas perdre de vue que la notion de "système" regroupe ad minima deux choses bien particulières qui sont:
moyen "simple et efficace" pour maintenir l'ensemble des composants d'un type particulier ensemble en mémoire et
une ou plusieurs fonctions permettant de manipuler ces composants
En plus, il ne faut pas oublier que certains systèmes ont besoin de plusieurs composants pour fonctionner
Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
× Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
× Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
Discord NaN. Mon site.
...
Discord NaN. Mon site.