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

Langage C++ Discussion :

Jeu de plateforme POO : Problème de design


Sujet :

Langage C++

  1. #1
    Futur Membre du Club
    Homme Profil pro
    Étudiant
    Inscrit en
    Juin 2018
    Messages
    3
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Belgique

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Juin 2018
    Messages : 3
    Points : 5
    Points
    5
    Par défaut Jeu de plateforme POO : Problème de design
    Bonjour,

    Je suis actuellement en train de développer un jeu de plateforme en 2D (en C++/OpenGL) afin de m'exercer à la programmation orientée objet mais je me retrouve face à un problème

    Tout d'abord, voici ma logique de conception :
    Nom : designPlatformer2DGLFW.png
Affichages : 411
Taille : 172,1 Ko

    Moveable est une classe abstraite où la méthode "move" et "moveOnMap" doivent être redéfinie. La méthode moveOnMap prend un pointeur sur la map dans laquelle le "Character" (ou "Item") va évoluer. Ainsi, lorsque l'on appellera la méthode "move", elle fera appel à la map afin de lui demander s'il y a une collision à telle ou telle position et ainsi mettre à jour sa vitesse et son accélération.

    La classe SpriteRenderer est commune pour tous les Sprite et permet d'afficher celui-ci à l'écran.

    La classe Transformable permet de définir une origine et effectuer des translations, rotations et mises à l'échelles sur le Sprite pour l'affichage.

    La classe Item représente un objet quelconque soumis à la gravité ainsi qu'aux différentes collisions sur la TileMap (d'où le fait qu'elle hérite de Moveable).

    La classe Character représente un joueur qui, en plus d'être soumis à la gravité et aux collisions sur la TileMap, peut sauter et se déplacer à gauche ou à droite.

    La classe TileMap représente une map et possède des méthodes permettant de vérifier si un Sprite touche un item ou un joueur inscrit dans la TileMap


    Mon problème est le suivant :

    Chaque objet Moveable doit s'inscrire sur une map dans laquelle il évolue. Ceci afin de faire appel aux fonctions de la map permettant de savoir s'il touche d'autres objets qui se sont inscrits dans la même TileMap. Pour ce faire, la méthode "moveOnMap" de Item et Character fait appel respectivement à la méthode "addItem" et "addPlayer" de TileMap. Ces méthodes prennent un shared_ptr en argument.

    Le problème étant que je dois construire un shared_ptr sur l'objet dans lequel je suis actuellement (puisque je le fais dans "moveOnMap"). Je n'ai donc d'autres choix que d'hériter de "enable_shared_from_this<T>" afin de pouvoir récupérer un shared_ptr sur this. Cela veut également dire que Character et Item doivent être gérées via un shared_ptr pour ne pas perdre la ressources lorsque je retirerai le joueur ou l'item de la TileMap. Or je veux pouvoir créer un Character ou un Item sur la pile.

    Un moyen de résoudre ce problème serait de prendre un pointeur nu en argument de "addItem" et "addPlayer" mais n'est-il pas possible de régler ce problème tout en gardant les pointeurs intelligents ?

    Un deuxième problème est que j'aimerais cacher les méthodes "addItem" et "addPlayer" pour qu'elles ne puissent être utilisées que par un Moveable.

    PS: C'est mon premier projet en orienté objet et je me doute que l'architecture laisse à désirer sur de nombreux points. Je suis d'ailleurs preneur si vous voyez des choses qui pourraient être améliorées ou qui ne sont pas correctes ou pas logiques.

  2. #2
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 614
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 614
    Points : 30 626
    Points
    30 626
    Par défaut
    Salut, et bienvenue sur le forum!
    Citation Envoyé par seba110298 Voir le message
    Bonjour,

    Je suis actuellement en train de développer un jeu de plateforme en 2D (en C++/OpenGL) afin de m'exercer à la programmation orientée objet mais je me retrouve face à un problème

    Tout d'abord, voici ma logique de conception :
    Attend, parce que je suis pris d'un doute au vu de ce diagramme : que représentent les flêches une composition une agrégation un héritage
    Moveable est une classe abstraite où la méthode "move" et "moveOnMap" doivent être redéfinie.
    Si tu le dis, c'est que tu veux qu'il en soit ainsi... Mais, alors, où sont les classes concrètes, celles qui redéfinissent jutement le comportement de move et de moveOnMap

    En UML, une flêche comme celle indiquée par ton diagramme représente une relation d'héritage, et je doute vraiment que ce soit le genre de relation que tu veux partout

    Car, si c'est le cas, autant t'arrêter tout de suite : si la classe Sprite peut hériter de Transformable (quoi, que... c'est déjà limite), elle ne peut absolument pas hériter de SpriteRenderer, car il n'y a rien à faire : un sprite n'est pas un objet qui permet d'afficher ... des sprites

    De même, s'il s'avère "logique" que la classe Item hérite de Movable, elle ne peut absolument pas hériter de Sprite, car, il n'y a -- là non plus -- rien à faire : un item n'est pas un sprite.

    Par contre, d'après ce que tu dis

    La classe Character représente un joueur qui, en plus d'être soumis à la gravité et aux collisions sur la TileMap, peut sauter et se déplacer à gauche ou à droite.
    il semblerait "logique" que la classe Character hérite de Item, vu que tu peux décemment transmettre un personnage partout où tu veux tester les collisions entre ... deux éléments différents.

    La méthode moveOnMap prend un pointeur sur la map dans laquelle le "Character" (ou "Item") va évoluer.
    Hum... je préférerais que cette fonction prenne la Map par référence, voir même par référence constante, vu que le but est de tester les collisions...

    Et, tant qu'à faire, je verrais d'avantage un nom qui indique clairement que la fonction fait des tests de collisions

    Ainsi, lorsque l'on appellera la méthode "move", elle fera appel à la map afin de lui demander s'il y a une collision à telle ou telle position et ainsi mettre à jour sa vitesse et son accélération.
    Ca, ca semble raisonnable

    La classe SpriteRenderer est commune pour tous les Sprite et permet d'afficher celui-ci à l'écran.
    Encore une fois,quelle est la relation entre la classe SpriteRenderer et la classe Sprite une agrégation une composition un héritage

    Je viens de t'expliquer que cela ne peut pas être un héritage. Et, d'un autre coté, cela ne peut pas être une composition, car les différentes instances de Sprite sont totalement indépendante de l'instance de SpriteRenderer.

    Ne reste donc plus que l'agrégation comme choix possible. Sauf qu'elle ne se représente pas sous la forme d'une fleche en UML, mais bien sous la forme d'un losange vide
    La classe Transformable permet de définir une origine et effectuer des translations, rotations et mises à l'échelles sur le Sprite pour l'affichage.

    La classe Item représente un objet quelconque soumis à la gravité ainsi qu'aux différentes collisions sur la TileMap (d'où le fait qu'elle hérite de Moveable).


    La classe TileMap représente une map et possède des méthodes permettant de vérifier si un Sprite touche un item ou un joueur inscrit dans la TileMap
    Sur ces différents points, je ne vois pas trop de raisons de me plaindre
    Mon problème est le suivant :

    Chaque objet Moveable doit s'inscrire sur une map dans laquelle il évolue. Ceci afin de faire appel aux fonctions de la map permettant de savoir s'il touche d'autres objets qui se sont inscrits dans la même TileMap.
    Ca, ca parrait logique

    Pour ce faire, la méthode "moveOnMap" de Item et Character fait appel respectivement à la méthode "addItem" et "addPlayer" de TileMap.
    Arghhhh!!!!

    oui, mais hé, ho ! STOP!!!

    Primo: je croyais que le but de cette fonction était de tester les collisions!!! Si elle s'occupe de cela, elle ne peut, en vertu du ==>SRP<==, pas soccuper de déplacer les objet sur la map !

    Ou, dans l'autre sens : si elle s'occupe de déplacer les objets sur la map, elle ne peut pas s'occuper de tester les collisions : ce sont deux responsabilités clairement distinctes, qui nécessitent le recours à deux fonctions bien séparées!

    Deuxio :Comment une fonction qui dit vouloir déplacer des élément pourrait elle avoir besoin d'en créer ????? D'ailleurs, en vertu de la ==>loi de Déméter<==, cette fonction n'a même pas à savoir comment la classe la classe TileMap gère les objets en interne!

    Dans le meilleur des cas, si le but est bel et bien de permettre à un objet Movable d'occasionner un déplacement sur la TileMap, cela devrait se faire sous une forme proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    void Movable::MoveOnMap(TileMap & map){
        map.move(this, newPosition);
    }
    Ces méthodes prennent un shared_ptr en argument.
    Pourquoi des shared_ptr

    As tu conscience de ce qu'implique le comptage de référence d'un shared_ptr en termes de performances

    Le problème étant que je dois construire un shared_ptr sur l'objet dans lequel je suis actuellement (puisque je le fais dans "moveOnMap"). Je n'ai donc d'autres choix que d'hériter de "enable_shared_from_this<T>" afin de pouvoir récupérer un shared_ptr sur this.
    C'est justement là l'astuce : tu n'as aucune raison de construire un shared_ptr au niveau de ta fonction moveOnMap

    Cela veut également dire que Character et Item doivent être gérées via un shared_ptr pour ne pas perdre la ressources lorsque je retirerai le joueur ou l'item de la TileMap. Or je veux pouvoir créer un Character ou un Item sur la pile.
    Hé non!

    Car ta classe TileMap pourrait contenir des unique_ptr sur des objet Movable. Lorsqu'un objet se déplace, tu déplace le pointeur sous-jacent de unique ptr d'une case à l'autre (au travers des fonction release() et reset() )

    D'un autre coté, si tu as tu as besoin d'une liste (dynamique) d'objets à traiter, rien ne t'empêche de travailler avec une collection de ==>reference_wrapper<== qui fournissent un accès direct (sous forme de référence) à l'objet qui se trouve à l'adresse représentée par le pointeur sous-jacent du unique_ptr (sous réserve, bien sur, que ce pointeur sous-jacent ne soit pas nul).

    Ca t'évite le recours au shared_ptr, et ca t'évite d'avoir à te poser systématiquement la question de savoir si ton pointeur est valide, null ou autre

    Un moyen de résoudre ce problème serait de prendre un pointeur nu en argument de "addItem" et "addPlayer" mais n'est-il pas possible de régler ce problème tout en gardant les pointeurs intelligents ?
    Le meilleur moyen est surtout de respecter la loi de déméter:
    on ajoute les pointeur dans le TileMap lorsque qu'on crées les objet (idéalement, on fournit d'ailleurs tout ce qu'il faut à la fonction pour créer l'objet / le joueur, ce qui se ferait au travers d'une ==>fabrique<==) et puis, on n'y touche plus (du moins, vu de l'extérieur de TileMap) : on délègue le déplacement, la destruction et autres joyeusetés à des fonctions spécifiques de TileMap

    Un deuxième problème est que j'aimerais cacher les méthodes "addItem" et "addPlayer" pour qu'elles ne puissent être utilisées que par un Moveable.
    Encore une fois, la loi de Déméter s'oppose à toi:

    Quand tu fais appel à une de ces fonctions, tu n'as absolument aucune raison de devoir connaitre le type Item ou le type Player : transmet les informations "de base" (position, accélération, vitesse et autres joyeusetés nécessaires) à la fonction, et laisse la se débrouiller pour créer l'Item (ou le Player) comme il le faut

    PS: C'est mon premier projet en orienté objet et je me doute que l'architecture laisse à désirer sur de nombreux points. Je suis d'ailleurs preneur si vous voyez des choses qui pourraient être améliorées ou qui ne sont pas correctes ou pas logiques.
    Tu ne te doutais sans doute pas que c'était à ce point

    Mais c'est un bel effort quand même
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  3. #3
    Futur Membre du Club
    Homme Profil pro
    Étudiant
    Inscrit en
    Juin 2018
    Messages
    3
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Belgique

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Juin 2018
    Messages : 3
    Points : 5
    Points
    5
    Par défaut
    Salut, merci pour ta réponse et le temps que tu m'accordes.

    Attend, parce que je suis pris d'un doute au vu de ce diagramme : que représentent les flêches une composition une agrégation un héritage
    Je t'avoue que je ne connaissais pas UML et ne savait pas ce qu'était une agrégation :/.
    Corrige-moi si je me trompe mais une agrégation est, comme une composition, représenté par un attribut de classe, avec la différence que l'agrégation peut-être liée à plusieurs instances (de classes différentes ou non) et ne dépend donc pas de la durée de vie de l'instance sur laquelle il est lié.
    Si ce que j'ai dit est correct, alors les attributs et méthodes de chaque classe sont montrés (sur le diagramme) en plus des héritages, mais la différence n'est pas faites entre agrégation et composition pour chaque attribut.
    Les flèches représentent donc une relation d'héritage.

    Moveable est une classe abstraite où la méthode "move" et "moveOnMap" doivent être redéfinie.
    Si tu le dis, c'est que tu veux qu'il en soit ainsi... Mais, alors, où sont les classes concrètes, celles qui redéfinissent jutement le comportement de move et de moveOnMap
    Ce sont Character et Item qui les redéfinissent.

    En UML, une flêche comme celle indiquée par ton diagramme représente une relation d'héritage, et je doute vraiment que ce soit le genre de relation que tu veux partout
    Ce diagramme a été généré sur le code que j'ai fait et donc toutes les flèches représentent une relation d'héritage (oui, c'est sûrement pas beau à voir toutes ces mauvaises relations ).

    De même, s'il s'avère "logique" que la classe Item hérite de Movable, elle ne peut absolument pas hériter de Sprite, car, il n'y a -- là non plus -- rien à faire : un item n'est pas un sprite.
    À vrai dire, la seule différence que je fais entre un Item et un Character (autre que le mouvement) est qu'un Character possède plusieurs images (animé) et que l'Item n'en contient qu'une. Et à la construction d'un Sprite, je lui donne le nombre d'images, à savoir 1 pour l'Item et le nombre d'images que contient la texture pour Character. Pourquoi ne puis-je donc pas dire qu'un Item est Sprite avec seulement 1 image ?

    Encore une fois,quelle est la relation entre la classe SpriteRenderer et la classe Sprite une agrégation une composition un héritage

    Je viens de t'expliquer que cela ne peut pas être un héritage. Et, d'un autre coté, cela ne peut pas être une composition, car les différentes instances de Sprite sont totalement indépendante de l'instance de SpriteRenderer.

    Ne reste donc plus que l'agrégation comme choix possible. Sauf qu'elle ne se représente pas sous la forme d'une fleche en UML, mais bien sous la forme d'un losange vide
    Pour la classe SpriteRenderer, l'idée que j'avais était que toutes les instances de SpriteRenderer possèdent les mêmes ressources (à l'aide de static) et que ces ressources soient créées et affectées lors de la première instanciation de la classe (et pas avant car d'autres choses doivent être initialisées avant la création des ressources), comme ceci:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
     
    class SpriteRenderer
    {
    protected:
    	SpriteRenderer();
    	void draw(const Texture& texture, unsigned element, const Vector2f& spritePosition, const Vector2i& spriteSize, const SquareMatrix& localTransformation, bool drawHitbox) const;
     
    private:
    	static unsigned VAO, VBO, EBO;
    	static std::unique_ptr<Shader> m_shader;
    	static std::unique_ptr<float[]> m_data;
    	static std::unique_ptr<unsigned int[]> m_indices;
    };
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
     
    SpriteRenderer::SpriteRenderer()
    {
    	if (!m_data && !m_indices && !m_shader)
    	{
                    m_data = std::make_unique<float[]>(4 * 4);
    		m_indices = std::make_unique<unsigned int[]>(6);
    		m_shader = std::make_unique<Shader>("../Platformer2DGLFW/spriteVertexShader.vs", "../Platformer2DGLFW/spriteFragmentShader.fs");
    		...
    	}
    }
    Après réflexion, je me rends compte qu'étant donné que toutes les instances de Sprite possèdent les mêmes ressources de SpriteRenderer, j'aurais pu mettre ces ressources communes directement dans Sprite. Le problème c'est que ces ressources permettent d'afficher un Sprite et donc là, je mélange la logique métier de son affichage... Et ça, c'est une mauvaise chose, non ?
    La meilleure solution est donc, comme tu l'as dit, de créer une relation d'agrégation en instanciant la classe SpriteRenderer une fois puis en la liant à tous les Sprite à l'aide d'une référence ?

    Ca, ca parrait logique

    Pour ce faire, la méthode "moveOnMap" de Item et Character fait appel respectivement à la méthode "addItem" et "addPlayer" de TileMap.
    Arghhhh!!!!
    oui, mais hé, ho ! STOP!!!

    Primo: je croyais que le but de cette fonction était de tester les collisions!!! Si elle s'occupe de cela, elle ne peut, en vertu du ==>SRP<==, pas soccuper de déplacer les objet sur la map !

    Ou, dans l'autre sens : si elle s'occupe de déplacer les objets sur la map, elle ne peut pas s'occuper de tester les collisions : ce sont deux responsabilités clairement distinctes, qui nécessitent le recours à deux fonctions bien séparées!

    Deuxio :Comment une fonction qui dit vouloir déplacer des élément pourrait elle avoir besoin d'en créer ????? D'ailleurs, en vertu de la ==>loi de Déméter<==, cette fonction n'a même pas à savoir comment la classe la classe TileMap gère les objets en interne!
    Je pense que je me suis mal exprimé (et les nom de mes méthodes sont TRÈS MAL choisis). La méthode "moveOnMap" est une méthode permettant uniquement d'inscrire un Moveable dans la TileMap. Et les méthodes "addItem" et "addPlayer" prennent en argument un shared_ptr<Item> et shared_ptr<Character> respectivement (que je vais devoir changer du coup) qui sera ajouté à la liste des inscrits pour cette TileMap.

    Voici l'implémentation de "moveOnMap" de la classe Item:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
     
    void Item::moveOnMap(std::shared_ptr<TileMap> map)
    {
    	if (m_map != nullptr) //Si l'item était inscrit dans une TileMap
    		m_map->removeItem(shared_from_this()); //On le supprime de la liste de TileMap
     
    	//On garde un pointeur de la map pour laquelle on s'inscrit (agrégation ?)
    	m_map = map;
    	//On rajoute l'item dans la liste des Item de TileMap
    	m_map->addItem(shared_from_this());
    }
    Voici l'implémentation de "addItem" de la classe TileMap:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
     
    void TileMap::addItem(std::shared_ptr<Item> item)
    {
    	//Ajoute l'Item dans la liste "d'inscrits"
    	m_items.push_back(item);
    }
    C'est ensuite la méthode "move" redéfinie sur Item et Character qui va "gérer" les collisions. En effet, la méthode "move" va calculer la position de Item ou Character après un certain et va demander ensuite à TileMap (via le pointeur sur TileMap gardé lors de l'inscription) s'il touche un élément afin de corriger la position et mettre à jour son accélération et vitesse.

    Voici l'implémentation de "move" de la classe Item
    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
     
    void Item::move(const Time& elapsedTime)
    {
    	pl::Time remainingToCalculate = elapsedTime;
    	if (!m_map->is1PxNextToHitboxOnBottom(*this) || m_speed.getComponents() != Vector2f(0.f, 0.f) || m_acceleration.getComponents() != Vector2f(0.f, 0.f))
    	{
    		while (remainingToCalculate > pl::Time::Zero)
    		{
    			//Set vertical acceleration if the air
    			if (m_map->is1PxNextToHitboxOnBottom(*this))
    			{
    				m_speed.setYComponent(0.f);
    				m_acceleration.setYComponent(0.f);
    			}
    			else
    				m_acceleration.setYComponent(VERTICAL_ACCELERATION_PX_PER_SECOND);
     
    			//Set horizontal speed
    			if (!m_acceleration.getYComponent()) //if on the ground
    			{
    				if (m_speed.getXComponent() < 0.f && MRUA1D::getEndSpeed(m_speed.getXComponent(), elapsedTime, SPEED_TO_ZERO_PX_PER_SECOND) < 0.f)
    					m_speed.setXComponent(MRUA1D::getEndSpeed(m_speed.getXComponent(), elapsedTime, SPEED_TO_ZERO_PX_PER_SECOND));
    				else if (m_speed.getXComponent() > 0.f && MRUA1D::getEndSpeed(m_speed.getXComponent(), elapsedTime, -SPEED_TO_ZERO_PX_PER_SECOND) > 0.f)
    					m_speed.setXComponent(MRUA1D::getEndSpeed(m_speed.getXComponent(), elapsedTime, -SPEED_TO_ZERO_PX_PER_SECOND));
    				else
    					m_speed.setXComponent(0.f);
    			}
     
    			if (m_map->is1PxNextToHitboxOnTop(*this) && m_speed.getYComponent() < 0.f)
    				m_speed.setYComponent(0.f);
    			if (m_map->is1PxNextToHitboxOnLeft(*this) && m_speed.getXComponent() <= 0.f)
    				m_speed.setXComponent(0.f);
    			else if (m_map->is1PxNextToHitboxOnRight(*this) && m_speed.getXComponent() >= 0.f)
    				m_speed.setXComponent(0.f);
     
    			Vector2f initialPosition = getPosition();
    			setPosition(MRUA2D::getNextPosition(getPosition(), m_speed.getYComponent(), m_speed.getXComponent(), remainingToCalculate, m_acceleration.getYComponent(), 0));
     
    			pl::Time calculated = remainingToCalculate;
    			while (m_map->isTouchingHitbox(*this))
    			{
    				calculated = pl::microseconds(calculated.asMicroseconds() / 2);
    				setPosition(MRUA2D::getNextPosition(initialPosition, m_speed.getYComponent(), m_speed.getXComponent(), calculated, m_acceleration.getYComponent(), 0));
    			}
     
    			m_speed.setYComponent(MRUA1D::getEndSpeed(m_speed.getYComponent(), calculated, m_acceleration.getYComponent()));
     
    			remainingToCalculate -= calculated;
    		}
    	}
    }
    L'appel à la méthode "is1PxNextToHitboxOnBottom" de TileMap par exemple, permet de savoir s'il y a une hitbox d'un joueur ou d'un item en dessous de lui.

    Voici l'utilisation dans la boucle principale (main)
    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
     
    //Inscription du joueur et de l'item dans la TileMap.
    player->moveOnMap(map);
    item->moveOnMap(map);
     
    //Démarre le chrono
    pl::Clock clock;
    while (!glfwWindowShouldClose(window))
    {
    	//rendering commands here
    	glClearColor(0.8125f, 0.953125f, 0.96484375f, 1.0f);
    	glClear(GL_COLOR_BUFFER_BIT);
     
    	//Récupère le temps écoulé et remets le chrono à 0
    	pl::Time clockTime = clock.restart();
     
    	//MOVE
    	player->move(clockTime);
    	item->move(clockTime);
     
    	//DRAW
    	//Dessine les tuiles qui composent la map
    	map->draw(tilemapShader.get(), player->getPosition());
    	item->draw();
    	player->draw();
     
    	// check and call events and swap the buffers
    	glfwSwapBuffers(window);
    	glfwPollEvents();
    }
     
    glfwTerminate();
    exit(EXIT_SUCCESS);
    Cela veut également dire que Character et Item doivent être gérées via un shared_ptr pour ne pas perdre la ressources lorsque je retirerai le joueur ou l'item de la TileMap. Or je veux pouvoir créer un Character ou un Item sur la pile.
    Hé non!
    Car ta classe TileMap pourrait contenir des unique_ptr sur des objet Movable. Lorsqu'un objet se déplace, tu déplace le pointeur sous-jacent de unique ptr d'une case à l'autre (au travers des fonction release() et reset() )
    Je n'ai pas bien compris ce que tu as voulu dire. Qu'entends-tu par "Lorsqu'un objet se déplace, tu déplace le pointeur sous-jacent de unique ptr d'une case à l'autre" ?

    Tu ne te doutais sans doute pas que c'était à ce point
    À vrai dire, si . J'ai énormément de mal à identifier les bonnes responsabilités pour chaque classe et donc je me doutais qu'il y avait pas mal de problèmes de conception.

    Encore merci pour ton aide

  4. #4
    Rédacteur/Modérateur


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

    Informations professionnelles :
    Activité : Network game programmer

    Informations forums :
    Inscription : Juin 2010
    Messages : 7 115
    Points : 32 967
    Points
    32 967
    Billets dans le blog
    4
    Par défaut
    Il est très important de découpler ton gameplay du rendu.
    Un personnage, je suppose aura une position, fera une action etc. C'est une entité gameplay.
    Mais ce n'est pas un sprite, il possède un sprite et manipule ce sprite pour afficher ce qui correspond à ce qu'il se passe (lancer une animation liée à son action en cours, mettre à jour la position d'affichage avec la position du personnage, ...).
    Je ne vois pas pourquoi tu as Sprite et SpriteRenderer. SpriteRenderer c'est normalement au plus une fonction d'affichage d'un sprite. Cette fonction peut être membre de Sprite : le Sprite contient toutes les infos nécessaires à s'afficher (position, texture, ...).
    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
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 614
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 614
    Points : 30 626
    Points
    30 626
    Par défaut
    Citation Envoyé par seba110298 Voir le message
    Corrige-moi si je me trompe mais une agrégation est, comme une composition, représenté par un attribut de classe, avec la différence que l'agrégation peut-être liée à plusieurs instances (de classes différentes ou non) et ne dépend donc pas de la durée de vie de l'instance sur laquelle il est lié.
    C'est exactement cela (même si les termes choisis sont un peu "exotiques" )

    Si on reprend ton schéma, toutes les instances de la classe Sprite pourraient se partager pourraient se partager "différentes instances" de la classe Texture, car, après tout, il n'y aurait rien d'étonnant à avoir deux sprites différents (metttons : un en haut à gauche du plateau et l'autre en bas à droite) qui seraient rendus à l'écran de la même manière
    La notion de composition se représente en C++ à l'aide de références ou de pointeurs :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    class Sprite{
    public:
        /* il faut une texture pour représenter le sprite à l'écran */
        Sprite(Texture const & tex):m_texture{tex}{
        }
    private:
        Texture const & m_texture; // <== Agregation en UML
    };
    La composition implique que l'instance "contenue" ne peut exister que ... tant que l'instance de la classe "contenant" existe:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    class Positionable{
    public:
        Posiionnable(int posX, int posY):m_position{posX,posY}{
        }
    private:
        Position m_position; // <== Composition en UML
    };
    Les flèches représentent donc une relation d'héritage.
    <...>

    Ce diagramme a été généré sur le code que j'ai fait et donc toutes les flèches représentent une relation d'héritage
    C'est bien ce que je craignais

    Car la relation d'héritage est la relation la plus forte que l'on puisse trouver entre deux classes, car elle implique de pouvoir transmettre une instance de la classe dérivée à n'importe quelle fonction s'attendant à recevoir instance de la classe de base comme paramètre). On parle de substituabilité.

    En d'autre termes, si on dit souvent (pour la facilité) que l'héritage représente une relation EST-UN, ce n'est pas tout "à fait juste", dans le sens où on devrait dire que l'héritage représente une relation "EST-SUBSTITUABLE-A"

    C'est pour cela que je te disais que Sprite peut hériter de Transformable (un sprite est un élément qui peut être substitué à un élément transformable), mais qu'il ne peut pas hériter de SpriteRenderer

    Ce sont Character et Item qui les redéfinissent.
    Si l'héritage fonctionne (aussi bien pour Character que pour Item) avec Movable (car les éléments et les personnages doivent pouvoir se déplacer), il ne fonctionne pas pour Sprite, parce que ni les objets ni les personnages ne sont, à proprement parler, des sprites : ils sont mis en relation (sans doute par agrégation) avec un sprite qui pourra être utilisé pour l'affichage à l'écran, mais, pour le reste, on n'a absolument aucun besoin de savoir ce genre de chose pour les manipuler.

    Cela se remarque très fort quand on place tout cela dans un contexte MVC (Model View Controler, ou Modèle Vue Contrôleur, si tu préfères en anglais): les classes Item et Character font partie du modèle : ce sont les "données métier" que l'on manipule tout au long du jeu.

    La classe Sprite, par contre, n'est là que ... pour nous permettre d'afficher les différents éléments à l'écran. Elle fait donc clairement dans la partie Vue du MVC.

    Or, si dépendance il doit y avoir entre le modèle et la vue, ce seront les éléments de la vue (la classe Sprite, en l'occurrence) qui doivent interroger les éléments du modèle (l'interface Movable, en l'occurrence) pour pouvoir se mettre à jour (par exemple : mettre à jour les matrices de translation et / ou de transformation), et non l'inverse

    (oui, c'est sûrement pas beau à voir toutes ces mauvaises relations ).
    Non, vraiment pas

    À vrai dire, la seule différence que je fais entre un Item et un Character (autre que le mouvement) est qu'un Character possède plusieurs images (animé) et que l'Item n'en contient qu'une. Et à la construction d'un Sprite, je lui donne le nombre d'images, à savoir 1 pour l'Item et le nombre d'images que contient la texture pour Character. Pourquoi ne puis-je donc pas dire qu'un Item est Sprite avec seulement 1 image ?
    Avec les explications que je viens de donner concernant le MVC, ce sera peut-être plus clair

    Le sprite, il s'en fout pas mal de savoir à quoi correspond ce qu'il affiche : tout ce qu'il doit savoir, c'est:
    1. le nombre d'images qui seront affichées et
    2. la position (sur l'écran) d'un des coins (le coin supérieur gauche, par exemple) à laquelle il doit s'afficher

    Qu'il affiche un personnage, un banc, un coffre, un arbre ou la lune, pour lui, ca ne fera absolument aucune différence

    Pour la classe SpriteRenderer, l'idée que j'avais était que toutes les instances de SpriteRenderer possèdent les mêmes ressources (à l'aide de static)
    Heu, oui, mais non ...

    Le problème est double :

    Primo et sauf erreur de ma part, on peut aisément craindre que le nombre de float par tableau, le nombre d'indices par tableau, et, surtout, les valeurs représentées dans ces différents tableaux diffèrent d'un élément à l'autre. Tu ne peux donc pas déclarer ces données comme étant statiques parce que... le simple fait de modifier ces informations pour une instance donnée de SpriteRenderer les modifierait ... pour toutes les instances existantes

    Ce qui nous mène tout droit au deuxième problème, qui est bien plus conceptuel :

    secundo (et je suis sur de mon fait, sur ce coup), ta classe SpriteRenderer déroge au SRP (Single Responsability Principle, ou, si tu préfères : le Principe de la Responsabilité Unique) qui nous dit que chaque type de donnée, chaque donnée, chaque fonction ne doit s'occuper que d'une seule et unique chose.

    Il manque donc deux notions importantes dans ton code, à savoir :
    • Une notion (appelons la RenderingData) qui regroupera les différentes donnée (VAO, VBO, EBO, shader, datas et indices) nécessaires à l'affichage et
    • Une notion (appelons la RenderingDataHolder) qui permettra de regrouper les différents éléments de type RenderingDataafin de les maintenir en mémoire, et de permettre au SpriteRenderer de les utiliser

    Cela te permettrait d'avoir un et un seul SpriteRenderer qui irait chercher les données dont il a besoin (regroupées au sein de la structure RenderingData) auprès du RenderDataHoler en fonction des informations reçues par le Sprite

    Je sais que cela a l'air un peu tiré par les cheveux, mais c'est sans doute ce qui t'offrira le plus de souplesse à l'utilisation

    étant donné que toutes les instances de Sprite possèdent les mêmes ressources de SpriteRenderer, j'aurais pu mettre ces ressources communes directement dans Sprite.
    Non, parce que cela n'aurait fait que déplacer le problème de respect du SRP
    Le problème c'est que ces ressources permettent d'afficher un Sprite et donc là, je mélange la logique métier de son affichage... Et ça, c'est une mauvaise chose, non ?
    Tout à fait

    L'un dans l'autre, même si la classe Sprite fait partie des notions qui entre dans la partie Vue du MVC, elle n'est là que pour aider la classe SpriteRenderer à sélectionner les informations qu'il doit utiliser pour effectuer l'affichage (surtout si tu adopte le point de vue que je viens d'exposer).

    Ceci étant dit, on pourrait même envisager que la notion de RenderingDataHolder ne soit effectivement là que pour maintenir en mémoire les différentes instances de RenderingData, et de n'y faire appel que ... lorsque l'on veut effectivement créer un nouveau sprite, en créant une agrégation au niveau de la classe Sprite.

    On modifie donc un tout petit peu le code que je donnais (et on en profite pour appliquer DIP pour l'affichage du sprite) plus haut pour lui donner la forme de
    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
    class Sprite{
    public:
        /* il faut une texture pour représenter le sprite à l'écran,
         * et les informations permettant l'affichage à l'écran
         */
        Sprite(Texture const & tex, RenderingData rend):m_texture{tex},m_rendering{rend}{
        }
        void draw(SpriteRenderer /* const */ & renderer){
            renerer.draw(m_rendering); /* tu auras compris qu'il faut adapter le prototype de la fonction draw
                                        * de la classe SpriteRenderer
                                        */
        }
    private:
        Texture const & m_texture; // <== Agregation en UML
         RenderingData const & m_rendering;
    };
    La meilleure solution est donc, comme tu l'as dit, de créer une relation d'agrégation en instanciant la classe SpriteRenderer une fois puis en la liant à tous les Sprite à l'aide d'une référence ?
    Ou, mieux encore : d'appliquer, comme je viens de le faire, le =>DIP<= (le cinquième principe SOLID : Dependancies Inversion Principle ou si tu préfères, le principe des inversion des dépendances)


    Je pense que je me suis mal exprimé (et les nom de mes méthodes sont TRÈS MAL choisis). La méthode "moveOnMap" est une méthode permettant uniquement d'inscrire un Moveable dans la TileMap.
    Non!!! En vertu de la loi de Déméter, ce qui apparitent à la TileMap doit rester à la TileMap : la classe Movable ne doit rien connaitre de ce qui permet en interne à la TileMap de travailler!

    C'est d'autant plus vrai que TileMap est une donnée "externe" à la classe Movable, vu que tu la transmet comme paramètre à la fonction, et que l'instance de TileMap contient -- a priori -- l'instance de Movable que l'on veut déplacer.

    Si le but est effectivement de faire déplacer l'élément Movable (qu'il s'agisse d'un Item ou d'un Character importe peu) dans ta TileMap, la manière de s'y prendre ressemble à:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    void Movable::moveOnMap(TileMap & map){
        map.move(this, // parce qu'il faut bien indiquer quel élément doit être déplacé
                 newPos); // parce qu'il faut indiquer où il veut aller dans la TileMap
    }
    Et les méthodes "addItem" et "addPlayer" prennent en argument un shared_ptr<Item> et shared_ptr<Character> respectivement (que je vais devoir changer du coup)
    Non!!!

    Encore une fois, la loi de Déméter est claire et sans appel : quand tu utilises (une instance de) la classe TileMap, ce qui est privé à la classe DOIT rester privé!

    Autrement dit, en tant qu'utilisateur de TileMap, tu ne dois déjà pas savoir qu'elle manipule des pointeurs intelligents en interne. Et, par conséquent, aussi surprenant que cela puisse paraître, tu ne dois pas savoir qu'elle manipule des objets de type Item et des objets de type Character!

    Tout ce que tu as le droit de savoir, ce sont les informations dont elles a besoin pour pouvoir créer elle-même les pointeurs intelligents qu'elle manipule; et par conséquent, pour pouvoir créer les objets qui dont la durée de vie sera gérée par ces pointeurs.

    Je ne sais pas quels sont les paramètres nécessaires aux constructeurs de tes classe Item et Character, mais, en gros, les fonctions addItem et addCharacter de ta classe TileMap devraient ressembler à quelque chose comme
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void TileMap::addItem(/* paramètres requis pour la création d'un Item */
                          /*, la position où le placer dans la tilemap*/){
        /* la création du pointeur intelligent et du Item  associé se fait ici 
         * et !!! NULLE PART AILLEURS !!!
         */
    }
    void TileMap::addCharacter(/* paramètres requis pour la création d'un Character */
                                /*, la position où le placer dans la tilemap*/){
        /* la création du pointeur intelligent et du Character associé se fait ici 
         * et !!! NULLE PART AILLEURS !!!
         */
    }
    C'est ensuite la méthode "move" redéfinie sur Item et Character qui va "gérer" les collisions.
    Alors, dis moi : quelle est la différence entre
    1. la collision d'un Item avec un Item
    2. la collision d'un Item avec un Character
    3. la collision d'un Character avec un Item
    4. la collision d'un Character avec un Character

    (concentre toi surtout sur les points (2) et (3) )

    Je veux bien que les classes Item et Character redéfinissent la fonction, mais, pour cela, il faut forcément qu'il y ait une différence de comportement, autrement, la redéfinition n'a aucun sens

    En outre, nous sommes de nouveau confrontés au SRP: D'après son nom, la fonction move a pour but de ... permettre de changer la position de l'élément Movable. A ce titre, elle ne peut donc pas prendre la responsabilité de vérifier si le déplacement occasionne une collision ou non.

    Et, si elle ne s'occupe pas de tester si le déplacement occasionne une collision (parce que cette responsabilité aura été déléguée à "quelque chose" qui ne fait que cela), on en arrive à se poser une autre question: quelle est la différence de comportement entre le fait de déplacer un Item et celui de déplacer un Character

    Or, a priori, il n'y aura aucune différence de comportement, car une forme proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void Movable :: move(){
        Position newPos;
        /* on calcule la "nouvelle position" ici... la logique est la même pour les deux */
        CollisionTester tester; //on délègue le test des collision à un objet qui ne s'occupe que de cela */
        if(! tester.collide(this, newPos)){   /* si l'objet courant n'entre pas en collision avec "autre chose
                                            * en allant vers sa nouvelle position
                                            */
           m_position = newPos; // alors, la nouvelle position devient la position de l'objet
        }
    }
    sera suffisamment "générique" que pour s'adapter à n'importe quel type d'élément Movable, qu'en penses tu
    Je n'ai pas bien compris ce que tu as voulu dire. Qu'entends-tu par "Lorsqu'un objet se déplace, tu déplace le pointeur sous-jacent de unique ptr d'une case à l'autre" ?
    Je présume que ton incompréhension vient du terme "pointeur sous-jacent"... Je vais donc commencer par expliquer ce point

    Le "pointeur sous-jacent" correspond au pointeur pour lequel les classes unique_ptr et shared_ptr prennent la responsabilité de libérer les ressources lorsqu'il n'est plus utile.

    Pour faire simple, cela correspond au pointeur "nu" que tu récupère lorsque tu fais appel aux fonctions membre get() et release() de ces classes.

    Pour le reste, au cas où j'aurais mal compris ta question (mais, au vu du code que tu présentais pour moveOnMap, je ne crois pas que ce soit le cas), on va commencer par enfoncer une porte ouverte : la notion de déplacement implique forcément de quitter une position afin d'en rejoindre une autre.

    Oui, je sais, cela fait drôle d'énoncer de la sorte quelque chose qui semble pourtant "tomber sous le sens", mais ca reste une bonne habitude de conception à prendre, surtout pour les choses qui semblent "tellement logiques" qu'elle "vont forcément de soi", pour être sur que ces chose "si logiques" soient exprimées (dans l'analyse des besoins, et, plus tard, dans le code ).

    Enfin, bref, la TileMap va représenter ... ton plateau de jeu. Il ne faudra pas s'étonner si on trouve en interne un code du genre de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    std::vector<std::unique_ptr<Movable>> m_cases; // toutes les cases du plateau
    dans la partie privée de la classe

    A priori, une case est "vide" lorsque le "pointeur sous-jacent" est égal à nullptr, et occupée lorsque ... ce n'est justement pas le cas

    La fonction move de la classe TileMap pourrait donc prendre une forme proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    void TileMap::move(Position const & start, Position const & arrival){
        /* on commence par transformer les deux positions en indices du tableau */
        size_t startIndex = toIndex(start);
        size_t arrivalIndex = toIndex(arrival);
        /* on s'assure que la position d'arrivée est vide (c'est un bon moyen de tester les collisions, tiens ... ) */
        if(m_cases[arrivalIndex]==nullptr){
            /* Chouette, l'objet (quel qu'il soit) qui se trouve à la position de départ
             * peut atteindre sa position d'arrivée 
             */
           auto * mover = m_cases[startIndex].release(); // il quitte sa position de départ
           m_cases[arrivalIndex].reset(mover); // pour rejoindre sa position d'arrivée
        }
    }
    Voilà, à peut de chose près ce que je voulais faire passer comme idée
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  6. #6
    Futur Membre du Club
    Homme Profil pro
    Étudiant
    Inscrit en
    Juin 2018
    Messages
    3
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Belgique

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Juin 2018
    Messages : 3
    Points : 5
    Points
    5
    Par défaut
    Je viens de corriger mon code en prenant en compte toutes les modifications données Merci encore pour ton aide !

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

Discussions similaires

  1. Problème de plateforme pour un jeu de plateforme
    Par Guiguimon dans le forum SDL
    Réponses: 4
    Dernier message: 26/10/2009, 16h01
  2. Un jeu d'invasion en POO - Problème de boucle
    Par <arobase> dans le forum Tkinter
    Réponses: 2
    Dernier message: 10/05/2008, 17h48
  3. [AS2] [POO] Problème de duplication d'un MovieClip
    Par segphault dans le forum ActionScript 1 & ActionScript 2
    Réponses: 5
    Dernier message: 11/01/2006, 15h44
  4. [POO] Problème de paramètre passé par référence
    Par dug dans le forum Général JavaScript
    Réponses: 1
    Dernier message: 31/08/2005, 20h29
  5. [POO] Problème lors de l'appel d'une propriété d'un objet.
    Par akecoocoo dans le forum Général JavaScript
    Réponses: 3
    Dernier message: 24/08/2005, 08h51

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