Allegro 5

Programmation de jeux en C ou C++


précédentsommairesuivant

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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
const int SCRX = 800;
const int SCRY = 600;

IX-B-2. Initialisation

L'initialisation est effectuée avec la fonction :

 
Sélectionnez
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 à :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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'en­tê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

Fichier d'en-têtes jeu.h
Sélectionnez
#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

 
Sélectionnez
#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 :

 
Sélectionnez
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 :

 
Sélectionnez
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).

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 (…) :

 
Sélectionnez
#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 (…) :

Le vaisseau tire des missiles
Sélectionnez
#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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 (…) :

 
Sélectionnez
#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.

Des ennemies arrivent
Sélectionnez
#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 :

 
Sélectionnez
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 :

 
Sélectionnez
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.

Image non disponible

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).

Image non disponible

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.

Savoir si un point est dans un triangle
Sélectionnez
#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 :

Image non disponible

(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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
Si (
x1+tx1 > x2 ET x1 < x2+tx2 ET
y1+ty1 > y2 ET y1 < y2+ty2
)
    Collision
Sinon
    Pas de collision

Soit la fonction :

 
Sélectionnez
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.

Détecter une intersection de rectangles
Sélectionnez
#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.

Détecter des différentiels de couleurs sur un fond
Sélectionnez
#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 :

Image non disponible

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 :

 
Sélectionnez
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 :

Image non disponible

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 :

 
Sélectionnez
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 :

 
Sélectionnez
#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 :

Base complète de jeu d'action simple
Sélectionnez
#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

Image non disponible

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.


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2015 Frédéric DROUILLON. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.