Jeu classique, bataille de tanks |
Autre classique, une bataille entre deux … ici deux tanks. Cet exemple est initialement tiré du livre de Jonathan Harbour, Game programming all in one, Thomson course technology, Boston, 2004. Le jeu Le jeu se joue à deux. Deux tanks évoluent dans un décor simple composé de rectangles dispersés et de couleurs différentes. L’objectif est d’atteindre son adversaire par des tirs et de marquer des points. La réalisation La
réalisation du jeu est décomposée en quatre étapes
principales. Chaque étape donne lieu à un programme compilé
qui fonctionne. A la dernière étape le jeu est terminé
: Modifications possibles, généraliser le modèle du jeu Bien
entendu il est possible de modifier le scénario et de prendre autre
chose que des tanks. |
![]() |
1. Première étape : décor, dessin tanks, affichage aaaTELECHARGER : aaaTexte des explications, aaaExecutable (Windows), aaaSource (utilise la librairie Allegro) Le choix graphique pour ce programme est de tout dessiner à partir de rectangles de triangles et de cercles. L’affichage est réalisé avec un double buffer. Structures
de données première partie : Définir un tank Le
premier point important est de définir un tank. Il a les caractéristiques
suivantes : Chaque caractéristique est représentée par une variable et l’ensemble des variables est regroupé dans une structure : aaatypedef
struct TANK{ Fonctions
essentielles première partie : 1) Initialisations
: encadrer la zone de jeu, créer le décor, poser les tanks aaaa#define COULEUR_CONTOUR aaaamakecol ( 255,242,169 ) La résolution graphique choisie pour le jeu est 8 bits mais le jeu ne crée pas de palette, la palette par défaut est utilisée et quelques couleurs sont créées. Le cadre est tracé une fois pour toute au début du programme directement à l’écran et il ne sera jamais effacé pendant le jeu. Il est tracé de la façon suivante : aaaavoid
init_cadre() (1) En haut dans la marge disponible le titre du jeu est affiché ainsi que la résolution écran à l’aide des variables SCREEN_H et SCREEN_W générée par la fonction set_gfx_mode() d’allegro (voir documentation) (2)
Le cadre de 2 pixels est tracé, deux rectangles imbriqués.
La fonction d'initialisation
du décor crée deux bitmaps à la taille de la zone
de jeu. L’une est destinée à stocker le décor
et l’autre sera le double buffer c'est-à-dire la bitmap d’assemblage
de l’ensemble des éléments graphiques (décor,
tanks, explosions, bonus etc.) (1)aaavoid
init_decor(BITMAP**deco,BITMAP**buffer) (2)aaaaaaa*deco=create_bitmap(SCREEN_W-5,SCREEN_H-17); (1) La fonction init_decor() prend en argument deux pointeurs passés par référence, c'est-à-dire deux pointeurs de pointeur. L’objectif est d’obtenir deux adresses pour des BITMAP* dans le contexte d’appel. (2) malloc avec contrôle d’erreurs des deux bitmap à l’aide de la fonction create_bitmap() de l’environnement Allegro. Les deux bitmaps sont ensuite initialisés à zéro avec la fonction clear() d’Allegro. (3)
La création du décor consiste à bombarder la bitmap
« deco » d’un nombre prédéfini de rectangles
: (4)
Pour chaque rectangle une position est déterminée au hasard.
Cette position tient compte de la taille maximum prédéfinie
d’un rectangle et diminue d’autant les possibilités
de positions horizontales : (5) chaque rectangle a également une taille aléatoire dans la fourchette prédéfinie par TAILLE_RECT_MAX avec une taille minimum de 8 pixels (6)
Création d’une couleur aléatoire et affichage final
du rectangle à sa position dans la bitmap de stockage du décor
« deco » Une
fonction pour l’initialisation des tanks (1)aaaavoid
init_tank(BITMAP *bmp,t_tank *t1,t_tank *t2) (1) La fonction prend deux tanks passés par référence en paramètre. Elle prend également une bitmap, en fait juste pour avoir sa taille. (2) Initialisation de la position du premier tank qui est en haut. La position (x, y) d’un tank correspond au centre du tank et non à un coin. Le tank est un carré de 32 fois 32 pixels. La position en x est au plus prés à 4 pixels du bord et en y à 1 pixel. (3)(7) Initialisation de la direction des tanks. Rappelons qu’il y a quatre directions possibles 0 pour nord, 1 pour est, 2 pour sud et 3 pour ouest. Le tank du haut ne peut pas aller vers le nord et la direction 0 est interdite. Le tank du bas ne peut pas aller vers le sud et c’est la direction 2 qui est interdite (4) Le pas d’avancement est initialement à zéro et sera modifié avec le mouvement des tanks (voir étape suivante) (5) Initialisation des couleurs de chaque tank en utilisant des indices de la palette par défaut. (6) idem 1, 2, 3, 4, 5 mais pour le tank du bas.
Nous avons opté ici pour un dessin sommaire de chaque tank avec juste un rectangle et un triangle. Le triangle indique en fait la direction du tank comme une flèche. La fonction de dessin doit tenir compte de la position de chaque tank : aaaaaa (1)aaaaaavoid
dessine_tank(BITMAP *bmp,t_tank *t) (1) La fonction de dessin des tank prend un tank passé par référence et la bitmap de destination (ce sera le double buffer au moment de l’appel) (2) Tout d’abord un carré est tracé à partir de la position (x, y) du tank considérée comme le centre. (3) Un triangle selon son orientation va indiquer la direction nord, est, sud, ouest..
Il
reste à mettre en place le main, et la boucle d’événements.
aaaaaa#ifndef
JEU_TANK aaaaaa#include
<allegro.h> (1)aaa#define
ECRAN_X 800 aaaaaatypedef
struct TANK{ aaaaaa//
fonctions partie 1 (1) Valeurs prédéfinies pour la résolution de l’écran et l’initialisation graphique. A propos de ECRAN_X et ECRAN_Y cette initialisation effectuée après appel de set_gfx_mode(), ce sont les variables globales SCREEN_W et SCREEN_H qui sont le plus souvent utilisées. MODE prédéfinit le mode graphique sélectionné. (2) Macro pour le contrôle des erreurs (3)
Les valeurs prédéfinies que nous avons déjà
rencontrées ainsi que la définition du type « t_tank
» pour les tanks et les déclarations des fonctions vues. Main et boucle d’événements aaaaaaint
main () (2)aaaaaa
aallegro_init(); (1) Pas de variable globales utilisée dans le programme, initialisation dans le main des variables nécéssaires au programme : les deux tanks, les bitmaps pour le décor et le double buffer. « done » va fournir le test d’arrêt de la boucle d’événements. (2) Les initialisations d’usage dans l’environnement Allegro (voir tutorial à ce sujet) (3) Appel des trois fonctions d’initialisation vues plus haut : création du cadre autour de la zone de jeu, initialisation du décor et du double buffer (4)(7)
Boucle d’événements qui prend fin avec la pression
d’une touche quelconque. (9) Ralentir le processus avec la fonction rest() en millisecondes
|
![]() |
2. Deuxième
étape : ajouter le mouvement aux tanks Structures de données seconde partie : Pas
de modification, juste l’ajout dans la librairie d’une valeur
prédéfinie pour la vitesse des tanks :
Trois nouveaux domaines de fonctions viennent compléter le socle précédent : mouvements, collisions et contrôle des entrées clavier. 3) Mouvements des tanks Le
mouvement est assuré avec cinq fonctions simples qui ont respectivement
pour objectifs de faire avancer, reculer, tourner à gauche, tourner
à droite et bouger chacun des tanks. aaaaavoid
avance (t_tank *t) aaaaavoid
recul (t_tank *t) aaaaavoid
tourne_gauche (t_tank *t) Tourner
à droite aaaaavoid
tourne_droite (t_tank *t) aaaaavoid
bouge(t_tank *t) 4) Collisions Une seule fonction contrôle les collisions des tanks avec les limites de la zone de jeu et les éléments du décor. La détection des collisions teste pour des points de pare choc des tanks la couleur de fond correspondante, si elle est noire, couleur du fond, il n’y a rien sinon le tank touche quelque chose et dans ce cas il s’arrête. le contrôle des collisions s’effectue dans la boucle d’évènement avant le déplacement c'est-à-dire avant appel à la fonction bouge() vue précédemment. En effet, en cas de collision le pas d’avancement est mis à zéro ce qui a pour effet d’immobiliser le char. (1)aaavoid
percute( BITMAP *bmp, t_tank *t) (3)aaaaaaaaaaif
(pas>0){ (1) La fonction percute() prend
en paramètre une bitmap, le double buffer sera passé en
argument au moment où il contient juste le décor (voir la
mise en œuvre plus bas). C’est d’ailleurs discutable
car du coup les tanks ne peuvent pas se détecter mutuellement.
Le second paramètre est le tank concerné par la recherche
de collision avec le décor. aaaaaa (2) Les instructions ne sont effectuées que si le tank est mobile. (3) Si le tank est mobile alors soit il avance soit il recule. Il avance si le pas d’avancement est supérieur à 0. Les points de pare chocs prennent alors leurs valeurs en fonction de la direction du tank. (4) Le tank recule si le pas est inférieur à 0. Les points du pare choc sont alors inversés en fonction des direction (nord donne pare choc sud, est donne pare choc ouest etc.) (5) Finalement le test sur les trois points de pare chocs concernés. Si dans la bitmap une valeur différente de 0, il y a quelque chose et le tank est arrêté. 5) Contrôle clavier Les actions sont commandées avec le clavier et une fonction unique regroupe tous les appels selon les touches appuyées. Le clavier est approché via les scancode (KEY_UP, KEY_DOWN, KEY_RIGHT, etc.) qui correspondent à des indice du tableau « key » de l’environnement Allegro. Ce tableau réactualisé en permanence de façon invisible dans le code et à chaque indice est affecté 0 pour une touche non appuyée et 1 lorsqu’elle est appuyée. Ainsi un test comme « if (key[KEY_UP]) » permet de savoir si la touche flèche haut est appuyée ou non. (1)aaaaaint
entree_clavier(t_tank *t1, t_tank *t2) (1) La fonction prend en argument chacun des deux tanks qui seront nécessaires selon les fonctions appelées avec l’appuie des touches. La fonction renvoie une valeur pour la touche de fin du programme, c’est la valeur de la variable « done » initialisée par défaut à 0 . (2) L’utilisation des
scancodes pour le contrôle du clavier présente l’intérêt
de ne pas bloquer le processeur dans l’attente qu’une touche
soit appuyée comme le fait par exemple la fonction readkey(). C’est
ce qui permet d’avoir plusieurs joueurs simultanément dans
le jeu. En revanche cela peut parfois poser le problème d’une
trop rapide répétition des touches et nécessiter
un contrôle spécifique si l’on ne veut pas avoir de
répétition. Le principe de ce contrôle est simple
: attendre que la touche repasse à 0 avant de considérer
qu’elle peut à nouveau déclencher une commande. C’est
dans cette perspective que sont utilisées des variables statiques.
Chaque touche concernée est doublée d’une variable
« static » qui ne repasse à 0 que si la valeur pour
la touche repasse également à 0 et pour envoyer une commande
il faut à la fois que la variable et la touche correspondent à
des valeurs de 0. (3)(4) Les différents tests selon les touches avec appels ou non des fonctions correspondantes. (5) Si la touche échappe est pressée la variable « done » qui fait l’objet du retour de la fonction passe à 1. Mise
en œuvre : compléter librairie et boucle d’événements aaaaa#define VITESSE_TANK 5 ainsi que les déclarations des fonctions : aaaaa//
fonctions part 2 Boucle
d’événements aaaaawhile
(!done){ aaaaapercute(buf,&t1); aaaaadessine_tank(buf,&t1); |
![]() |
3. Troisième
étape : ajouter les tirs et les scores Modification : 1)
Chaque tank a deux caractéristiques supplémentaires qui
complètent la structure t_tank : Le score est un entier mais les boulets ont eux-mêmes plusieurs caractéristiques qui font l’objet d’une structure intégrée ensuite à celle des tanks. 2)
Caractéristiques des boulets : aaaatypedef
struct BOULET{ (1) La position horizontale et verticale u boulet (2) Chaque tank dispose d’un
seul boulet pour tirer. Lorsqu’un tank tire le boulet s’en
va et le tank doit attendre qu’il ait terminé sa trajectoire
avant de pouvoir tirer une seconde fois. (3) La direction du boulet est donnée en fait au moment du tir, selon la position du tank avec les deux pas d’avancement pasx et pasy. (4) L’affichage du boulet,
comme pour les tanks, est un dessin et il n’y a pas d’utilisation
d’images sprites dans ce programme. La variable « c »
donne la couleur du boulet et « r » donne le rayon du boulet.
En effet les boulets sont représentés par des cerceaux de
taille différentes, comme des sortes de soucoupes. aaaatypedef
struct TANK{
Modifications
: 1) Initialisation des tanks Au moment de l’initialisation des tanks avec la fonction init_tank() une ligne est rajoutée qui « désactive » les boulets de chacun des deux tanks : aaaavoid
init_tank(BITMAP *bmp,t_tank *t1,t_tank *t2){ 2) Affichage des tanks Le score de chaque tank est affiché directement sur chaque tank et fait parti du dessin. Au moment de l’affichage des tanks deux lignes, la première pour définir le mode d’affichage du texte paramétré pour un fond transparent et la seconde qui affiche le score : aaaavoid
dessine_tank(BITMAP *bmp,t_tank *t){ 5) Contrôle clavier Les tirs sont concrétisés à l’aide d’un appel à la fonction tir() qui est détaillée plus bas. Cette fonction est appelée avec pression de la touche espace pour le tank 1 et de la touche enter pour le tank 2. Les lignes suivantes sont ainsi à ajouter dans la fonction : aaaaint
entree_clavier(BITMAP *bmp,t_tank *t1, t_tank *t2){ Nouveautés
: 6) Tirs
et boulets (1)aaaavoid
tir(BITMAP *bmp,t_tank *t) (2)aaaaaaaif
( ! t->boule.actif){ (3)aaaaaaaaaaswitch(dir){ aaaaaaaaaaaaaaaacase
1 : (1) La fonction prend comme paramètres une bitmap, le double buffer, et un tank. Elle ne renvoie rien. Les variables x,y et dir prennent le valeurs respectives du tank concerné et sont uniquement présentes pour faciliter l’écriture. (2) Le tir n’est possible que si le boulet est inactif. Si tel est le cas, le boulet devient actif, le rayon du cercle qui va représenter le boulet est donné aléatoirement compris entre les valeurs 2 et 11 (la variable « r » va faciliter l’écriture ensuite) et pour finir une couleur aléatoire est donnée au boulet. (3) La structure boulet du
tank concerné est initialisée en fonction de la direction
du tank (nord 0, est 1, sud 2, ouest 3). La position (x, y) initiale du
boulet est calculée à partir de la position du tank et rappelons
que la position du tank correspond au centre du tank. La position du boulet,
selon la direction du tank est décalée de 18 pixels par
rapport à celle du tank (le tank est un carré de 32x32 pixels
et sa position est à 16 pixels des bords). Ce décalage est
augmenté de la taille du rayon du boulet obtenu juste avant le
switch. La position du boulet, comme pour le tank, correspond également
au centre du boulet. (1)aaaaint
cntl_tir(BITMAP *bmp,t_tank *t1,t_tank *t2) (2)aaaaaif(t1->boule.actif){
(1) La fonction a trois paramètres, la bitmap qui sera le double buffer puis chacun des deux tanks. Elle renvoie une valeur de 1 lorsque l’obus touche quelque chose et 0 sinon, cette fonctionnalité permettra de déclencher des explosions plus tard. (2) Dans le cas où le boulet n’est pas actif rien ne se passe mais si oui alors la future position du boulet est stockée dans les variable x et y locales de la fonction. (3) Ensuite teste pour savoir si le boulet sort de la bitmap (le double buffer qui représente la zone de jeu), si oui le boulet est désactivé. Mais si le boulet est dans la zone de jeu un autre teste est fait pour voir s’il touche quelque chose. Dans ce cas aussi il est désactivé et sa course s’arrête, la variable « res » qui donne la valeur du retour de la fonction est mise à 1 et il reste à savoir si le tank adverse a été touché où non avec appel de la fonction cntl_score() qui se charge de cette évaluation ainsi que de la mise à jour éventuelle des scores. (4) Si le boulet ne sort pas de la zone de jeu et ne touche rien il continue simplement sa course et est affiché à sa nouvelle position. (5) A l’issue de la fonction la variable « res » initialisée par défaut à 0 est renvoyée pour indiquer si l’obus a touché ou pas quelquechose.
La fonction cntl_score() évalue si le boulet d’un tank touche ou non le tank adverse et si oui incrémente le score du tireur. (1)aaaavoid
cntl_score(t_tank *t1,t_tank *t2) (1)
La fonction prend en paramètre deux tanks, le premier est le tank
tireur le second le tank adverse peut-être touché. aaaaa aaaatypedef
struct BOULET{ aaaatypedef
struct TANK{ aaaa(…) aaaa//
6. Tirs et boulets aaaa//
7. Controle des scores aaaawhile
(!done){ |
![]() |
4. Quatrième
étape : ajouter les explosions Structures de données troisième partie : Modification : Ces
cercles ou carrés reprennent les caractéristiques des boulets
et ils sont stockés dans un tableau de boulets. Ce tableau constitue
la matière d’une explosion, en quelque sorte le nombre des
éclats de l’explosion. Comme il y a deux tanks et que deux
explosions peuvent avoir lieu simultanément, il y a deux tableaux
de boulets, un pour les explosions possibles des boulets de chaque tank.
La taille de ces tableaux correspond au nombre des éclats d’une
explosion est prédéfini par une macro : Les deux tableaux sont déclarés dans le main() : aaaat_boule
tab1[TAB_EXPLOSION] ;
Nouveauté : En ce qui concerne les fonctions il n’y a aucune modification des fonctions existantes et trois nouvelles fonctions sont ajoutées. La première fonction initialise au départ les deux tableaux avec des valeurs de 0. La seconde initialise tous les éclats de l’explosion lorsqu’un obus touche quelque chose et la troisième contrôle dans la boucle d’évènements le déroulement de l’explosion, c'est-à-dire la trajectoire des éclats. 8. Explosions aaaavoid
init_explosion(t_boule *tab1,t_boule *tab2) (1)aaaavoid
arme_explosion(t_tank *t, t_boule *tab) (2)aaaaaaafor
(i=0; i<TAB_EXPLOSION;i++) (1) La fonction ne renvoie rien et prend un tank et un tableau d’éclats en argument. (2)
Toutes les position du tableau sont passées en revue et chaque
éclat inactif est initialisé. En premier il devient actif.
Les éclats pendant l’explosion durent plus ou moins longtemps
et le champ « actif » code non seulement le fait que l’éclat
est actif mais également la durée de son activité
(ce mécanisme est présenté dans la fonction suivante
de contrôle des explosions). Au départ tous les éclats
ont la même position qui est celle du boulet. En revanche ils n’ont
ni les même trajectoires ni les mêmes vitesse de déplacement.
La vitesse maximum des éclats est définie par la macro : (1)aaaavoid
cntl_explosion(BITMAP *bmp, t_boule *tab) aaaaaaaaaaafor
(i=0;i<TAB_EXPLOSION;i++){ (1) La fonction ne renvoie rien et a comme paramètres l’adresse d’une bitmap et un pointeur t_boule*, lors de l’appel les arguments seront le double buffer et un tableau d’éclats. (2) Chaque position du tableau est visitée. La variable du champ « actif » code et l’activité de l’éclat et le nombre d’affichages de l’éclat avant qu’il ne commence à diminuer de taille et disparaitre. Ainsi lorsqu’une position est active et supérieure à 1 la variable « actif » est tout d’abord décrémentée de 1 avant l’affichage en (3). Lorsque la variable du champ « actif » est égale à 1, c’est la diminution de la taille de l’éclat qui commence avec la décrémentation à chaque passage du rayon « r » et lorsque le rayon est égale à 0 l’éclat est rendu inactif. (3) L’affichage de l’éclat a lieu s’il est toujours actif. A ce moment également la position de l’éclat est modifiée en fonction de son pas d’avancement. Le dessin est ici un rectangle de couleur claire. La couleur est prédéfinie avec la macro COLOR_CLAIR.
aaaa#define
TAB_EXPLOSION 15 Ainsi que les déclarations de fonctions : aaaa//
FONCTIONS PART 4 aaaat_boule
tab1[TAB_EXPLOSION]; et les lignes en gras sont ajoutées dans la boucle d’événements : aaaawhile
(!done){ |
![]() |