Ton exemple est un très bon cas d'étude. Il me rappelle un cas similaire sur Bomberman qui a des similitudes avec ton cas et duquel tu pourras sans doute t'inspirer.
Première chose : il n'est pas sûr que ton cas soit idéal pour utiliser un système à entités. Tu pourrais rester dans une conception classique et ça marcherait, je pense. Maintenant, on peut très bien utiliser un système à entité et je vais te dire comment je ferais.
Deuxième chose : tout ne se règle pas à coups de composants. Typiquement, le fond de carte n'est dans aucun composant, tu vas la dessiner en entier et ensuite, tu dessines le reste. Un autre classique, c'est la position du héros qui peut être dans un composant mais qui doit être accessible dans la plupart des systèmes.
Maintenant sur le fond, reprenons.
Du coup, j’ai la tuile (sol, eau ou mur en gros) qui est dessiné en premier, ensuite les mécanismes (interrupteurs, portes, pics qui sortent du sol à intervalle régulières...) et enfin les «objets» (je ne sais pas comment nommer cette catégorie: personnages, monstres, caisses...).
En fait, il n'y a pas vraiment trois couches, il y en a deux, voire une et demie. Tu as ton fond de carte (ce que tu appelles tuile et qui ne change pas normalement donc que tu dessines en une seule fois avant tout le reste, ça compte donc pour un demi). Ensuite, tout le reste, ça fait partie de la même catégorie, celle des objets du jeu, éventuellement avec un composant Collideable qui contient un champ height (comme dans l'exemple ci-dessus) qui va indiquer qu'est-ce qui est à quel niveau, ou level comme tu le dis après, c'est la même idée.
Au début je me disais que seul les objets pouvaient bouger mais maintenant, je veux avoir des mécanismes qui peuvent bouger par exemple. Du coup je me dis qu’un ECS ça pourrait être pratique: un composant «mécanisme», un composant «mouvement», un composant «objet» pour savoir si c’est un objet qui flotte ou non et savoir et une autre propriété du style, etc. Et même avec des spécialisations, genre certains mécanismes ne sont pas à mettre à jour automatiquement ne sont pas à mettre à jour automatiquement (portes et interrupteurs sont déclenchés par l’action du joueur mais les pics qui sortent puis rentrent du sol sont automatiques).
Ton début n'est pas mauvais. Je ne mettrai pas de composant «mécanisme», parce qu'un mécanisme, c'est une entité, pas une partie d'une entité. Il n'y a pas de données dans un mécanisme (sauf peut-être un type mais en fait, chaque type de composant va être défini par ses capacités, pas par un entier qui ne veut rien dire en tant que tel). Idem pour le composant «objet». En revanche, d'après ta description, tes entités mécanisme et objet vont avoir un composant Position. Ceux qui bougent auront un composant Mouvement indiquant l'amplitude du mouvement (on peut se restreindre à un mouvement soit horizontal soit vertical). Et tu auras donc un système qui prend toutes les entités avec une position et un mouvement pour mettre à jour la position en fonction du mouvement. Tu pourrais avoir un composant Ouverture pour l'entité interrupteur qui contient l'entité porte correspondante (ou même plusieurs entités si ça peut ouvrir plusieurs portes). Et donc, un système qui prend toutes les entités avec une position et une ouverture et qui va regarder la position actuelle du héros et ouvre la porte si jamais le héros et sur la même position que l'interrupteur. Pour tes pics, c'est un peu la même idée que pour mouvement, c'est le cas typique de l'animation. Donc tu vas avoir un composant Animation qui indique le nombre d'animation et la frame courante et les durées entre chaque frame. Ce composant, tu pourras le réutiliser pour d'autres mécanismes animés. Et pour les pics, tu peux avoir un composant Danger qui indique le type de danger (destruction, etc) et à quelle frame ils apparaissent. Tu auras un système qui va prendre les entités avec animation et danger pour dire si l'entité est actuellement en mode danger ou en mode pas danger, puis un système qui va prendre les entités avec danger et position, et qui va regarder si le héros est sur un danger ou pas et si ce danger est actif ou pas, et qui peut faire la même chose avec n'importe quel objet en fait et si ça fait mal, tu ajoutes un composant Lifetime (dont j'ai parlé à plusieurs reprises dans ce thread) qui indique que l'entité est morte. Ce dernier système va également fonctionner pour les danger pas animé. Et tu auras un système qui détruit toutes les entités qui sont mortes.
Après, avec cette conception, tu peux avoir des mix intéressants, genre des dangers animés qui servent aussi d'interrupteur (dans ton ancienne conception, je suis certain que tu n'y avais même pas pensé, ici c'est gratuit, il suffit de mettre les bons composants dans l'entité).
Le truc c’est que par exemple une IA (très basique) pour les monstres, c’est une méthode. Or j’avais compris que les composants ne stockaient que les données! Alors où se trouver la logique du jeu?
Non, ce n'est pas une méthode, c'est un système ! Tu auras un composant IA (avec ce qu'il faut pour que ça marche, par exemple fuite ou recherche pour savoir si l'entité fuit le héros ou le recherche) et ton système va prendre tes entités avec un composant IA et une position et va mettre à jour tout ça. Du coup, tu peux ajouter un composant IA à n'importe quel entité, montre ou même interrupteur, ou même porte (une porte qui fuit le héros, ça c'est fun).
Bref, en gros je pensais à un tableau en 3 dimensions pour stocker les tuiles, les mécanismes et les objets. Je mets à jour tout ce beau monde: automatiques + joueur + tout ce qui pourrait avoir été affecté par les précédents → ça évite que pour chaque case d’eau je vérifie si une caisse est au-dessus à chaque tour par exemple (je pense même à ne presque rien mettre en mise à jour auto et n’inscrire les caisses en automatiques que quand il y a déplacement par exemple).
Heu, à moins que tu aies des milliers d'objets (et donc des cartes énormes), je pense que c'est une optimisation prématurée. Quant au tableau, il est parfaitement inutile, sauf pour les tuiles (donc il est à deux dimensions). Tout le reste, ce seront des entités. Et la mise à jour se fait dans des systèmes. Je n'ai pas évoqué tous les systèmes mais tu auras un système pour les collisions par exemple.
Du coup grâce à cette technique je peux les dessiner dans le bon ordre, de sorte que les sprites qui dépassent sur la case du dessus ne soit pas dessinés en-dessous!
La manière dont chaque entité est dessiné et la logique du jeu sont deux choses séparées. Ton système de dessin va effectivement regarder toutes les entités avec un sprite et un level, peut les ordonner au préalable et les dessiner, mais quelque part, peu importe leur type, ça reste des sprites avec une hauteur.
Un entité conteneur de composant vs pas de conteneur et id pour connaitre les entités, quels sont les avantages et les inconvénients? J’aime bien la deuxième méthode mais ça risque de compliquer mon tableau à 3 dimensions! De toute manière, il va falloir faire attention que si un composant «meurt», tous les autres composants de l’entité doivent suivre. C’est pas très compliqué sauf si on fait des listes par composants (est-ce que la vue aurait chaque composant «vue» de chaque entité? Du coup la logique et la partie graphique du jeu auraient une méthode où on passe l’id de l’entité et qui détruit ses composants).
Du coup, tu peux choisir la deuxième, je t'ai donné des pistes pour supprimer ton tableau à trois dimensions. Pour la durée de vie des entités, tu peux regarder le tutoriel dont le parle, je donne une piste à base de composant Lifetime.
Si je modifie un composant j’aimerais bien pouvoir accéder à l’entité pour lui dire de mourir proprement, avec des composants à id on peut faire ça proprement. Ou alors ma super grille ne contient que des int qui sont des id!
Normalement, dans les systèmes, tu modifies des entités et tu accèdes à leurs composants. Donc tu as les entités correspondant aux composants. Et ta super grille, supprime la ! ;)
Pour les composants, j’ai l’impression qu’ils ont forcément une interface commune (au moins une méthode update), mais pas plus?
Non ! Ce sont des données, rien de plus. Le code de ta méthode update, il va dans un système, pas ailleurs.
N’est-il pas plus logique d’avoir un pointeur pour chaque type de composant qui est à null s’il n’en a pas?
Si tu as une entité container de composant, peut-être. Sinon, ça n'arrive pas puisque tu traites dans ton système uniquement les entités qui ont les bons composants.
Notamment parce que je ne vois pas l’intérêt pour mon jeu de rajouter des composants à la volée.
Pour l'instant non. Mais le composant Lifetime, tu peux l'ajouter à la volée, le composant IA, tu peux l'ajouter à la volée (en déclenchant le réveil du monstre avec un interrupteur invisible !). Bref, je vois plein d'extension facile avec les quelques composants que j'ai décrit.
Du coup il va falloir que je fasse de l’héritage sur les composants, j’ai l’impression à lire çà et là des trucs sur les ECS que c’était «mal»!
Oui, saimal ! Si tu hérites tes composants, c'est qu'en fait, tu as deux composants : un avec les données de la classe mère et un avec les données de la classe fille (sans les données de la classe mère). Ou alors, tes composants ne sont pas assez atomiques. Ou alors, tes composants sont mal faits.
Est-ce que je pourrait avoir un exemple très basique d’implantation de mon concept (en pseudo-code ou C++/Java/Python/...)? Parce quand c’est les concepts des autres j’ai du mal à suivre... Là je pense (j’espère) que ça m’aiderait carrément.
J'ai fait ce que j'ai pu, j'espère que ça t'aura éclairé. Je te conseille vraiment d'aller voir l'exemple donné au début (et aussi de suivre le lien donné au début de l'article).
[^] # Re: C’est abstrait mais c’est intéressant
Posté par rewind (Mastodon) . En réponse à la dépêche Je crée mon jeu vidéo E01 : les systèmes à entités. Évalué à 5.
Ton exemple est un très bon cas d'étude. Il me rappelle un cas similaire sur Bomberman qui a des similitudes avec ton cas et duquel tu pourras sans doute t'inspirer.
Première chose : il n'est pas sûr que ton cas soit idéal pour utiliser un système à entités. Tu pourrais rester dans une conception classique et ça marcherait, je pense. Maintenant, on peut très bien utiliser un système à entité et je vais te dire comment je ferais.
Deuxième chose : tout ne se règle pas à coups de composants. Typiquement, le fond de carte n'est dans aucun composant, tu vas la dessiner en entier et ensuite, tu dessines le reste. Un autre classique, c'est la position du héros qui peut être dans un composant mais qui doit être accessible dans la plupart des systèmes.
Maintenant sur le fond, reprenons.
En fait, il n'y a pas vraiment trois couches, il y en a deux, voire une et demie. Tu as ton fond de carte (ce que tu appelles tuile et qui ne change pas normalement donc que tu dessines en une seule fois avant tout le reste, ça compte donc pour un demi). Ensuite, tout le reste, ça fait partie de la même catégorie, celle des objets du jeu, éventuellement avec un composant
Collideablequi contient un champheight(comme dans l'exemple ci-dessus) qui va indiquer qu'est-ce qui est à quel niveau, ou level comme tu le dis après, c'est la même idée.Ton début n'est pas mauvais. Je ne mettrai pas de composant «mécanisme», parce qu'un mécanisme, c'est une entité, pas une partie d'une entité. Il n'y a pas de données dans un mécanisme (sauf peut-être un type mais en fait, chaque type de composant va être défini par ses capacités, pas par un entier qui ne veut rien dire en tant que tel). Idem pour le composant «objet». En revanche, d'après ta description, tes entités mécanisme et objet vont avoir un composant
Position. Ceux qui bougent auront un composantMouvementindiquant l'amplitude du mouvement (on peut se restreindre à un mouvement soit horizontal soit vertical). Et tu auras donc un système qui prend toutes les entités avec une position et un mouvement pour mettre à jour la position en fonction du mouvement. Tu pourrais avoir un composantOuverturepour l'entité interrupteur qui contient l'entité porte correspondante (ou même plusieurs entités si ça peut ouvrir plusieurs portes). Et donc, un système qui prend toutes les entités avec une position et une ouverture et qui va regarder la position actuelle du héros et ouvre la porte si jamais le héros et sur la même position que l'interrupteur. Pour tes pics, c'est un peu la même idée que pour mouvement, c'est le cas typique de l'animation. Donc tu vas avoir un composantAnimationqui indique le nombre d'animation et la frame courante et les durées entre chaque frame. Ce composant, tu pourras le réutiliser pour d'autres mécanismes animés. Et pour les pics, tu peux avoir un composantDangerqui indique le type de danger (destruction, etc) et à quelle frame ils apparaissent. Tu auras un système qui va prendre les entités avec animation et danger pour dire si l'entité est actuellement en mode danger ou en mode pas danger, puis un système qui va prendre les entités avec danger et position, et qui va regarder si le héros est sur un danger ou pas et si ce danger est actif ou pas, et qui peut faire la même chose avec n'importe quel objet en fait et si ça fait mal, tu ajoutes un composant Lifetime (dont j'ai parlé à plusieurs reprises dans ce thread) qui indique que l'entité est morte. Ce dernier système va également fonctionner pour les danger pas animé. Et tu auras un système qui détruit toutes les entités qui sont mortes.Après, avec cette conception, tu peux avoir des mix intéressants, genre des dangers animés qui servent aussi d'interrupteur (dans ton ancienne conception, je suis certain que tu n'y avais même pas pensé, ici c'est gratuit, il suffit de mettre les bons composants dans l'entité).
Non, ce n'est pas une méthode, c'est un système ! Tu auras un composant IA (avec ce qu'il faut pour que ça marche, par exemple fuite ou recherche pour savoir si l'entité fuit le héros ou le recherche) et ton système va prendre tes entités avec un composant IA et une position et va mettre à jour tout ça. Du coup, tu peux ajouter un composant IA à n'importe quel entité, montre ou même interrupteur, ou même porte (une porte qui fuit le héros, ça c'est fun).
Heu, à moins que tu aies des milliers d'objets (et donc des cartes énormes), je pense que c'est une optimisation prématurée. Quant au tableau, il est parfaitement inutile, sauf pour les tuiles (donc il est à deux dimensions). Tout le reste, ce seront des entités. Et la mise à jour se fait dans des systèmes. Je n'ai pas évoqué tous les systèmes mais tu auras un système pour les collisions par exemple.
La manière dont chaque entité est dessiné et la logique du jeu sont deux choses séparées. Ton système de dessin va effectivement regarder toutes les entités avec un sprite et un level, peut les ordonner au préalable et les dessiner, mais quelque part, peu importe leur type, ça reste des sprites avec une hauteur.
Du coup, tu peux choisir la deuxième, je t'ai donné des pistes pour supprimer ton tableau à trois dimensions. Pour la durée de vie des entités, tu peux regarder le tutoriel dont le parle, je donne une piste à base de composant Lifetime.
Normalement, dans les systèmes, tu modifies des entités et tu accèdes à leurs composants. Donc tu as les entités correspondant aux composants. Et ta super grille, supprime la ! ;)
Non ! Ce sont des données, rien de plus. Le code de ta méthode update, il va dans un système, pas ailleurs.
Si tu as une entité container de composant, peut-être. Sinon, ça n'arrive pas puisque tu traites dans ton système uniquement les entités qui ont les bons composants.
Pour l'instant non. Mais le composant Lifetime, tu peux l'ajouter à la volée, le composant IA, tu peux l'ajouter à la volée (en déclenchant le réveil du monstre avec un interrupteur invisible !). Bref, je vois plein d'extension facile avec les quelques composants que j'ai décrit.
Oui, saimal ! Si tu hérites tes composants, c'est qu'en fait, tu as deux composants : un avec les données de la classe mère et un avec les données de la classe fille (sans les données de la classe mère). Ou alors, tes composants ne sont pas assez atomiques. Ou alors, tes composants sont mal faits.
J'ai fait ce que j'ai pu, j'espère que ça t'aura éclairé. Je te conseille vraiment d'aller voir l'exemple donné au début (et aussi de suivre le lien donné au début de l'article).