
Nous ne savons pas si vous avez remarqué, mais en C il est assez dur, enfin plutôt encombrant, de devoir manipuler tous les objets composant le jeu dans la boucle des événements.
On peut bien sûr utiliser des fonctions pour déléguer le travail, mais au final ça revient un peu au même : c'est difficile à suivre. Dans ce chapitre, nous allons voir comment créer une liste qui référence tous les éléments du jeu. Ces éléments sont des objets qui auront des méthodes qu'il faudra appeler pour les afficher à l'écran, par exemple. Ca va permettre de mieux organiser notre code : nous pourrons après ce chapitre coder chaque élément du jeu dans une classe à part.
Prérequis : POO de base, héritage, notions sur les listes chaînées.
Introduit : Polymorphisme, les listes avec la STL, les virtuelles (pures).
Savez-vous ce qu'est une liste chaînée ?
Il existe des tutoriels qui l'expliquent, mais nous allons faire une rapide introduction ici :
Une liste chaînée, est ce qu'on appelle une structure de données. En gros, c'est un système qui va vous permettre
d'organiser vos données d'une certaine manière. Les tableaux, par exemple, sont une structure de données.
(note : Ici, les données sont les objets qui composent notre jeu.)
Les listes chaînées vont nous permettre d'avoir une sorte de tableau... Mais qui n'aura pas un nombre de cases précis. On pourra en retirer ou en ajouter à la volée très simplement. De plus, il deviendra très simple grâce à cette liste, d'insérer un élément entre deux autres sans avoir à décaler toutes les autres données à chaque fois.
Alors, convaincus ? Il y a tout de même une petite chose à savoir : une liste chaînée est très lente par rapport à un tableau.
En effet, pour arriver à l'élément numéro X d'une liste chaînée, il faut parcourir toute la liste avant; contrairement au tableau où on peut avoir directement la case.
Par contre, si on veut parcourir toute la liste case par case, la différence de temps n'est pas très handicapante.
Ce qui est parfait, car c'est exactement ce que nous allons faire. :)
Elles n'existent pas nativement en langage C. Par contre, nous pouvons les créer nous même, et c'est d'ailleurs ce que nous allons faire, à titre d'exercice. Nous reviendrons ensuite au langage C++, où nous verrons qu'elles existent déjà dans la bibliothèque standard. Néanmoins, pour que vous compreniez le principe, nous allons d'abord les recoder nous même.
Une liste chaînée est une suite de noeuds.
Voici comment on peut définir un noeud :
struct noeud
{
noeud *suivant;
int donnees;
};
Comme pour les tableaux, on a des listes chaînées d'entiers, d'autres de caractères, etc. Bref, du type que l'on
veut.
Pour le moment nous avons codé la structure qui représente un noeud d'une liste d'entiers.
Vous voyez le pointeur "Suivant" ? Pour le premier noeud (la première case) de la liste, il va pointer sur le noeud N°2. Le "suivant" du noeud N°2 sera le N°3.
Et ainsi de suite jusqu'au dernier noeud, qui lui a un suivant qui pointe sur NULL, c'est d'ailleurs grâce à ça qu'on le reconnaît. Nous avons donc des listes chaînées comme ceci :
Element 1 --> Element 2 --> Element 3 --> Element 4 --> NULL
Vous voyez à présent comme il est facile, si on dispose d'un pointeur vers l'Element 1, d'accéder à la liste.
Il suffit d'utiliser une boucle ou la récursivité pour parcourir les noeuds jusqu'a celui qui nous intèresse.
Pour rajouter un objet à la fin de la liste, il suffit de créer un nouveau noeud, d'y mettre son objet
(nouveauNoeud->donnees = montruc), puis de faire pointer le "suivant" du dernier élément de la liste sur ce nouveau
Noeud.
Nous vous laissons vous documenter plus précisément sur le sujet, et vous recommandons également de recoder votre
structure de noeud pour entraînement.
Puis de créer par exemple des fonctions de manipulation, qui prendront en paramètre le premier élément d'une liste et qui pourront faire diverses choses : retourner l'objet numéro X de la liste, insérer un objet en fin de liste, à une position précise dans la liste, supprimer un élément de la liste, etc...
Tout ça en manipulant les pointeurs "suivant" de vos éléments. Chaque fonction prendra uniquement le premier noeud de la liste en paramètre, le reste étant des nombres (le N° de l'objet à supprimer, le nombre à insérer dans la liste,
etc...).
Bon, l'exercice était de les recoder en C, mais en C++ elles existent nativement !
En fait elles font partie de la bibliothèque standard du C++ qui contient toutes sortes de types de conteneurs dont vous n'avez plus qu'a vous servir.
Cette bibliothèque s'appelle la STL. Elle comporte de nombreux modules, alors nous allons simplement ici présenter le module des listes. Tout d'abord, nous avons besoin d'inclure le header :
#include <list>
Ensuite, pour créer une liste chaînée, c'est tout bête :
std::list<int> maListe;
Ici, nous avons créé une liste d'entiers, mais vous pouvez mettre n'importe quoi entre les crochets.
Pointeurs, types de base, structures, classes... Tout passe très bien !
Comme d'habitude, vous pouvez vous passer du std:: si jamais vous faites un "using namespace std;" dans le même fichier .cpp.
Base numéro deux : comment insérer un objet dans une liste.
Pour ça, il va vous falloir un itérateur. Un itérateur est un objet qui représente un "curseur" dans votre liste.
Tout est en effet géré en interne, vous ne voyez pas comment std::list s'occupe de la gestion de vos données.
Un itérateur vous indique donc une "position" dans la liste. Pour en obtenir un, il vous suffit de faire :
std::list<int>::iterator iter;
Maintenant, il faut affecter cette variable itérateur à la fin de la liste. Pour cela faites simplement :
iter = maListe.end();
N'oubliez pas, quand vous créez l'itérateur, de mettre exactement le même type entre crochets que celui de la liste a laquelle il correspond.
Maintenant, vous pouvez insérer des éléments à la fin de la liste :
maListe.insert(iter, 18);
Le problème, est que l'itérateur pointe maintenant sur la nouvelle valeur 18. Le curseur n'a en effet pas bougé. Il faut donc le refaire ""pointer"" sur la fin de la liste. Comme il peut se révéler agaçant de toujours réaffecter l'itérateur, on a l'habitude de procéder comme ceci :
Liste.end(), 34);
De cette manière, nous n'avons plus besoin de passer par un itérateur. A moins que vous ne vouliez ajouter une valeur à un endroit précis de la liste. Vu qu'on en aura pas besoin, nous ne vous l'expliquons pas ici, mais vous trouverez sûrement comment faire en vous documentant sur std::list. Nous allons plutôt voir comment parcourir notre liste pour en lire les valeurs. Sauf que la, vous allez avoir besoin d'un itérateur. ^^
La méthode pour parcourir la liste ressemble beaucoup un parcours de tableau à l'aide d'une boucle for et d'une variable i. Explications :
std::list<int> maListe;
maListe.insert(maListe.end(), 18);
maListe.insert(maListe.end(), 34);
std::list<int>::iterator i;
for(i = maListe.begin(); i != maListe.end(); i++)
{
int k = (*i);
std::cout << k << std::endl;
}
Vous comprenez maintenant pourquoi nous avons employé l'expression "un itérateur pointe vers un endroit de la liste", c'est presque comme un pointeur ! On peut le déréférencer avec * pour obtenir la valeur correspondante, et on peut aussi l'incrémenter (i++) pour qu'il pointe sur la case mémoire suivante (sauf qu'ici c'est l'élément suivant de la liste). Avec cette comparaison des pointeurs, vous devriez un peu mieux saisir ce qu'est un itérateur. Nous ne nous étendons pas trop là dessus car il y a d'une part des livres entiers qui ont été écrits sur la STL, que d'autre part ce n'est pas l'objet de ce cours même si cette introduction était nécessaire, et qu'enfin, nous ne somme pas expert en STL.
En tous cas, vous savez assez vous servir d'une liste chaînée pour ce qu'on va en faire : stocker tous les éléments de notre jeu puis parcourir régulièrement la liste.
Vous vous demandez peut être quel type on va donner à notre liste chaînée. Après tout, les objets appartiendront peut être à des classes différentes, et une liste chaînée ne peut avoir qu'un type d'éléments. C'est là qu'une notion très importante de la POO entre en scène : l'héritage, et ce qui en découle : le polymorphisme.
En effet, si nous créons une classe appelée Base de laquelle nous faisons hériter toutes nos autres classes, nous pourrons créer une liste de Base s et après y inclure tous les objets appartenant à des classes dérivées, et ce, sans aucun problème ! Par contre, on ne pourra appeler que les méthodes définies (même si elles sont redéfinies dans les enfants) dans base, et pas les autres.
A partir de là, il nous reste à bien concevoir notre base, et après tout coulera de source !
Un indice : Une base a besoin d'un constructeur, même s'il ne fait rien, d'un destructeur virtuel (cela signifiant qu'il est redéfinissable dans les classes filles), et au moins d'une méthode "frame" virtuelle aussi, qui sera... appelée à chaque frame. ^^
On peut pousser un peu plus loin : tous nos éléments de jeu seront affichés d'une manière ou d'une autre à l'écran.
Enfin, une méthode dessiner() qui affiche l'objet à l'écran sera utile (virtuelle aussi).
Une fois que vous aurez construit cette classe, vous pourrez utiliser le polymorphisme et créer tous les éléments du jeu via des classes qui héritent de Base. Pour un pong, on créera une classe raquette, de laquelle on dérivera RaquetteGauche et RaquetteDroite, et une classe balle. Tout cela dérivera donc directement (ou pas) de Base, en redéfinissant Frame() et Dessiner() selon leurs besoins.
Après, reste la boucle principale. C'est simple, vous devez tout d'abord initialiser la SDL et toutes les bibliothèques dont vous pourriez avoir besoin (par exemple SDL_TTF, SDL_Mixer, ...).
Ensuite, vous créez une liste chaînée de type Base. Vous instanciez alors les objets du jeu (par exemple, deux raquettes & une balle) et vous les ajoutez à la liste.
Puis il ne reste qu'à boucler à l'infini, et pour chaque frame à parcourir la liste pour appeler la méthode dessiner() de chaque objet. Et voilà. :)
Pour la gestion des événements, nous vous recommandons de laisser ça à vos objets : occupez vous simplement de sortir
de la boucle si jamais on attrape un SDL_QUIT.
Les objets récupéreront les événements via des fonctions comme SDL_GetKeyStates() ou SDL_GetMouseState(), par exemple.
Et voilà, pour un petit jeu, genre snake, pong, sokoban, ou quoi, vous avez tout ce qu'il vous faut.
Pour un jeu un peu plus gros, il vous faudra certainement faire une classe Carte, avec un format de fichier spécial pour pouvoir sauvegarder vos cartes et les charger. Mais nous n'y sommes pas encore... ;)
Vous venez de découvrir (ou redécouvrir ;) ) succinctement deux nouveaux concepts, les listes chaînées et le polymorphisme. Nous vous avons expliqué un peu comment nous allons les coupler pour organiser, en gros, le code de notre jeu. En dehors de la fonction principale, la seule chose à coder sera alors les divers éléments qui peuplent le jeu. C'est une vision proche de la pensée humaine : on code ce qu'est une balle, une raquette, etc, et on obtient un pong.