Partage
  • Partager sur Facebook
  • Partager sur Twitter

Downcasting pour ECS

Downcasting pour ECS

4 juillet 2019 à 10:06:05

Bonjour à tous,

J'essaye d'implémenter un ECS, et je suis me suis persuadé que c'est impossible sans downcasting. Comme je n'y connais pas grand chose et que je sais que ça peut poser problème, je me demandais. Ce code est-il correcte ? Peut-il poser problème ? à quelles conditions ? (ca ne fonctionne pas avec des unique_ptr et c'est normal je pense...).

class base {};
class derive : public base
{
	// plein de choses
}; 

int main()
{
    base *b = new derive {} ;
    [[maybe_unused]] derive * d = static_cast<derive *>(b) ;
    return 0;
}


Le but étant de pouvoir écrire du code de ce style:

struct position
{
	float x;
	float y;
};

struct vitesse
{
	float x;
	float y;
};
int main()
{
    Registry Reg;
    unsigned int ID = Reg.get_new_entity();

    Reg.add<position>(ID, {2.32f, 12.9f});
    Reg.add<vitesse>(ID, {2.64f, 12.6f});

    Reg.get<position>(ID).x += Reg.get<vitesse>(ID).x; // possible sans downcasting dans Registery::get() ?
    Reg.get<position>(ID).y += Reg.get<vitesse>(ID).y;
}



-
Edité par Umbre37 8 juillet 2019 à 23:13:49

  • Partager sur Facebook
  • Partager sur Twitter
4 juillet 2019 à 11:29:48

Salut,

Le fait, c'est surtout qu'un ECS n'utilise pas l'héritage public: chaque composant est strictement indépendant de tous les autres, il n'y a pas la moindre hiérarchie de classes dans leurs rangs!

Tu n'as donc pas besoin du moindre transtypage ;)

  • Partager sur Facebook
  • Partager sur Twitter
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
4 juillet 2019 à 11:47:07

Salut, merci de ta réponse.

Evidemment que les components n'h'éritent pas les uns des autres. En revanche, les components sont stockés dans des component_container, il y en a un par type (type inconnu à l'avance, donc utilisation des template à fond), et ces component_container doivent eux aussi être quelque part. dans un std::vector<component_container> par exemple. et là, héritage obligé... parce que chaque component_container est templatisé par le type de component qu'il contient, ils doivent tous hériter d'une base pour être stockés dans le registre.

Pour t'en convaincre : regarde mon deuxième bout de code. Essaye de coder la classe Registery pour qu'il compile et fonctionne. Je parie que tu ne pourras pas sans transtypage.

D'ailleurs dans tous les codes d'ECS en c++ auquel j'ai pu jeter un oeil et qui fonctionnent, il n'y en a aucun qui n'utilise pas massivement le transtypage pour récupérer les données.

un exemple :

https://github.com/skypjack/entt/blob/master/src/entt/entity/registry.hpp

je ne comprends pas tout parce que je n'ai pas le niveau, je suis amateur, mais je comprends bien qu'il cast dès qu'il veut récupérer une donnée.

-
Edité par Umbre37 4 juillet 2019 à 12:04:49

  • Partager sur Facebook
  • Partager sur Twitter
4 juillet 2019 à 14:03:31

Je ne vois pas pourquoi ça serait nécessaire.

Tu vas savoir à priori que tu as un velocity_component_list, un position_component_list, etc. Soit stockés directement soit dans une BD. Et quand tu veux faire des mises à jour des positions dans le système qui va bien, tu sais précisément dans quelle listes de composants il faut taper. Pas besoin de downcaster quoique ce soit.

Ce que tu vas avoir ce sont des concepts de composants et des concepts de conteneurs de composants. Et dans ce monde de canards statiques tu vas toujours pouvoir manipuler tes types directement. Non?

  • Partager sur Facebook
  • Partager sur Twitter
C++: Blog|FAQ C++ dvpz|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS| Bons livres sur le C++| PS: Je ne réponds pas aux questions techniques par MP.
4 juillet 2019 à 14:23:35

Je ne veux pas préjuger de ce à quoi pourra me servir cet ECS demain, c'est un outil puissant un peu passe partout. Donc je ne sais pas a priori quels types il contiendra, et je ne veux pas en refaire un pour chaque projet, ni ajouter à la main des spécialisations de template dans la class Registery parce que je veux ajouter un type.

Moi j'ai un objectif : je veux juste arriver à faire en sorte que le deuxième bout de code fonctionne tel qu'il est écrit, sans que les types passés en argument template des getters soient connus à l'avance par le registery. C'est mon cahier des charges en quelque sorte. Il ne fait que 22 lignes :) Et il faut juste coder la classe Registery pour ca. (En fait il y a déjà 400 lignes de code) Je pose juste la question sur le point précis qui m'intéresse.

Deux questions :

1) est-ce possible sans transtypage ? Je suis convaincu que non, est-ce que j'ai tort ?

2) est-ce que le premier bout de code, qui est un exemple basique de transtypage quand même pas violent (une instance d'un type est réinterprétée dans son propre type, via sa base) peut poser problème, si oui en quoi ? Je demande parce que je ne fais jamais de transtypage, je le fuis comme la peste, d'habitude.

-
Edité par Umbre37 5 juillet 2019 à 10:57:08

  • Partager sur Facebook
  • Partager sur Twitter
4 juillet 2019 à 17:36:35

Quel intérêt il y a-t-il à ce que la résolution soit dynamique?

Si dans tes systèmes tu downcastes, c'est que tes systèmes pourraient à la place poser la question à des conteneurs de composants dédiés.

  • Partager sur Facebook
  • Partager sur Twitter
C++: Blog|FAQ C++ dvpz|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS| Bons livres sur le C++| PS: Je ne réponds pas aux questions techniques par MP.
4 juillet 2019 à 18:10:57

lmghs a écrit:

Quel intérêt il y a-t-il à ce que la résolution soit dynamique?

Non non, pas de résolution dynamique. Je ne pense pas que ce soit nécessaire. Ca peut se faire à la compilation. Je veux juste que ce soit templatisé pour ne pas avoir à écrire 1 getter + 1 setter + un remove + un has() +.... et que sais-je encore par type. Comment écrire une fonction get<type>(ID) ? une seule fois pour tous les types ?

vac a écrit:

Très bon tuto ici: https://www.youtube.com/watch?v=NTWSeQtHZ9M


merci je vais regarder ca :)

  • Partager sur Facebook
  • Partager sur Twitter
5 juillet 2019 à 10:26:17

Umbre37 a écrit:

Je ne veux pas préjuger de ce à quoi pourra me servir cet ECS demain, c'est un outil puissant un peu passe partout.

Houla, c'est parti pour la construction de châteaux dans les nuages.


> Je veux juste que ce soit templatisé pour ne pas avoir à écrire 1 getter + 1 setter + un remove + un has() +.... et que sais-je encore par type.

Peut etre que c'est un problème XY ? Tu cherches à faire marcher un truc Y (je veux "juste" pouvoir écrire le code comme ceci / pas comme ça) dont tu penses qu'il résoud le problème X.

-
Edité par michelbillaud 5 juillet 2019 à 10:32:01

  • Partager sur Facebook
  • Partager sur Twitter
5 juillet 2019 à 10:38:37

michelbillaud a écrit:

Peut etre que c'est un problème XY ? Tu cherches à faire marcher un truc Y (je veux "juste" pouvoir écrire le code comme ceci / pas comme ça) dont tu penses qu'il résoud le problème X.

Oui c'est sûr que je voudrais que le deuxième petit bout de code fonctionne tel qu'il est écrit. J'ai mis ça pour montrer l'idée de l'outils que je souhaite réaliser. Ca n'a rien de très original d'ailleurs. Je l'ai déjà vu dans quelques projets. je me demande juste :

1) est-ce possible sans transtypage ? Je suis convaincu que non, est-ce que j'ai tort ?

2) est-ce que le premier bout de code, qui est un exemple basique de transtypage quand même pas violent (une instance d'un type est réinterprétée dans son propre type, via sa base) peut poser problème, si oui en quoi ? Je demande parce que je ne fais jamais de transtypage, je le fuis comme la peste, d'habitude.

-
Edité par Umbre37 5 juillet 2019 à 10:56:48

  • Partager sur Facebook
  • Partager sur Twitter
5 juillet 2019 à 13:43:30


Umbre37 a écrit:

Non non, pas de résolution dynamique. Je ne pense pas que ce soit nécessaire. Ca peut se faire à la compilation. Je veux juste que ce soit templatisé pour ne pas avoir à écrire 1 getter + 1 setter + un remove + un has() +.... et que sais-je encore par type. Comment écrire une fonction get<type>(ID) ? une seule fois pour tous les types ?

En fait, il y a une solution, même si, à la base, on plutôt défavorable:waw: : une partie du travail pourrait parfaitement être générée de manière automatique à l'aide de macros.  Je m'explique:

On peut partir du principe que, quoi qu'il advienne, les parties constituantes des différents composants seront publiques, car cela nous évitera déjà d'avoir à placer des accesseurs et des mutateurs qui ne servent à rien. Les composants seront donc non pas des classes, mais des structures.

Pour chaque composant, nous pourrions utiliser trois symboles (utilisés par le préprocesseur) particuliers à savoir:

  • COMPONENT(componentName) ,
  • COMPONENT_PART(partType, partName, componentName) et
  • COMPONENT_END .

qui nous permettraient de définir les composants, par exemple, sous la forme de

dans position.hpp

#include <check.hpp> 
COMPONENT(position)
COMPONENT_PART(float, x, position)
COMPONENT_PART(float, y, position)
COMPONENT_PART(float, z, position)
COMPONENT_END

ou, encore, sous la forme de

dans move.hpp

#include <check.hpp>
COMPONENT(move)
COMPONENT_PART(float, angle, move)
COMPONENT_PART(float, speed, move)
COMPONENT_END


Tu auras remarqué l'inclusion de check.hpp.  Ce fichier va juste s'assurer que les trois symboles en question sont bel et bien définis à chaque fois qu'ils sont utilisés, et provoquer une erreur de compilation si ce n'est pas le cas, grâce, encore une fois, au préprocesseur.  Cela se ferait sous la forme de

dans check.hpp

#ifndef COMPONENT
    #error "COMPONENT(componentName) should be defined"
#endif
#ifndef COMPONENT_PART
    #error "COMPONENT(partType, partName, componentName) should be defined"
#endif
#ifndef COMPONENT_END
    #error "COMPONENT_END should be defined"
#endif

De plus, nous regrouperons l'ensemble des fichiers de composants dans un seul fichier d'en-tête, sous la forme de

dans all_components.hpp

#include <position.hpp>
#include <movement.hpp>

!!! L'ordre dans lequel les différents fichiers sont inclus aura une importance CAPITALE pour la suite. si on décidait de modifier cet ordre par la suite, cela provoquerait des incompatibilité majeures entre les versions de l'ECS mis en place !!!!

Cela étant fait, il est temps d'ajouter un peu de magie grâce à des directives préprocesseur destinées à définir nos symboles.  Par exemple,nous pourrions créer l'ensemble des strutures pour les composants sous la forme de

dans allStructs.hpp

#ifndef ALLSTRUCTS_HPP
#define ALLSTRUCTS_HPP
#define COMPONENT(componentName) struct componantName{
#define COMPONENT_PART(partType, partName, componentName) \
partType partName;
#define COMPONENT_END };

/* il n'y a plus qu'à ajouter all_components.hpp pour que la magie opère
 */
#include <all_components.hpp>
/* Et, pour éviter tout risque de contagion, à supprimer la
 * définition de ces symboles
 */
#undef COMPONENT
#undef COMPONENT_PART
#undef COMPONENT_END
#endif // ALLSTRUCTS_HPP

Mais nous pourrions aussi créer une déclaration anticipée de tous les composants que nous avons définis, sous la forme de

dans predecls.hpp

#ifndef PREDECLS_HPP
#define PREDECLS_HPP
#define COMPONENT(componentName) struct componantName ;
#define COMPONENT_PART(partType, partName, componentName)
#define COMPONENT_END

/* il n'y a plus qu'à ajouter all_components.hpp pour que la magie opère
 */
#include <all_components.hpp>
/* Et, pour éviter tout risque de contagion, à supprimer la
 * définition de ces symboles
 */
#undef COMPONENT
#undef COMPONENT_PART
#undef COMPONENT_END
#endif // PREDECLS_HPP

Nous pourrions même envisager de créer une énumération, qui pourrait nous venir bien à point si on décide d'utiliser un système de passe-partout,  sous une forme proche de

dans enum.hpp

#ifndef ENUM_HPP
#define ENUM_HPP
#define COMPONENT(componentName) componantName##Index,
#define COMPONENT_PART(partType, partName, componentName)
#define COMPONENT_END

/* il n'y a plus qu'à ajouter all_components.hpp pour que la magie opère
 */
enum class ComponentKeys{
#include <all_components.hpp>
KeyMax
};
/* Et, pour éviter tout risque de contagion, à supprimer la
 * définition de ces symboles
 */
#undef COMPONENT
#undef COMPONENT_PART
#undef COMPONENT_END
#endif // ENUM_HPP

Et, bien sur, nous pourrions déclarer les fonctions spécifiques (addXXX, removeXXX), en considérant que ID correspond au type représentant l'identifiant des entités sous la forme de

dans componentFunctions.hpp

#ifndef COMPONENTFUNCIONS_HPP
#define COMPONENTFUNCIONS_HPP
#define COMPONENT(componentName) \
void add##componentName(ID id, componentName && component); \
void remove##componentName(ID id); \
componentName & find##componentName(ID id);
#define COMPONENT_PART(partType, partName,componentName) void update##partName##from##componentName(ID id, partType && newValue);
#define COMPONENT_END

/* il n'y a plus qu'à ajouter all_components.hpp pour que la magie opère
 */

#include <all_components.hpp>

/* Et, pour éviter tout risque de contagion, à supprimer la
 * définition de ces symboles
 */
#undef COMPONENT
#undef COMPONENT_PART
#undef COMPONENT_END
#endif // COMPONENTFUNCIONS_HPP

Et, pour peu que nous disposions d'une classe template (component_list ? ) dont je passe les détails d'implémentation, mais qui prenne une forme proche de

template <typename T>
class component_list{
/* je ne présente que les fonctions publiques qui m'intéressent,
 * le reste est de ta responsabilité
 */
public:
   void add(ID id, T&& );
   void remove(ID id);
   T & find(ID id);
};

nous pourrons même fournir l'implémentation de ces fonction sous la forme de

dans componentFunctions.cpp

// je vais travailler en plusieurs étapes pour éviter les problèmes...
/* 1- je m'assure que la liste de composant existe:  */
#define COMPONENT(componentName)
component_list<componentName> componentName##_component_lits;
#define COMPONENT_PART(partType, partName, componentName)
#define COMPONENT_END
#include <all_components.hpp>
#undef COMPONENT

/* 2- j'implémente la fonction add  */
#define COMPONENT(componentName) \
void add##componentName(ID id, componentName && value){ \
    componentName##_component_lits.add(id, value); \
}
#include <all_components.hpp>
#undef COMPONENT

/* 3- j'implémente la fonction remove */
#define COMPONENT(componentName) \
void remove##componentName(ID id){ \
    componentName##_component_lits.remove(id); \
}
#include <all_components.hpp>
#undef COMPONENT

/* 4- j'implémente la fonction find */
#define COMPONENT(componentName) \
componentName &find##componentName(ID id){ \
    return componentName##_component_lits.find(id); \
}
#include <all_components.hpp>
#undef COMPONENT
#undef COMPONENT_PART

/* 5- j'implémente les fonctions update */
#define COMPONENT(componentName)
#define COMPONENT_PART(partType, partName, componentName) \
void update##partName##From##componentName(ID id, partType && value){ \
    find##componentName(id).partName = value; \
}
#include <all_components.hpp>
#undef COMPONENT
#undef COMPONENT_PART
#undef COMPONENT_END

Il y a surement encore d'autres trucs à faire, mais ces explications devraient t'avoir mis sur la voie pour le reste ;)

-
Edité par koala01 6 juillet 2019 à 19:15:03

  • Partager sur Facebook
  • Partager sur Twitter
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
5 juillet 2019 à 15:44:15

Merci bcp Koala01 pour tout ce code. Tu as du y passer bcp de temps. Je ne connaissais pas cette technique. Je la trouve tout de même un peu lourde à mettre en place. Je suis tombé par hasard sur une manière d'associer chaque type à un ID de manière template, et je trouve ca peut-être un peu plus souple. qu'en penses-tu ?

class assign_ID_to_type
{
private:
   inline static unsigned int counter;
public:
    template<typename Type>
    inline static const unsigned int ID = counter++;
};

on peut complexifier le principe pour faire des choses plus intéressantes, mais l'idée principale est là est là.

Ensuite, pour écrire une fonction get on pourrait faire quelque chose comme ça (très schématiquement):

class Base_DC {};

template <typename Data_type>
class datas_container : public Base_DC 
{
public:
	Data_type & get(unsigned int const ID)
	{
		return vec_datas[ID];
	}

private:
	std::vector<Data_type> vec_datas; // contient toutes les datas d'un type
};

class registery
{
public:
	template <typename Data_type>
	Data_type & get(unsigned int const ID)
	{
		static_cast<datas_container<Data_type>*>(vec_reg[ assign_ID_to_type:: template ID<Data_type>])->get(ID);
	}
private:

	class assign_ID_to_type
	{
	private:
	   	inline static unsigned int counter;
	public:
	    template<typename Type>
	    inline static const unsigned int ID = counter++;
	};

	std::vector<Base_DC*> vec_reg; // contient toutes les datas de tous les types
};


Evidemment ça ne peut pas fonctionner tel que c'est écrit là : il manque plein de choses (chez moi il y a déjà 500 lignes en 6 classes). Mais le principe très général c'est celui là. Et il compile et fonctionne ! Ce qui m'inquiète, ce sont juste les pointeurs nus et le cast.

-
Edité par Umbre37 5 juillet 2019 à 16:18:11

  • Partager sur Facebook
  • Partager sur Twitter
5 juillet 2019 à 21:53:58

Umbre37 a écrit:

Merci bcp Koala01 pour tout ce code. Tu as du y passer bcp de temps.

Je ne crois pas avoir passé plus d'une heure à la rédaction de ce message :D

Umbre37 a écrit:

Je la trouve tout de même un peu lourde à mettre en place.

A voir!!!  Si on fait le compte, les macros telles que définies ici nécessitent royalement 101 lignes de code (dont certaines auraient d'ailleurs encore pu être remplacée par l'inclusion d'un fichier.

D'accord, je ne t'ai as montré l'implémentation de ma classe component_list, que je n'ai d'ailleurs pas pris en compte dans ce nombre,  et que tu le met en relation avec le système similaire dont tu dis toi-même:

Umbre37 a écrit:


Evidemment ça ne peut pas fonctionner tel que c'est écrit là : il manque plein de choses (chez moi il y a déjà 500 lignes en 6 classes).

Même si j'avais besoin d'une centaine de liste de code pour implémenter ma component_list (et je suis sur qu'il ne me faudrait pas cela), que j'en rajoutais encore une centaine pour créer une key_list pour mettre en place un système de passe-partout et que je devais encore rajouter trois lignes à mon fichier d'implémentation pour prendre ce système en charge, je serais encore largement en boni par rapport au nombre de lignes qu'il t'a fallu pour y arriver ;)

Avant d'atteindre ton niveau de "lourdeur", je pourrais encore en ajouter, des macros (commpe, par exemple, la possibilité de créer une partie de composant sous la forme d'un tableau d'éléments, ou autres ;) )

De plus, le fait de disposer de valeurs énumérées permettant d'identifier chaque composant, au lieu d'une valeur calculée au travers de ta classe assign_ID_to_type et de n'avoir aucun besoin de recourir à un pseudo héritage de pacotille et au transtypage qu'il t'impose faciliterait énormément la vie de l'utilisateur du système.

Car, même si nos deux visions du problème souffrent de la même faiblesse (l'ordre d'inclusion des fichiers d'en-tête est essentielle au niveau de la compatibilité binaire entre les versions), la plupart des IDE seraient sans doute en mesure de savoir qu'il existe une valeur énumérée proche (selon mon code) de positionIndex, et même d'en évaluer la valeur ;)

Enfin, c'est très bien d'avoir des classes et des fonctions template, mais il ne faut pas oublier que le code binaire correspondant n'est réellement généré que pour les instances spécifiques de la classe, ou pour les appels spécifiques des fonctions ...

A priori, ton code nécessiterait donc plus de travail que le mien de la part de l'utilisateur, vu qu'il lui suffit, dans mon cas:

  • de créer un nouveau composant dans un fichier d'en-tête
  • d'inclure le nouveau fichier d'en-tête dans all_components.hpp

et le tour sera joué ;) .

Je ne dis pas que ton approche est mauvaise, loin de là! Je dis juste que mon approche n'est, en tous cas, certainement pas pire :D

  • Partager sur Facebook
  • Partager sur Twitter
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
5 juillet 2019 à 23:51:22

Non mais dans mes 500 lignes il y a la gestion des ID des entités, des fonctions pour faciliter l'utilisation des algos sur les components, un filtre pour sélectionner des collections, sparse_set... Enfin tout ça est en chantier...

Pour en revenir à mon approche, l'utilisateur n'a rien du tout à faire. Il n'a pas à s'occuper de l'ordre des déclarations de component ou de quoi que ce soit. Il fait juste reg.add<MonComponentPréféré>(entityID, MonComponentPréféré {}) et ici, je parle de l'ID de l'entité, pas du component, ça c'est géré tout seul en interne, à la compilation donc, coût nul à l'exécution, que du bonheur pour l'utilisateur (enfin j'espère). Aucun problème pour les différentes versions, on peut ajouter autant de types dans l'ordre que l'on veut ça ne change rien. D'ailleurs il n'y a rien à ajouter, puisque comme je l'expliquais, un simple add<Component>(entityID, cpnt) inscrit tout seul le type s'il n'est pas référencé.

Pour le "pseudo héritage de pacotille", c'est pas très gentils j'aimais bien mon idée :) mais tu as raison il m'impose le transtypage et c'est pas vraiment de l'héritage au sens de la POO, c'est plutôt une astuce pour contourner un problème.

J'aimerai justement revenir à ce transtypage. Je ne fais jamais ça d'habitude et je n'y connais rien. Est-ce que ça pose problème dans ce contexte ? Si oui quel genre de problème ? performance ? sécurité ? c'est ça ma question depuis le début.

En tout cas merci beaucoup de l'intérêt que tu portes au problème. Je vais vraiment me pencher sur ton idée, en fait ça me donne d'autre idées. Ce que moi je veux faire à la compilation, toi tu veux le faire au moment du traitement préprocesseur. Ta solution est plus souple même, parce qu'en copiant du texte avec des variables on peut générer toute sorte de code intéressant automatiquement. Il y a même certainement des cas un peu complexes où la déduction de type bloquerait, mais où ta solution permettrait de s'en sortir.

Cependant, j'aimerais bien avoir quelques infos sur le transtypage dans le contexte où je l'utilise. Pardon je suis tenace.

-
Edité par Umbre37 6 juillet 2019 à 1:29:33

  • Partager sur Facebook
  • Partager sur Twitter
6 juillet 2019 à 3:41:38

Umbre37 a écrit:

Non mais dans mes 500 lignes il y a la gestion des ID des entités, des fonctions pour faciliter l'utilisation des algos sur les components, un filtre pour sélectionner des collections, sparse_set... Enfin tout ça est en chantier...

La gestion des entités, avec espace de noms séparé, fonction libres déléguées, création, destruction, émission de signal à la destruction et recyclage, ca se fait en ... 78 lignes, si on dispose d'un système de signaux et de slots (qui ne demande que 207 lignes pour s'implémenter, mais qui n'a pas à entrer en ligne de compte dans ce projet, vu que c'est un outil tout à fait indépendant ; 479 si tu compte les informations de licences et autre commentaires destinés à la génération automatique de la documentation ;) )

Grâce à la macro qui permet de créer l'énumération, le filtre (qui n'est rien d'autre que ce que j'appelais "système de passe-partout") ne demande qu'une grosse soixantaine de lignes pour être implémenté.

Et tu peux me croire, je ne donne pas ces chiffres à la légère, et je tiens même les fichiers qui me les on fournis à ta disposition ;).  Bref, en 500 lignes de code, je me demande à quel point je serais avancé par rapport à toi :D

Umbre37 a écrit:

Pour en revenir à mon approche, l'utilisateur n'a rien du tout à faire. Il n'a pas à s'occuper de l'ordre des déclarations de component ou de quoi que ce soit. Il fait juste reg.add<MonComponentPréféré>(entityID, MonComponentPréféré {}) et ici, je parle de l'ID de l'entité, pas du component, ça c'est géré tout seul en interne, à la compilation donc, coût nul à l'exécution, que du bonheur pour l'utilisateur (enfin j'espère). Aucun problème pour les différentes versions, on peut ajouter autant de types dans l'ordre que l'on veut ça ne change rien. D'ailleurs il n'y a rien à ajouter, puisque comme je l'expliquais, un simple add<Component>(entityID, cpnt) inscrit tout seul le type s'il n'est pas référencé.

Mais, le problème de ton approche, si je la comprend bien, c'est que c'est ton entité qui s'occupe de maintenir les éventuels composants dont elle est constituée.  Et, si j'ai bien compris le système, pour y arriver, tu oblige ton entité à ... contenir un tableau (de pointeurs en plus, j'espère au moins qu'ils sont intelligents !!!) dont la taille correspond au nombre de composants possibles, avec comme effet de bord le fait que si tu as dix types de composants différents, tu te retrouve d'office avec un tableau de dix pointeurs, alors que, si cela se trouve, ton entité n'est composée que... de deux composants différents.

Autrement dit, tu oblige ton entité à contenir un tas de pointeurs dont la majorité risque de valoir nullptr, ce qui t'oblige, quoi que tu puisse en penser, à ... tester l'existence du composant dont tu as besoin  avant de pouvoir espérer en profiter.

Ce n'est, purement et simplement pas la bonne approche!

La bonne approche est de se dire qu'une entité se contente ... d'exister, et d'être identifiable.  Autrement dit, tu peux la limiter à sa plus simple expression : un identifiant qui sera unique, et rien de plus (et comme tu aurais du mal à faire moins ... :p ).

D'un autre coté, chaque type de composant doit pouvoir être maintenu dans une liste qui ne s'occupe de gérer que... ce type de composant particulier.

Autrement dit, si tu as 10 type de composants différents, tu dois avoir... dix liste de composants, chacune prévue pour contenir les composant d'un type particulier (et, accessoirement, permettre de faire le lien entre l'identifiant de l'entité et le composant qui s'y rapporte).

Umbre37 a écrit:

Pour le "pseudo héritage de pacotille", c'est pas très gentils j'aimais bien mon idée :) mais tu as raison il m'impose le transtypage et c'est pas vraiment de l'héritage au sens de la POO, c'est plutôt une astuce pour contourner un problème.

Ce n'est peut-être pas gentil, mais, avec le recul, tu te rendras compte que c'est justifié, car, si tu poursuis dans cette voie, le simple fait d'avoir voulu contourner un problème va t'en occasionner des dizaines d'autre, bien plus compliqués à résoudre, et pour lesquels tu finira par choisir un "pis aller" qui te permettra de les contourner, mais qui ouvriront eux aussi la porte chacun à une dizaine d'autres problèmes dont tu n'a même pas encore conscience aujourd'hui.

Et, au final, tu te retrouveras avec un truc monolithique, totalement ingérable, auquel tu auras peur de changer la moindre ligne de code car cela signifie faire planter le bousin à dix endroits (ou plus) différents du code

Umbre37 a écrit:

J'aimerai justement revenir à ce transtypage. <snip>. Est-ce que ça pose problème dans ce contexte ? Si oui quel genre de problème ? performance ? sécurité ? c'est ça ma question depuis le début.

Quel que soit le transtypage utilisé, c'est en réalité un mensonge que l'on fait au compilateur par lequel on veut le forcer à manipuler une donnée de type A comme s'il s'agissait d'une donnée de type B.

A ce titre, la solution ne doit -- quoi qu'il arrive -- n'être envisagée qu'en tout dernier recours, et encore : à condition de ... savoir exactement ce que l'on fait en mentant au compilateur, car:

  • reinterpret_cast est très certainement le plus dangereux, car il n'y a aucune vérification possible: tu peux faire passer une voiture pour un haricot si tu le veux... mais je doute que ta salade n'ait vraiment le gout espéré ;)
  • const_cast permet de jouer avec un invariant, une restriction forte qui est placée sur la donnée : elle ne peut normalement pas être modifiée. on ne peut donc faire sauter cette restriction que... si on est sur que la donnée en question ne se trouve pas dans une zone de mémoire "non modifiable".
  • static_cast apporte un tout petit peu de sécurité car il permet au moins de s'assurer à la compilation que le transtypage est possible, mais on n'a aucune garantie que, à l'exécution, le véhicule que l'on veut transtyper en voiture n'est pas une moto
  • dynamic_cast enfin est sans doute le transtypage qui offre le plus de garanties, car il occasionne une vérification à l'exécution. Mais cela veut dire :
  1. qu'il met (beaucoup!!!) plus longtemps à fournir un résultat, et que les performances s'en ressentent
  2. qu'il faut impérativement vérifier la validité du résultat avant d'essayer de le manipuler, sous peine de catastrophe

Umbre37 a écrit:

Je ne fais jamais ça d'habitude et je n'y connais rien.

Dans ce cas, j'ai envie de dire "c'est très bien, continue à ne pas  l'utiliser":waw::'(.

Il est vrai que, si les choses sont bien faites dans les règles, cela ne devrait pas poser de problème particulier dans ce contexte bien précis.

Seulement, où serait le plaisir si nous pouvions nous contenter d'une réponse aussi simple??? Car le seul moyen de pouvoir valider cette réponse serait ... de voir de quelle manière tu met le tout en oeuvre.  Et comme on n'a as vu la première ligne de code à ce sujet et que nos boules de crystal sont en rade, il nous est totalement impossible de nous faire la moindre idée qui ait une chance d'être correcte :'(.

-
Edité par koala01 6 juillet 2019 à 4:40:15

  • Partager sur Facebook
  • Partager sur Twitter
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
6 juillet 2019 à 7:42:59

koala01 a écrit:

La gestion des entités, avec espace de noms séparé, fonction libres déléguées, création, destruction, émission de signal à la destruction et recyclage, ca se fait en ... 78 lignes, si on dispose d'un système de signaux et de slots (qui ne demande que 207 lignes pour s'implémenter, mais qui n'a pas à entrer en ligne de compte dans ce projet, vu que c'est un outil tout à fait indépendant ; 479 si tu compte les informations de licences et autre commentaires destinés à la génération automatique de la documentation ;) )

C'est ton ECS ? Est-ce que tu as un git où je pourrais regarder un peu ou bien il est privé ?

koala01 a écrit:

La bonne approche est de se dire qu'une entité se contente ... d'exister, et d'être identifiable.  Autrement dit, tu peux la limiter à sa plus simple expression : un identifiant qui sera unique, et rien de plus (et comme tu aurais du mal à faire moins ... :p ).

D'un autre coté, chaque type de composant doit pouvoir être maintenu dans une liste qui ne s'occupe de gérer que... ce type de composant particulier.

Autrement dit, si tu as 10 type de composants différents, tu dois avoir... dix liste de composants, chacune prévue pour contenir les composant d'un type particulier (et, accessoirement, permettre de faire le lien entre l'identifiant de l'entité et le composant qui s'y rapporte).

Oui c'est exactement ce que je fais. un datas_container contient tous les components d'un type, sans cache miss. C'est dans le registery où il y a tous les components de toutes les entités de l'ECS,  qu'il y a un vector<Base_datas_container*>, le pointeur est un pointeur de component_holder si tu veux, pas de component. Une entité n'est donc bien qu'un unsigned integer.

Quant à l'approche que tu critiques, ce n'est pas la mienne, mais elle existe et fonctionne. On peut très bien concevoir une entité comme un agrégat de données de différents types. Regarde dans la video qu'a posté vac à 4 min 11.

koala01 a écrit:

Car le seul moyen de pouvoir valider cette réponse serait ... de voir de quelle manière tu met le tout en oeuvre.  Et comme on n'a as vu la première ligne de code à ce sujet et que nos boules de crystal sont en rade, il nous est totalement impossible de nous faire la moindre idée qui ait une chance d'être correcte :'(.

Ca vient :) il faut que je mette des choses en ordre.

-
Edité par Umbre37 6 juillet 2019 à 8:11:52

  • Partager sur Facebook
  • Partager sur Twitter
6 juillet 2019 à 20:22:30

Umbre37 a écrit:

C'est ton ECS ? Est-ce que tu as un git où je pourrais regarder un peu ou bien il est privé ?

A part le système de signaux et de slots, qui est sur mon répertoire git, avec exemples, tests unitaires et génération de documentation (seul le fichier Signal.hpp qui se trouve dans le dossier lib/Tools/include a de l'importance :D ), les fichiers que je te montre (et que j'ai créés en pas très longtemps pour ma dernière réponse) ne font même pas partie d'un projet réel.

Mais je peux t'en donner le code sans aucun problème ;)

Umbre37 a écrit:

Oui c'est exactement ce que je fais. un datas_container contient tous les components d'un type, sans cache miss. C'est dans le registery où il y a tous les components de toutes les entités de l'ECS,  qu'il y a un vector<Base_datas_container*>, le pointeur est un pointeur de component_holder si tu veux, pas de component. Une entité n'est donc bien qu'un unsigned integer.

 Comme Einstien le disait si bien:

Il faut rendre les choses aussi complexes que nécessaire, mais guère plus

Vu que chaque liste de composant est, par nature, totalement indépendante des autres (il n'y a, en définitive, que les services qui devront peut-être manipuler plusieurs composants de type différents), pourquoi voudrais tu créer une "liste de listes de composants"?

Est-ce que cela ne te semble pas "beaucoup plus complexe" que de créer... une liste de composant pour chaque type de composant que tu pourrait envisager?

Je comprend la raison qui t'inciterait à travailler de la sorte : elle te permettrait d'accéder à n'importe quelle liste de composant depuis à peu près n'importe où

Mais, seulement, cette approche t'oblige à créer la notion de liste de composant "sans précision", afin de pouvoir maintenir les listes de composant bien particulier qui en dériveront.

Et comme toutes tes listes de composant bien particulier seront maintenue en mémoire en étant connues comme "des listes de composant sans précision", tu te retrouves à devoir contourner le problème avec des techniques qui, bien utilisées, pourraient ne poser aucun problème, mais qui sont en réalité aussi sécurisante que le fait de se balader dans un champs de mines.

Or, il se fait qu'il est tout à fait possible de faire en sorte que les différentes listes de composant soient maintenues en mémoire de manière strictement indépendante, en étant connues comme des listes contenant des composants du type qu'elle contiennent réellement, et qui pourraient donc t'éviter d'entrer dans ce champs de mine.

Par exemple, tu pourrait créer la notion de "localisateur de services" (en anglais, on parle de service locator) qui permettrait de localiser chaque liste de composant de manière strictement unique (et indépendante de toutes les autres listes de composant) sous une forme (je fais au plus simple ici ;) ) proche de

/* on considère qu'il existe une classe
 * template <typename T>
 * class component_list ;
 * qui permet la gestion au quotidien des composants de type T
 */
template <typename T>
class component_list_locator{
public:
     using list_type = component_list<T>;
     static list_type & get(){
         return list;
     }
     static list_type const & get_const(){
         return list;
     }
private:
    static list_type list;
};
template <typename T>
component_list<T> component_list_locator<T>::list = component_list<T>{};

Une classe aussi simple que cela pourrait être avantageusement desservie par deux fonctions proches de

template <typename T>
component_list<T>& get(){
    return component_list_locator<T>::get();
}
/* et */
template <typename T>
component_list<T> const & get_const(){
    return component_list_locator<T>::get_const();
}

Mais ces fonctions ne nous sont en définitive pas particulièrement utiles, vu qu'elle obligeraient l'utilisateur à avoir recours à un code sans doute proche de

auto & la_liste = template get<position>();
/* on peut manipuler les composants de la liste ici */

En utilisant le système de macro que je présentais plus haut, nous pouvons générer toute une liste de fonctions dont le nom indiquera explicitement à quelle liste de composant nous voulons accéder et sous quelle forme, et, le mieux de tout, c'est que cela peut se faire de manière strictement automatique.

Il n'y a qu'à modifier un tout petit peu le contenu  de componetFunctions.hpp pour lui donner la forme de

#ifndef COMPONENTFUNCIONS_HPP
#define COMPONENTFUNCIONS_HPP
#define COMPONENT(componentName) \
void add##componentName(ID id, componentName && component); \
void remove##componentName(ID id); \
componentName & find##componentName(ID id); \
component_list<componentName> & get_##componentName##_list();  \
component_list<componentName> const & get_const_##componentName##_list();
#define COMPONENT_PART(partType, partName,componentName) \
void update##partName##from##componentName(ID id, partType && newValue);
#define COMPONENT_END
 
/* il n'y a plus qu'à ajouter all_components.hpp pour que la magie opère
 */
 
#include <all_components.hpp>
 
/* Et, pour éviter tout risque de contagion, à supprimer la
 * définition de ces symboles
 */
#undef COMPONENT
#undef COMPONENT_PART
#undef COMPONENT_END
#endif // COMPONENTFUNCIONS_HPP

pour que l'on puisse disposer de fonctions dont les noms (selon les deux exemples de composants que j'ai donné) soient aussi mélodieux que get_position_list, get_const_position_list, get_move_list ou get_const_move_list.

Mais bien sur, il faudra aussi modifier le fichier componentFunctions.cpp pour fournir l'implémentation de ces fonctions.  Ben, qu'est que l'on attend, faisons le et donnons lui la forme de

// je vais travailler en plusieurs étapes pour éviter les problèmes...
/* je n'ai plus besoin de m'assurer que les listes de composant existent...
 * je vais pourtant garder la numérotation, mais je vais aussi modifier 
 * le code des fonctions pour tenir compte de ces modifications  
 */
 
/* 2- j'implémente la fonction add  */
#define COMPONENT(componentName) \
void add##componentName(ID id, componentName && value){ \
    get_##componentName##_lits().add(id, value); \
}
#include <all_components.hpp>
#undef COMPONENT
 
/* 3- j'implémente la fonction remove */
#define COMPONENT(componentName) \
void remove##componentName(ID id){ \
    get_##componentName##_lits().remove(id); \
}
#include <all_components.hpp>
#undef COMPONENT
 
/* 4- j'implémente la fonction find */
#define COMPONENT(componentName) \
componentName &find##componentName(ID id){ \
    return get_##componentName##_lits().find(id); \
}
#include <all_components.hpp>
#undef COMPONENT
#undef COMPONENT_PART
 
/* 5- j'implémente les fonctions update */
#define COMPONENT(componentName)
#define COMPONENT_PART(partType, partName, componentName) \
void update##partName##From##componentName(ID id, partType && value){ \
    find##componentName(id).partName = value; \
}
#include <all_components.hpp>
#undef COMPONENT

/* 6- la fonction d'accès aux liste en lecture + écriture */
define COMPONENT(componentName) \
component_list<componentName> & get_##componentName##_list(){ \
    return component_list_locator<componentName>::get(); \
}
#include <all_components.hpp>
#undef COMPONENT
/* 7- l'accès en lecture seule aux listes de composant */
define COMPONENT(componentName) \
component_list<componentName> const & get_const_##componentName##_list(){ \
    return component_list_locator<componentName>::get_const(); \
}
#undef COMPONENT_PART
#undef COMPONENT_END

Et voilà, le tour est joué :D : dés que l'utilisateur introduira un nouveau composant (en utilisant les symboles que j'ai présentés et en incluant le fichier d'en-tête correspondant dans all_components.hpp) nous aurons les fonctions get et get_const correspondantes qui joueront leur role à la perfection ;)

Umbre37 a écrit:

Quant à l'approche que tu critiques, ce n'est pas la mienne, mais elle existe et fonctionne. On peut très bien concevoir une entité comme un agrégat de données de différents types. Regarde dans la video qu'a posté vac à 4 min 11.

Je n'ai jamais dit qu'elle est impossible ou qu'elle ne fonctionnerait pas ;) je l'ai critiquée à juste titre en essayant de mettre en exerge les problèmes qu'elle occasionne tout en restant concis.

Mais, si tu veux, je peux rentrer dans les détails du problème pour que tu le comprenne mieux:

Le probème d'une entité qui contiendrait ses propres composant commence dés la toute première question: comment pourrait on faire pour représenter l'ensemble des combinaisons de composants possibles?

La première solution qui pourrait venir à l'esprit, c'est de créer une hiérarchie de classes de différents type d'entités, sous une forme proche de

class BaseEntity{
    /* aucun intérpet */
};
class EntityWithComp1 : public BaseEntity{
private:
   Component1 c1;
};
class EntityWithComp2 : public BaseEntity{
private:
   Component2 c2;
};
/* ... */
class EntityWithCompN : public BaseEntity{
private:
   ComponentN cN;
};

Tant que chaque type d'entité n'aurait qu'un seul composant, nous pourrions nous en sortir.  Mais, quand il s'agira de commencer à  combiner plusieurs composants, le nombre de classes à créer va exploser.  On doit donc l'écarter d'office ;)

L'alternative consisterait à regrouper tous les composants au sein d'une collection de composants dont le contenu pourra évoluer au fil du temps.  Mais, pour que la magie opère, et que l'on soit en mesure de faire cohabiter un composant de type "position" avec un composant de type "mouvement", par exemple, il faut donc se mettre à créer une hiérarchie de classes dans laquelle chaque composant particulier dérive du même type de "composant sans autre précision".

Nous pourrions donc partir sur un code proche de

struct BaseComponent{
    /* toute classe intervenant dans une hiérarchie de
     * classes a forcément sémantique d'entité
     */
    BaseComponent(BaseComponent const &) = delete;
    BaseComponent & operator=(BaseComponent const &)= delete;
    virtual ~BaseComponent() = default;
    /* pour malgré tout permettre "une certaine forme"
     * de copie
     */
    virtual BaseComponent * clone() const = 0;
};
struct PositionComponent : public BaseComponent{
    /* ... sans intérêt*/
};
struct MoveComponent : public BaseComponent{
    /* ... sans intérêt*/
};
/* ... */
struct XXXComponent : public BaseComponent{
    /* ... sans intérêt*/
};

(Au fait, as tu remarqué le premier commentaire que j'ai placé, à propos de la sémantique d'entité ?  N'est-ce pas un comble d'utiliser un type de donnée qui présente une sémantique d'entité pour représenter une donnée qui est, par nature, destinée à représenter... des valeurs ???? )

Et, pour maintenir la liste des composants associés à chaque entité, nous avons en réalité deux solutions:

Soit, l'entité utilise une collection de pointeurs (forcément intelligents) qui ne contient que les composants associés à l'entité en question, et dont la taille varie en fonction des ajouts et des suppressions de composants qui surviennent;

Soit l'entité utilise une collection de pointeur (forcément intelligents) dont la taille (fixe) lui permet de représenter un pointeur sur l'ensemble des types de composants qui existent.

Quel que soit la solution envisagée, nous serons déjà confrontés à un problème majeur : chaque composant associé à une entité est connu comme étant ... "un composant sans autre précision", alors que, pour pouvoir manipuler nos composants, nous devons impérativement savoir... quel en est le type réel.

La première solution présenterait au moins l'avantage d'être sur (à condition, bien sur, d'avoir pris soin de supprimer les composants devenus inutiles) que tous les composants que l'on trouve dans la collection existent bel et bien pour l'entité avec laquelle on travaile. 

Mais elle présenterait l'énorme inconvénient que nous n'avons aucun moyen de prédire l'ordre dans lequel les différents composants apparaissent, car cet ordre dépendra des créations et des destructions de composants qui auront eu lieu pour l'entité concernée.

La deuxième soluton présenterait l'avantage que nous pouvons justement prédire le type de composant qui se trouve à une position donnée dans la collection: Si on décide que le composant de type "mouvement" se trouve en tout premier dans la collection, il sera toujours en tout premier dans la collection.

L'énorme inconvénient, c'est que l'on ne peut pas garantir qu'un composant particulier (par exemple, celui qui devrait se trouver en quatrième position dans la collection) existe bel et bien pour l'entité sur laquelle on travaille:'(.

Dans les deux cas, il n'y a rien de réellement insurmontable : on peut récupérer le type réel de chaque composant à l'aide du double dispatch (car je refuse de cautionner une logique qui se baserait sur le RTTI pour en définir le type réel), et, tant que l'on ne travaille que sur un type bien particulier de composant, il est "assez facile" de s'assurer de l'existance du composant en question.

Mais, encore une fois, je tiens à rappeler que les composants sont destinés à être utilisés de manière combinatoire : une entité (un arbre, par exemple) peut avoir une position, mais être fixe, alors que l'entité suivante (un oiseau) aura aussi une position, mais pourra se déplacer, et disposera donc aussi d'un composant de type "déplacement", qui devra être pris en compte pour calculer la position de l'oiseau de frame en frame.

La grosse difficulté consistant, justement, à fournir la logique permettant cette utilisation combinatoire.  Et je peux t'assurer que, en l'état, elle ne respectera jamais l'OCP.

Tant que tu en resteras avec deux ou trois composants et deux ou trois services, tu pourras sans doute encore arriver à t'en sortir, mais, dés que le nombre de composants et de services deviendra plus important, cela deviendra la galère intégrale :-°:'(.

  • Partager sur Facebook
  • Partager sur Twitter
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
7 juillet 2019 à 23:10:58

Merci encore de ce long message. Je vais m’absenter une semaine mais je voudrais tout de même réagir sur quelques points :

« Pourquoi une liste de liste de components  »

Alors en fait, il y a au moins une très bonne raison : pouvoir simplement supprimer une entité (tous ses composants). La classe de base possède en fait quelques méthodes virtuelles : toutes celles qui n’ont pas besoin de connaitre le type du component : bool has(ID), erase(ID), retours d’iterateurs sur la liste d’ID d’entité…Toutes ces méthodes n’ont pas besoin de static_cast. En fait la classe de base contient une liste d’ID. La dérivée template ne fait qu’y associer une liste de components d'un certain type. En effet, il peut être utile de pouvoir créer des listes d’entités sans data associée, pour faire subir à ces entités un comportement particulier. Par exemple parmi les entités qui ont une boite de collision, toutes ne sont pas clicables. Lorsqu’on veut qu’une entité soit clicable, on ajoute son ID à cette liste, sur laquelle on exécute un algo de collision avec la souris. C’est une caractéristique à laquelle aucune data particulière n’est associée. (on peut bien sûr faire autrement, ce n’est qu’un exemple pour dire qu’on peut imaginer des traitements sur des entités auquel ne correspond pas un component en particulier, ni même un ensemble de components).

Ensuite côté l'utilisateur, cela lui permet d'ajouter n'importe quel component, sans avoir à se préoccuper de l'ordre de déclaration de ses components par rapport à l'ECS. En effet, en réalité ce sont bien des listes de components différentes qui sont utilisées à l'exécution. Elles sont justes générées à la compilation automatiquement de manière template, et elles sont, il est vrai, utilisées via un pointeur (mais cela est caché à l’utilisateur, il n’a pas besoin de s’en occuper). Il y a aussi un système d'ID pour les types, en plus du système d'ID pour les entités (les deux étants séparés et très différents) , pour pouvoir "retrouver ses petits" :).

Pour le « champ de mine ». Tant que je m’assure de caster un type en lui-même (c’est juste qu’il est manipulé via un pointeur de la classe parente) où est le risque ?

Pour le design agrégat de components différents, j'aime pas ça. L'intérêt de l'ECS c'est aussi de pouvoir utiliser les algos à fond sur des collections homogènes et contigües, pour la simplicité d'approche et pour les performances. Traiter les carottes avec les carottes et les choux avec les choux :) En plus de tous les problèmes d'implémentation que tu évoques…

Cependant il y aurait peut-être moyen de faire un truc un peu fou en mélangeant les deux approches. Un entité pourrait être une collection de pointeurs sur des components rangés dans des listes homogènes et contiguës. Et on pourrait alors manipuler l'entité comme un objet, tout en ayant la possibilité d’accéder aux composants par type … ça ouvre des possibilités. Je pense que pour retrouver quelle entité possède quel component et tout ca, il doit exister des optimisation mathématiques avec des tables, des clefs, des signatures...  c’est surement complexe, je n'y connais pas grand chose et je ne prends pas cette direction là de toute façon... :)

PS: je ne suis qu’un amateur, je ne n’aurai jamais ta hauteur de vue sur le problème, ni une connaissance du langage aussi approfondie. J’essaye juste de partager mon point de vue et d’avoir quelques retours… PS: nous avions déjà parlé d’ECS il y a quelques mois/années, ma vision a pas mal évolué depuis, bien à toi.

-
Edité par Umbre37 7 juillet 2019 à 23:53:56

  • Partager sur Facebook
  • Partager sur Twitter
8 juillet 2019 à 3:13:36

Umbre37 a écrit:

Merci encore de ce long message. Je vais m’absenter une semaine mais je voudrais tout de même réagir sur quelques points :

« Pourquoi une liste de liste de components  »

Alors en fait, il y a au moins une très bonne raison : pouvoir simplement supprimer une entité (tous ses composants). La classe de base possède en fait quelques méthodes virtuelles : toutes celles qui n’ont pas besoin de connaitre le type du component : bool has(ID), erase(ID), retours d’iterateurs sur la liste d’ID d’entité…Toutes ces méthodes n’ont pas besoin de static_cast.

Je suis bien d'accord avec ce que tu dis, mais pas avec la conclusion que tu en tires car la virtualité d'une fonction a un cout énorme en terme de performances.  Or, les performances sont, justement, la pierre d'achopement qui nous incite à passer à une approche de type ECS.

De plus, il y a parfaitement moyen de s'en sortir avec une classe (liste de composant) template, n'ayant aucun besoin de recourir à des fonctions virtuelles, si on accepte l'idée de n'avoir par une liste de listes de composants.

Alors, il est vrai que j'ai parlé des macros, qui permettent d'automatiser la génération de code, mais, si, déjà, tu dispose de la notion de service locator dont je parlais plus haut et d'une liste de composant template (car toutes les listes de composant vont agir de la même manière), tu pourrais tout aussi bien avoir des fonctions libres (template) proches de

template <typename Comp>
void add(ID id, Comp && c){
    ServiceLocator<ComponentList<Com>>::get().add(id, c);
}
template <typename Comp>
void remove(ID id){
       ServiceLocator<ComponentList<Com>>::get().remove(id);
}

template <typename Comp>
void remove(ID id, Comp && newVal){
       ServiceLocator<ComponentList<Com>>::get().update(id, newVal);
}

qui pourraient même être utilisées "telles qu'elles" sous une forme qui serait proche de

/* Soit un composant de type "position" */
struct Position{
    /* ... sans intérêt */
};
int main(){
   auto id{createEntity()};
   addComponent(id, {/* ... */}); 
   /* ... */
   template remove<Positoin>(id); // faut tricher un peu ici 
}

Et, d'un autre coté, pour tout ce qui a trait au passe partout et autres joyeusetés, on pourrait parfaitement s'en sortir à l'aide d'un peu de meta programmation.

Par exemple, un simple

using AllComponents = MP::TypeList<Position, Mouvement, Vitesse, Couleur
                                   /*, et tous les autres composants créés*/>;

(où MP correspondrait à l'espace de noms dans lequel est développée la bibliothèque de meta programmation...  Si tu ne veux pas la développer, ce que je comprendrais très bien, sache qu'il en exsite d'excellentes, comme Boost::hana :)

pourrait tout à fait servir de base à tout ce pour quoi nous devons disposer d'une liste exhaustive des composants existants. Et, quand  viendra le moment de faire interagir les composants entre eux, cela pourra se faire à l'aide d'un code proche de

/* exemple parmis tant d'autre: déplacer les entités 
 * qui doivent l'être
 */
using MovingList =  MP::TypeList<Position, Mouvement, Vitesse>; 
/* faut voir ce que l'on veut faire, et que la bibliothèque
 * MP nous aide à faire 
 */

Umbre37 a écrit:

il peut être utile de pouvoir créer des listes d’entités sans data associée, pour faire subir à ces entités un comportement particulier. Par exemple parmi les entités qui ont une boite de collision, toutes ne sont pas clicables.

Nous sommes bien d'accord sur le fait que tous les composants n'ont pas forcément de données manipulables, et que, dans de nombreux cas, il nous suffit de savoir que le composant existe, tout simplement, voir, de pouvoir dresser la liste des entités qui disposent de ce composant.

Mais cela ne justifie toujours pas l'utilisation d'une liste de listes de composant!  Car ce problème  pourrait être beaucoup plus facilement résolu à l'aide d'un système "de clés et de serrures".

Pour faire simple: chaque entité existante disposerait d'une "clé" -- en réalité, cela se réduit à un gros bitset, donc chaque bit correspond à la présence (si le bit vaut 1) ou à l'absence (si le bit vaut 0) d'un composant particulier -- qui la suit tout au long de son existence, et qui est mise à jour chaque fois qu'on lui rajoute ou que l'on en retire un composant.

A coté de cela, chaque fois que l'on voudra faire interagir plusieurs composants entre eux, nous créerons une "serrure", qui est également un bitset, mais dont les bits qui valent 1 représente chaque fois un composant requis pour l'interaction.

Si tous les bits qui sont à 1 dans la serrure sont également à 1 dans la clé de l'entité qui nous intéresse, c'est que l'entité est susceptible de subir l'interaction que l'on envisage de mettre en œuvre.

Et, à ce titre, il "suffirait" de chercher toutes les entités dont le bit représente -- parmi d'autre particularités qui pourraient nous intéresser -- le composant "cliquable" (qui n'a pas de valeur à proprement parler) pour pouvoir en dresser la liste

Umbre37 a écrit:

on peut bien sûr faire autrement, ce n’est qu’un exemple pour dire qu’on peut imaginer des traitements sur des entités auquel ne correspond pas un component en particulier, ni même un ensemble de components

C'est surtout le parfait exemple qui justifie la mise en place d'un système de clé et de serrure (que j'appelais plus haut le "passe-partout" ;) ). Et qui, par conséquent, justifie pleinement le fait de dispsoser "quelque part" d'une liste exhaustive (et "ordonnée"pour la compatibilité binaire) de l'ensemble des composants existants, y compris ceux qui n'utilisent aucune données ;).

Umbre37 a écrit:

Ensuite côté l'utilisateur, cela lui permet d'ajouter n'importe quel component, sans avoir à se préoccuper de l'ordre de déclaration de ses components par rapport à l'ECS. En effet, en réalité ce sont bien des listes de components différentes qui sont utilisées à l'exécution.

Alors, pourquoi persister à vouloir les placer dans une liste de listes???

Umbre37 a écrit:

et elles sont, il est vrai, utilisées via un pointeur (mais cela est caché à l’utilisateur, il n’a pas besoin de s’en occuper).

Et pourquoi te casser la tête à faire du si compliqué là où tu pourrais faire si simple??

Umbre37 a écrit:

Pour le « champ de mine ». Tant que je m’assure de caster un type en lui-même (c’est juste qu’il est manipulé via un pointeur de la classe parente) où est le risque ?

Et si deux composants sont en réalité des alias sur le même type, tu fais quoi?  Car, juste pour rire, étais tu au courant qu'un code proche de
#include <iostream>
#include <type_traits>
int main()
{
    using a = int;
    using b = int;
    std::cout<<std::is_same<a, b>::value<<"\n";
}
affichera "1", démontrant que, pour le compilateur, a et b sont du même type?

Umbre37 a écrit:

Pour le design agrégat de components différents, j'aime pas ça. L'intérêt

de l'ECS c'est aussi de pouvoir utiliser les algos à fond sur des collections homogènes et contigües, pour la simplicité d'approche et pour les performances. Traiter les carottes avec les carottes et les choux avec les choux :) En plus de tous les problèmes d'implémentation que tu évoques…

Mais, justement: dans ce cas, pourquoi voudrais tu mettre ta caisse de carottes à coté d'une caisse de choux dans une pièce dans laquelle tu ne vois pas le bout de ton nez?  Selon toi, combien de chances as-tu de sortir la caisse de carottes alors que tu voulais celle de choux, si elles sont identiques au toucher?

Umbre37 a écrit:

Cependant il y aurait peut-être moyen de faire un truc un peu fou en mélangeant les deux approches. Un entité pourrait être une collection de pointeurs sur des components rangés dans des listes homogènes et contiguës. Et on pourrait alors manipuler l'entité comme un objet, tout en ayant la possibilité d’accéder aux composants par type … ça ouvre des possibilités. Je pense que pour retrouver quelle entité possède quel component et tout ca, il doit exister des optimisation mathématiques avec des tables, des clefs, des signatures...  c’est surement complexe, je n'y connais pas grand chose et je ne prends pas cette direction là de toute façon... :)

S'il y a effectivement deux approches à "mélanger", ce n'est pas -- contrairement à ce que tu sembles croire, l'approche "macro" que j'ai exposée jusqu'ici et l'approche orientée objet, dont toute ton argumentation semble démontrer que tu n'arrives pas à te débarrasser.

S'il y a deux approches à mélanger, c'est la méta programmation (l'utilisation des template et de tout ce qu'elle permet de faire) et des macros, pour générer le code adéquat automatiquement pour l'utilisateur ;)

Umbre37 a écrit:

PS: je ne suis qu’un amateur, je ne n’aurai jamais ta hauteur de vue sur le problème, ni une connaissance du langage aussi approfondie. J’essaye juste de partager mon point de vue et d’avoir quelques retours… PS: nous avions déjà parlé d’ECS il y a quelques mois/années, ma vision a pas mal évolué depuis, bien à toi.

Et on te répète depuis le début de cette discussion le seul point qui ait réellement de l'importance et que tu ne sembles pas déterminer à admettre : chaque composant est strictement indépendant des autres dans la manière dont il est représenté en mémoire, et la raison qui t'incite malgré tout à créer une hiérarchie avec les composant (être en mesure de créer une liste de listes de composant) est la pire de toutes!
  • Partager sur Facebook
  • Partager sur Twitter
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
8 juillet 2019 à 10:45:43

koala01 a écrit:

Et on te répète depuis le début de cette discussion le seul point qui ait réellement de l'importance et que tu ne sembles pas déterminer à admettre : chaque composant est strictement indépendant des autres dans la manière dont il est représenté en mémoire, et la raison qui t'incite malgré tout à créer une hiérarchie avec les composant (être en mesure de créer une liste de listes de composant) est la pire de toutes!


Donc ça pour toi c'est juste ce qu'il y a de pire :

https://github.com/skypjack/entt

si tu as le temps, regarde les classes sparse_set, storage, et la manière dont c'est utilisé dans le registry.

Je ne dis pas que je n'aime pas ton approche ou qu'elle est moins bien. Pas du tout ! ca me fait même découvrir des techniques, que je te remercie beaucoup de partager avec cette pédagogie.

Mais pourquoi vouloir m'imposer ton approche, sachant très bien que je suis parti sur un autre design que j'ai précisé dès le début. Moi je demande de l'aide pour comprendre et développer quelque chose dans ce sens là (à mon humble mesure), et en particulier des infos sur le transtypage. Je ne suis pas arrivé en disant : "je ne sais pas quoi utiliser comme architecture, aidez-moi". Et je me retrouve à devoir me justifier de faire comme si et pas comme ca, comme si il n'y avait qu'une bonne solution.

-
Edité par Umbre37 8 juillet 2019 à 11:11:35

  • Partager sur Facebook
  • Partager sur Twitter
8 juillet 2019 à 20:56:11

As tu remarqué que le dépot git n'est composé -- en dehors des tests et des exemples -- que de fichiers d'en-tête ?  Il y a une raison à cela : toutes les classes qu'il propose, y compris basic_registry et parse_set, sont des classes template.

Cela n'a l'air de rien, mais cet aspect est parmi les plus importants qui soient, et, manque de bol, tu n'en as pas forcément pris conscience.

Pourquoi ? simplement, parce que, si tu as deux types de composants (mettons, pour l'explication, le type A et le type B), basic_registry<A> et basic_registry<B> fourniront exactement les mêmes services, même si la première fournira ces services adaptés à un A et que la deuxième fournira ces mêmes services adaptés à un B. MAIS, et c'est le plus important: il n'y a absolument aucun lien entre basic_registry<A>  et basic_registry<B>.

Tu ne peux, par exemple, absolument pas envisager de les place "côte à côte" dans une collection quelconque.

Toi, ce que tu cherches à faire, c'est de créer une relation qui n'a absolument aucune raison d'être entre A et B, dans le but -- tout à fait inutile -- d'être en mesure de créer une liste de listes, et donc de les faire cohabiter côte à côte dans une collection de basic_registry.

Et tout le problème vient de là : tu n'as pas besoin de cette liste de listes, et, du coup, tu n'as pas d'avantage besoin de cette relation artificielle entre les composants ;)

Umbre37 a écrit:

Mais pourquoi vouloir m'imposer ton approche, sachant très bien que je suis parti sur un autre design que j'ai précisé dès le début.

Comprenons nous bien, je n'essaye absolument pas de t'imposer mon approche!  j'essaye juste de te convaincre d'une vérité qui ne pourra jamais être démentie : chaque fois que l'on prend des "libertés" avec les principes de conception sous prétexte que "c'est plus simple", on en paye un prix incalculable sur le long terme; le seul moyen de ne pas rajouter "plus de complexité que nécessaire" à un projet est, très clairement, d'avoir une conception sans faille qui en respecte les principes à la lettre.

Je ne prétend pas que l'approche que j'ai présentée est la meilleure.  J'en suis d'ailleurs très loin, tant je préfère très certainement l'approche suivie par le dépot git que tu utilises comme exemple ;) .  Mais mon approche -- et celle du dépot git -- présentent néanmoins un mérite incontestable : celui de démontrer très clairement qu'il y a d'autres solutions sur lesquelles se pencher.

Umbre37 a écrit:

Moi je demande de l'aide pour comprendre et développer quelque chose dans ce sens là (à mon humble mesure)

Mais la meilleure aide que nous puissions t'apporter n'est-elle pas de te faire prendre conscience des limites et des imperfections de ton approche, quitte à présenter des approche tout à fait différentes qui présente l'avantage de lever ces limites ?

Umbre37 a écrit:

et en particulier des infos sur le transtypage.

Et tu les as obtenues : le transtypage étant en soi un mensonge, tu dois impérativement  veiller à n'y avoir recours qu'en toute dernière extrémité, et à condition de savoir exactement ce que tu fais.

Dans le cas du static_cast, en particulier, les vérifications dont il est capables ne pouvant être effectuée qu'à la compilation, tu ne peux envisager d'y avoir recours que dans un domaine strictement statique (comprends : dans un domaine où toutes les vérifications peuvent se faire à la compilation).

Si tu introduis, de quelque manière que ce soit, la moindre trace de dynamisme avec du polymorphisme d'inclusion, de l'héritage et des fonctions virtuelles, C'EST FOUTU!!!

Umbre37 a écrit:

Et je me retrouve à devoir me justifier de faire comme si et pas comme ca, comme si il n'y avait qu'une bonne solution.

Aucune solution ne peut considérée mauvaise si elle permet d'obtenir le résultat escompté, à la condition expresse qu'elle permette d'éviter de se casser la gueule sur les "cas peau de banane".  Ce qui implique qu'il y a, effectivement, souvent plus d'une solution "correcte".

Mais, parmi les solutions "correctes" qui  existent, certaines sont beaucoup plus efficaces que d'autres.  Et ton but doit, justement, de trouver celle qui sera "la plus efficace" de ton point de vue.

Et, surtout, en marge de toutes les solutions "correctes", il y en a une flopée qui ne satisfont pas aux exigences, car il y aura toujours un "cas peau de banane", dont on n'a généralement pas conscience quand la solution en question, qui trainera à gauche ou à droite.

Le problème de ces "cas peau de banane" non suspectés, c'est qu'ils finiront tôt ou tard par apparaitre.  Avec "un peu de chance", il apparaitront très tôt après la prise de décision, en rendant possible le fait de reconsidérer la décision prise.

Mais, le plus souvent, ils apparaitront "trop longtemps après" la décision d'origine, car d'autres décisions, qui dépendent de l'approche choisies auront été prises.  Ce qui rendra beaucoup plus difficile le fait de reconsidérer la décision d'origine (car cela implique le plus souvent de renoncer à toutes les décisions qui ont été prises depuis); et qui nous obligera à prendre des décisions -- dont on aura sans doute conscience qu'elles sont "mauvaises" -- mais pour lesquelles on considérera "ne pas avoir le choix" car "il faut bien arriver à quelque chose".

Le problème de ces "mauvaises décisions", c'est qu'elles produiront par la suite un "effet boule de neige" car elles nous obligerons à prendre d'autres mauvaises décisions pour contourner les problèmes qu'elles auront généré, et ainsi de suite.

Voilà exactement ce que l'on appelle la dette technique : un tas de mauvaises décisions prises "parce que l'on n'avait pas le choix" pour contourner des problème qui sont apparus à cause d'autres mauvaises décisions.

La dette technique est le pire ennemi du développeur, quel que soit son niveau, car:

  • elle "s'auto alimente", dans le sens où elle oblige le développeur à l'alourdir s'il veut malgré tout arriver à un résultat
  • elle occasionne quantité de bugs dont le temps de résolution est pris sur le temps qu'il aurait pu passer à développer de nouvelles fonctionnalités
  • elle occasionne quantité d'incompatibilités qui nous empêchent de "faire les choses proprement"
  • elle fini par donner peur au développeur d'aller changer la moindre ligne de code, de peur  de "tout casser"

Il n'existe qu'un seul moyen connu pour, si pas arriver à éviter la dette technique, au moins pour arriver à  la garder "sous contrôle" et qu'elle reste en permanence à un niveau raisonnable : la conception doit être sans faille et les principes de conception suivis à la lettre et, à défaut, toute liberté prise avec ces principes doit impérativement être confinée à un domaine, à une portée la plus limitée possible.

En décidant de créer une base commune (au sens orienté objets du terme) à tous tes composants, tu prend une très sérieuse liberté avec le LSP (j'espère au moins que tu est convaincu de ce fait).  En soi, ce fait semble sans importance!  Si ce n'est que cette décision porte sur les fondations, sur les racines même de tout ton projet.  Comment veux tu qu'elle ne se répande pas "comme un trainée de poudre" à tout le reste?

Tu pourras me ressortir deux commandement du chef : "bah, YAKA" ou "bah, YAKAPA".  Et tu n'aurais sans doute pas tors, car "YAKA" ou "YAKAPA".  Mais tu oublierais sans doute la règle de l'emmerdement maximum: si quelque chose peut mal tourner, cela tournera forcément mal, et de préférence au pire moment qui soit".  Es-tu vraiment toujours décidé à prendre ce risque ?

-
Edité par koala01 8 juillet 2019 à 20:59:47

  • Partager sur Facebook
  • Partager sur Twitter
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
8 juillet 2019 à 22:54:08

Si je puis me permettre, tu as mal lu le code du git je pense.

La classe basic_registry prend en parametre template un type Entity (pas component )défini ici :

https://github.com/skypjack/entt/blob/master/src/entt/entity/entity.hpp

Il n'y a donc bien qu'un seul registry pour tous les types de component, le template sert juste à choisir quelle taille d'int tu veux pour tes ID.

Je vais te montrer où est la "liste de pointeur de liste de components" qui te fait tant horreur, il est vrai que c'est complexe mais j'ai passé pas mal de temps à lire ce code :

https://github.com/skypjack/entt/blob/master/src/entt/entity/registry.hpp

ligne 1676 tu as :

std::vector<pool_data> pools;

Qu'est ce qu'un pool_data ? réponse ligne 200 :

struct pool_data {
        std::unique_ptr<sparse_set<Entity>> pool;
        std::unique_ptr<sparse_set<Entity>> (* clone)(const sparse_set<Entity> &);
        void (* remove)(sparse_set<Entity> &, basic_registry &, const Entity);
        ENTT_ID_TYPE runtime_type;
    };

Qu'est-ce qu'un sparse_set<Entity> ? Je de te le donne en mille, c'est la classe de base d'un basic_storage, qui lui, est une liste de components d'un même type, défini ici :

 https://github.com/skypjack/entt/blob/master/src/entt/entity/storage.hpp

Conclusion, mon approche est exactement la même que la leur. Je ne vais pas te montrer tous les endroits où ils utilisent le transtypage. Un simple ctrl+f de "_cast" sur le code du registry en trouve 23. Devine à quoi ils servent...

Sinon, je ne puis qu'être d'accord avec tous les principes de gestion de projet que tu évoques ici sur la dette technique. C'est du bon sens.

Concernant l'architecture. J'ai essayé plein d'approches avant d'arriver à la conclusion que celle-ci me convenait le mieux, parce qu'elle est très souple, et résout beaucoup de problèmes. Elle nécessite cependant une grosse bouillie de template. Je n'avais pas imaginé ton idée avec les macros et je te remercie de me l'avoir partagée.

-
Edité par Umbre37 8 juillet 2019 à 23:32:00

  • Partager sur Facebook
  • Partager sur Twitter
9 juillet 2019 à 15:31:22

Umbre37 a écrit:

Si je puis me permettre, tu as mal lu le code du git je pense.

La classe basic_registry prend en parametre template un type Entity (pas component )défini ici :

https://github.com/skypjack/entt/blob/master/src/entt/entity/entity.hpp

C'est toi qui te trompe... Regarde bien les lignes 42 et 43 du fichier registry.hpp...: on y lit

template<typename Entity>
class basic_registry {

le mot clé typename que l'on voit avant le terme Entity nous indique qu'il s'agit bel et bien d'un paramètre template que l'on nomme, pour la facilité Entity, ce qui n'a rien à voir avec le type qui permet de représenter les entité, à savoir l'alias de type entity_type que l'on trouve dans les différents type traits nommés entt_traits.

Umbre37 a écrit:

Qu'est ce qu'un pool_data ? réponse ligne 200 :

struct pool_data {
        std::unique_ptr<sparse_set<Entity>> pool;
        std::unique_ptr<sparse_set<Entity>> (* clone)(const sparse_set<Entity> &);
        void (* remove)(sparse_set<Entity> &, basic_registry &, const Entity);
        ENTT_ID_TYPE runtime_type;
    };

Qu'est-ce qu'un sparse_set<Entity> ? Je de te le donne en mille, c'est la classe de base d'un basic_storage, qui lui, est une liste de components d'un même type, défini ici :

Attention, tu manques d'attention dans ta lecture, mais c'est compréhensible: j'ai du ouvrir le fichier dans un outils qui me permet "d'enrouler" les blocs de code pour m'en assurer ;) :

les structure pool_data (ligne 200), group_data (ligne 207) et ctx_variable (ligne 213) sont des structures internes à la classe basic_registry.

Bien que cela n'apparaisse pas au premier abord, elles dépendent donc du parametre template Entity de base_registry.

Si ces structures n'avaient pas été à usage strictement interne de la classe basic_registry, elles auraient pris les formes de

    template< typename Entity>
    struct pool_data {
        std::unique_ptr<sparse_set<Entity>> pool;
        std::unique_ptr<sparse_set<Entity>> (* clone)(const sparse_set<Entity> &);
        void (* remove)(sparse_set<Entity> &, basic_registry &, const Entity);
        ENTT_ID_TYPE runtime_type;
    };

de

    template <typename Entity>
    struct group_data {
        const std::size_t extent[3];
        std::unique_ptr<void, void(*)(void *)> group;
        bool(* const is_same)(const ENTT_ID_TYPE *);
    };

et de

    template <typename Entity>
    struct ctx_variable {
        std::unique_ptr<void, void(*)(void *)> value;
        ENTT_ID_TYPE runtime_type;
    };

D'ailleurs, cela se remarque très bien au niveau de pool_data, vu que sa seule donnée membre est de type ...std::unique_ptr<sparse_set>; ce qui indique que chaque pool_data peut disposer d'un sparse_set ... Ou non...

A la ligne 1676, c'est donc bel et bien comme si l'on avait un code équivalent à

template <typename Entity> // qui vient de basic_registry
std::vector<pool_data<Entity>> pools;

Alors, bien sur, je ne peux pas nier que sparse_set est une forme de liste.  Mais, c'est quoi en réalité???  La réponse nous est donné par le cartouche:

* Sparse set or packed array or whatever is the name users give it.<br/>
 * Two arrays: an _external_ one and an _internal_ one; a _sparse_ one and a
 * _packed_ one; one used for direct access through contiguous memory, the other
 * one used to get the data through an extra level of indirection.<br/>

Sparce set, ou packed array ou quel que soit le noms donné par l'utilisateur

Deux tableaux : un externe et un interne; un "clairsemé" et un emballé, un utilisé pour l'accès direct à de la mémoire contigue, l'autre utilisé pour obtenir la donnée au travers d'un niveau d'indirection suplémentaire.

(traduction approximative ;)

Et, si on y regarde bien, le tableau "clairsemé" va -- a priori -- contenir l'idenfiant de l'entité auquel la donnée se rapporte (typename traits_type::entity_type) et le tabeau "emballé" va contenir... l'index dans un tableau donné (using size_type = std::size_t;).

La classe basic_registry va donc effectivement contenir une liste de listes d'identifiants d'entités et une liste de ... liste d'index, dont certains élément de la liste peuvent ne pas exister, au travers de sa donnée pools.

Cela n'a rien à voir avec un liste de listes de composant!  Et encore moins, avec une liste étérogène de listes de composants différents!

Umbre37 a écrit:

Conclusion, mon approche est exactement la même que la leur.

Justement, non!  Il existe au moins deux différences majeures entre ton code et celui-ci!

La première, c'est que ce dépot git a une approche exclusivement statique, basée sur le polymorphisme paramétrique : toutes les vérifications, toutes les décisions, tout ce qui a trait -- de près ou de loin -- au choix du type d'une donnée se fait à la compilation; alors que la tienne introduit une part  -- même si ce n'est qu'un soupçon -- de polymorphisme d'inclusion (ce que l'on appelle couramment, par abus de langage "polymorphisme" au sens orienté objet, avec héritage et fonctions virtuelles), si bien que "certaines choses" ne pourront être déterminées qu'à l'exécution.

La deuxième, c'est que tu essayes de créer une liste (hétérogène) de listes de composant, alors que, sur le dépot git, on a une liste de composants qui peuvent (ou non) être associés à une liste d'indexes et d'entités.

Cela fait de fameuses différences, tu ne trouves pas ?

Umbre37 a écrit:

Je ne vais pas te montrer tous les endroits où ils utilisent le transtypage. Un simple ctrl+f de "_cast" sur le code du registry en trouve 23. Devine à quoi ils servent...

Et, sur ces 23 transtypage, combien y en a-t-il, selon toi, qui dépendent d'une manière ou d'une autre de ce qui se passe à l'exécution ???  Je peux te donner la réponse sans même avoir vérifié, vu que c'est un projet "header only" : aucun!

Pourquoii?  parce que tout se fait ... à la compilation!

Umbre37 a écrit:

Concernant l'architecture. J'ai essayé plein d'approches avant d'arriver à la conclusion que celle-ci me convenait le mieux, parce qu'elle est très souple, et résout beaucoup de problèmes. Elle nécessite cependant une grosse bouillie de template. Je n'avais pas imaginé ton idée avec les macros et je te remercie de me l'avoir partagée.

Alors, essaye un peu de te mettre à ma place:

Tu viens avec ce que j'appelerai volontiers une "fausse bonne idée".  Pourquoi est ce que je l'appelle comme cela, selon toi?  Hé bien parce que je ne doute absolument pas que tu considère ton idée comme excellente, mais que repère de graves erreurs de conception dans les premières phrases que tu utilise pour me l'exposer.

Je peux donc te garantir -- par expérience -- sans avoir besoin d'aller plus loin, que cette approche va littéralement te péter à la figure tôt ou tard. Je ne suis peut-être pas en mesure de te dire comment elle le fera, ni même quand elle le fera -- cela pourra arriver demain, ou dans trois mois, six mois ou un an -- mais je peux te garantir que c'est inéluctable.

J'ai donc un choix à faire :

L'une des possibilités qui m'est offerte est de te fournir la solution technique à ton besoin.  Ce serait cool de ma part, car cela te satisfairait sur le moment même. Mais quelles en seraient les conséquences?  Je peux t'en citer quelques unes:

  • Ca planterait les graines de nombreux problèmes qui n'apparaitront que "plus tard", et pour lesquels je me sentirais obligé de te donner un coup de main, vu que je t'aurais en quelques sortes encouragé dans ta solution (que je sais mauvaise);
  • cela te fera perdre un temps bête à essayer de résoudre les inombrables bugs qui se ne manqueront pas de se présenter et surtout
  • cela te laisserait prendre une habitude contre laquelle je passe mon temps à mettre les gens en garde

Bien sur, cela ne me dérange absolument pas de continuer à t'aider ponctuellement.  Et, pour être tout à fait franc, je m'en fous pas mal que tu passe plus de temps à débuger ton projet qu'à y apporter de nouvelles fonctionnalités.  Après tout, c'est toi qui perd ton temps sur ces conneries ;)

La dernière conséquence me pose beaucoup plus de problème, car

  • même si je me fous pas mal de ce que les gens pensent de moi, cela donne quand même l'impression d'une certaine inconsistance de ma part et
  • Si, "plus tard", tu décide de démarrer un autre projet pour lequel l'utilisation d'une liste hétérogène de liste d'éléments semble convenir, tu partira "naturellement" sur une solution sensiblement identique.  Et ca, c'est un très sérieux problème :'(.

L'alternative qui m'est offerte à tout cela, c'est d'essayer de te convaincre que ta solution est bancale et de t'inciter à revoir ta copie, de préférence avant que le projet ne soit trop avancé que pour qu'il soit presque impossible de faire "marche arrière".

C'est surement "moins cool de ma part", car la "satisfaction immédiate" est absente.  Et pourrant, c'est -- très certainement -- la solution qui posera le moins de problème sur le long terme.

Alors, dis moi: Si tu étais à ma place, quel pari "sur le long terme" ferais tu?  Celui de la satisfaction immédiate, malgré tous les problèmes que tu saurais par avance que cela occasionnera, ou celui de "tuer dans l'oeuf" une idée pour laquelle il n'y a finalement que quelques adaptations à apporter pour qu'elle se transforme en "vrai bonne idée" ?

A titre personnel, j'ai choisi la deuxième solution, car j'estime que ce sera largement plus bénéfique pour tous ;).  Libre à tous d'être d'un avis différent, mais qu'on ne vienne pas pleurer chez moi par la suite, car un "je l'avais bien dit" tombera inéluctablement ;)

Umbre37 a écrit:

Concernant l'architecture. J'ai essayé plein d'approches avant d'arriver à la conclusion

J'ai l'impression que tu saute très rapidement aux conclusions.

Car, je suis sur qu'il y a quantité d'approches que tu n'a pas encore essayées, et dont l'une ou l'autre conviendrait encore bien mieux ;)

-
Edité par koala01 9 juillet 2019 à 15:33:25

  • Partager sur Facebook
  • Partager sur Twitter
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
9 juillet 2019 à 22:36:42

Merci encore, d'avoir pris le temps de me lire et de me répondre. Mais là, je ne suis juste pas d'accord avec toi (tu vas en avoir assez de moi, j'en ai peur... :) ). Soit je suis complètement à coté de la plaque (c'est pas impossible), soit tu as mal lu (c'est compréhensible, tu as bien d'autres choses à faire que de t'occuper de mes histoires, et je te suis déjà très reconnaissant tu temps passé pour moi).

Mais, comment la démo basique donnée en exemple sur la page d'accueil serait-elle possible puisque le main ne déclare qu'un seul registry, et qu'il ajoute deux components de type différent dedans ?

https://github.com/skypjack/entt

int main() {
    entt::registry registry;
    std::uint64_t dt = 16;

    for(auto i = 0; i < 10; ++i) {
        auto entity = registry.create();
        registry.assign<position>(entity, i * 1.f, i * 1.f);
        if(i % 2 == 0) { registry.assign<velocity>(entity, i * .1f, i * .1f); }
    }

    update(dt, registry);
    update(registry);

    // ...
}

Comment expliques-tu la définition de la classe storage ici, ligne 50 :

https://github.com/skypjack/entt/blob/master/src/entt/entity/storage.hpp

template<typename Entity, typename Type, typename = std::void_t<>>
class basic_storage: public sparse_set<Entity> {//...

Je répète qu'un registry contient une liste de sparse-set, qui est la classe de base de storage. 

Si tu veux connaitre la différence entre un basic_storage et un storage, ou un basic-registry et un et un registry c'est ici que ca se passe :

https://github.com/skypjack/entt/blob/master/src/entt/entity/fwd.hpp

C'est pas compliqué, il n'y en a pas.

Si tu veux savoir ce qu'est le paramètre template <entity> dans ce contexte c'est ligne 56 :

// @brief Alias declaration for the most common use case.
ENTT_ENTITY_TYPE(entity, std::uint32_t)

Je te montre l'endroit où cette macro est définie :

https://github.com/skypjack/entt/blob/master/src/entt/entity/entity.hpp

#define ENTT_ENTITY_TYPE(clazz, type)\
    enum class clazz: type {};\
    constexpr auto to_integer(const clazz entt) ENTT_NOEXCEPT {\
        using traits_type = entt_traits<std::underlying_type_t<clazz>>;\
        return typename traits_type::entity_type(entt);\
    }}

C'est donc bien un entt_trait !! et pas un component.

Pour le cast, je sais bien que tout est static, je n'ai pas voulu faire autrement ! J'ai dit depuis le début que je voulais gérer ça à la compilation. Là où on est pas d'accord, c'est que pour toi, c'est incompatible avec l'héritage. Mais en tout cas eux le font ! et je ne pense pas que minecraft plante très souvent parce que l'ECS se mélange les pinceaux dans les types... (quoique c'est pas impossible tu me diras...)

Pour les autres approches, tu as raison, je n'ai essayé que ce qui me passait par la tête, c'est à dire pas grand chose :) ! mais j'ai cherché et j'ai découvert des choses ici et là sur le net. Pardonne-moi d'insister ainsi, mais je suis de bonne foi, si je pensais m'être trompé, je le reconnaîtrais volontier. 

Je pense avoir avoir bien analysé leur code, et je pense qu'il est très performant. Ce que je veux bien entendre, c'est que cette approche est trop compliquée pour mon niveau (ça c'est très possible). Il y a 47000 lignes dans le git, et vouloir implémenter une  version "light" à moi tout seul n'est peut-être pas réalisable... Tu peux aussi dire que tu n'aimes pas leur manière de faire (tu devrais si tu es cohérent avec toi-même).

PS: j'ai eu des soucis pour poster ce message, j'essayais d'insérer ça dans un balise c++ et le forum bloquait, j'ai mis un bout de temps à comprendre d'où ça venait en envoyant tout par petits bouts :

/*! @brief Alias declaration for the most common use case. */

-
Edité par Umbre37 9 juillet 2019 à 23:09:01

  • Partager sur Facebook
  • Partager sur Twitter
10 juillet 2019 à 3:19:44

Tu m'as fait hésiter, car je dois avouer que je n'avais pas étudié le code (et surtout les exemples) avec assez d'attention.

Et, de fait, la classe basic_registry n'aurait pas du exister, ou, du moins, pas sous ce nom, car c'est une énormité conceptuelle, même si on peut comprendre la raison qui a incité le développeur à la mettre en place.

Notes que ce qui est valable pour l'un (le développeur de entt) l'est aussi forcément pour l'autre (toi) sur ce coup ;)

Umbre37 a écrit:

#define ENTT_ENTITY_TYPE(clazz, type)\
    enum class clazz: type {};\
    constexpr auto to_integer(const clazz entt) ENTT_NOEXCEPT {\
        using traits_type = entt_traits<std::underlying_type_t<clazz>>;\
        return typename traits_type::entity_type(entt);\
    }}

Sais tu en quoi cette macro transforme ENTT_ENITY_TYPE(entity, std::uint32_t)? (au fait, tu as pris l'accolade fermante de l'espace de noms en copiant le code, et ca m'a fait douter quelques instants, le temps de me rendre compte de ton erreur ;)

Je me suis posé la question, et j'ai même demandé à mon compilateur de me confirmer que je ne m'étais pas trompé.

ENTT_ENITY_TYPE(entity, std::uint32_t)est remplacé par le préprocesseur par

enum class entity: std::uint32_t {
	
}; 
constexpr auto to_integer(const entity entt) ENTT_NOEXCEPT { 
    using traits_type = entt_traits<std::underlying_type_t<entity>>; 
	return typename traits_type::entity_type(entt); 
}

C'est très intéressant, car on y apprend que entity est une énumération forte dont la taille est forcée à 32bits, et qu'il existe une fonction constexpr qui permet de transormer les valeurs énumérées (dont aucune n'est explicitement désignée) en entier, mais cela n'apporte rien à l'affaire qui nous occupe ;)

(par contre, j'ai "corrigé" la mise en forme du résultat, car l'ensemble apparaissait sur une seule ligne, à cause du préprocesseur :p )

Umbre37 a écrit:

Là où on est pas d'accord, c'est que pour toi, c'est incompatible avec l'héritage.

Attention, car tous les termes comptes sur ce coup là...

Si j'ai parlé d'incompatibilité (si tant est que je l'ai fait, je pers un peu la mémoire ce soir :p ) , j'aurai dit qu'une approche strictement statique (au travers de laquelle toutes les vérifications sont faites et toutes les décisons sont prise à la compilation) est incompatible avec l'héritage d'inclusion (tel que mis en oeuvre dans une approche orienté objets), et, principalement avec les mécanismes propres aux fonctions virtuelles. (*)

Par contre, ce que je revendique, le point sur lequel je persiste et signe, c'est que tu n'as pas besoin d'introduire la moindre touche d'héritage d'inclusion dans un ECS, que cela ne fait qu'introduire une complexité inutile qui se retournera contre toi tôt ou tard.

(*) Je n'ai jamais dit, et je ne dirai jamais, qu'une approche générique est incompatible avec la notion d'héritage, sous quelle que forme que ce soit.  Bien au contraire, je compte sans doute parmi les gens les plus souples en termes d'héritage "bizaroïdes", car je ne rechigne à aucune forme, pour autant qu'elle soit correcte conceptuellement.

parmi les bizareries qui ne me font absolument pas peur et dont je suis "assez coutumier", tu trouvera, entre autres:

1- une classe template qui dérive (au sens orienté objets) d'une classe "normale", par exemple

class Visitor;
class Piece{
public:
    Piece(Piece const &) = delete;
    Piece & operator = (Piece const &) = delete;
    virtual ~Piece() = default;
    virtual void accept(Visitor const &) = 0;
};
/* une énumération sympa qui entre en ligne de compte */
enum class Type{
    pawn, 
    tower,
    knight,
    bishop,
    queen,
    king,
    MAX
};
template <Type t>
class ConcretePiece : public Piece{
public:
   void accept(Visitor const & v) final override{
        v.visit(*this); 
   }
};

(c'est, à peu de chose près, un code tiré de mon livre, mais de mémoire ;) )
2- de l'héritage (d'inclusion) multiple, sous une forme proche de

/** !!! aucun lien d'héritage entre B1 et B2 !!!! */
class B1{
    /*... */
}; 
class B2{
    /* ... */
};
class Derivee : public B1, public B2{
    /* ... */
};

3- Du CRTP, sous sa forme "classique" comme

template <typename T>
class Base{
    /* ... */
};
class Derivee: public Base<Derivee>{
    /* ... */
};

4- Du CRTP des forme plus complexes, utilisant l'héritage multiple

template <typename T>
class B1{
    /* ... */
};
template <typename T>
class B2{
    /* ... */
};
class Derivee : public B1<T>, public B2<T>{

};

5- de l'héritage multiple dont une classe de base est une classe "normale" est les autres ont recours au CRTP

class Base{
    /* ... */
};
template <typename T>
class B1{
    /* ... */
};
template <typename T>
class B2{
    /* ... */
}:
class Derivee : public Base,
                public B1<Derivee>,
                public B2<Derivee>{
    /*...*/
};

j'en passe, et sans doute de meilleures.

Et tu sais quoi? rien de tout cela ne me fait peur, parce que je veille en permanence à respecter les principes de conception.  Par contre, si tu me présente de telles approche, tu devra me convaincre que toi aussi tu respecte les principes de conception à la lettre avant que je ne puisse envisager de te laisser aller plus loin ;)

Umbre37 a écrit:

minecraft plante très souvent parce que l'ECS se mélange les pinceaux dans les types... (quoique c'est pas impossible tu me diras...)

Je n'ai jamais joué à minecraft, donc, je ne saurais pas dire quoi que ce soit à ce sujet.

Par contre, je peux te garantir que c'est le parfait contre exemple d'un ECS "digne de ce nom", et tu sais pourquoi? parce qu'il est écrit en java, et que tous les types, y compris les types primitifs, sont considérés commes des classes qui dérivent de manière implicite (directement ou non) de la classe Object.

Du point de vue purement conceptuel (LSP en priorité), c'est la pire aberration qui puisse exister ;)

Umbre37 a écrit:

Pardonne-moi d'insister ainsi, mais je suis de bonne foi, si je pensais m'être trompé, je le reconnaîtrais volontier. 

Il n'y a rien à pardonner, car ta bonne foi ne fait aucun doute, et que je ne la remet absolument pas en cause ;)

Ce que je met en doute, c'est ta capacité à prendre un "certain recul" par rapport à un code trouvé sur internet afin de l'évaluer de manière objective:

Au delà d'un code et d'une documentation "bien écrits" (ou au contraire "rédigés avec les pieds") et d'exemple qui fonctionneront toujours à la perfection, dans quelle mesure es tu capable de débusquer les erreurs conceptuelles non justifiées ou de comprendre la pertinence de la justification d'un écart face aux principes de conception ?

Pour être franc, je doute que, à ton niveau actuel, tu sois capable de prendre "tout le recul nécessaire".   Mais il n'y a aucune raison de t'en excuser, car, tu connais le proverbe

On ne peut pas être et avoir été

C'est le lot de tout débutant d'avoir des faiblesses, surtout dans un langage aussi complexe que le C++

Et c'est le lot de tout débutant d'entrer dans un cycle dans lequel il se rend compte de ses faiblesses de manière progressive, et dans lequel il tend à y remédier

En tant que débutant, tu ne devra jamais (*) te sentir obligé de t'excuser pour tes faiblesses, quelles qu'elles soient

(*) du moins, aussi longtemps que tu accepteras l'idée que les "plus habitués" risquent de te les signaler, et qu'il faudra alors essayer d'y remédier ;)

Umbre37 a écrit:

Ce que je veux bien entendre, c'est que cette approche est trop compliquée pour mon niveau (ça c'est très possible). Il y a 47000 lignes dans le git, et vouloir implémenter une  version "light" à moi tout seul n'est peut-être pas réalisable...

De mon point de vue, j'aurais tendance, au vu de tes questions, à penser que c'est effectivement le cas.  Mais ce n'est jamais qu'un ressenti personnel provoqué par notre discussion, et il doit donc être pris pour ce qu'il est : un ressenti personnel ;)

Car la seule personne capable de donner une réponse objective à ce sujet, je reste persuadé que c'est ... toi-même: dans quelle mesure arrives tu à comprendre l'ensemble du code? dans quelle mesure arrives tu à te créer une "grande image globale" des fonctionnalités éparpillée dans les différents fichiers et de leurs relations? Quelle est la proportion de ce que tu comprends par rapport à ce qui te passe "au dessus de la tête"?

Seule une réponse honnête à ces questions (que tu n'es pas du tout tenu de donner ici, que tu peux parfaitement garder pour toi) te permettra de trouver la "bonne réponse" ;)

  • Partager sur Facebook
  • Partager sur Twitter
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
10 juillet 2019 à 7:58:18

Bonjour Koala01, merci de ton message. Désolé pour l'accolade du namespace, j'ai pas fait gaffe. l'entt (le code du git) est utilisé par minecraft, c'est pour ca que j'en parle. Il n'y a pas que du java dedans... Je ne sais pas trop pourquoi ils s'en servent, mais ils s'en servent.

https://minecraft.net/en-us/attribution/

Ca m'impressionne un peu, j'ai du mal à me dire que c'est un mauvais exemple.

En fait, j'en étais arrivé à la même conception qu'eux (sur l'architecture générale dont nous avons bcp discuté ), avant de tomber sur ce git. Pourquoi ? simplement parce que je cherchais un moyen pour que l'ECS n'ait pas besoin de connaitre à l'avance les types ou le nombre des components. Je voulais pourvoir écrire quelque chose comme ca,

#include <MonSuperECS>

struct vitesse {
//...
};

struct position {
//...
};

int main()
{
    ECS ecs {};

    auto entity = ecs.creer_une_nouvelle_entité() ;

    ecs.ajouter_composant<vitesse>(entity,{ /* ...*/ }) ;
    ecs.ajouter_composant<position>(entity,{ /* ...*/ }) ;
}

Pour le rendre le plus modulaire possible.

Ton approche aussi a des "défauts de conception". Tes listes de components, déclarées par les macros sont plus ou moins des variables globales (des singletons ou des trucs statiques quelque part - dans ton component_list_locator). Et ça, on m'a appris que c'était mal... C'est d'ailleurs plus ou moins ce que j'avais fait au début (sans les macros, ni le locator). On m'a dit sur ce forum que c'était pas bien, j'ai donc cherché une autre voie... 

Pour répondre à la dernière partie de ton message sur mon niveau, il est vrai que je ne comprends pas tout le code. Je comprends bien l'architecture et le principe général. Je suis content de me frotter à des choses qui me fassent progresser un peu aussi :) Il faut tout de même avouer que je le trouve assez complexe (je parle de celui du git).

-
Edité par Umbre37 10 juillet 2019 à 21:36:51

  • Partager sur Facebook
  • Partager sur Twitter
10 juillet 2019 à 9:02:13

Umbre37 a écrit:

Et ça, on m'a appris que c'était mal...

Il ne faut pas se fier aux "c'est mal".  Il y a des outils techniques (les macros, les pointeurs, les gotos, les singletons etc) qui peuvent être mal employés, et le sont d'ailleurs la plupart du temps par les gens qui ne font pas gaffe, et qui ne savent pas qu'il y a de meilleures solutions, ou qui ont la flemme.

Après, il y a des cas où c'est la meilleure solution, sur des critères de lisibilité, performance, etc.  Il faut donc faire preuve de discernement, parfois.

Le "c'est mal" doit être accompagné d'une explication, autre que "et c'est comme ça" ou "c'est machin qui le dit". Quand l'explication ne s'applique pas au cas considéré, le "c'est mal", OSEF.



-
Edité par michelbillaud 10 juillet 2019 à 9:04:57

  • Partager sur Facebook
  • Partager sur Twitter
10 juillet 2019 à 13:43:28

Umbre37 a écrit:

Ton approche aussi a des "défauts de conception". Tes listes de components, déclarées par les macros

Mais comprend bien que je n'essaye absolument pas d'imposer mon approche à base de macro.  Pour être honnête, c'est sans doute la pire approche qui soit et qui m'ait été donné de proposer.

Mais cette approche était la réponse à une phrase de ta part qui indiquait clairement ton souhait de simplifier au maximum la vie de l'utilisateur et d'automatiser le plus de choses possibles

Et, sur ce point au moins, l'approche présentée est infaillible, surtout si surtout si elle est soutenue par un coeur basé sur une bonne base de classes template et une bonne dose de métaprogrammation ;).

Umbre37 a écrit:

des variables globales (des singletons ou des trucs statiques quelque part - dans ton component_list_locator). Et ça, on m'a appris que c'était mal... C'est d'ailleurs plus ou moins ce que j'avais fait au début (sans les macros, ni le locator).

Et on a raison de le dire que "ces mal" à un débutant!!!Parce que ce sont, effectivement des techniques qui présentent un risque majeur d'être mal utilisée, avec des conséquences catastrophique

D'ailleurs, tant que le débutant en question n'a pas la curiosité d'en savoir un peu plus sur la règle, c'est que cela continuera à "être mal".

Mais, à force d'expérience, le débutant se transformera en développeur, et il commencera à se rendre compte qu'aucune règle trop rigide ne survit très longtemps à la dure réalité des circonstances auxquelles il est confronté, et il commencera donc à se poser des questions aux sujet de ces règles.  Peu de temps après, il trouvera (généralement) lui-même les réponses à ses questions, et cela indiquera qu'il est -- a priori -- suffisamment sur de lui et de ce qu'il fait pour pouvoir y déroger ;)

Au risque de faire une comparaison choquante, c'est un peu le même principe que pour le dressage d'un chien : les premières séances, on va tenir la laisse près du collier, parce que le chien aura envie d'aller attaquer (ou jouer avec) les autres. 

Au fur et à mesure qu'il comprendra "qu'il ne peut pas" aller attaquer ses congénères, nous pourrons lui "lacher la bride" de plus en plus.

Et, un jour, nous saurons que, quoi qu'il arrive, (même si un chien qui vient pour la première fois vient aboyer à vingt centimetre de lui) le chien restera fidèlement au pieds de son maître (à moins que le maitre lui dise de faire autre chose ;) ), et nous pourrons lui retirer la laisse.

En disant "c'est mal", on t'incite à te... tenir toi-mêmeau collier.  A toi de te démontrer que tu es digne de confiance, et que tu peux progressivement te lâcher la bride et, au final, te retirer la laisse ;)

Quelques points plus spécifiques:

Concernant les macros : cette entrée de la faq de isocpp(et les quatre lien: #evil1, #evil2, #evil3 et #evil4 qui s'y trouve) et cet article de Karpov (sur le même site) pourraient t'intéresser au plus haut point ;)

le DP "singleton" et le DP "service locator" sont effectivement très proches. Mais l'un est une aberration, au point qu'on le considère souvent comme un anti pattern alors que l'autre est "la moins mauvaise solution" à un problème qui n'en présente aucune bonne.  C'est dans ce cadre très particulier que je te l'ai présenté ;)

Les variables statiques sont, effectivement, des variables globales bien enrobées.  Mais, encore une fois, on n'a parfois pas vraiment le choix, et tout dépend alors de l'enrobage que l'on donne à ces variables ;)

Umbre37 a écrit:

Pour répondre à la dernière partie de ton message sur mon niveau, il est vrai que je ne comprends pas tout le code. Je comprends bien l'architecture et le principe général.

Merci, tu confirmes mon ressenti général :D

Umbre37 a écrit:

Il faut tout de même avouer que je le trouve assez complexe (je parle de celui du git).

Et pour cause, entre les 75 000 lignes de code (selon tes propres chiffres, je te crois sur parole :D ) et les techniques mises en œuvre, il y a largement de quoi le trouver "assez complexe", même si il n'entre encore que dans la catégorie des projets de "complexité moyenne supérieure" de par son nombre de fichier et de lignes de code :D
  • Partager sur Facebook
  • Partager sur Twitter
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
11 juillet 2019 à 6:08:12

Tes liens sur la faq isocpp et l'article ne sont pas passés. Peux-tu me les renvoyer stp ?

Dans le git il y a 45k lignes pas 75k (il y a un compteur sur la page d'accueil, c'est pas moi qui les ai comptées :) ), et je pense que pour les 3/4 c'est du commentaire ou des exemples. Au final en code "pur" il n'y en a pas tant que ça...

Pour les variables globales, tu remarqueras qu'avec mon approche, aucun component n'est statique, pas d'objet global ou autre... C'est aussi la force de cette solution, même si elle a tous les défauts dont tu parles.

Je vois bien ce que tu veux dire avec ton analogie canine. J'ai moi-même enseigné, dans d'autres domaines.

J'ai terminé mon projet dans les grandes lignes. Il fonctionne. Je vais le peaufiner et peut-être en poster par petits bouts ici (oui je sais je suis un peu maso... :lol: ). Pour l'instant, j'arrive à retrouver les caisses de choux et de carottes dans le noir...

Encore merci.

-
Edité par Umbre37 11 juillet 2019 à 9:09:28

  • Partager sur Facebook
  • Partager sur Twitter