IX. Modèle de jeu simple▲
IX-A. Introduction▲
Voici une base pour la création de jeu d'arcade du type action. Typiquement une « chose » est déplacée par le joueur avec les flèches du clavier et cette chose doit utiliser contre des ennemis mortels essentiellement des armes de jet : lancés de casseroles, de pommes pourries, de sorts magiques, tirs d'armes à feu, bombes nucléaires… selon l'état d'esprit au moment de la conception.
La maquette est un semblant de scroll horizontal minimaliste. Un vaisseau s'avance de la gauche vers la droite. Des ennemis surgissent à droite et tentent de l'empêcher. Le vaisseau doit les abattre pour continuer et survivre. Nous allons procéder par étape : 1) le joueur seul se déplace, 2) le joueur lance des missiles, 3) des ennemis arrivent, 4) le joueur peut détruire les ennemis. Le code sera ajouté au fur et à mesure.
Le programme résultant est indépendant de toute histoire, en quelque sorte tout nu ou réduit à l'essentiel, comme un standard de jazz (morceau de musique fait pour être toujours recréé par des interprétations nouvelles). C'est une bonne base de départ pour l'approche de nombreux jeux. Vous pouvez passer d'un scroll horizontal à un scroll vertical, voire à pas de scroll du tout juste en modifiant les déplacements du joueur et des ennemis ainsi que leur mode d'apparition.
IX-B. Mouvements du vaisseau▲
Nous avons un vaisseau et il réagit aux flèches du clavier, comme le rectangle expérimenté dans le chapitre Les événementsÉvénements. Sauf que cette fois son déplacement est contraint visuellement à aller de la gauche vers la droite. Première question : de quoi est constitué le vaisseau ?
IX-B-1. Structure de données du vaisseau▲
Que fait le vaisseau ? Il se déplace donc il a une position et une vitesse. Il est visible, donc il a une taille et une image. Il marque des points et il peut mourir, donc il conserve un score et un indicateur de son état.
Ainsi, nous avons : une position, un pas de déplacement qui définit aussi sa vitesse, une taille horizontale et verticale, éventuellement une ou plusieurs images, un score et un indicateur d'état (vivant ou mort, actif ou non).
Toutes ces caractéristiques peuvent être codées en valeurs entières ou flottantes sauf les images qui sont des ALLEGRO_BITMAP* et l'ensemble est réuni dans une structure ce qui donne :
typedef
struct
{
int
x, y; // position
int
dv; // déplacement et vitesse
int
tx, ty; // taille
int
vie; // vivant ou pas, actif ou pas
int
score; // les points obtenus
ALLEGRO_BITMAP*
image; // une image pour le vaisseau
}
t_vaisseau;
Par ailleurs les touches du clavier sont doublées (comme vu dans la section Donner de la fluidité aux mouvements du rectangle du chapitre Les événementsÉvénements) ce qui signifie un tableau de booléens pour stocker l'état des touches utilisées ([Flèche en haut], [Flèche en bas], [Flèche à droite], [Flèche à gauche] et [Espace] pour tirs) et un enum pour nommer les indices correspondants aux touches :
enum
KEYS{
UP, RIGHT, DOWN, LEFT, SPACE, KEY_MAX}
;
bool key[KEY_MAX]={
false
}
;
Le tableau comme l'enum sont mis en global. Toujours en global, nous mettons la taille de l'écran, ce sont les deux constantes :
const
int
SCRX =
800
;
const
int
SCRY =
600
;
IX-B-2. Initialisation▲
L'initialisation est effectuée avec la fonction :
void
init_vaisseau
(
t_vaisseau*
p)
{
p->
x=
20
;
p->
y=
SCRY/
2
;
p->
dv=
7
;
p->
tx=
30
;
p->
ty=
20
;
p->
vie=
3
;
p->
score=
0
;
// l'image du vaisseau : un triangle
// création d'une bitmap ou possibilité de loader une image
p->
image=
al_create_bitmap
(
p->
tx,p->
ty);
// pour pouvoir dessiner dans la bitmap, avant chaque
// opération de dessin
al_set_target_bitmap
(
p->
image);
// dessin dedans
al_draw_filled_triangle
(
0
,0
,0
,p->
ty, p->
x, p->
ty/
2
,
al_map_rgb
(
0
,255
,0
));
}
La structure à initialiser est passée par référence. Les valeurs sont fixes. Le vaisseau commence toujours à la même place. L'image n'est pas chargée mais elle est dessinée avec un dessin très simple (juste un triangle qui pointe vers l'avant).
Attention, après l'appel de la fonction, la bitmap sélectionnée pour les affichages est celle du vaisseau. Il ne faut pas oublier de resélectionner le double buffer du display après l'appel de init_vaisseau() avec un appel à :
al_set_target_backbuffer
(
display)
Nous pourrions aussi bien le faire ici. Dans ce cas, il faut ajouter un paramètre ALLEGRO_DISPLAY* et lui passer la fenêtre d'affichage display à l'appel de la fonction ou mettre la fenêtre d'affichage display en global.
IX-B-3. Affichage ▲
L'affichage consiste uniquement à afficher la bitmap du vaisseau à sa position courante. C'est la fonction :
void
affiche_vaisseau
(
t_vaisseau*
p)
{
al_draw_bitmap
(
p->
image, p->
x, p->
y, 0
);
}
IX-B-4. Déplacements▲
Le déplacement consiste à ajouter le pas dv de déplacement à la position courante selon la direction prise par le vaisseau. Il dépend de l'appui sur les flèches [Flèche en haut], [Flèche à droite], [Flèche en bas], [Flèche à gauche]. Nous proposons une fonction par direction :
void
monte
(
t_vaisseau*
p)
{
p->
y =
(
p->
y -
p->
dv <
0
) ? 0
: p->
y -
p->
dv;
}
void
droite (
t_vaisseau*
p)
{
p->
x =
(
p->
x +
p->
dv >=
SCRX/
3
) ? SCRX/
3
: p->
x +
p->
dv ;
}
void
descend
(
t_vaisseau*
p)
{
p->
y =
(
p->
y +
p->
ty +
p->
dv >=
SCRY) ?
SCRY -
p->
ty : p->
y +
p->
dv;
}
void
gauche (
t_vaisseau*
p)
{
p->
x =
(
p->
x -
p->
dv <
0
) ? 0
: p->
x -
p->
dv ;
}
À chaque fois, un test est fait pour empêcher le vaisseau de dépasser le tiers de l'écran ou de sortir sur les bords en haut et en bas. Dans la boucle d'événement nous aurons quelque chose comme :
- si la flèche [Flèche en haut] est appuyée, appel de monte() ,
- si la flèche [Flèche à droite] est appuyée, appel de droite() ,
- etc.
Le si pourra être remplacé par un switch.
IX-B-5. Boucle événementielle du jeu, organisation du code▲
C'est la même boucle événementielle que celle du carré piloté avec les flèches. Elle est assortie d'une grande fluidité de déplacement grâce au doublage de l'état des touches dans un tableau de booléens. La boucle événementielle est dans le main() .
L'ensemble du code est réparti sur deux fichiers :
- le fichier source avec main() et définitions des fonctions ;
- le fichier d'en-têtes qui contient toutes les définitions de type, les déclarations de variables en globale et les déclarations de fonctions. Le fichier d'entêtes fait l'objet d'une inclusion dans le fichier source.
Voici le code complet de cette première étape :
IX-B-5-a. Fichier d'en-têtes jeu.h▲
#ifndef _GENERAL
#define _GENERAL
#include <allegro5\allegro.h>
#include <allegro5\allegro_native_dialog.h>
#include <allegro5\allegro_primitives.h>
// taille ecran
const
int
SCRX =
800
;
const
int
SCRY =
600
;
// vaisseau
typedef
struct
{
int
x, y; // position
int
dv; // déplacement et vitesse
int
tx, ty; // taille
int
vie; // vivant ou pas, actif ou pas
int
score; // les points obtenus
ALLEGRO_BITMAP*
image; // une image pour le vaisseau
}
t_vaisseau;
// pour le contrôle du clavier
enum
KEYS{
UP, RIGHT, DOWN, LEFT, SPACE, KEY_MAX }
;
bool key[KEY_MAX] =
{
false
}
;
/**
***************************************************************
VAISSEAU /
****************************************************************
*/
void
init_vaisseau
(
t_vaisseau*
p);
void
affiche_vaisseau
(
t_vaisseau*
p);
void
monte
(
t_vaisseau*
p);
void
droite
(
t_vaisseau*
p);
void
descend
(
t_vaisseau*
p);
void
gauche
(
t_vaisseau*
p);
void
erreur
(
const
char
*
txt);
/**
***************************************************************
****************************************************************
*/
#endif
IX-B-5-b. Fichier source jeu.c▲
#include "jeu.h" // ne pas oublier
/**
***************************************************************
****************************************************************
*/
int
main
(
)
{
//------------------------Allegro et gestion evénements
ALLEGRO_DISPLAY *
display;
ALLEGRO_EVENT_QUEUE*
queue;
ALLEGRO_TIMER*
timer;
bool fin =
false
;
bool dessin =
false
;
t_vaisseau vaisseau;
if
(!
al_init
(
))
erreur
(
"
init allegro
"
);
if
(!
al_init_primitives_addon
(
))
erreur
(
"
init primitives
"
);
// pour avoir le clavier
if
(!
al_install_keyboard
(
))
erreur
(
"
install keyboard
"
);
// la fenêtre
display =
al_create_display
(
SCRX, SCRY);
if
(!
display)
erreur
(
"
display
"
);
// la file d'événements
queue =
al_create_event_queue
(
);
if
(!
queue)
erreur
(
"
queue
"
);
// initialiser le timer
timer =
al_create_timer
(
1
.0
/
50
); // temps en seconde
if
(!
timer)
erreur
(
"
timer
"
);
// enregistrement des types d'événements à recueillir
al_register_event_source
(
queue,
al_get_display_event_source
(
display));
al_register_event_source
(
queue,
al_get_keyboard_event_source
(
));
al_register_event_source
(
queue,
al_get_timer_event_source
(
timer));
// démarrage timer
al_start_timer
(
timer);
// -----------------------------initialisations actions
init_vaisseau
(&
vaisseau);
al_set_target_backbuffer
(
display);
while
(!
fin){
ALLEGRO_EVENT ev;
al_wait_for_event
(
queue, &
ev);
if
(
ev.type ==
ALLEGRO_EVENT_DISPLAY_CLOSE)
fin =
true
;
// Si l'événement est une touche appuyée et si c'est
// une touche qui intéresse le programme, la valeur
// correspondante dans le tableau de booléens est mise
// à true
else
if
(
ev.type ==
ALLEGRO_EVENT_KEY_DOWN)
{
switch
(
ev.keyboard.keycode){
case
ALLEGRO_KEY_UP:
key[UP] =
true
;
break
;
case
ALLEGRO_KEY_RIGHT:
key[RIGHT] =
true
;
break
;
case
ALLEGRO_KEY_DOWN:
key[DOWN] =
true
;
break
;
case
ALLEGRO_KEY_LEFT:
key[LEFT] =
true
;
break
;
// sortie
case
ALLEGRO_KEY_ESCAPE:
fin =
true
;
break
;
}
}
// Si l'événement est une touche relevée et si c'est
// une touche qui intéresse le programme, la valeur
// correspondante dans le tableau de booléens est mise
// à false
else
if
(
ev.type ==
ALLEGRO_EVENT_KEY_UP)
{
switch
(
ev.keyboard.keycode){
case
ALLEGRO_KEY_UP:
key[UP] =
false
;
break
;
case
ALLEGRO_KEY_RIGHT:
key[RIGHT] =
false
;
break
;
case
ALLEGRO_KEY_DOWN:
key[DOWN] =
false
;
break
;
case
ALLEGRO_KEY_LEFT:
key[LEFT] =
false
;
break
;
}
}
// le déplacement du vaisseau est mis à jour à chaque
// tic du minuteur (timer)
else
if
(
ev.type ==
ALLEGRO_EVENT_TIMER)
{
// bouger, action
if
(
key[UP])
monte
(&
vaisseau);
if
(
key[RIGHT])
droite
(&
vaisseau);
if
(
key[DOWN])
descend
(&
vaisseau);
if
(
key[LEFT])
gauche
(&
vaisseau);
dessin =
true
;
}
/*Il y a dessin si la file est vide et si un événement timer a eu lieu.
Il peut arriver que juste après l'événement timer une touche soit
appuyée avant les opérations de dessin, dans ce cas priorité au
clavier avec une éventuelle mise à jour de la position du vaisseau.*/
if
(
dessin ==
true
&&
al_is_event_queue_empty
(
queue)){
// les opérations d'affichage
// 1 effacer le double buffer
al_clear_to_color
(
al_map_rgb
(
0
, 0
, 0
));
// 2 afficher les entités à leurs positions
affiche_vaisseau
(&
vaisseau);
// 3 passer le double buffer à l'écran
al_flip_display
(
);
dessin =
false
;
}
}
// nettoyage sortie
al_destroy_event_queue
(
queue);
al_destroy_display
(
display);
al_destroy_timer
(
timer);
return
0
;
}
/**
**************************************************************
VAISSEAU / Initialisation
****************************************************************
*/
void
init_vaisseau
(
t_vaisseau*
p)
{
p->
x =
20
;
p->
y =
SCRY /
2
;
p->
dv =
7
;
p->
tx =
30
;
p->
ty =
20
;
p->
vie =
3
;
p->
score =
0
;
// l'image du vaisseau : un triangle
// création d'une bitmap ou possibiité de loader une image
p->
image =
al_create_bitmap
(
p->
tx, p->
ty);
// pour pouvoir dessiner dans la bitmap, avant chaque
// opération de dessin
al_set_target_bitmap
(
p->
image);
// dessin dedans
al_draw_filled_triangle
(
0
, 0
, 0
, p->
ty, p->
x, p->
ty /
2
,
al_map_rgb
(
0
, 255
, 0
));
}
/**
**************************************************************
VAISSEAU / affichage
****************************************************************
*/
void
affiche_vaisseau
(
t_vaisseau*
p)
{
al_draw_bitmap
(
p->
image, p->
x, p->
y, 0
);
}
/**
************************************************************
VAISSEAU / mouvement
**************************************************************
*/
void
monte
(
t_vaisseau*
p)
{
p->
y =
(
p->
y -
p->
dv <
0
) ? 0
: p->
y -
p->
dv;
}
void
droite
(
t_vaisseau*
p)
{
p->
x =
(
p->
x +
p->
dv >=
SCRX /
3
) ? SCRX /
3
: p->
x +
p->
dv;
}
void
descend
(
t_vaisseau*
p)
{
p->
y =
(
p->
y +
p->
ty +
p->
dv >=
SCRY) ? SCRY -
p->
ty : p->
y +
p->
dv;
}
void
gauche
(
t_vaisseau*
p)
{
p->
x =
(
p->
x -
p->
dv <
0
) ? 0
: p->
x -
p->
dv;
}
/**
************************************************************
TOOLS
**************************************************************
*/
void
erreur
(
const
char
*
txt)
{
ALLEGRO_DISPLAY *
d;
d =
al_is_system_installed
(
) ? al_get_current_display
(
) : NULL
;
al_show_native_message_box
(
d, "
Erreur
"
, txt, NULL
, NULL
, 0
);
exit
(
EXIT_FAILURE);
}
/**
************************************************************
**************************************************************
*/
IX-C. Lancement de missiles▲
Deuxième étape : ajouter des missiles envoyés lorsque le joueur tape [Espace].
Première question : de quoi est constitué un missile ?
IX-C-1. Structure de données du missile▲
Un missile a une position, un déplacement, il est actif (lancé) ou non. Cela fait quatre variables réunies dans une structure :
typedef
struct
{
int
x, y; // position
int
dv; // déplacement
bool actif;
}
t_missile;
Comme il y a plusieurs missiles, nous avons un tableau de missiles dont la taille est définie par une constante :
const
int
NBMAXMISSILES=
5
;
t_missile missiles[NBMAXMISSILES];
La constante pour le nombre de missiles est déclarée en global dans l'en-tête, mais le tableau des missiles est déclaré en local dans le main() . Plutôt que de le mettre en global, nous avons choisi de le faire circuler en paramètre pour toutes les fonctions qui agissent avec les missiles.
Nous avons également choisi, pour que le code soit le plus minimaliste possible, de ne pas mettre d'image. Les missiles seront visualisés par des petits cercles ou ovales tous de la même couleur. Mais ajouter une image n'est pas compliqué (ajouter un champ ALLEGRO_BITMAP* à la structure et l'initialiser dans la fonction d'initialisation)
IX-C-2. Initialisation▲
Si le joueur appuie sur [Espace], un missile est tiré. C'est-à-dire qu'un missile devient actif, visible et progresse sur sa trajectoire. Sa position est initialisée dans la boucle d'événements à partir de celle du vaisseau au moment où il est lancé quand le joueur appuie sur [Espace]. Ensuite il progresse au rythme du timer et selon son pas d'avancement. Donc au départ, avant d'entrer dans la boucle d'événements, il faut initialiser le pas d'avancement, à savoir la vitesse de chaque missile. Et tous les missiles doivent être inactivés (en mettant actif à false).
void
init_all_missiles
(
t_missile m[])
{
int
i ;
for
(
i=
0
; i<
NBMAXMISSILES; i++
){
m[i].dv =
10
;
m[i].actif=
false
;
}
}
IX-C-3. Affichage▲
Les missiles sont des formes obtenues avec les fonctions de dessin. Pour faire au plus simple, ce sont des dessins d'ellipses ou de cercles. Seuls les missiles actifs sont affichés :
void
affiche_all_missiles
(
t_missile m[])
{
int
i ;
for
(
i=
0
; i<
NBMAXMISSILES; i++
){
if
(
m[i].actif==
true
)
al_draw_filled_ellipse
(
m[i].x,m[i].y,3
,2
,
al_map_rgb
(
0
,255
,0
));
}
}
IX-C-4. Mouvement▲
Bouger le missile consiste à ajouter son pas à sa position sachant qu'il va droit devant lui de la gauche vers la droite. Seuls les missiles actifs sont concernés et si un missile sort de la fenêtre, il redevient inactif :
void
avance_all_missiles
(
t_missile m[])
{
int
i ;
for
(
i =
0
; i<
NBMAXMISSILES; i++
){
if
(
m[i].actif==
true
){
m[i].x +=
m[i].dv;
if
(
m[i].x >=
SCRX)
m[i].actif =
false
;
}
}
}
IX-C-5. Lancement▲
Lorsque le joueur appuie sur [Espace], nous regardons s'il y a un missile inactif disponible. Si oui, sa position est initialisée à partir de la position du vaisseau et il est marqué comme actif. Pour avoir la position du vaisseau, il est nécessaire de passer le vaisseau à la fonction :
void
lancement_missile
(
t_missile m[], t_vaisseau*
p)
{
int
i ;
for
(
i=
0
; i<
NBMAXMISSILES; i++
){
if
(
m[i].actif ==
false
){
m[i].actif =
true
;
m[i].x =
p->
x +
p->
tx;
m[i].y =
p->
y +
p->
ty/
2
;
break
; // provoque la sortie de la boucle
}
}
}
Dès qu'un missile inactif est trouvé et passe à actif, l'instruction break provoque une sortie immédiate de la boucle.
IX-C-6. Mise à jour du code, action▲
IX-C-6-a. La bibliothèque jeu.h ▲
Dans la bibliothèque (le fichier d'en-tête) jeu.h, nous devons ajouter la définition de la structure missile, la constante pour le nombre des missiles et toutes les déclarations des fonctions pour le traitement des missiles. Nous ne répétons pas ici tout le code mais uniquement ce qu'il y a à ajouter, en gras et le code sauté est remplacé par (…) :
#ifndef _GENERAL
#define _GENERAL
// (...)
// les missiles
typedef
struct
{
int
x, y; // position
int
dv; // déplacement
bool actif;
}
t_missile;
const
int
NBMAXMISSILES =
5
;
// (...)
/**
***************************************************************
VAISSEAU /
****************************************************************
*/
// (...)
/**
***************************************************************
MISSILES /
****************************************************************
*/
void
init_all_missiles
(
t_missile m[]);
void
affiche_all_missiles
(
t_missile m[]);
void
avance_all_missiles
(
t_missile m[]);
void
lancement_missile
(
t_missile m[], t_vaisseau*
p);
/**
***************************************************************
****************************************************************
*/
#endif
IX-C-6-b. Le fichier C jeu.c ▲
Dans le fichier C, il faut ajouter toutes les définitions des fonctions ci-dessus (les fonctions complètes) et aussi les appels dans le main() . Nous donnons ici en gras les nouveautés dans le main() , le code sauté est remplacé par (…) :
#include "jeu.h" // ne pas oublier
/**
***************************************************************
****************************************************************
*/
int
main
(
)
{
//------------------------Allegro et gestion evénements
// (...)
t_missile missiles[NBMAXMISSILES];
// (...)
// -----------------------------initialisations actions
init_vaisseau
(&
vaisseau);
al_set_target_backbuffer
(
display);
init_all_missiles
(
missiles);
while
(!
fin){
ALLEGRO_EVENT ev;
al_wait_for_event
(
queue, &
ev);
if
(
ev.type ==
ALLEGRO_EVENT_DISPLAY_CLOSE)
fin =
true
;
else
if
(
ev.type ==
ALLEGRO_EVENT_KEY_DOWN)
{
switch
(
ev.keyboard.keycode){
// (...)
// tirs missiles
case
ALLEGRO_KEY_SPACE:
lancement_missile
(
missiles, &
vaisseau);
break
;
// (...)
}
}
else
if
(
ev.type ==
ALLEGRO_EVENT_KEY_UP)
{
// (...)
}
else
if
(
ev.type ==
ALLEGRO_EVENT_TIMER)
{
// bouger, action
if
(
key[UP])
monte
(&
vaisseau);
if
(
key[RIGHT])
droite
(&
vaisseau);
if
(
key[DOWN])
descend
(&
vaisseau);
if
(
key[LEFT])
gauche
(&
vaisseau);
// avancer les missiles
avance_all_missiles
(
missiles);
dessin =
true
;
}
// dessin si file vide (priorité au clavier)
if
(
dessin ==
true
&&
al_is_event_queue_empty
(
queue)){
// les opérations d'affichage
// 1 effacer le double buffer
al_clear_to_color
(
al_map_rgb
(
0
, 0
, 0
));
// 2 afficher les entités à leurs positions
affiche_vaisseau
(&
vaisseau);
affiche_all_missiles
(
missiles);
// 3 passer le double buffer à l'écran
al_flip_display
(
);
dessin =
false
;
}
}
(
...)
}
IX-D. Avancée des ennemis▲
Dans cette étape, il n'y a pas d'interaction entre missiles et ennemis. Les ennemis arrivent et avancent vers le vaisseau du joueur, c'est tout. Première question comme d'habitude : de quoi est constitué un ennemi ?
IX-D-1. Structure de données de l'ennemi▲
Pour la structure de données, c'est le même principe que pour les missiles. L'ennemi a une position, un pas de déplacement, il est actif ou non, il a une taille en largeur et en hauteur, ce qui donne la structure de l'ennemi suivante :
typedef
struct
{
int
x, y ; // position
int
dv; // déplacement
int
tx, ty; // taille
bool actif; // actif ou pas
}
t_ennemi;
Le nombre total d'ennemis est donné en global par une constante :
const
int
NBMAXENNEMIS=
10
;
Il correspond au nombre maximum d'ennemis simultanément à l'écran. En effet les ennemis morts peuvent être recréés ce qui donne en fait un nombre d'ennemis infini. L'ensemble des ennemis visibles est stocké dans un tableau déclaré en local dans le main() comme pour les missiles :
t_ennemi ennemis[NBMAXENNEMIS];
La visualisation des ennemis est assurée comme pour les missiles par un dessin d'ovale ou de cercle mais en plus gros que les missiles.
IX-D-2. Initialisation▲
Les ennemis apparaissent à droite en face du vaisseau et ils foncent vers la gauche. Au départ ils sont inactifs, ils ont un pas d'avancement et une taille pris au hasard chacun dans une fourchette, ce qui donne la fonction :
void
init_all_ennemis
(
t_ennemi e[])
{
for
(
int
i =
0
; i<
NBMAXENNEMIS; i++
){
e[i].actif =
false
;
e[i].dv =
5
+
rand
(
)%
5
;
e[i].tx =
5
+
rand
(
)%
20
;
e[i].ty =
5
+
rand
(
)%
20
;
}
}
La fonction prend en paramètre le tableau des ennemis. La position de départ est donnée à l'apparition de l'ennemi dans une autre fonction (voir section Apparition plus loinApparition).
IX-D-3. Affichage▲
Pour faire très simple, chaque ennemi est un ovale dessiné avec la fonction al_draw_filled_ellipse() selon sa taille tx, ty :
void
affiche_all_ennemis
(
t_ennemi e[])
{
for
(
int
i =
0
; i<
NBMAXENNEMIS; i++
){
if
(
e[i].actif ==
true
)
al_draw_filled_ellipse
(
e[i].x,e[i].y,e[i].tx,e[i].ty,
al_map_rgb
(
255
,rand
(
)%
256
,rand
(
)%
256
));
}
}
Le fait de changer la couleur à chaque appel ajoute une petite animation aux ennemis dont la couleur « vibre » avec une forte tendance rouge fixe.
IX-D-4. Mouvement▲
Chaque ennemi actif avance de son pas d'avancement dv et s'il sort à gauche il redevient inactif :
void
avance_all_ennemis
(
t_ennemi e[])
{
for
(
int
i =
0
; i<
NBMAXENNEMIS; i++
){
if
(
e[i].actif==
true
){
e[i].x -=
e[i].dv; // attention soustraction
if
(
e[i].x -
e[i].tx <
0
)
e[i].actif =
false
;
}
}
}
IX-D-5. Apparition▲
Les ennemis apparaissent au hasard. À chaque tour si un ennemi est inactif et qu'un nombre tiré au hasard dans une fourchette est au-dessous d'une certaine valeur (10 dans le code), alors il est activé. C'est alors qu'il se positionne au hasard sur la ligne de départ :
void
apparition_ennemi
(
t_ennemi e[])
{
for
(
int
i =
0
; i<
NBMAXENNEMIS; i++
){
if
(
e[i].actif==
false
&&
rand
(
)%
1000
<
10
){
e[i].x =
SCRX -
e[i].tx;
e[i].y =
e[i].ty +
rand
(
)%(
SCRY -
(
e[i].ty*
2
));
e[i].actif=
true
;
}
}
}
La position horizontale est fixe sur le bord droit. La position verticale est aléatoire et vérifie que l'ennemi est tout entier dans la fenêtre compte tenu de sa taille.
IX-D-6. Mise à jour du code, action▲
IX-D-6-a. La bibliothèque jeu.h ▲
Dans la bibliothèque, nous devons ajouter la définition de la structure des ennemis, la constante pour le nombre des ennemis et toutes les déclarations de fonctions. Comme pour l'étape précédente, nous spécifions ici en gras le code à ajouter, le reste, qui est identique à l'étape précédente, est remplacé par (…) :
#ifndef _GENERAL
#define _GENERAL
#include <allegro5\allegro.h>
#include <allegro5\allegro_native_dialog.h>
#include <allegro5\allegro_primitives.h>
// (...)
// les ennemis
typedef
struct
{
int
x, y; // position
int
dv; // déplacement
int
tx, ty; // taille
bool actif; // actif ou pas
}
t_ennemi;
const
int
NBMAXENNEMIS =
10
;
// (...)
/**
***************************************************************
VAISSEAU /
****************************************************************
*/
// (...)
/**
***************************************************************
MISSILES /
****************************************************************
*/
// (...)
/**
*******************************************************
ENNEMIS /
********************************************************
*/
void
init_all_ennemis
(
t_ennemi e[]);
void
affiche_all_ennemis
(
t_ennemi e[]);
void
avance_all_ennemis
(
t_ennemi e[]);
void
apparition_ennemi
(
t_ennemi e[]);
/**
***************************************************************
****************************************************************
*/
#endif
IX-D-6-b. Le fichier C jeu.c ▲
Dans le fichier C, il faut ajouter toutes les définitions des fonctions déclarées dans l'en-tête (les fonctions complètes) et aussi les appeler dans le main(). Nous donnons ici en gras les appels et leurs positions, le reste du code est identique à celui de l'étape précédente et est remplacé par (…). Les ennemis sont complètement automatiques et ne répondent à aucune touche. Ils sont animés uniquement par le timer.
#include "jeu.h"
int
main
(
)
{
// (...)
t_vaisseau vaisseau;
t_missile missiles[NBMAXMISSILES];
t_ennemi ennemis[NBMAXENNEMIS];
// (...)
// -----------------------------initialisations actions
init_vaisseau
(&
vaisseau);
al_set_target_backbuffer
(
display);
init_all_missiles
(
missiles);
init_all_ennemis
(
ennemis);
while
(!
fin){
ALLEGRO_EVENT ev;
al_wait_for_event
(
queue, &
ev);
// (...)
else
if
(
ev.type ==
ALLEGRO_EVENT_TIMER)
{
// bouger, action
if
(
key[UP])
monte
(&
vaisseau);
if
(
key[RIGHT])
droite
(&
vaisseau);
if
(
key[DOWN])
descend
(&
vaisseau);
if
(
key[LEFT])
gauche
(&
vaisseau);
// avancer les missiles
avance_all_missiles
(
missiles);
// gestion ennemis
apparition_ennemi
(
ennemis);
avance_all_ennemis
(
ennemis);
dessin =
true
;
}
// dessin si file vide (priorité au clavier)
if
(
dessin ==
true
&&
al_is_event_queue_empty
(
queue)){
// les opérations d'affichage
// 1 effacer le double buffer
al_clear_to_color
(
al_map_rgb
(
0
, 0
, 0
));
// 2 afficher les entités à leurs positions
affiche_vaisseau
(&
vaisseau);
affiche_all_missiles
(
missiles);
affiche_all_ennemis
(
ennemis);
// 3 passer le double buffer à l'écran
al_flip_display
(
);
dessin =
false
;
}
// (...)
IX-E. Collisions▲
Repérer les collisions permet de savoir si une image ou un dessin rencontre une autre image ou un autre dessin. Il y a plusieurs techniques, dont voici les plus utilisées, les plus simples : savoir si un point est dans un rectangle, intersection de rectangles, savoir si un point est dans un triangle.
IX-E-1. Point dans un rectangle▲
Savoir si un point est dans telle ou telle zone rectangulaire peut être bien utile. Par exemple déterminer si un clic souris a lieu sur tel ou tel bouton ou non. Cela donne aussi la possibilité de détecter une collision entre deux formes. Un ensemble de points est alors localisé autour d'une forme (comme une sorte de pare-chocs) et l'un de ces points est à l'intérieur d'un rectangle signifie une collision avec ce rectangle. C'est la fonction :
bool point_in_rect
(
int
x, int
y,
int
top, int
left, int
right, int
bottom)
{
return
(
x>=
left &&
x<=
right &&
y>=
top &&
y<=
bottom);
}
Les bords de la zone sont considérés comme faisant partie de la zone (d'où <= et >=).
La fonction retourne vrai ou faux selon que le point de coordonnées x,y est dans la zone rectangulaire délimitée par top, left, right, bottom ou non. La fonction prouve qu'un point est dans la zone. Mais nous pouvons l'écrire autrement afin de prouver au contraire qu'un point n'est pas dans la zone :
bool point_in_rect
(
int
x, int
y,
int
top, int
left, int
right, int
bottom)
{
return
(
x<
left ||
x>
right ||
y<
top ||
y>
bottom) ;
}
IX-E-2. Point dans un triangle▲
Pour savoir si un point est dans un triangle, nous utilisons la règle du déterminant. Celle-ci permet de savoir si un point est à gauche ou à droite d'un vecteur. La règle du déterminant est la suivante : soit 3 points O (ox,oy), I (ix,iy) et P (px,py). P est à gauche du vecteur OI si le déterminant oix*opy oiy*opx des vecteurs OI(oix,oiy) et OP(opx,opy) est positif, et à droite dans le cas contraire.
Ensuite, soit le triangle ABC. Si le point P est à gauche de AB, de BC et de CA, alors il est dans le triangle. Le triangle est considéré dans le sens inverse des aiguilles d'une montre (triangle direct).
Pour ce faire, nous procédons avec deux fonctions. L'une, la fonction gauche() pour savoir si un point est à gauche d'un segment. Cette fonction a comme paramètre la position à tester, et les deux points nécessaires au segment. L'autre, la fonction in_triangle() , prend la position à tester et les trois points du triangle à tester.
IX-E-3. Expérimentation ▲
Le test ci-dessous permet de générer des triangles aléatoires en appuyant sur la touche [Entrée]. Si la souris se trouve dans le triangle affiché, le triangle change de couleur.
#include <allegro5/allegro.h>
#include <allegro5/allegro_native_dialog.h>
#include <allegro5/allegro_primitives.h>
#define COLORALEA al_map_rgb(rand()%256,rand()%256,rand()%256)
const
int
SCRX =
800
;
const
int
SCRY =
600
;
void
erreur
(
const
char
*
txt)
{
ALLEGRO_DISPLAY*
d;
d =
al_is_system_installed
(
) ? al_get_current_display
(
) : NULL
;
al_show_native_message_box
(
d, "
Erreur
"
, txt, NULL
, NULL
, 0
);
exit
(
EXIT_FAILURE);
}
/**
***************************************************************
fonction principale qui permet de savoir si un point est à gauche
d'un vecteur.Nous utilisons la règle du déterminant : soit trois
points O, I et P, P est à gauche de OI si le déterminant
oix*opy-oiy*opx des vecteurs OI(oix,oiyi) et OP(opx,opy) est
positif et à droite sinon.
****************************************************************
*/
int
gauche
(
int
px, int
py, int
ox, int
oy, int
ix, int
iy)
{
int
oix, oiy, opx, opy, determinant;
// avoir les vecteurs oi et op
oix =
ix -
ox;
oiy =
iy -
oy;
opx =
px -
ox;
opy =
py -
oy;
// calculer le déterminant
determinant =
oix*
opy -
oiy*
opx;
// inversion pour le point de vue de l'observateur, retourne 0
// si positif et à droite et 1 si négatif et à gauche pour
// l'observateur
return
(
determinant>
0
) ? 0
: 1
;
}
/**
***************************************************************
à partir de la fonction gauche, en tournant dans le sens inverse
des aiguilles d'une montre, voyons si le point (x,y) est ou pas
dans triangle a-b-c-
****************************************************************
*/
int
in_triangle
(
int
x, int
y,
int
ax, int
ay, int
bx, int
by, int
cx, int
cy)
{
int
res =
0
;
// le point (x,y) est-il à gauche de chaque segment ?
res +=
gauche
(
x, y, ax, ay, bx, by);
res +=
gauche
(
x, y, bx, by, cx, cy);
res +=
gauche
(
x, y, cx, cy, ax, ay);
return
(
res ==
3
) ? 1
: 0
;
}
/**
***************************************************************
initialisation d'un triangle
****************************************************************
*/
void
nouveau_triangle
(
int
*
x1,int
*
y1,int
*
x2,int
*
y2,int
*
x3,int
*
y3)
{
// efface le triangle courant
al_clear_to_color
(
al_map_rgb
(
0
, 0
, 0
));
// nouveaux sommets
*
x1 =
rand
(
) %
SCRX /
2
;
*
y1 =
SCRY /
2
+
rand
(
) %
SCRY /
2
;
*
x2 =
SCRX /
2
+
rand
(
) %
SCRX /
2
;
*
y2 =
SCRY /
2
+
rand
(
) %
SCRY /
2
;;
*
x3 =
rand
(
) %
SCRX;
*
y3 =
rand
(
) %
SCRY /
2
;
// affichage
al_draw_filled_triangle
(*
x1, *
y1, *
x2, *
y2, *
x3, *
y3, COLORALEA);
}
/**
***************************************************************
****************************************************************
*/
int
main
(
)
{
ALLEGRO_DISPLAY*
display;
ALLEGRO_KEYBOARD_STATE key;
ALLEGRO_MOUSE_STATE mouse;
int
x1, y1, x2, y2, x3, y3;
if
(!
al_init
(
))
erreur
(
"
al_init()
"
);
if
(!
al_install_keyboard
(
))
erreur
(
"
al_install_keyboard()
"
);
if
(!
al_install_mouse
(
))
erreur
(
"
al_install_mouse()
"
);
if
(!
al_init_primitives_addon
(
))
erreur
(
"
al_init_primitives_addon()
"
);
display =
al_create_display
(
SCRX, SCRY);
if
(!
display)
erreur
(
"
al_create_display()
"
);
// un triangle au départ
nouveau_triangle
(&
x1, &
y1, &
x2, &
y2, &
x3, &
y3);
do
{
al_get_keyboard_state
(&
key);
al_get_mouse_state
(&
mouse);
// nouveau triangle
if
(
al_key_down
(&
key, ALLEGRO_KEY_ENTER))
nouveau_triangle
(&
x1, &
y1, &
x2, &
y2, &
x3, &
y3);
// si dans triangle changer la couleur
if
(
in_triangle
(
mouse.x, mouse.y, x1, y1, x2, y2, x3, y3))
al_draw_filled_triangle
(
x1, y1, x2, y2, x3, y3, COLORALEA);
al_flip_display
(
);
}
while
(!
al_key_down
(&
key, ALLEGRO_KEY_ESCAPE));
al_destroy_display
(
display);
return
0
;
}
/**
***************************************************************
****************************************************************
*/
À partir de la fonction gauche(), il est possible de savoir si un point se trouve dans un polygone constitué de plusieurs segments à condition qu'il soit convexe. Il suffit de vérifier que le point est à gauche de tous les segments constitutifs de la forme en tournant autour d'elle dans le sens inverse des aiguilles d'une montre.
IX-E-4. Intersection de rectangles▲
Autre option pour la détection de collision, l'intersection de rectangles. Soit deux rectangles R1 et R2 :
(x1, y1) et (x2,y2) sont les coordonnées des points en haut à gauche de chaque rectangle, tx1, ty1 et tx2, ty2 correspondent aux tailles respectives des deux rectangles, les coordonnées des points en bas à droite sont respectivement (x1+tx1, y1+ty1) et (x2+tx2, y2+ty2). Il y a plusieurs façons de s'y prendre pour savoir s'il y a collision entre R1 et R2.
Nous pouvons chercher à savoir si R1 est totalement à gauche ou à droite ou au-dessus ou en dessous de R2, soit le test suivant :
SI(
x1+tx1 < x2 OU x1 > x2+tx2 OU
y1+ty1 < y2 OU y1 < y2+ty2
)
Pas de collision
Sinon
Collision
Il suffit qu'une des expressions soit vraie pour que le test soit vrai et les quatre expressions ne seront vérifiées qu'en cas de collision entre R1 et R2. Soit la fonction :
bool collision_rect(x1,y1,tx1,ty1, x2,y2,tx2,ty2)
{
return
(
x1+tx1 < x2 || x1 > x2+tx2 ||
y1+ty1 < y2 || y1 < y2+ty2
) ;
}
Nous pouvons regarder inversement s'il y a intersection de la façon suivante :
Si (
x1+tx1 > x2 ET x1 < x2+tx2 ET
y1+ty1 > y2 ET y1 < y2+ty2
)
Collision
Sinon
Pas de collision
Soit la fonction :
bool collision_rect
(
x1,y1,tx1,ty1, x2,y2,tx2,ty2)
{
return
(
x1+
tx1 >
x2 &&
x1 <
x2+
tx2 &&
y1+
ty1 >
y2 &&
y1 <
y2+
ty2
) ;
}
IX-E-4-a. Expérimentation ▲
Un rectangle fixe bleu est initialisé au départ et un rectangle mobile rouge est piloté avec la souris. Il passe en vert si intersection avec le rectangle fixe.
#include <allegro5/allegro.h>
#include <allegro5/allegro_native_dialog.h>
#include <allegro5/allegro_primitives.h>
typedef
struct
{
int
x, y, tx, ty;
}
t_rect;
/**
************************************************************
**************************************************************
*/
void
erreur
(
const
char
*
txt)
{
ALLEGRO_DISPLAY*
d;
d =
al_is_system_installed
(
) ? al_get_current_display
(
) : NULL
;
al_show_native_message_box
(
d, "
Erreur
"
, txt, NULL
, NULL
, 0
);
exit
(
EXIT_FAILURE);
}
/**
************************************************************
**************************************************************
*/
bool collision_rect
(
t_rect r1, t_rect r2)
{
return
(
r1.x >
r2.x +
r2.tx ||
r1.x +
r1.tx <
r2.x ||
r1.y >
r2.y +
r2.ty ||
r1.y +
r1.ty <
r2.y);
}
/*
// autre version
bool collision_rect(t_rect r1, t_rect r2)
{
return (r1.x < r2.x+r2.tx && r1.x+r1.tx > r2.x &&
r1.y < r2.y+r2.ty && r1.y+r1.ty > r2.y );
}
*/
/**
************************************************************
**************************************************************
*/
int
main
(
)
{
ALLEGRO_DISPLAY*
display;
ALLEGRO_KEYBOARD_STATE key;
ALLEGRO_MOUSE_STATE mouse;
int
scrx, scry;
t_rect r1, r2;
ALLEGRO_COLOR color;
if
(!
al_init
(
))
erreur
(
"
al_init()
"
);
if
(!
al_install_keyboard
(
))
erreur
(
"
al_install_keyboard()
"
);
if
(!
al_install_mouse
(
))
erreur
(
"
al_install_mouse()
"
);
if
(!
al_init_primitives_addon
(
))
erreur
(
"
al_init_primitives_addon()
"
);
scrx =
800
;
scry =
600
;
display =
al_create_display
(
scrx, scry);
if
(!
display)
erreur
(
"
al_create_display()
"
);
// le rectangle r1 est piloté par la souris
r1.y =
r1.x =
-
1
;
r1.ty =
r1.tx =
40
;
// le rectangle r2 est fixe de taille et de
// position aléatoires
r2.tx =
rand
(
) %
200
+
100
;
r2.ty =
rand
(
) %
200
+
100
;
r2.x =
rand
(
) %
(
scrx -
r2.tx);
r2.y =
rand
(
) %
(
scry -
r2.ty);
do
{
al_get_keyboard_state
(&
key);
al_get_mouse_state
(&
mouse);
if
(
r1.x !=
mouse.x ||
r1.y !=
mouse.y){
r1.x =
mouse.x;
r1.y =
mouse.y;
al_clear_to_color
(
al_map_rgb
(
0
, 0
, 0
));
if
(
collision_rect
(
r1, r2))
color =
al_map_rgb
(
0
, 255
, 0
);
else
color =
al_map_rgb
(
255
, 0
, 0
);
al_draw_filled_rectangle
(
r2.x, r2.y,
r2.x +
r2.tx, r2.y +
r2.ty,
al_map_rgb
(
0
, 0
, 255
));
al_draw_filled_rectangle
(
r1.x, r1.y,
r1.x +
r1.tx, r1.y +
r1.ty,
color);
al_flip_display
(
);
}
}
while
(!
al_key_down
(&
key, ALLEGRO_KEY_ESCAPE));
al_destroy_display
(
display);
return
0
;
}
/**
************************************************************
**************************************************************
*/
IX-E-5. Recherche d'une couleur sur un fond▲
Autre principe pour la détection de collisions, des informations codées sous forme de couleurs sont laissées sur un fond invisible. Un personnage ou une forme se déplace sur un fond qui est une bitmap colorée exactement de la même taille que l'écran. Il regarde à chaque pas la couleur des pixels sur lesquels il arrive. Selon ces couleurs, il sait sur quoi il avance. Cette bitmap n'est pas visible à l'écran. Elle reste toujours cachée mais elle double le décor en délimitant chacune des parties du décor (route, herbe, mur, eau, feu, etc.). La bitmap de fond devra être très simple avec pour chaque partie une seule couleur. Lorsque l'objet, la forme ou le personnage se déplace sur le décor, selon sa position et la couleur qu'il trouve dans la bitmap à cette position il sait sur quoi il est ou s'il rencontre quelque chose.
Cette technique peut aussi être utilisée pour détecter des collisions entre entités mobiles. À chaque pas, l'entité visible à l'écran est en plus copiée dans la bitmap invisible de fond à la même place. Une autre entité pourra savoir si elle la rencontre en regardant dans la bitmap fond. Selon la complexité du programme, plusieurs bitmaps invisibles de fond mais servant de support d'informations peuvent être utilisées.
IX-E-5-a. Expérimentation ▲
Dans ce programme, une bitmap fond est créée de la taille de l'écran et initialisée en noir au départ. Un décor est créé qui remplace le précédent en appuyant sur la touche [Entrée], ce sont des rectangles de couleurs bombardés aléatoirement dans la fenêtre. La souris trace en permanence un cercle à sa position et ce cercle n'est pas effacé de sorte que le chemin suivi par la souris est visible. La couleur du cercle est rouge s'il n'y a rien sur le fond (couleur noire sur le fond) et verte si au contraire une couleur est rencontrée sur le fond. La touche [F1] affiche le décor à l'écran.
#include <allegro5/allegro.h>
#include <allegro5/allegro_native_dialog.h>
#include <allegro5/allegro_primitives.h>
const
int
SCRX =
800
;
const
int
SCRY =
600
;
void
erreur
(
const
char
*
txt)
{
ALLEGRO_DISPLAY*
d;
d =
al_is_system_installed
(
) ? al_get_current_display
(
) : NULL
;
al_show_native_message_box
(
d, "
Erreur
"
, txt, NULL
, NULL
, 0
);
exit
(
EXIT_FAILURE);
}
/**
***************************************************************
La fonction teste l'égalité de deux couleurs et retourne le
résultat. Les couleurs sont des structures et il est nécessaire de
comparer chaque champ. Le champ a du canal alpha pour la
transparence n'est pas prise en compte.
****************************************************************
*/
bool meme_couleur
(
ALLEGRO_COLOR c1, ALLEGRO_COLOR c2)
{
return
(
c1.r ==
c2.r &&
c1.g ==
c2.g &&
c1.b ==
c2.b);
}
/**
***************************************************************
****************************************************************
*/
void
creation_decor
(
ALLEGRO_BITMAP*
fond, ALLEGRO_DISPLAY*
display)
{
al_clear_to_color
(
al_map_rgb
(
0
,0
,0
));
al_set_target_bitmap
(
fond);
int
x =
rand
(
) %
SCRX;
int
y =
rand
(
) %
SCRY;
int
tx =
(
rand
(
) %
(
SCRX -
x)) /
2
;
int
ty =
(
rand
(
) %
(
SCRY -
y)) /
2
;
ALLEGRO_COLOR c =
al_map_rgb
(
rand
(
) %
256
,
rand
(
) %
256
,
rand
(
) %
256
);
al_draw_filled_rectangle
(
x, y, x +
tx, y +
ty, c);
al_set_target_backbuffer
(
display);
}
/**
***************************************************************
****************************************************************
*/
int
main
(
)
{
ALLEGRO_DISPLAY*
display;
ALLEGRO_KEYBOARD_STATE key;
ALLEGRO_MOUSE_STATE mouse;
ALLEGRO_COLOR vert, rouge, noir, color;
ALLEGRO_BITMAP*
fond;
if
(!
al_init
(
))
erreur
(
"
al_init()
"
);
if
(!
al_install_keyboard
(
))
erreur
(
"
al_install_keyboard()
"
);
if
(!
al_install_mouse
(
))
erreur
(
"
al_install_mouse()
"
);
if
(!
al_init_primitives_addon
(
))
erreur
(
"
al_init_primitives_addon()
"
);
//création écran
display =
al_create_display
(
SCRX, SCRY);
if
(!
display)
erreur
(
"
al_create_display()
"
);
// création fond (déjà noir par défaut)
fond =
al_create_bitmap
(
SCRX, SCRY);
if
(!
fond)
erreur
(
"
al_create_bitmap()
"
);
noir =
al_map_rgb
(
0
, 0
, 0
); // couleur de fond
vert =
al_map_rgb
(
0
, 255
, 0
);// couleur si quelque chose
rouge =
al_map_rgb
(
255
, 0
, 0
);// couleur si rien
// premier décor invisible
creation_decor
(
fond, display);
do
{
al_get_keyboard_state
(&
key);
al_get_mouse_state
(&
mouse);
// rempalcement du décor courant
if
(
al_key_down
(&
key, ALLEGRO_KEY_ENTER))
creation_decor
(
fond, display);
// pour voir le décor à l'écran
if
(
al_key_down
(&
key, ALLEGRO_KEY_F1))
al_draw_bitmap
(
fond, 0
, 0
, 0
);
// récupérer info sur fond
if
(
meme_couleur
(
al_get_pixel
(
fond, mouse.x, mouse.y), noir))
color =
rouge; // rien
else
color =
vert;// quelque chose
// dessiner un cercle à la position de la souris
al_draw_filled_circle
(
mouse.x, mouse.y, 5
, color);
al_flip_display
(
);
}
while
(!
al_key_down
(&
key, ALLEGRO_KEY_ESCAPE));
al_destroy_display
(
display);
return
0
;
}
/**
***************************************************************
****************************************************************
*/
IX-F. Destruction des ennemis▲
Dernier volet pour notre base de jeu, le vaisseau tire des missiles et atteint des ennemis. Il nous reste à mettre en place la détection des collisions, collisions entre missiles et ennemis mais aussi collisions entre ennemis et vaisseau du joueur.
IX-F-1. Détection de collisions missiles-ennemis▲
À chaque tour, il faut regarder pour chaque ennemi si un missile le percute. Il y a donc deux boucles dont une est imbriquée dans la première. La première boucle passe en revue tous les ennemis. Pour chaque ennemi actif trouvé, une seconde boucle cherche les missiles actifs et pour chaque missile actif trouvé, un test regarde si l'ennemi courant est en intersection avec lui.
Pour l'intersection nous n'avons pas de rectangle mais des ovales définis par un centre x,y, une largeur tx et une hauteur ty qui sont des rayons horizontaux et verticaux. Alors, pour la collision, il y a trois conditions :
- à l'horizontale le centre (mx,my) du missile doit dépasser le centre de l'ennemi moins son rayon horizontal (ex-tx).
Et pour la verticale, il y a deux cas possibles :
- Premier cas, le centre du missile est compris dans le segment haut de l'ennemi qui part du centre ey de l'ennemi moins son rayon vertical (ey-ty).
- Second cas, le centre du missile est compris dans le segment bas de l'ennemi qui part du centre ey de l'ennemi plus son rayon vertical (ey+ty).
Le mieux est de le visualiser avec un croquis :
Le test résultant est le suivant :
SI ( mx > ex - tx ET my > ey - ty ET my < ey + ty)
alors il y a collision
Ce qui nous donne la fonction :
void
collision_missiles
(
t_missile m[],t_ennemi e[],t_vaisseau*
p)
{
int
i,j;
for
(
j=
0
; j<
NBMAXENNEMIS; j++
){
if
(
e[j].actif==
true
){
for
(
i=
0
; i<
NBMAXMISSILES; i++
){
if
(
m[i].actif==
true
&&
m[i].x >
e[j].x-
e[j].tx &&
m[i].y >
e[j].y-
e[j].ty &&
m[i].y <
e[j].y+
e[j].ty ){
m[i].actif =
false
;
e[j].actif =
false
;
p->
score++
;
}
}
}
}
}
IX-F-2. Détection de collisions entre vaisseau et ennemis▲
À chaque tour, on regarde si un ennemi percute le vaisseau. La zone de l'ennemi ovale qui intéresse la collision est sa moitié rectangulaire gauche ainsi que l'avant du vaisseau en pointillé sur le schéma :
Pour contrôler s'il y a une collision du vaisseau avec un ennemi, on passe en revue tous les ennemis et, pour chaque ennemi actif, on regarde s'il y a ou non une intersection de rectangles avec le vaisseau (l'intersection de rectangles est expliquée à la section Intersection de rectanglesIntersection de rectangles ? ). C'est la fonction :
void
collision_vaisseau
(
t_vaisseau*
p, t_ennemi e[])
{
int
i;
for
(
i=
0
; i<
NBMAXENNEMIS; i++
){
if
(
e[i].actif==
true
){
// pour simplifier l'écriture de la zone
// x1,y1 - x2,y2 de collision ennemi
int
x1 =
e[i].x-
e[i].tx;
int
y1 =
e[i].y-
e[i].ty;
int
x2 =
e[i].x;
int
y2 =
e[i].y+
e[i].ty;
// test
if
(
p->
x+
p->
tx >
x1 &&
p->
x <
x2 &&
p->
y+
p->
ty >
y1 &&
p->
y <
y2 ){
// conséquences collision
p->
vie--
;
e[i].actif=
false
;
}
}
}
}
Les collisions sont à tester après les déplacements, c'est-à-dire à chaque événement du minuteur timer.
IX-G. Code complet du programme▲
Voici la base de jeu complète. Elle comprend vaisseau, missiles, ennemis, collisions entre vaisseau et ennemis et entre missiles et ennemis.
Tout d'abord le fichier d'en-tête jeu.h :
#ifndef _GENERAL
#define _GENERAL
#include <allegro5\allegro.h>
#include <allegro5\allegro_native_dialog.h>
#include <allegro5\allegro_primitives.h>
// taille ecran
const
int
SCRX =
800
;
const
int
SCRY =
600
;
// vaisseau
typedef
struct
{
int
x, y; // position
int
dv; // déplacement et vitesse
int
tx, ty; // taille
int
vie; // vivant ou pas, actif ou pas
int
score; // les points obtenus
ALLEGRO_BITMAP*
image; // une image pour le vaisseau
}
t_vaisseau;
// les missiles
typedef
struct
{
int
x, y; // position
int
dv; // déplacement
bool actif;
}
t_missile;
const
int
NBMAXMISSILES =
5
;
// les ennemis
typedef
struct
{
int
x, y; // position
int
dv; // déplacement
int
tx, ty; // taille
bool actif; // actif ou pas
}
t_ennemi;
const
int
NBMAXENNEMIS =
10
;
// pour le contrôle du clavier
enum
KEYS{
UP, RIGHT, DOWN, LEFT, SPACE, KEY_MAX }
;
bool key[KEY_MAX] =
{
false
}
;
/**
***************************************************************
VAISSEAU /
****************************************************************
*/
void
init_vaisseau
(
t_vaisseau*
p);
void
affiche_vaisseau
(
t_vaisseau*
p);
void
monte
(
t_vaisseau*
p);
void
droite
(
t_vaisseau*
p);
void
descend
(
t_vaisseau*
p);
void
gauche
(
t_vaisseau*
p);
void
collision_vaisseau
(
t_vaisseau*
p, t_ennemi e[]);
void
erreur
(
const
char
*
txt);
/**
***************************************************************
MISSILES /
****************************************************************
*/
void
init_all_missiles
(
t_missile m[]);
void
affiche_all_missiles
(
t_missile m[]);
void
avance_all_missiles
(
t_missile m[]);
void
lancement_missile
(
t_missile m[], t_vaisseau*
p);
void
collision_missiles
(
t_missile m[], t_ennemi e[],
t_vaisseau*
p);
/**
*******************************************************
ENNEMIS /
********************************************************
*/
void
init_all_ennemis
(
t_ennemi e[]);
void
affiche_all_ennemis
(
t_ennemi e[]);
void
avance_all_ennemis
(
t_ennemi e[]);
void
apparition_ennemi
(
t_ennemi e[]);
/**
***************************************************************
****************************************************************
*/
#endif
Et sur un fichiers .c le code des fonctions, main() compris :
#include "jeu.h"
/**
***************************************************************
****************************************************************
*/
int
main
(
)
{
//--------------------------------Allegro et gestion events
ALLEGRO_DISPLAY *
display;
ALLEGRO_EVENT_QUEUE*
queue;
ALLEGRO_TIMER*
timer;
bool fin =
false
;
bool dessin =
false
;
t_vaisseau vaisseau;
t_missile missiles[NBMAXMISSILES];
t_ennemi ennemis[NBMAXENNEMIS];
if
(!
al_init
(
))
erreur
(
"
init allegro
"
);
if
(!
al_init_primitives_addon
(
))
erreur
(
"
init primitives
"
);
// pour avoir le clavier
if
(!
al_install_keyboard
(
))
erreur
(
"
install keyboard
"
);
// la fenêtre
display =
al_create_display
(
SCRX, SCRY);
if
(!
display)
erreur
(
"
display
"
);
// la file d'événements
queue =
al_create_event_queue
(
);
if
(!
queue)
erreur
(
"
queue
"
);
// initialiser le timer
timer =
al_create_timer
(
1
.0
/
50
); // temps en seconde
if
(!
timer)
erreur
(
"
timer
"
);
// enregistrement des types d'événements à recueillir
al_register_event_source
(
queue,
al_get_display_event_source
(
display));
al_register_event_source
(
queue,
al_get_keyboard_event_source
(
));
al_register_event_source
(
queue,
al_get_timer_event_source
(
timer));
// démarrage timer
al_start_timer
(
timer);
// -----------------------------initialisations actions
init_vaisseau
(&
vaisseau);
al_set_target_backbuffer
(
display);
init_all_missiles
(
missiles);
init_all_ennemis
(
ennemis);
while
(!
fin){
ALLEGRO_EVENT ev;
al_wait_for_event
(
queue, &
ev);
if
(
ev.type ==
ALLEGRO_EVENT_DISPLAY_CLOSE)
fin =
true
;
else
if
(
ev.type ==
ALLEGRO_EVENT_KEY_DOWN)
{
switch
(
ev.keyboard.keycode){
case
ALLEGRO_KEY_UP:
key[UP] =
true
;
break
;
case
ALLEGRO_KEY_RIGHT:
key[RIGHT] =
true
;
break
;
case
ALLEGRO_KEY_DOWN:
key[DOWN] =
true
;
break
;
case
ALLEGRO_KEY_LEFT:
key[LEFT] =
true
;
break
;
// tirs missiles
case
ALLEGRO_KEY_SPACE:
lancement_missile
(
missiles, &
vaisseau);
break
;
// sortie
case
ALLEGRO_KEY_ESCAPE:
fin =
true
;
break
;
}
}
else
if
(
ev.type ==
ALLEGRO_EVENT_KEY_UP)
{
switch
(
ev.keyboard.keycode){
case
ALLEGRO_KEY_UP:
key[UP] =
false
;
break
;
case
ALLEGRO_KEY_RIGHT:
key[RIGHT] =
false
;
break
;
case
ALLEGRO_KEY_DOWN:
key[DOWN] =
false
;
break
;
case
ALLEGRO_KEY_LEFT:
key[LEFT] =
false
;
break
;
}
}
else
if
(
ev.type ==
ALLEGRO_EVENT_TIMER)
{
// bouger, action
if
(
key[UP])
monte
(&
vaisseau);
if
(
key[RIGHT])
droite
(&
vaisseau);
if
(
key[DOWN])
descend
(&
vaisseau);
if
(
key[LEFT])
gauche
(&
vaisseau);
// missiles
avance_all_missiles
(
missiles);
// gestion ennemis
apparition_ennemi
(
ennemis);
avance_all_ennemis
(
ennemis);
// collisions
collision_missiles
(
missiles, ennemis,
&
vaisseau);
collision_vaisseau
(&
vaisseau, ennemis);
dessin =
true
;
}
// dessin si file vide (priorité au clavier)
if
(
dessin ==
true
&&
al_is_event_queue_empty
(
queue)){
// les opérations d'affichage
// 1 effacer le double buffer
al_clear_to_color
(
al_map_rgb
(
0
, 0
, 0
));
// 2 afficher les entités à leurs positions
affiche_vaisseau
(&
vaisseau);
affiche_all_missiles
(
missiles);
affiche_all_ennemis
(
ennemis);
// 3 passer le double buffer à l'écran
al_flip_display
(
);
dessin =
false
;
}
}
// nettoyage sortie
al_destroy_event_queue
(
queue);
al_destroy_display
(
display);
al_destroy_timer
(
timer);
return
0
;
}
/**
**************************************************************
VAISSEAU / Initialisation
****************************************************************
*/
void
init_vaisseau
(
t_vaisseau*
p)
{
p->
x =
20
;
p->
y =
SCRY /
2
;
p->
dv =
7
;
p->
tx =
30
;
p->
ty =
20
;
p->
vie =
3
;
p->
score =
0
;
// l'image du vaisseau : un triangle
// création d'une bitmap ou possibiité de loader une image
p->
image =
al_create_bitmap
(
p->
tx, p->
ty);
// pour pouvoir dessiner dans la bitmap, avant chaque // opération de dessin
al_set_target_bitmap
(
p->
image);
// dessin dedans
al_draw_filled_triangle
(
0
, 0
, 0
, p->
ty, p->
x, p->
ty /
2
, al_map_rgb
(
0
, 255
, 0
));
}
/**
**************************************************************
VAISSEAU / affichage
****************************************************************
*/
void
affiche_vaisseau
(
t_vaisseau*
p)
{
al_draw_bitmap
(
p->
image, p->
x, p->
y, 0
);
}
/**
************************************************************
VAISSEAU / mouvement
**************************************************************
*/
void
monte
(
t_vaisseau*
p)
{
p->
y =
(
p->
y -
p->
dv <
0
) ? 0
: p->
y -
p->
dv;
}
void
droite
(
t_vaisseau*
p)
{
p->
x =
(
p->
x +
p->
dv >=
SCRX /
3
) ? SCRX /
3
: p->
x +
p->
dv;
}
void
descend
(
t_vaisseau*
p)
{
p->
y =
(
p->
y +
p->
ty +
p->
dv >=
SCRY) ?
SCRY -
p->
ty : p->
y +
p->
dv;
}
void
gauche
(
t_vaisseau*
p)
{
p->
x =
(
p->
x -
p->
dv <
0
) ? 0
: p->
x -
p->
dv;
}
/**
************************************************************
VAISSEAU / collisions ennemis
**************************************************************
*/
void
collision_vaisseau
(
t_vaisseau*
p, t_ennemi e[])
{
int
i;
for
(
i =
0
; i<
NBMAXENNEMIS; i++
){
if
(
e[i].actif ==
true
){
// la zone x1,y1 - x2,y2 de collision
// (simplifie écriture)
int
x1 =
e[i].x -
e[i].tx;
int
y1 =
e[i].y -
e[i].ty;
int
x2 =
e[i].x;
int
y2 =
e[i].y +
e[i].ty;
// test
if
(
p->
x +
p->
tx >
x1 &&
p->
x <
x2 &&
p->
y +
p->
ty >
y1 &&
p->
y <
y2){
// conséquences collision
p->
vie--
;
e[i].actif =
false
;
}
}
}
}
/**
************************************************************
MISSILES / initialisation
initialisse ce qui est fixe (vitesse) et état (non actif)
La position sera initialisée au momebt du tir en fonction de la
position du vaisseau
**************************************************************
*/
void
init_all_missiles
(
t_missile m[])
{
int
i;
for
(
i =
0
; i<
NBMAXMISSILES; i++
){
m[i].dv =
10
;
m[i].actif =
false
;
}
}
/**
************************************************************
MISSILES / affichage
**************************************************************
*/
void
affiche_all_missiles
(
t_missile m[])
{
int
i;
for
(
i =
0
; i<
NBMAXMISSILES; i++
){
if
(
m[i].actif ==
true
)
al_draw_filled_ellipse
(
m[i].x, m[i].y, 3
, 2
,
al_map_rgb
(
0
, 255
, 0
));
}
}
/**
************************************************************
MISSILES / bouger
**************************************************************
*/
void
avance_all_missiles
(
t_missile m[])
{
int
i;
for
(
i =
0
; i<
NBMAXMISSILES; i++
){
if
(
m[i].actif ==
true
){
m[i].x +=
m[i].dv;
if
(
m[i].x >=
SCRX)
m[i].actif =
false
;
}
}
}
/**
************************************************************
MISSILES / mise à feu
**************************************************************
*/
void
lancement_missile
(
t_missile m[], t_vaisseau*
p)
{
int
i;
for
(
i =
0
; i<
NBMAXMISSILES; i++
){
if
(
m[i].actif ==
false
){
m[i].actif =
true
;
m[i].x =
p->
x +
p->
tx;
m[i].y =
p->
y +
p->
ty /
2
;
break
; // sortir de la boucle
}
}
}
/**
************************************************************
MISSILES / collisions
**************************************************************
*/
void
collision_missiles
(
t_missile m[], t_ennemi e[], t_vaisseau*
p)
{
int
i, j;
for
(
j =
0
; j<
NBMAXENNEMIS; j++
){
if
(
e[j].actif ==
true
){
for
(
i =
0
; i<
NBMAXMISSILES; i++
){
if
(
m[i].actif ==
true
&&
m[i].x >
e[j].x -
e[j].tx &&
m[i].y >
e[j].y -
e[j].ty &&
m[i].y <
e[j].y +
e[j].ty){
m[i].actif =
false
;
e[j].actif =
false
;
p->
score++
;
}
}
}
}
}
/**
************************************************************
ENNEMIS / initialisation
**************************************************************
*/
void
init_all_ennemis
(
t_ennemi e[])
{
int
i;
for
(
i =
0
; i<
NBMAXENNEMIS; i++
){
e[i].actif =
false
;
e[i].dv =
5
+
rand
(
) %
5
;
e[i].tx =
5
+
rand
(
) %
20
;
e[i].ty =
5
+
rand
(
) %
20
;
}
}
/**
************************************************************
ENNEMIS / affichage
**************************************************************
*/
void
affiche_all_ennemis
(
t_ennemi e[])
{
int
i;
for
(
i =
0
; i<
NBMAXENNEMIS; i++
){
if
(
e[i].actif ==
true
)
al_draw_filled_ellipse
(
e[i].x, e[i].y,
e[i].tx, e[i].ty,
al_map_rgb
(
255
, rand
(
) %
256
, rand
(
) %
256
));
}
}
/**
************************************************************
ENNEMIS / mouvement
**************************************************************
*/
void
avance_all_ennemis
(
t_ennemi e[])
{
int
i;
for
(
i =
0
; i<
NBMAXENNEMIS; i++
){
if
(
e[i].actif ==
true
){
e[i].x -=
e[i].dv; // attention soustraction
if
(
e[i].x -
e[i].tx <
0
)
e[i].actif =
false
;
}
}
}
/**
************************************************************
ENNEMIS / lancement
**************************************************************
*/
void
apparition_ennemi
(
t_ennemi e[])
{
int
i;
for
(
i =
0
; i<
NBMAXENNEMIS; i++
){
if
(
e[i].actif ==
false
&&
rand
(
) %
1000
<
10
){
e[i].x =
SCRX -
e[i].tx;
e[i].y =
rand
(
) %
(
SCRY -
(
e[i].ty *
2
));
e[i].actif =
true
;
}
}
}
/**
************************************************************
TOOLS
**************************************************************
*/
void
erreur
(
const
char
*
txt)
{
ALLEGRO_DISPLAY *
d;
d =
al_is_system_installed
(
) ? al_get_current_display
(
) : NULL
;
al_show_native_message_box
(
d, "
Erreur
"
, txt, NULL
, NULL
, 0
);
exit
(
EXIT_FAILURE);
}
/**
************************************************************
**************************************************************
*/
Obtenir ce livre▲
Ce document est une retranscription autorisée du livre Allegro 5 - Programmation de jeux enC et C++ écrit par Frédéric DROUILLON. Initialement publié aux éditions ENI, vous pouvez commander le livre sur le site de l'éditeur. |