IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

 C++ Discussion :

Gestion mémoire et découpage


Sujet :

C++

  1. #1
    Membre éclairé Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Par défaut Gestion mémoire et découpage
    Bonjour à tous,

    Au cours de mes "exercices", je me suis posé la problématique suivante : "Comment découper un vecteur (ou mieux, une plage mémoire donnée) de données contigües en plusieurs vecteurs différents, SANS perdre pour autant la contiguïté de la plage de données initiale ?"
    Je ne le cacherais pas, l'objectif derrière tout ça est de pouvoir manipuler des portions de la mémoire allouée initialement de manière indépendante, et permettre une itération sur l'ensemble des données initiales, le tout sur une plage de données contiguës.

    Dans un premier temps, je considère que, lors de l'ajout d'un élément d'un "sous_vecteur", l'ensemble des données suivantes sont simplement poussées les unes après les autres.

    Pour ce premier aspect, je visualise plutôt bien les choses. Là où ça se gâte, c'est comment m'assurer que l'ensemble des vecteurs liés à la "Base" (le block de mémoire contenant l'ensemble des éléments) sont bien systématiquement décalés ?

    Pour réaliser ça, j'ai tenté de procéder de deux manières différentes :
    - en manipulant des pointeurs bruts... Méthode classique (ou presque)
    - en manipulant des vecteurs (std::vector).

    J'ai donc réussi à peu près à atteindre l'objectif, mais les implémentations générées ne me plaisent pas du tout, le système pondu semble à la fois fragile et hyper rigide à l'utilisation, bref, je dois refondre le code de A à Z.
    Je suis très surpris de me retrouver à buter sur un tel sujet qui me semble, dans l'idée en tout cas, très simple....

    Mes questions sont donc les suivantes :
    - Que pensez-vous des méthodes proposées ?
    - connaissez-vous des méthodes alternatives plus simples à mettre en œuvre ?
    - Sauriez-vous m’orienter vers des sources d'inspiration pour implémenter un tel système ?

    Bref, toute aide est la bien-venue... Pour aider un peu la compréhension, voici, in fine, comment je vois l'utilisation de cet outil :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    global_memory<int> base;
    sub_vector<int> v1 = base.generate_vector(); // renvoie un vecteur(ou un simple tableau, peu importe) pointant uniquement sur une portion de "base"
    sub_vector<int> v2 = base.generate_vector(); // typiquement, v2.begin() == v1.end() == &base[v1.size()]
     
    v1.push_back(3);	// ici, v1.size() augmente, donc, v1.end() aussi, donc, v2.begin() aussi !
    v2.push_back(5);	// ici, seul v2 est affecté, car v1 a été initialisé avant... 
     
    sub_vector<int> v3 = base.generate_vector();	// là aussi, v3.begin() == v2.end() == &base[v1.size() + v2.size()]

    Espérant trouver une solution aisée à mettre en œuvre...

  2. #2
    Membre Expert
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2011
    Messages
    767
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Hérault (Languedoc Roussillon)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juin 2011
    Messages : 767
    Par défaut
    En fait ce que tu veux, c'est une vue mutable. Généralement, une vue sur une séquence ne change pas la taille, mais agrandir la séquence d'origine invalide la vue lorsqu'il y a une nouvelle allocation, car la vue ne modifie pas sa plage de valeur et reste donc avec l'ancienne (qui est supprimée). Faire une synchronisation avec les vues et le segment d'origine complexifie grandement le problème.

    Une manière de faire, et c'est peut-être celle que tu as choisie, consiste à avoir l'ensemble des vues à l'intérieur du l'objet global et les sub_vector contiendrait 2 pointeurs: un sur la position de la vue, l'autre sur l'objet global. Comme ça lorsque l'objet global ajoute des éléments, il peut actualiser chaque intervalle des sub_vector.

    Après il y a toute une problématique de durée de vie des sub_vector et comment ils doivent se comporter lorsqu'ils sont détruits dans le désordre si cela est autorisé. En plus, la synchronisation prend probablement plus de temps qu'un simple ajout et concaténer tous les vecteurs peut s'avérer plus efficace. Je pense que pour avoir quelque chose qui fonctionne bien il faudrait savoir exactement quelle est la problématique et quelles sont les contraintes.

    Par exemple, si le but et parcourir plusieurs conteneurs comme un seul, alors la solution est un itérateur spécialisé. Si le nombre de vue est connue, le problème est simplifié. Idem si le nombre d'élément total est connu. Permettre la synchronisation post-insertion sera plus efficace que le faire pour chaque insertion. Etc

  3. #3
    Membre éclairé Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Par défaut
    Bonjour, et merci beaucoup pour ce retour, qui tombe justement.... Au moment où je viens d'arriver à faire une implémentation potable et fonctionnelle (mais qui reste à finaliser et à refaire au propre... c'est plus un "proof of concept" pour le moment) !

    Pour la problématique, c'est simplement dans la continuité de mon passe-temps actuel qui se résume à faire un ECS adaptable au besoin. J'ai bien conscience que la solution que je propose ici n'en est qu'une parmi tant d'autres qui peuvent probablement être plus simples, mais dans toutes les versions d'ECS que j'ai réalisé, je me trouvais finalement à devoir ajouter un composant à une entité juste pour pouvoir déterminer son type, et finalement devoir itérer sur le type plutôt que sur les composants réellement importants. Par exemple, pour reprendre l'exercice de javaquarium, si je souhaite n'afficher que le nom des poissons et non des algues, je me retrouve à devoir itérer sur le type, puis sur le nom, ce que je trouve idiot (et encore plus si on ne souhaite afficher que les poissons qui ont un nom précis, un sexe précis, un age précis etc...).
    Avec la solution proposée ici, je pourrais itérer, soit sur tous les noms de la base de données, soit uniquement sur les noms des poissons, soit uniquement sur les noms des algues... Et dans tous les cas, les données sont continues ce qui limite les cache-misses, et malgré tout l'outil reste utilisable comme les ECS standard que j'ai réalisé jusqu'à présent si c'est ce que l'on souhaite.

    En fait, comme tu dis, j'ai réalisé un tableau dynamique basique (en gros, j'ai ré-implémenté une sorte de std::vector mais avec le strict minimum nécessaire), qui contiens aussi un tableau de mini-structures contenant uniquement un offset (de type T*) et une taille.
    Compte tenu du fait que les "sous_vecteur" sont nécessairement reliés à un tableau global (baptisé "memory" dans mon implémentation actuelle), alors c'est cette classe "memory" qui va attribuer un id unique aux sous_vecteurs, id qui sera en réalité l'index correspondant du tableau de mini structures.

    en gros :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    template<typename T>
    class sub_vector
    {
     
    private:
     
    	std::size_t idx;
    	memory<T>* mem;
     
    public:
    	sub_vector(std::size_t const & id, memory<T>* m_): idx{id}, mem{m_} {}
     
    };
     
    template<typename T>
    struct frame
    {
    	T* offset;
    	std::size_t size_;
    };
     
    template<typename T>
    class memory
    {
    private:
     
    	T* values_;
    	std::size_t capacity_;
    	std::size_t size_;
     
    	std::vector<frame<T> > frame_;
     
    public:
     
    	memory(std::size_t const & new_capacity): values_{m_alloc().allocate(new_capacity)}, 
    											capacity_{new_capacity}, size_{0}
    	{
    	}
     
    	sub_vector<T> create_vector()
    	{
    		frame_.push_back(frame<T>{end(), 0});
     
    		return sub_vector<T>{frame_.size()-1, this};
    	}
     
    };
    Je mettrais un code complet quand je l'aurais refait au propre, mais les quelques lignes ci-dessus illustrent à peu près les interactions entre ces classes.

    Concernant l'ajout d'éléments, le code reste là aussi très simple, et consiste à "swap" les éléments de début et de fin de chaque frame jusqu'à ce que l'élément appartienne à la bonne frame :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    void push(std::size_t const & id, T const & val)
    {
    	assert(is_valid_table(id) && "no created id");
     
    	values_[size_] = val;
    	size_++;
     
    	for(auto i = frame_.size()-1; i != id; --i)
    	{
    		swap(end(i), begin(i));
    		frame_[i].offset_ = ++;
    	}
     
    	frame_[id].size_++;
    }
    A savoir, les méthodes begin() et end() sont surchargées de manière à retourner respectivement le début et la fin de chaque frame.

    En fin, à l'utilisation, je retrouve le code suivant :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    int main()
    {
    	memory<int> global;
    	//global.push(0, 5);  // peut aussi être utilisé en dirrect
     
    	sub_vector<int> mv = global.create_vector();
    	mv.push(12);
    	mv.push(87);
     
    	sub_vector<int> cv = global.create_vector();
    	cv.push(128);
    	cv.push(692);
     
    	mv.push(57);
    	mv.push(36);
     
    	cv.push(458);
     
    	for(auto it {mv.begin()}; it != mv.end(); ++it)
    	{
    		std::cout << *it << std::endl;
    	}
     
    	std::cout << std::endl;
    	for(auto it {cv.begin()}; it != cv.end(); ++it)
    	{
    		std::cout << *it << std::endl;
    	}
     
    	std::cout << std::endl;
    	for(auto it {global.begin()}; it != global.end(); ++it)
    	{
    		std::cout << *it << std::endl;
    	}
     
     
    	return 0;
    }

    Et ça fonctionne bien. L’opérateur operator[] est bien entendu lui aussi intégré.


    Il me reste à implémenter la partie "suppression" des éléments, et aussi, comme tu le soulignes, des sub_vector.
    Pour le premier point, sans trop se casser la tète, ce qui me vient à l'esprit, c'est la même méthode que push() mais en fonctionnement inverse... Il me reste à réfléchir à ça, mais là comme ça, cela devrait marcher.
    Pour supprimer une table et bien.... La aussi je dois y réfléchir, mais à mon avis, le but est de supprimer tous les éléments de la table un par un selon la méthode proposée ci-dessus (donc O(log(n)) malheureusement). Sinon, une méthode un peu plus brutale (surtout si on a beaucoup de sub_vectors et beaucoup d'éléments): une réallocation de toute la table globale...

    Pour tes derniers points d'interrogation, tout doit être "dynamique" donc nombre de sub_vector non fixé, nombre d'éléments non fixés etc.... Pour cette raison, l'allocation initiale de base de la table "globale" doit être tout de même assez juste si on ne souhaite pas tout ré-allouer à la volée toutes les deux minutes.
    Techniquement, pour les applications que j'imagine, je ne devrais pas avoir à supprimer de sub_vector très souvent. Je pourrais effectivement presque être capable de déterminer leur nombre à l'avance, mais j'aime autant que ce soit dynamique.


    Selon vous, cette méthode semble-t-elle viable pour la suite ?

    Merci.

  4. #4
    Rédacteur/Modérateur


    Homme Profil pro
    Network game programmer
    Inscrit en
    Juin 2010
    Messages
    7 157
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 38
    Localisation : Canada

    Informations professionnelles :
    Activité : Network game programmer

    Informations forums :
    Inscription : Juin 2010
    Messages : 7 157
    Billets dans le blog
    4
    Par défaut
    std::span, disponible en C++20.
    Pour le moment, tu peux créer ton propre truc, c'est rien de plus que
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    template<class T, size_t size>
    class VectorView
    {
    T* mData;
    size_t mDataSize;
    };
    Mais bien sûr les push_back suivant peuvent invalider les pointeurs.
    Si tu veux pouvoir créer ces vues puis push_back, préfère un
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    template<class T, size_t size>
    class VectorView
    {
    public:
    VectorView(vector<t>& vec, size_t first, size_t size);
    T& operator[](size_t idx) { assert(idx >= mOffset && idx < mOffset + mDataSize); return mData[mOffset + idx];
    private:
    std::vector<T>& mData;
    size_t mOffset;
    size_t mDataSize;
    };
    Pensez à consulter la FAQ ou les cours et tutoriels de la section C++.
    Un peu de programmation réseau ?
    Aucune aide via MP ne sera dispensée. Merci d'utiliser les forums prévus à cet effet.

  5. #5
    Membre éclairé Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Par défaut
    Bonjour,

    Oui, ça ressemble effectivement beaucoup à std::span !

    En ce qui concerne ta proposition d'implémentation, je suis justement en train de voir comment je peux faire avec des std::vector plutôt que des pointeurs bruts.
    L'idée de l'opérateur [] directement dans le vectorView est bonne, cependant, lorsque l'on ajoute où supprime des éléments, des swap sont réalisés (j'ai ajouté la fonctionnalité de suppression hier soir btw). Il est donc nécessaire que l'accès aux éléments du vectorView soient indexés.

    Dans le bout de code que j'ai présenté hier, j'ai donc dû ajouté un index aussi :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    template<typename T>
    	struct frame
    	{
    		std::size_t id_;
    		T* data_;
    		std::size_t size_;
     
    		std::vector<std::size_t> index{};
    	};
    Ca marche, mais le code final au complet reste "moche" (ressemble beaucoup à de l'hybride C - C++, j'aime pas trop ça...).
    Je vais voir ce que j'arrive à faire en ne me basant que sur des std::vector ; car au final, je ne vois rien qui justifie de conserver un tableau dynamique de type C.

    Merci pour ce retour !



    EDIT : je remarque aussi que dans la solution proposée, on se retrouve, dans le vectorView, avec une référence sur le vecteur complet. Ceci n'est-il pas trop gourmand au niveau de la RAM ?





    Bon, j'ai refait l'ensemble du système, avec des std::vector cette fois ci.
    J'y gagne un tout petit peu en terme de lisibilité, mais c'est tout de même bien améliorable. Je me demande si le code ne devrais pas être splité en plus de classes.
    Pour le moment j'ai :
    • une classe sector : celle qui gère l'allocation des secteurs en ram (les "grands" vecteurs principaux)
    • une classe frame : la classe sector contiens un vecteur de frame. C'est en fait les "vectorView". Cette classe contiens aussi l'indexe des données qui lui sont liées
    • une classe fragment : correspond à un fragment de chaque secteur. C'est ce que manipule l'utilisateur.


    Quoi qu'on puisse dire de l'implémentation de tout ça, l'utilisation correspond à ce que je souhaitais :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    fragment<int> a_frag;	// création d'un fragment, relié à un secteur générique (statique)
    a_frag.push(4);
    a_frag.push(68);
    // [...]
    a_frag.pop(0)	// suppresion de l'élément 0 ; ici 4
     
    fragment<int> a_bis;
    a_bis.push(12);
     
    assert(a_frag[0] != a_bis[0]);
     
    sector<int> int_sec;	// création d'un nouveau secteur de int
    fragment<int> b_frag(&int_sec);	// création d'un fragment, relié au secteur souhaité
    b_frag.push(87);
    b_frag.push(324);
     
    sector<std::string> str_sec;
    fragment<std::string> c_frag(&str_sec);
    c_frag.push("un string");
    c_frag.push("une autre");
    c_frag.push("etc...");
    En gros, quasiment comme un simple vecteur, à la différence que je peux itérer sur toutes les valeurs d'un secteur.

    Je suis content du résultat, mais moins de la gueule du code.

    Voici, par exemple, la classe "sector" (celle sur laquelle tout repose) :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    template<typename T>
    class sector
    {
    private:
     
    	std::vector<T> data_;
    	std::size_t size_;
     
    	std::vector<frame<T> > frame_;
     
     
    public:
     
    	using iterator = typename std::vector<T>::iterator;
    	using const_iterator = typename std::vector<T>::const_iterator;
     
    	sector(std::size_t const & cap=500): size_{0} { data_.reserve(cap); }
     
    	std::size_t create_frame()
    	{
    		frame_.push_back(frame<T>{frame_.size(), data_, size_, 0});
     
    		return frame_.size()-1;
    	}
     
    	void push(std::size_t const & fid, T const & val)
    	{
    		data_.push_back(val);
     
    		for(auto i = frame_.size()-1; i > fid; --i)
    		{
    			std::swap(*begin(i), *end(i));
    			frame_[i].first_++;
    			frame_[i].index_[0] = frame_[i].size_-1;
    		}
     
    		frame_[fid].add_index(frame_[fid].size_);
     
    		frame_[fid].size_++;
    		size_++;
    	}
     
    	void pop(std::size_t const & fid, std::size_t const & idx)
    	{
    		std::swap(frame_[fid][idx], *(end(fid)-1));
    		frame_[fid].swap_indexes(frame_[fid].size_-1, idx);
    		--frame_[fid].size_;
     
    		for(auto i = fid+1; i != frame_.size(); ++i)
    		{
    			--frame_[i].first_;
    			frame_[i].swap_indexes(frame_[i].size_-1, 0);
    			std::swap(*begin(i), *end(i));
    		}
     
    		data_.erase(end()-1);
    		frame_[fid].pop_index(idx);
     
    		--size_;
    	}
     
    	T & get(std::size_t const & fid, std::size_t const & idx)
    	{
    		return frame_[fid][idx];
    	}
     
     
     
    	iterator begin()
    	{
    		return data_.begin();
    	}
     
    	iterator end()
    	{
    		return data_.end();
    	}
     
    	std::size_t const & size() const
    	{
    		return data_.size();
    	}
     
    	iterator begin(std::size_t const & id)
    	{
    		return begin() + frame_[id].first_;
    	}
     
    	iterator end(std::size_t const & id)
    	{
    		return begin(id) + frame_[id].size_;
    	}
     
    	std::size_t const & size_of_frame()
    	{
    		return frame_.size();
    	}
    };
    J'aimerais rendre ça plus lisible, compréhensible, et surtout, simplifier ce qui peut l'être...
    Peut-être avez-vous des idées à ce sujet ?
    J'imagine qu'il n'est pas simple de comprendre ce code sans avoir les autres éléments, mais le post risque de prendre plus d'une page sinon. Je posterais les deux autres classes si vous le souhaitez...


    Qu'en pensez-vous ?

  6. #6
    Expert confirmé
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Février 2005
    Messages
    5 543
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 53
    Localisation : France, Val de Marne (Île de France)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Conseil

    Informations forums :
    Inscription : Février 2005
    Messages : 5 543
    Par défaut
    Je comprend pas trop la problématique dans une approche ECS.

    Dans une approche ECS, chaque système maintient une liste d'élément ayant un nombre d'attribut FIXE, qui lui est nécessaire pour travailler (le système), triés selon un ordre qui permet d'avoir la meilleure performance du système.
    Donc un système = une liste d'élément (peut-être finie) de taille FIXE (les éléments).
    Dans les attributs des éléments, il y a l'id de l'entité ainsi qu'un flag pour savoir si l'élément est toujours valide.
    Avec ce boolean "valide", il est possible de régulièrement compacter cette liste.

    Il n'y pas de pointeur/références qui doivent se balader entre les systèmes.

    Si vous devez passer des attributs d'un système à un autre, c'est toujours via des tables d'index d'indirection, donc le contenu sera à customiser en fonction des besoins.

    La redondance de l'information n'est pas un problème.

  7. #7
    Membre éclairé Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Par défaut
    Bonjour, et merci pour cette précision sur les "systèmes".

    Dans ce cas précis, les systèmes ne sont pas encore du tout implémentés. Il s'agit ici de pouvoir accéder de différentes manières aux composants, quels-que soient les systèmes. En gros, j'en suis encore uniquement à réaliser un container.

    Ce container a pour but de stocker l'ensemble des composants d'un même type, pour l'ensemble des entités. cependant, je souhaite pouvoir réaliser des "groupes" d'entités.

    Pour continuer sur l'exemple de javaquarium, je veux pouvoir n'afficher que les noms des poissons et non des algues, ou l'inverse, ou tous les noms du programme et tout ça, simplement en fournissant aux systèmes (ici je parle bien des systèmes au niveau ECS) un groupe différent.


    Après.... Maintenant que tu me parles de ça, il est possible que je doive réapprendre comment fonctionne un ECS, mais, bien qu'il me reste encore à travailler sur les systèmes, je pense avoir compris que, en gros, les systèmes exploitent les données un peu sous la forme d'itérateurs.
    Pour simplifier au max, je dirais qu'un ECS est :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    // composant :
    struct Name
    {
    	std::string name_;
    };
     
    // déclaration d'une fonction à coupler à une système
    void list_names(std::vector<Name> & v)
    {
    	for(auto it {v.begin()}; it != v.end(); ++it)
    		{
    			std::cout << it->name_ << std::endl;
    		}
    }
     
     
    int main()
    {
    	using tab_t = std::vector<Name>;
    	using sys_t = std::function<void(tab_t&)>;
     
    	// entité :
    	std::size_t entity {8};
     
    	// container de composant :
    	std::vector<Name> table(10, Name{});
     
    	// affectation d'un composant à une entité :
    	table[entity] = Name{"mon nom"};
     
     
     
    	// création d'un système :
    	std::vector<sys_t> ecsys;
    	sys_t msys {list_names};
    	ecsys.push_back(msys);
     
    	// exploitation d'un système :
    	for(auto it { ecsys.begin() }; it != ecsys.end(); ++it)
    	{
    		(*it)(table);
    	}
     
     
    	return 0;
     
    }
    Seulement, je cherche à pouvoir faire des tables de tables. Je peux ainsi afficher tous les noms de clients que j'ai dans une ville précise, ou tous les noms de clients que j'ai dans le pays entier, et ce, avec le même système, et sans que j'ai a ajouter un composant "ville" et pays à mon entité... Après, dans ce morceau de code, j'ai volontairement retiré tout liens potentiels que l'on pourrait réaliser entre les tables et les systèmes, simplement parce que je dois encore réfléchir au fonctionnement des systèmes.... Pour en revenir à mes premières lignes de ce post, je m'intéresse pour le moment encore à la manière de pouvoir réaliser un container contigu de composant, indexé par un id de table ET d'entités, tout en permetant à une entité d'appartenir à une table ainsi qu'a une table parente de la précédente.

    Après, il existe très certainement de meilleures idées, j'en suis tout à fait conscient et ouvert à ces dernières, mais dans l'idée que j'ai de l'utilisation que je veux de mon "ECS", l'idée présentée me parait viable, et je souhaite réaliser un code fiable venant appuyer mon idée.


    Qu'en pensez-vous ?

  8. #8
    Expert confirmé
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Février 2005
    Messages
    5 543
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 53
    Localisation : France, Val de Marne (Île de France)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Conseil

    Informations forums :
    Inscription : Février 2005
    Messages : 5 543
    Par défaut
    En gros, j'en suis encore uniquement à réaliser un container.
    C'est là, je pense, que vous trompé lourdement.
    Un container ne peut être générique à moins de perdre tous les attraits d'un ECS.
    Vous ne pouvez pas utiliser le même container pour le système qui gérera les textures dans la carte graphique et le système en charge de la simulation physique, etc...
    Les différences de contraintes entre chaque système rend même la conception d'une classe de base de container peu pertinente.

    Concevez vos systèmes, puis choisissez un container adapté au système en particulier, un par un.

    Pour continuer sur l'exemple de javaquarium, je veux pouvoir n'afficher que les noms des poissons et non des algues, ou l'inverse,
    Si vous avez pris la précaution de mettre les poissons et les algues dans des layouts différents, vous n'avez qu'à communiquer au système de rendu que la liste des layout affichables.
    Un poisson ou une algues, dans votre ECS, ça correspond à quoi ?
    Vous pouvez imaginer tout un ensemble de mécanisme pour communiquer un ensemble d'ID d'entités que chaque module pourra traiter selon ces structures de données internes (index, listes de tri, arbre binaire, octrees, optimisation en fonction de l'angle de vue, etc...).

    Si vous voulez un système "Aristote" (qui voulait donner un nom à tout ce qui bouge sous les cieux), rien ne vous empêche de le faire. Il regroupera les ID d'entités par "type/tag" etc....
    Vous demandez au système "Aristote" la liste des entités à afficher (ou à cacher) et vous la fournissez au(x) système(s) d'affichage, pour qu'ils mettent à jour leurs données.

    S'il y a avantage à fusionner les containers entre systèmes, c'est au cas par cas.

  9. #9
    Membre éclairé Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Par défaut
    Merci pour ce retour.

    Effectivement, maintenant que je passe un peu de temps à voir comment réaliser les "systems", je me rends compte qu'il vaut probablement mieux exploiter la route des containers par systems tel que suggéré.
    jusqu'à présent, mon but était de pouvoir stocker dans un seul "objet" plusieurs containers ; à savoir, un container par type de composant. Ce mode de fonctionnement permet d'accéder facilement aux différents types de composants de manière simple. Mais maintenant que j'aborde les sytems, je me rends compte d'une part, que les containers de composant peuvent êtres séparés, mais surtout que cela pousse à réfléchir un peu plus à ce que doivent contenir les différents composants.

    Voici, ci-dessous, ma classe table qui correspond actuellement à mon container :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    class table
    {
     
    private:
     
    	ecs::key_t const tkey_{0};
     
    	template<typename T>
    	ecs::map<key_t, T> & get_map()
    	{
    		static ecs::map<key_t, T> map_;
    		return map_;
    	}
     
    public:
     
    	template<typename T>
    	using iterator = typename ecs::map<key_t, T>::iterator;
     
    	template<typename T>
    	using const_iterator = typename ecs::map<key_t, T>::const_iterator;
     
    	table() {}
     
    	key_t provide_key()
    	{
    		static key_t ekey_{0};
    		return ekey_++;
    	}
     
    	template<typename T, typename ...Args>
    	void push(key_t const & eid, Args&&... args)
    	{
    		get_map<T>().push(eid, args...);
    	}
     
    	template<typename T>
    	T & get(key_t const & eid)
    	{
    		return get_map<T>()[eid];
    	}
     
    	template<typename T>
    	void pop(key_t const & eid)
    	{
    		get_map<T>().pop(eid);
    	}
     
     
     
    	template<typename T>
    	inline std::size_t size() const
    	{
    		return get_map<T>().size();
    	}
     
    	template<typename T>
    	inline iterator<T> begin()
    	{
    		return get_map<T>().begin();
    	}
     
    	template<typename T>
    	inline iterator<T> end()
    	{
    		return get_map<T>().end();
    	}
     
    	template<typename T>
    	inline const_iterator<T> cbegin() const
    	{
    		return get_map<T>().cbegin();
    	}
     
    	template<typename T>
    	inline const_iterator<T> cend() const
    	{
    		return get_map<T>().cend();
    	}
     
     
    };
    les itérateurs ainsi surchargés permettent facilement de choisir les types de composants à traiter dans les systèmes. Je suis encore au tout début de ma réflexion sur les systèmes,, mais voici le code pondu me permettant de me faire une première idée :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    class system
    {
    private:
     
    	ecs::table * tab_;
     
    public:
    	system(ecs::table * tab): tab_{tab} {}
     
    	void update()
    	{
    		for(auto it { tab_->begin<Base>() }; it != tab_->end<Base>(); ++it)
    		{
    			std::cout << it->name_;
     
    			if(it->type_ == EType::Fish)
    			{
    				print_gender(tab_->get<Gender>(it->id_));
    			}
     
    			std::cout << " || life : " << it->life_ << " || ttl : " << it->ttl_;
     
    			std::cout << std::endl;
    		}
    	}
     
    	void print_gender(Gender & g)
    	{
    		std::cout << " gender : ";
    		if(g.gender_ == EGen::Male)
    		{
    			std::cout << "Male";
    		}
    		else if(g.gender_ == EGen::Female)
    		{
    			std::cout << "Female";
    		}
    		else
    		{
    			std::cout << "Unknown";
    		}
    	}
    };


    Ici, en grosse mailles, je ne représente que le système permettant d'afficher les informations relatives aux différentes entités. SSi l'entité est un poisson, alors, on affiche son genre, sinon si c'est une algue, on peu développer autre choses etc...
    De cette manière, je peux me permettre de réaliser un système pour l'affichage (un peu comme ce dernier), un système pour la reproduction, un système pour les déplacements etc... Néanmoins, bien que cela élargisse un peu l'horizon des implémentations possibles et plus intelligentes, je reste néanmoins obligé de conserver, dans le système, un test sur le type de l'entité en cours d'affichage. Globalement, il faudrait que la fonction statique que l'on retrouve dans la classe table ne soit pas statique :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    template<typename T>
    ecs::map<key_t, T> & get_map()
    {
    	static ecs::map<key_t, T> map_;
    	return map_;
    }
    je souhaite pouvoir créer une table pour les poissons et une autre table pour les algues, mais que les composants identiques aux deux se retrouvent dans le même std::vector (ou ecs::map dans mon cas).

    Ceci étant dit, et réfléchissant en même temps que j'écris ce post, d'autres solutions peuvent se présenter :
    * créer un container d'entité par type d'entité, les systèmes itèrent non plus sur les composants directement mais sur les entités... Cependant, je pense que cette méthode fait perdre certains avantages d'un ECS
    * adapter un couple "composant / system" un peu comme je le fait ici, sauf qu'au lieu d'avoir des systèmes orientés "fonction", j'aurais des systèmes orientés entités... Mais là aussi, je pense qu'il vaut mieux que j'en reste aux fonctions dans le sens où, selon moi, c'est sensé être l'unique rôle des systèmes (sinon, autant revenir à un pattern strategy).

    Je pense que je vais re-factoriser tout ça, mais cette fois, en partant des systèmes et non des composants.

    Bref, maintenant que je commence à m'intéresser aux systèmes, effectivement, pas mal d'idées et de solutions viennent compléter ma réflexion... Je pense que j'y verrais plus clair sur l'organisation globale du code une fois que je me serais fait une meilleure idée de l’organisation à mettre en place pour les systèmes.





    Je viens d'avancer un peu dans ma compréhension des systèmes ; ma classe initiale table que j'avais bâtie de manière à pouvoir accéder facilement à n'importe quel composant en tant que "container", se trouve en fait être la base d'un système mais global effectivement.
    Ce que je comprends ici, c'est que je dois créer au moins un système pour les poissons et un système pour les algues. Je pense qu'il est possible de regrouper les deux systèmes mais je dois d'abord m'assurer de la pertinence de réaliser un tel objet.
    Par contre, ce que je peux faire, ce sont des classes abstraites pour les systèmes. Ainsi, je peux faire un système fishRender pour les poissons et un autre seaweedRender pour les algues ; idem pour le système de "comportement" de chacun de ces deux types d'entités, et intégrer plus tard ces systèmes dans une classe qui pilotera tout ça.... Plus vraiment besoin d'une classe omnipotente pour gérer les composants.

    Un autre aspect sur lequel je dois réfléchir aussi concerne les relations entre différents composants, ainsi que les systèmes communs...

    Bref, je vais déjà tâcher de factoriser une première base pour tout ça ce week-end ; je devrais y voir plus clair à l'issue.

  10. #10
    Expert confirmé
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Février 2005
    Messages
    5 543
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 53
    Localisation : France, Val de Marne (Île de France)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Conseil

    Informations forums :
    Inscription : Février 2005
    Messages : 5 543
    Par défaut
    Votre implémentation de table est assez carrée mais elle n'est pas du tout thread-safe.
    Elle est aussi minimaliste et j'ai du mal à voir comment elle peut être utilisé sans avoir recourt à des cast dans tous les sens.
    Une implémentation par défaut du container pour un système, ok, mais à l'usage, ça me semble compliqué et chaque système a des besoins tellement différent.
    A la rigueur, définir une "Interface" (classe avec aucune implémentation) que chaque container devrait implémenter, pour factorisé du code "glue", pourquoi pas.

    Le classe system utilise des pointeurs nus, et c'est pas bien.
    Pourquoi pas une référence ?
    Comme table est générique, pour faire fonctionner cette classe, ça va être des cast à tout les étages (get<Genre>, c'est un cast déguisé).

    Votre classe "system" est un cas particulier, c'est un système "Aristote".
    Et en tant que system "Aristote", il devrait utiliser une structure qui optimise le calcul du nom d'une entité.
    Il faudra peut-être implémenté dans ce système "Aristote" un itérateur qui permet d'itérer selon l'ordre des numéros d'entité, mais c'est clairement pas le cas d'usage que la structure interne devra optimiser.
    Ce que ce système doit optimiser, c'est de répondre à la question "Quel est ne nom de l'entité de nom {x}" pour qu'il puisse être utile à d'autres systèmes.

    De cette manière, je peux me permettre de réaliser un système pour l'affichage (un peu comme ce dernier), un système pour la reproduction, un système pour les déplacements
    Chacun ayant des contraintes/cas d'usage différent, demandant des structures internes bien différentes.

    un test sur le type de l'entité en cours d'affichage
    Au grand non de non.
    Il ne faut surtout pas associer une entité à un type.
    A la rigueur, vous voulez savoir si un composant X ou un composant Y y est associé, mais surtout pas de "type", ça casse toute l'élégance d'un ECS.
    Savoir si une entité a un composant X, il suffit de le demander au système en charge des composants X. Cas d'usage du système qu'il faut optimiser.
    La très grande majorité des systèmes n'auront qu'une très faible fraction de toutes entités.
    Pourquoi mettre une entité sans affichage (sound, trigger, etc...) dans les structures du système d'affichage ?

    Globalement, il faudrait que la fonction statique que l'on retrouve dans la classe table ne soit pas statique :
    Je ne vois aucun méthode statique, juste des variables locales statiques.
    Si vous n'avez pas de structure globale "map", vous n'avez pas besoin de variables statiques ou autres singletons.

    je souhaite pouvoir créer une table pour les poissons et une autre table pour les algues
    Pourquoi ????

    mais que les composants identiques aux deux se retrouvent dans le même std::vector (ou ecs::map dans mon cas).
    C'est le comportement attendu d'un ECS, chaque composant à un système dédié qui lui-même contient sa propre structure interne.
    Vous pouvez avoir un système "Aristote" qui récupère les entités "Taguées" de tous poils (suffit qu'on lui associe un composant avec juste une chaine de caractère).
    Vous pouvez avoir un système "Simulation Animale" avec les informations dans le composant associé à cette simulation, qui ne contiendra que les poissons et autres animaux.
    Vous pouvez avoir un système "Simulation Végétale" avec les informations dans le composant associé à cette simulation, qui ne contiendra que les algues et autres végétaux.
    Si vous avez besoins de la liste des poissons, c'est à Aristote qu'il faut s'adresse, pas au simulateur de vie animale.

    * créer un container d'entité par type d'entité,
    NOPE, il faut vous sortir du crane qu'une entité à un type.
    Elle a un ensemble de composant, qui est variable au cours du temps.
    Comment savoir si une entité a un composant, demandé au système qui s'en sert.

    les systèmes itèrent non plus sur les composants directement mais sur les entités
    La très grande majorité des système n'auront qu'une très faible fraction de toutes entités.(BIS)

    Cependant, je pense que cette méthode fait perdre certains avantages d'un ECS
    Heu, à peu près toutes, à mon avis.

    j'aurais des systèmes orientés entités
    ???

    créer au moins un système pour les poissons et un système pour les algues
    Si les données pour chacun sont différentes, c'est obligatoire.

    Ainsi, je peux faire un système fishRender pour les poissons et un autre seaweedRender pour les algues
    Très mauvaise idée.
    Laissez le système "Renderer" faire son taf.
    Il n'a besoin que des informations pour afficher est c'est le même type, aussi bien pour un poisson et une algue, un mesh.
    Le type du composant sera le même, ce ne sont que les valeurs des champs dans le composant qui distingue un poisson d'une algue, du point de vue du Renderer.

    les relations entre différents composants,
    Elles doivent être le plus réduites possibles, quitte à faire de la redondance d'information.
    Pas besoin de faire une association entre le moteur physique et le moteur de rendu par ce qu'ils doivent partager les coordonnées des objets qui sont à la fois visible et physique.
    Une fois la simulation physique achevée, vous déversez ces informations de position directement dans le système de rendu qui fera sa tambouille pour mettre à jours ses données internes.

  11. #11
    Membre éclairé Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Par défaut
    Bonjour, et merci pour cette analyse du sujet.

    Concernant le type des entités
    Au grand non de non.
    Il ne faut surtout pas associer une entité à un type.
    A la rigueur, vous voulez savoir si un composant X ou un composant Y y est associé, mais surtout pas de "type", ça casse toute l'élégance d'un ECS.
    Savoir si une entité a un composant X, il suffit de le demander au système en charge des composants X. Cas d'usage du système qu'il faut optimiser.
    J'entends bien par là un composant associé à une entité qui, de par sa présence (ou non) défini si l'objet est un poisson, une algue, une particule ou autre... Le terme employé est différent, mais je pense que l'on visualise finalement la même chose. C'est bien entendu ce que je souhaite éviter aussi. Je pense qu'il y a de meilleures solutions pour "identifier" / "trier" les entités... Et j'en reviens ici à la solution première proposée dans ce post (voir premier message), consistant à ranger de manière contiguë les composants par entités. Mais là n'est plus trop la question. Je pense avoir d'autres questions plus fondamentales à régler en premier lieu.

    Et en tant que system "Aristote", il devrait utiliser une structure qui optimise le calcul du nom d'une entité.
    Qu'entendons nous par "optimisation du calcul du nom d'une entité" ? Pour info, a confirmer que nous parlons bien de la même chose, mais mes entités sont des std::size_t et non des std::string. Ce que j'appelle "nom" dans mon programme, c'est, par exemple, le nom réel d'une entité (par exemple Merlu ; je peux donc avoir plusieurs entités portant le même nom).


    La très grande majorité des systèmes n'auront qu'une très faible fraction de toutes entités.
    Pourquoi mettre une entité sans affichage (sound, trigger, etc...) dans les structures du système d'affichage ?
    Je suis d'accord avec le fait que c'est à éviter autant que possible. Cependant, on tombe rapidement sur ce dilemme avec... le moteur de rendu et le moteur physique. Pour afficher un Mesh au bon endroit, j'ai besoin de récupérer la position qui elle est dans le moteur physique. Certes, avant de commencer à mélanger tous les systèmes, il faut réfléchir à la méthode la plus adaptée ; cependant, si je vais au plus direct, je pense qu'un pointeur entre le moteur de rendu et le moteur physique est une solution parmi d'autres. Après, il ne s'agira peut-être que d'une pointeur vers la map des positions, mais tout de même.


    je souhaite pouvoir créer une table pour les poissons et une autre table pour les algues
    Pour mes tests, le rendu ne concerne que l'affichage de certains éléments, comme le nom, la vie l'age etc... tout ceci est commun aux différentes entités (poissons et algues). Cependant, je compte aussi afficher le sexe de l'individu par exemple, et savoir s'il est carnivore etc... Et ceci n'est pas commun. Aussi, j'ai en horreur de devoir faire des conditions de conditions etc... comme
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    if(is_fish(entity))
    {
    	if(is_male(entity))
    	{
    		// do some stuff...
    		if(is_carnivore(entity))
    		{
    			// etc...
    		}
    	}
    }
    On m'excusera du terme, mais je trouve ceci imbitable... donc si je peux itérer directement sur les poissons, ça enlève déjà un niveau. Bien entendu, maintenant que je réfléchis un peu plus au fonctionnement des systèmes, la problématique est en partie réglée pour certains systèmes, mais pour l'affichage, tel que décrit ci-dessus, je vais devoir réfléchir un peu plus sur le sujet.
    La question est donc moins d'actualité que le jour où j'ai posé ce post, mais j'y reviendrais néanmoins peut être un peu plus tard.

    NOPE, il faut vous sortir du crane qu'une entité à un type.
    Elle a un ensemble de composant, qui est variable au cours du temps.
    Comment savoir si une entité a un composant, demandé au système qui s'en sert.
    cf plus haut, on est d'accord sur ce sujet, mais maintenant au moins, je suis vraiment fixé sur ce point.

    les relations entre différents composants,
    Elles doivent être le plus réduites possibles, quitte à faire de la redondance d'information.
    Pas besoin de faire une association entre le moteur physique et le moteur de rendu par ce qu'ils doivent partager les coordonnées des objets qui sont à la fois visible et physique.
    Une fois la simulation physique achevée, vous déversez ces informations de position directement dans le système de rendu qui fera sa tambouille pour mettre à jours ses données internes.
    J'ai compris qu'ici, je dois trouver une meilleure solution... Je vais réfléchir à la manière de bien séparer les différents systèmes.


    concernant les variables statiques, effectivement, si je bâtie correctement mes systèmes, je n'aurais plus trop de problèmes à cet égard. Idem pour les "cast à outrance" qui, j'en conviens tout à fait, sont à limiter dans mes prochaines factorisations (voire à proscrire dans certains cas).


    Donc concernant l'architecture, si je comprends bien j'ai :
    * un système pour le rendu (considérant que chaque entité possède les mêmes composants principaux pour le rendu)
    * un système physique différent pour chaque entité différente (compte tenu des comportements différents)
    * autres systèmes, a voir selon besoin (son, IA, etc...).
    * ce sont les systèmes qui "stockent" les composants dont ils ont besoin


    Je vais pouvoir faire quelques test dès ce soir.


    Merci !

  12. #12
    Membre éclairé Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Par défaut
    Bonjour à tous !


    Bon... Voici quelques résultats :
    Nom : particles.jpg
Affichages : 180
Taille : 418,4 Ko

    Pour en arriver là, j'ai réalisé une classe particles permettant de générer des particules. Cette classe demande aux systèmes render et dynamics de stocker les composants nécessaires. La classe particles, elle, ne sert qu'à gérer la création / suppression des particules en fonction d'un composant ttl.
    La boucle principale ne fait donc qu’appeler les 3 systèmes.

    Je suis content d'avoir enfin quelque-chose d'affichable, mais je rencontre tout de même quelques problèmes de performances :
    Je reste à 144fps jusqu'à 12000 particules, et je tombe en continu après ça, jusqu'à atteindre les 60fps pour ~30000 particules. A 64000, j’atteins les 25 fps. Je ne suis pas allé au-delà, mais selon moi, un code viable devrait pouvoir faire au moins 2 fois mieux si ce n'est bien plus compte tenu de mon matériel (core I7 8086K @5.2Ghz / 12Mb cache L3 + 16Go DDR4 @3200Mhz CL14).

    J'utilise SFML2.0 pour la création du contexte et le rendu des sprites.

    Je dois trouver comment faire mieux !

    Ci-dessous, les systèmes de calculs et de rendu. Pour info, entre le moment où j'ai pris l'image ci-dessus et le moment où j'ai récupéré le code, j'ai fait quelques modifications mineures plus liées au format des particules qu'autre choses.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    #pragma once
     
    #include "map.hpp"
    #include "components.hpp"
     
     
    using index_t = std::size_t;
     
     
    class dynamics
    {
    private:
     
    	ecs::map<index_t, Position> positions_;
    	ecs::map<index_t, Velocity> velocity_;
     
    public:
     
    	dynamics() {}
     
    	void add(index_t const & id, Position const & pos)
    	{
    		positions_.push(id, pos.x, pos.y);
    	}
     
    	void add(index_t const & id, Velocity const & vel)
    	{
    		velocity_.push(id, vel.x, vel.y);
    	}
     
    	void destroy(index_t const & id)
    	{
    		positions_.pop(id);
    		velocity_.pop(id);
    	}
     
    	Position & get_position(index_t const & id)
    	{
    		return positions_[id];
    	}
     
    	void update(float const & tf)
    	{
    		update_velocity(tf);
     
    		for(auto it{positions_.begin()}; it != positions_.end(); ++it)
    		{
    			if(velocity_.is_recorded(it->id_))
    			{
    				it->x += velocity_[it->id_].x * tf;
    				it->y += velocity_[it->id_].y * tf;
    			}
    		}
    	}
     
    	void update_velocity(float const & tf)
    	{
    		for(auto it{velocity_.begin()}; it != velocity_.end(); ++it)
    		{
    			it->y += 9.81f * tf; // multiplié par tf uniquement pour l'imiter l'impact de la gravité sur les particules. ce n'est pas définitif
    		}
    	}
     
     
    };
     
     
    class render
    {
    private:
     
    	sf::RenderWindow& win_;
     
    	ecs::map<index_t, Shape> shapes_;
     
    public:
     
    	render(sf::RenderWindow & w): win_{w} {}
     
    	void add(index_t const & id, Shape const & shp)
    	{
    		shapes_.push(id, shp.shape_, shp.color_, shp.radius_, shp.active_);
    	}
     
    	void destroy(index_t const & id)
    	{
    		shapes_.pop(id);
    	}
     
    	void update(dynamics & dyn)
    	{
    		for(auto it{shapes_.begin()}; it != shapes_.end(); ++it)
    		{
    			if(it->active_)
    			{
    				it->shape_.setFillColor(it->color_);
    				it->shape_.setPosition(dyn.get_position(it->id_).x, dyn.get_position(it->id_).y);
    				win_.draw(it->shape_);
    			}
    		}
    	}
     
    };
    Les composants (rien de bien magique ici):
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    #pragma once
     
    #include <SFML/Graphics.hpp>
     
     
     
    struct Position
    {
    	float x;
    	float y;
    };
     
    struct Velocity
    {
    	float x;
    	float y;
    };
     
    struct Shape
    {
    	sf::CircleShape shape_;
    	sf::Color color_;
    	float radius_;
     
    	bool active_{true};
    };
     
    struct Life
    {
    	float ttl_;
    };

    Et ici, le générateur de particules :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    #pragma once
     
    #include "map.hpp"
    #include "components.hpp"
    #include "systems.hpp"
     
    #include "random.hpp"
    using random = effolkronium::random_static;
     
     
    class particle
    {
    	using index_t = std::size_t;
     
    private:
     
    	render& rnd_s;
    	dynamics& dyn_s;
     
    	ecs::map<index_t, Life> life_;
     
    	std::vector<index_t> removed_;
    	std::size_t number_;
     
    	index_t generate_id()
    	{
    		static index_t id{0};
    		return id++;
    	}
     
    public:
     
    	particle(render & rs, dynamics & ds): rnd_s{rs}, dyn_s{ds}, number_{0} {}
     
    	void create()
    	{
    		create(generate_id());
    	}
     
    	void create(index_t const & id)
    	{
    		dyn_s.add(id, Position{800.0f, 800.0f});
    		dyn_s.add(id, random_vel(-250.0f, 250.0f));
    		rnd_s.add(id, Shape{sf::CircleShape(1.0f), sf::Color(222.0f, 193.0f, 30.0f), 1.0f, true});
     
    		life_.push(id, random::get(1.5f, 3.5f));
    	}
     
    	Velocity random_vel(float const & min, float const & max)
    	{
    		float x = random::get(-250.0f, 250.0f);
    		float y = random::get(-250.0f, .0f);
     
    		return Velocity{x, y};
    	}
     
    	std::size_t get_number()
    	{
    		return life_.size();
    	}
     
    	void destroy(index_t const & id)
    	{
    		dyn_s.destroy(id);
    		rnd_s.destroy(id);
     
    		life_.pop(id);
    	}
     
    	void clean()
    	{
    		for(auto i : removed_)
    		{
    			destroy(i);
    		}
    	}
     
    	void update(float const & tf)
    	{
    		if(life_.size() < 64000)
    		{
    			for(int i{0}; i != 50; ++i)
    			{
    				this->create();
    			}
    		}
     
    		for(auto it{life_.begin()}; it != life_.end(); ++it)
    		{
    			it->ttl_ -= tf;
    			if(it->ttl_ <= .0f)
    			{
    				removed_.push_back(it->id_);
    			}
    		}
     
    		//clean();
     
    		for(auto i : removed_)
    		{
    			destroy(i);
    			this->create(i);
    		}
     
    		removed_.clear();
    	}
     
     
    };
    Au passage, j'utilise le générateur random de effolkronium. Je ne sais pas si c'est le meilleur, je suis tombé dessus par hasard et l'ai trouvé très pratique.

    On peut voir dans le précédent code le moyen employé pour réutiliser les id (entités) supprimées. C'est une technique temporaire ; je compte réaliser, par la suite un meilleur système de gestion des entités.

    Autre élément important, mes "map" perso :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    #pragma once
     
    #include <vector>
    #include <cassert>
    #include <limits>
     
    #define MAX_ENTITY 0xFFFF
     
     
     
    namespace ecs
    {
    	template<typename K>
    	struct wrap_id
    	{
    		K id_;
    	};
     
    	template<typename K, typename T>
    	struct wrapper: public wrap_id<K>, public T
    	{
    		wrapper(): wrapper{0} {}
     
    		template<typename ...Args>
    		wrapper(K const & id, Args&&... args): wrap_id<K>{id}, T{args...} {}
    	};
     
    	template<typename K, typename T>
    	class map
    	{
    		using index_t = K;
    		using data_t = T;
    		using wrap_t = ecs::wrapper<K, T>;
     
    	private:
     
    		std::vector<wrap_t> data_;
    		std::vector<std::size_t> indexes_;
     
    		static constexpr std::size_t invalid_id() { return std::numeric_limits<index_t>::max(); };
    		static constexpr std::size_t max_id() { return 0xFFFF; };
     
    		inline std::size_t const & index_of(index_t const & id)
    		{
    			assert(is_recorded(id) && "not recorded id");
     
    			return indexes_[id];
    		}
     
    	public:
     
    		using iterator = typename std::vector<wrap_t>::iterator;
    		using const_iterator = typename std::vector<wrap_t>::const_iterator;
     
    		map()
    		{
    			indexes_.resize(max_id(), invalid_id());
    		}
     
    		template<typename ...Args>
    		void push(index_t const & id, Args&&... args)
    		{
    			assert(!is_recorded(id) && "already recorded key");
     
    			data_.push_back(wrap_t{id, args...});
    			indexes_[id] = size()-1;
    		}
     
    		void pop(index_t const & id)
    		{
    			if(is_recorded(id))
    			{
    				index_t lid { data_[size()-1].id_ };
    				std::swap(data_[index_of(id)], data_[size()-1]);
    				std::swap(indexes_[id], indexes_[lid]);
    				indexes_[id] = invalid_id();
     
    				data_.resize(size()-1);
    			}
     
    		}
     
    		data_t & operator[](index_t const & id)
            {
            	return data_[index_of(id)];
            }
     
     
     
     
    		inline bool is_recorded(index_t const & id) const
    		{
    			return indexes_[id] != invalid_id();
    		}
     
    		inline std::size_t size() const
    		{
    			return data_.size();
    		}
     
    		inline iterator begin()
    		{
    			return data_.begin();
    		}
     
    		inline iterator end()
    		{
    			return data_.end();
    		}
     
    		inline const_iterator cbegin() const
    		{
    			return data_.cbegin();
    		}
     
    		inline const_iterator cend() const
    		{
    			return data_.cend();
    		}
     
     
    	};
     
     
    }
    Là aussi je pense qu'il y a moyen de réaliser quelque chose de plus robuste. Pour le moment, c'est le type de code que je suis capable de réaliser avec mon niveau actuel...


    Selon le gestionnaire des tâches Windows, l'application générée, demande 120Mo de mémoire pour 64000 particules. A voir plus précisément ce que ça donne dès que j'aurais récupéré ma machine Linux.

    Encore une fois, j'estime avoir fait un pas en avant, mais je dois pratiquer bien d'avantage avant d'arriver à pondre un code qui me semble bien clair et bien organisé. Comme vous le voyez très certainement ici, je dois apprendre a faire du code plus épuré, maintenable etc... Mais avant tout, je vise l'idée d'avoir quelque chose de très convivial, et selon moi, j'ai encore une belle marge de progrès à ce niveau.

    J'imagine aussi, une fois encore, que je vais devoir m'imprégner et revoir tous les conseils que vous m'avez donné jusque là. Je suis persuadé que pas mal de notions importantes que vous avez abordés restent à digérer, comprendre, revoir.


    Tout ceci m’amène à poser les questions suivantes :
    * compte tenu du contexte matériel et logiciel (ma config, utilisation de SFML), quelles performances devrais-je pouvoir atteindre afin de pouvoir considérer mon code performant (et j'entends bien performances pures, et non maintenabilité etc..) ? J'ai trouvé sur le net des types qui montent jusqu'à 1M particules avec SFML... Il me reste pas mal de taf je pense.
    * en se basant sur les morceaux de code fourni ici, bien que j'ai conscience que vous referiez probablement tout de A à Z, si vous n'aviez qu'une partie à optimiser (en terme de performance et/ou maintenabilité et/ou convivialité), par où commenceriez-vous ? En gros, quels sont les éléments que vous n'accepteriez jamais dans votre code ?




    Bon, après deux ou trois moditications mineures, j'arrive enfin à pousser jusqu'à 256000 particules en tournant à 60 fps. Effectivement, utiliser les vertex arrays plutôt que des primitives est beaucoup plus optimisé (moins de draw calls). Cependant, je pense devoir pouvoir faire mieux. Certes, mon object n'est pas, ici, de faire un moteur de particules puissant (il me faut bien plus d'expérience avant de pouvoir prétendre y arriver de toutes façons), mais que j'arrive à architecturer correctement mon code autour des data dans un objectif de performance globale.

    Mes questions ci-dessus restent d'actualité .

  13. #13
    Expert confirmé
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Février 2005
    Messages
    5 543
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 53
    Localisation : France, Val de Marne (Île de France)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Conseil

    Informations forums :
    Inscription : Février 2005
    Messages : 5 543
    Par défaut
    J'entends bien par là un composant associé à une entité qui, de par sa présence (ou non) défini si l'objet est un poisson, une algue, une particule ou autre
    Si un poisson, c'est une entité avec un comportement d'animal et qui est assujetti au moteur physique, un poisson n'est qu"une entité avec ces 2 composants ?
    Non, une pieuvre aurait les mêmes composants.
    Si vous avez besoin d'un "Tag/Type" associé à une entité, c'est à vous de le gérer intelligemment.
    Avoir un système gérant un composant spécifique "IsFish", c'est peut-être un peu overkill, non.
    Et quand le poisson meurt, il n'est plus "IsFish", mais c'est toujours une entité connu du moteur physique mais plus un "animal ayant un comportement".
    Un système "Aristote" gérant un "nom hiérarchique" par entité devrait faire le taf : "animaux/poisson/femelle".
    Quand un poisson change de sexe, vous prévenez Aristote et vous faite les modifications de composants correspondant à un changement de sexe.
    etc...

    Le terme employé est différent, mais je pense que l'on visualise finalement la même chose.
    Oui, mais le terme "type" ou classe ne devrait pas transparaitre.

    Et j'en reviens ici à la solution première proposée dans ce post (voir premier message), consistant à ranger de manière contiguë les composants par entités.
    Non, comme je l'ai dit plusieurs fois, il faut stocker les composants d'un même système de manière contiguë, pas toutes les entités.
    Et l'ordre est fonction du système ; par exemple selon le z-index pour le système de rendu pour pouvoir appliquer l'algorithme du peintre pour la gestion des objets transparents, etc...

    Qu'entendons nous par "optimisation du calcul du nom d'une entité" ? Pour info, a confirmer que nous parlons bien de la même chose, mais mes entités sont des std::size_t et non des std::string.
    Les entités sont chez vous un indice (std::size_t) ou une valeur positive, ok, mois je dirait que c'est l'identifiant de l'entité.
    L'entité étant l'ensemble des composants ayant le même identifiant dans chacun des systèmes.
    Quand je parlais de nom de l'entité, c'était en terme de dénomination "aristotélicienne".
    Je donne au système "Aristote" le nom de "poisson" et il se démerde pour me donner la liste des identifiants d'entités correspondant aussi bien à des osteichthyens ou des chondrichthyens.
    Mais le système "Aristote" peut aussi répondre à la question "qui est femelle" ou "qui est mâle" (chose qui n'est pas constant chez les poissons), si c'est indiqué dans le "tag" associé à l'entité, lors de son enregistrement dans le système Aristote.
    Si vous n'avez pas de système dédié au sexe d'une entité, il faudra prévenir Aristote lors du changement de sexe d'une entité par le système "comportement animal", etc...
    Le système "comportement animal" ne connait le sexe que des animaux, pas celui des végétaux, mais le système Aristote peut gérer tous "types" d'entité (algue, poisson, cailloux, etc...) (avec une classification hiérarchique pour répondre rapidement aux questions).
    Aristote peut très bien gérer en interne les noms avec des identifiants de chaines, pas des chaines de caractères elles-mêmes.

    le nom réel d'une entité (par exemple Merlu ; je peux donc avoir plusieurs entités portant le même nom).
    C'est vague, un merlu c'est aussi bien un poisson qu'un supporter de FC Lorient, Aristote est plus rigoureux pour le coup.

    j'ai besoin de récupérer la position qui elle est dans le moteur physique
    Oui, mais uniquement des entités qui sont affichables et s'ils ont changés de position par la simulation.
    A la fin de l'étape de simulation, vous ne synchronisez dans le moteur de rendu qu'un nombre restreint de valeur, et, avec un peu de tambouilles bas niveau (DMA, etc...), il est possible de faire ces synchronisations en masse sans usage intensif du CPU ni du bus de données.

    je pense qu'un pointeur entre le moteur de rendu et le moteur physique est une solution parmi d'autres.
    C'est surtout le meilleur moyen d'avoir des problèmes soit de cohérence de données car problèmes de multi-threading, soit d'avoir des performances pourraves pour causse de contention d'accès.
    Il faut faire des compromis, mais avoir les données à plusieurs endroits est loin d'être un problème.
    Chaque système dispose des données dont il a besoin, quand il en a besoin, selon une organisation mémoire optimale pour SES usages.

    TO BE CONTINU

  14. #14
    Membre éclairé Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Par défaut
    Ok ok,

    Bon, je vais tâcher de faire plus attention à mon lexique ; j'en perçois directement l'importance ici.

    Néanmoins, après quelques nouveaux départs pour mon code, j'en suis arrivé à la solution suivante :

    * mes composants sont et restent de simples structures C-style (PODs)
    * je créé un container indexé par type de composant. Chacun possédant une méthode "update" différente
    - un pour la vitesse, un pour la position, un pour la couleur, un pour les shapes etc...
    * si je souhaite créer un système plus "complexe" (ou de plus haut niveau ?), alors ce système viens simplement manipuler les composants de son choix par le biais des containers appropriés.

    globalement, cela donne le concept suivant :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    template<typename T>
    class pool
    {
    	using index_t = std::size_t;
    protected:
    	std::vector<T> data_;
    	std::vector<std::size_t> indexes_;
     
    public:
    	pool() {}
    	T & bind(index_t const & id, T const & obj);
    	void destroy(index_t const & id);
    	T & operator[](index_t const & id);
    	iterator begin();
    	iterator end();
    	std::size_t size();
     
    	virtual void update() = 0;
     
    };
     
    struct position
    {
    	float x, y;
    };
     
    struct velocity
    {
    	float x, y;
    };
     
    class position_sys: public pool<position>
    {
    public:
     
    	virtual void update()
    	{
    		for(auto it{begin()}; it != end(); ++it)
    		{
    			std::cout << it->x << " : " << it->y << std::endl;
    		}
    		std::cout << std::endl;
    	}
    };
     
    position_sys pos_s;
    pos_s.bind(0, position{.0f, .0f, .0f});
     
    class velocity_sys: public pool<velocity>
    {
    public:
     
    	virtual void update()
    	{
    		for(auto it{begin()}; it != end(); ++it)
    		{
    			pos_s[it->id_].x += it->x;
    			pos_s[it->id_].y += it->y;
     
    			std::cout << it->x << " : " << it->y << std::endl;
    		}
    		std::cout << std::endl;
    	}
    };
     
    velocity_sys vel_s;
    vel_s.bind(0, velocity{.0f, 2.3f, 5.4f});
    vel_s.update();

    Dans l'idée, ce bout de code fonctionne, cependant, je dis adieu à la règle de SRP... Je vais tout de même remodeler mon "moteur de particules" autour de ça, voir ce que ça donne dans la pratique.

+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. Thread POSIX et gestion mémoire
    Par pier* dans le forum POSIX
    Réponses: 1
    Dernier message: 07/07/2006, 22h36
  2. TAO, Value types et gestion mémoire
    Par TiChabin972 dans le forum CORBA
    Réponses: 1
    Dernier message: 25/04/2006, 21h55
  3. [D7] Tableau dynamique et Gestion mémoire
    Par Cl@udius dans le forum Langage
    Réponses: 7
    Dernier message: 13/03/2006, 16h16
  4. [Gestion mémoire] SetLength sur TDoubleDynArray
    Par MD Software dans le forum Langage
    Réponses: 14
    Dernier message: 24/04/2005, 22h11
  5. Gestion mémoire des Meshes (LPD3DXMESH)
    Par [Hideki] dans le forum DirectX
    Réponses: 1
    Dernier message: 08/07/2003, 21h34

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo