Bonjour, je suis en train de créer un Snake en C++, je sépare mon programme ainsi : J'ai une classe App qui contient : la map(qui contient les objets statiques), le snake(qui contient un vector qui stock les parties du serpent). Seul problème j'aimerais pouvoir afficher le programme de telle façon à ce que j'ai une classe externe aux autres qui aurait comme seul but d'afficher le jeu, tout simplement. Seulement, les données de mes classes (comme les cases de ma classe Map) sont privées, par conséquent je n'ai pas moyen de les afficher.
Au lieu de fournir une référence vers ton application à ce que l'on peut appeler "ton contexte d'affichage", tu peux appliquer le principe appelé le DIP (pour Dépendency Inversion Principle) et ... transmettre ce contexte à tout ce qui a besoin d'être affiché.
Si bien que tu prévoirais l'interface de ton AppRenderer (ce que j'appelle moi ton "contexte d'affichage"), de telle manière à ce qu'elle soit en mesure d'afficher les différents éléments qui doivent être affichés, par exemple:
la tête du serpent
les parties du "corps" de ton serpent
les vitamines
tout ce que je peux avoir oublié.
Et, d'un autre coté, tu veillerais à fournir une fonction draw(AppRenderer &) à ... tous les éléments qui sont susceptibles d'être tracés.
Cela te permettrait, par exemple, d'avoir une fonction d'affichage de ton serpent qui serait proche de
void Snake::draw(AppRenderer & render) const{
/* j'ai considéré le fait que la tête du serpent
* était gérée séparément du reste du corps
head_.draw(render);
for(auto const & p : parts_){
p.draw(render);
}
};
avec une classe particulière pour la tête qui disposerait également de cette fonction draw sous une forme proche de
void Head::draw(AppRenderer & render) const{
/* si on sait que l'on trace la tete, on a
* besoin:
* - du coté dans lequel elle regarde et
* - de la position où elle se trouve
*/
render.drawHead(position_, orientation_);
}
Et, d'un autre coté, tu aurais donc une notion propre aux ... autre partie du corps du serpent, qui disposerait également de cette fonction draw et qui ressemblerait à quelque chose comme
void SnakePart::draw(AppRenderer & render) const{
/* Si on sait qu'on trace une partie du serpent,
* tout ce qu'il nous faut c'est... la position
* à laquelle elle se trouve
*/
render.drawPart(position_);
}
Sans oublier la notion de vitamine, qui apparait également dans ton jeu, qui disposerait également de cette fameuse fonction, sous une forme proche de
void Vitamine::draw(AppRenderer & render) const{
/* si on sait que l'on affiche une vitamie,
* tout ce qu'il nous faut c'est... la position
* où elle se trouve
*/
render.drawVitamine(position_);
}
Et tu auras donc deviné que ton AppRenderer ressemblera à quelque chose comme
(je vais te laisser implémenter ces fonctions particulières )
L'énorme avantage de cette technique, c'est que, pour peu que tu déclares ces trois fonctions comme étant virtuelles (et que tu penne les mesures adéquates pour assurer la sémantique d'entité à ta notion de AppRenderer), tu peux décider de créer à tout moment une classe qui en dérive pour obtenir un contexte d'affichage différent:
l'un qui utiliserait la SFML
l'autre qui utiliserait la console
un troisième qui utilise Qt
voir, en tirant un peu sur la corde (car le terme de "draw" est alors peut-être mal choisi) pour sauvegarder l'état de ton jeu à un moment donné (quoi que, s'il s'agit de sauvegarder un "screenshot", le terme draw en vaut un autre )
Bien sur, tu devras choisir entre utiliser un affichage à base de SFML ou à base de Qt, mais cela peut t'ouvrir énormément de possibilités
Une autre solution pourrait être d'utiliser un système de signaux et de slots ( !!! je ne pense pas forcément à celui de Qt: il est possible d'en mettre un en place en moins de 200 lignes de code !!!) grâce auquel chaque partie serait en mesure d'émettre un signal particulier pour chaque étape importante de sa vie.
Tu pourrais alors te contenter de connecter ton contexte d'affichage à ces différents signaux, et faire en sorte qu'il maintienne -- d'une façon ou d'une autre -- sa "propre représentation" de l'ensemble du plateau de jeu à jour.
Il ne manquerait alors qu'un signal "update", émis sans doute par ton plateau de jeu, auquel ton contexte d'affichage serait évidemment connecté et qui provoquerait, au niveau du contexte d'affichage la... mise à jour de l'affichage.
L'idée derrière tout cela serait de rendre les deux parties (la partie métier, représentée par ta classe App et tout ce qu'elle contient et la vue, représentée par ta AppRenderer (*) ) totalement indépendante l'une de l'autre, tout en leur permettant malgré tout de communiquer au travers du système de signaux et de slot
(*) qui pourrait carrément sortir de la classe App pour l'occasion )
Pour un jeu "aussi simple" qu'un snake, la première solution est très largement suffisante.
Mais, dés le moment où tu commenceras à multiplier les éléments affichable à l'envi, la deuxième solution sera sans doute préférable
- Edité par koala01 16 avril 2018 à 23:45:33
Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
@koala01 Bonjour, merci de m'avoir répondu. Le problème est que la classe Vitamine, dans mon cas, ce n'est pas une classe, j'ai une classe Map qui contient des cases, et ces cases contiennent un std::string indiquant ce que contient la case, "v" pour du vide, "a" pour une pomme etc... Par conséquent je ne sais pas comment utiliser ma "méthode" drawApple() si ma vitamine dans ce contexte, n'est pas une classe. Dois-je créer une classe case dont-je ferais hériter toutes les autres ?
petit detail: la lib standard de C++ utilise du snake_case pour ses classes, c'est une bonne pratique (peu importe le langage) de suivre le format utilisé par le langage, ainsi on n'a pas à se poser des questions inutile tel que
class foo_bar
class FooBar
class Foo_Bar
class FOO_BAR
Si toutes les libs et tous les developpeurs utilisaient la même façon de faire, on aurais déjà une galère en moins :)
Bon, j'ai programmer pratiquement tout le snake, sauf qu'une erreur(non pas de compilation) intervient Et ça doit faire 2-3 H que je cherche à déboguer mon programme, et donc pour éviter de créer 3000 sujets pour chaque erreurs je préfère poster mon problème ici . L'erreur intervient dans la "méthode" add_snake_part dans le fichier Snake.cc, voici le github : https://github.com/Asstryfi/Snake (il y a surement des erreurs de syntaxes quelque part, du à mes essaies de corrections) .
tu devrais transmettre ton contexte par référence aux différentes fonctions draw, car c'est typiquement une classe qui aura sémantique d'entité, vu qu'il est possible de l'intégrer dans une hiérarchie de classe
Au lieu d'utiliser un tableau pour maintenir la liste des parties du serpent, tu devrais envisager d'utiliser une liste, car on peut envisager le déplacement comme étant:
l'ajout d'une partie du serpent à l'emplacement où se trouvait la tête juste avant et
le retrait de la dernière partie du serpent
Avec une petite astuce, lorsque le serpent mange la vitamine (ou la pomme, ou quel que soit le nom que tu lui donne): il grandit "d'une partie", et on ne doit donc pas retirer la dernière, on peut très facilement arriver à un résultat correct
l'énorme avantage de cette technique sera que tu n'es pas obligé de calculer, pour chaque partie du serpent, la nouvelle position de celle-ci à chaque déplacement (d'autant plus que la nouvelle position ne dépend pas de la direction dans laquelle le serpent regarde mais de la partie de serpent précédente).
On ne le répétera jamais assez: si les accesseurs (getXXX) peuvent, sous certaines conditions, être tout à fait justifiés, les mutateurs n"ont absolument aucun intérêt et ouvrent la porte à une mauvaise manipulation de la classe. J'ai écrit quelques interventions sur le sujet ici même, je vais te laisser
Par contre, on peut considérer le fait de "tourner" comme étant un service que tu es effectivement en droit d'attendre de la part du serpent (comme on peut considérer le fait de "bouger" de la même manière)
La différence entre une fonction "turn" et une fonction "set_direction" pourrait ne pas être évidente, mais le fait est que "tourner" peut tout à fait faire "un peu plus" que de changer la direction dans laquelle le serpent regarde.
Elle pourrait -- par exemple -- éviter que le serpent n'essaye de faire un demi tour complet pour regarder, finalement, dans la direction de son corps. Et ca, ce serait vachement utile, car aucun serpent n'est capable de le faire
PS: n'oublie pas d'ajouter la fonction draw au serpent, pour lui permettre de la "transmettre" à l'ensemble de ses parties
Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
Merci @koala01 d'avoir pris le temps de lire mon code et de m'en donner un avis
koala01 a écrit:
Deux petites choses concernant ton code:
tu devrais transmettre ton contexte par référence aux différentes fonctions draw, car c'est typiquement une classe qui aura sémantique d'entité, vu qu'il est possible de l'intégrer dans une hiérarchie de classe
Je ne vois pas vraiment ce que tu veux dire par "passer ton contexte par référence", il faut que je fournisse les classes aux différentes fonctions draw() ?
koala01 a écrit:
On ne le répétera jamais assez: si les accesseurs (getXXX) peuvent, sous certaines conditions, être tout à fait justifiés, les mutateurs n"ont absolument aucun intérêt et ouvrent la porte à une mauvaise manipulation de la classe. J'ai écrit quelques interventions sur le sujet ici même, je vais te laisser
Donc je dois laisser ma variable "Position" publique ?
koala01 a écrit:
PS: n'oublie pas d'ajouter la fonction draw au serpent, pour lui permettre de la "transmettre" à l'ensemble de ses parties
Je ne sais pas si c'est ce que tu voulais dire mais j'ai fais cette méthode qui permet de dessiner toutes les parties du serpent :
Je ne vois pas vraiment ce que tu veux dire par "passer ton contexte par référence", il faut que je fournisse les classes aux différentes fonctions draw() ?
koala01 a écrit:
Il y a trois moyen de transmettre une donnée à une fonction qui en a besoin pour travailler
par valeur: la fonction appelée utilise une copie de la donnée, ce qui laisse la donnée d'origine "en l'état"
par référence(éventuellement constante): la fonction appelée utilise un "alias" de la donnée d'origine,si bien que les modifications apportées à cet alias seront prises en compte (s'il y en a) au niveau de la fonction appelante
par pointeur:on transmet l'adresse mémoire à laquelle se trouve la donnée. Hormis quelques cas bien particulier, on évitera autant que faire se peut d'avoir recours à cette méthode
Le code que j'ai vu sur github utilise la transmission par valeur. En plus de provoquer un tas de copies inutiles qui nuiront largement aux performances, cela va occasionne une série de problèmes (nommons les pour ce qu'ils sont :des bugs) parce que, bien qu'elles portent le même nom, la variable renderer que l'on utilise dans Snake::draw est une donnée différente de celle que l'on utilise dans Part::draw (*).
Si tu observes attentivement le code que je t'ai donné en exemple, tu constatera qu'il y a régulièrement une esperluette '&' qui se glisse entre l'identifiant de type et le nom d'une donnée (il y a souvent le mot clé const qui se glisse entre les deux, mais c'est juste pour indiquer que je n'envisage pas de les modifier ).
Quelques exemples:
void Snake::draw(AppRenderer & render) const{
void Head::draw(AppRenderer & render) const{
for(auto const & p : parts){
(y en a surement d'autres)
Cette esperluette indique au compilateur qu'il s'agit d'une référence, et que c'est donc un alias de la donnée qui a servi à définir la valeur
Si bien que tous les ordres que je pourrai donner à -- mettons -- renderer seront effectivement transmis à la "donnée originelle", et non à "une pale copie de la donnée originelle".
(*) j'ai d'ailleurs beaucoup insisté sur la possibilité qui nous était donnée de faire dériver différentes classes de AppRenderer en fonction du contexte dans lequel l'affichage devrait avoir lieu. Je ne l'ai pas fait par hasard.
J'aurais sans doute du être plus explicite et dire clairement que, du fait de cette possibilité, ta classe AppRenderer a ce que l'on appelle une sémantique d'entité, dont l'une des caractéristique est d'interdire purement et simplement la copie et l'assignation.
En donnant cette sémantique d'entité à ta classe AppRenderer, le compilateur aurait été en mesure de se rendre compte le passage se faisait par valeur et occasionnait une copie, et il aurait purement et simplement refusé d'aller plus loin, parce que tu lui demande de faire quelque chose qu'on lui a explicitement interdit de faire
Donc je dois laisser ma variable "Position" publique ?
C'est une des possibilités, en effet... Mais c'est très certainement la pire des possibilités que tu puisse envisager
Car il faut bien te dire que, si tu place une donnée dans l'accessibilité publique, "tout le monde" peut y accéder sans la moindre restriction et en faire strictement "ce qu'il veut"
D'ici à ce qu'un imbécile distrait décide de définir cette positon à 419,812 alors que tu n'a qu'une grille de 20 / 20, il n'y a qu'un pas.
Peu importe comment il sera arrivé à ces nombres: comme le - et le * sont juste l'un à coté de l'autre au niveau d'un pavé numérique, il aura sans doute multiplié au lieu de soustraire, ou bien il aura fait une erreur dans sa logique...
Le "pourquoi" et le "comment" n'ont pas vraiment d'intérêt ici. Ce qui importe, c'est que ca peut arriver, et que si ca arrive, tu sera dans une merde noire.
C'est la raison pour laquelle il faut radicalement changer la manière dont tu réfléchis aux différents types de données que tu crée:
Au lieu de réfléchir en termes des données qui les composent (ex: "la tête est représentée par sa position et la direction dans laquelle elle regarde"), on va se poser la question des questions auxquelles on veut qu'elle puisse répondre et des ordres auxquelles on veut qu'elle soit en mesure d'obéir.
Pour la tête, il n'y a que trois ordres auxquelles elle doit obéir:
tourne toi (dans la direction indiquée)
déplaces-toi (d'une case, dans la direction où tu regarde) et
demande au renderer de t'afficher
Alors, bien sur, il faudra bien que la classe dispose de certaines données pour pouvoir obéir à ces trois ordres. Mais, on se rend tout de suite compte que la manière dont ces données sont représentées n'a absolument aucun intérêt pour quelqu'un qui manipule l'instance de cette classe
On pourrait -- aussi (si cela s'avère utile)-- envisager de se donner l'occasion de poser plusieurs questions à la tête de notre serpent, à savoir:
Dans quelle direction regardes-tu? et
quelle est ta position actuelle?
(voire) Quelle sera ta prochaine position?
Et c'est là, surtout avec la dernière question, que l'on prendra conscience du fait que les réponses ne correspondent pas forcément aux données qui permettent à la classe de fournir les services que l'on attend de sa part.
En effet, la prochaine position de la tête devra forcément ... être calculée. Comment? sans doute en prenant en compte la position actuelle et l'orientation de la tête.
Mais ca, c'est la "popote interne" de la classe
Je ne sais pas si c'est ce que tu voulais dire mais j'ai fais cette méthode qui permet de dessiner toutes les parties du serpent :
Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
On peut aller plus loin et définir une fonction membre qui fait ce qu'on veut avec les parties du serpent, le ce-qu-on-veut étant indiqué par un paramètre fonctionnel.
Exemple
void afficher(int x, int y) {
std::cout << x << " " << y << std::endl;
}
int main()
{
Snake s;
s.grow(1,10).grow(2,11).grow(3,12);
s.foreach_part(afficher); // That's it
return 0;
}
avec
class Snake {
private:
std::vector<std::pair<int,int>> parts;
public:
Snake & grow(int x, int y) {
parts.emplace_back(x,y);
return *this;
}
void foreach_part(std::function<void(int, int)> f) { // définition
for (auto & part : parts) {
f(part.first, part.second);
}
}
};
[I like chaining : petite chanson] qui marcherait aussi avec un "Renderer", simulé ici :
class Renderer {
std::string name;
public:
Renderer(const std::string &n) : name{n} {};
void render_part(int x, int y) { // une fonction membre
std::cout << "drawing " << x << " " << y
<< " on " << name << std::endl;
}
};
- Edité par michelbillaud 18 avril 2018 à 16:04:26
Séparer les classes et l'affichage
× Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
× Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
Architecte logiciel - Software craftsmanship convaincu.
Architecte logiciel - Software craftsmanship convaincu.