Sommaire

BONJOUR
Musique "Invention BWV988" J.S. BACH 1685-1750

Tutoriel et Help

Library SDL


Tutoriel SDL

Index SDL image SDL

Chapitre I : Installation de SDL |
Chapitre I-Bis : Installation d'une extension de SDL |
Chapitre II : Première application avec SDL | (telecharger la version pdf (984 ko - 17 pages): )
Chapitre III : SDL_TTF | (telecharger la version pdf (317 ko - 6 pages): )
Chapitre IV : Gestion des événements avec SDL | (telecharger la version pdf (684 ko - 12 pages): )
Chapitre V : Transparence | (telecharger la version pdf (1536 ko - 6 pages): )
Chapitre VI : Sprites | (telecharger la version pdf (117 ko - 8 pages): )
Chapitre VII : Jouer du son avec SDL | (telecharger la version pdf (366 ko - 21 pages): )
Chapitre VIII : Timer et SDL | (telecharger la version pdf (593 ko - 12 pages): )
Chapitre IX : Frame Rate et SDL | (telecharger la version pdf (338 ko - 11 pages): )
Chapitre X : Mouvements avec SDL | (telecharger la version pdf (72 ko - 8 pages): )
Chapitre XI : Détection de collision avec SDL | (telecharger la version pdf (141 ko - 21 pages): )
Chapitre XII : Animation avec SDL | (telecharger la version pdf (149 ko - 16 pages): )
Chapitre XIII : Sauvegarde de jeu avec SDL | (telecharger la version pdf (83 ko - 11 pages): )
Chapitre XIV : Scrolling avec SDL | (telecharger la version pdf (310 ko - 11 pages): )
Chapitre XV : Gestion des événements avancé avec SDL | (telecharger la version pdf (134 ko - 21 pages): )
Chapitre XVI : Alpha blending avec SDL | (telecharger la version pdf (85 ko - 7 pages): )
Chapitre XVII : Utilisation d'un Font Bitmap avec SDL | (telecharger la version pdf (126 ko - 14 pages): )
Chapitre XVIII : Thread et SDL nouveau | (telecharger la version pdf (84 ko - 6 pages): )


Tutoriel SDL

Chap I : Installer SDL

Pour installer SDL, la première chose que vous avez à faire est de télécharger le binaire et les fichiers d'entête (*.h).
Vous pouvez les trouver sur le site de SDL ici
Allez dans la section Development Libraries et descendez en bas pour trouver les différents liens pour télécharger SDL.
Nous allons maintenant voir desquels vous avez besoin de télécharger dans les parties qui vont suivre.

image devc++ A : Installer SDL sous DevC++ Version pdf ( 380 ko - 9 pages):
image code::blocks B : Installer SDL sous Code::Blocks Version pdf ( 391 ko - 10 pages):
image tux C : Installer SDL sous Linux en ligne de commande Version pdf ( 81 ko - 4 pages):
image c++ builder D : Installer SDL sous C++ Builder Version pdf ( 56 ko - 4 pages):
image eclipse E : Installer SDL sous Eclipse CDT Version pdf ( 52 ko - 3 pages):


Tutoriel SDL

Chap I-Bis : Installation des bibliothèques SDL

Avant d'installer une bibliothèque SDL, il vous faut installer SDL.
L'installation de SDL est décrite ici : Installer SDL

Dans ce tutoriel, je vais vous décrire comment installer les bibliothèques SDL les plus utilisés comme SDL_TTF, SDL_image et SDL_mixer.
L'installation de ces trois bibliothèques étant identique, je ne vais aborder que l'installation de SDL_image.
Si des demandes sont faîtes pour l'installation de SDL_TTF, SDL_mixer ou tout autre bibliothèque SDL, j'essaierai de combler le manque dans ce tutoriel.

image devc++ A : Installer une bibliothèque SDL sous DevC++ Version pdf ( 173 ko - 7 pages):
image code::blocks B : Installer une bibliothèque SDL sous Code::Blocks Version pdf (170 ko - 8 pages):
image tux C : Installer une bibliothèque SDL sous Linux en ligne de commande Version pdf ( 84 ko - 5 pages):
image c++ builder D : Installer une bibliothèque SDL sous C++ Builder


Tutoriel SDL

Chapitre II : Premières applications avec SDL

  • II. Débuts avec SDL
    • II-A. Créer une fenêtre SDL
    • II-B. Hello World - Blitting

C'est ici que débute notre apprentissage de SDL avec la création de nos deux premiers programmes.

II. Débuts avec SDL

Bien, maintenant que SDL est bien installé, nous allons pouvoir commencer à rentrer dans le vif du sujet :
créer une application SDL.

Pour se familiariser avec les différentes fonctions que nous offrent SDL et ses extensions, nous allons procéder pas à pas.
Pour commencer, dans le chapitre II-A, nous allons apprendre à créer une fenêtre SDL vierge, ce qui nous apprendra à initialiser SDL et à quitter SDL correctement.

Ensuite, dans le chapitre II-B, nous allons apprendre à charger des images sur la fenêtre SDL et de les appliquer les unes sur les autres, cela s'appelle le blitting

Par la suite, dans les chapitres IV (Gestion des événements) et XV (gestion des événements avancés), je vais tenter de vous expliquer comment marche la gestion des événements sous SDL et ainsi que vous puissiez créer une application qui puisse réagir lorsque l'utilisateur appuie sur une touche, clique à un endroit précis, etc..

Le chapitre V quant à lui vous permettra d'apprendre à utiliser une fonction particulière de SDL : SDL_SetColorKey(), qui vous sera très utile si vous souhaitez créer un jeu ou même pour plein d'autres applications concrètes dans lesquels vous avez besoin de manipuler des images.

Nous finirons notre familiarisation de SDL avec la création d'une feuille de sprite et son utilisation dans une application SDL (chapitre VI).

II-A. Créer une fenêtre SDL

Voici le code source vous permettant de créer une fenêtre SDL, je vais vous l'expliquer point par point dans la suite.

fenêtre SDL
#include <stdlib.h>
#include <stdio.h>
#include <SDL/SDL.h>

int main( int argc, char *argv[ ] )
{
    SDL_Surface *screen;
    if( SDL_Init( SDL_INIT_VIDEO ) == -1 )
    {
        printf( "Can't init SDL:  %s\n", SDL_GetError( ) );
        return EXIT_FAILURE;
    }

    atexit( SDL_Quit );
    screen = SDL_SetVideoMode( 640, 480, 16, SDL_HWSURFACE );

    if( screen == NULL )
    {
        printf( "Can't set video mode: %s\n", SDL_GetError( ) );
        return EXIT_FAILURE;
    }

    SDL_Delay( 3000 );

    return EXIT_SUCCESS;
}

Dans les 2 premières lignes, on inclut le fichier d'en-tête nécessaire pour l'utilisation de la SDL (SDL.h) et la librairie stdlib.h pour l'utilisation de exit() et atexit().

Ensuite vient la fonction bien connue int main() avec ces 2 arguments obligatoire pour un programme SDL (nous y reviendrons dans le chapitre suivant).

Une ligne plus bas on peut déjà trouver le premier type inconnu, SDL_Surface, qui est utilisé ici pour une surface d'écran.
Ensuite viens la première fonction inconnue, elle est nommée: int SDL_Init(Uint32 flags) et est utilisée pour initialiser la SDL où le paramètre flags est(sont) le(s) subsystem(s) qui doit(-vent) être initialisé(s).
La combinaison de plusieurs flags est possible en introduisant des '|' entre chaque subsystem, donc peut être de la forme : SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO), qui initialisera le subsystem de la vidéo et de l'audio.
Les flags possibles sont:

SDL_INIT_AUDIO
SDL_INIT_VIDEO
SDL_INIT_CDROM
SDL_INIT_TIMER
SDL_INIT_JOYSTICK
SDL_INIT_EVENTTHREAD

Nous n'initialiserons que le sous-système de la vidéo puisque nous n'utilisons pas encore d'autres fonctions que celle de la vidéo.
Pour ceux qui utilisent tous les sous-systèmes peuvent par contre simplement déclarer SDL_INIT_EVERYTHING qui les initialisent tous.

Pour les curieux la SDL donne également la possibilité d'utiliser la fonction int SDL_InitSubSystem(Uint32 flags) qui est peut être très utile lorsque vous ne voulez pas initialiser un subsystem que lorsque celui-ci est choisi en option, ou bien que vous ne l'utilisez que plus tard dans le programme.

Un bon exemple pour ça est par exemple le joystick, car on sait rarement dès le début du jeu que l'utilisateur veut utiliser son joystick (s'il en possède un). Donc on place cela dans les options et si l'utilisateur décide de l'utiliser, le sous-système sera initialisé.

Pour fermer les sous-systèmes il faut utiliser int SDL_Quit( ) qui ferme tous les sous-systèmes initialisés, mais si vous ne voulez en fermer qu'un seul vous pouvez utiliser void SDL_QuitSubSystem(Uint32 flags).
Il est tout de même possible que l'initialisation retourne -1 et donc rate, c'est pour cela que nous faisons un test.
Quand cela se passe, on met un message dans la console d'erreur de la SDL, avec SDL_GetError() qui retourne l'erreur qui s'est produite. Dans le cas où il y a une erreur, nous terminons le programme en retournant 1

Si aucune erreur n'est intervenue, on appelle la fonction atexit() avec en paramètre SDL_Quit, ce qui veut dire que quand le programme est terminé il doit utiliser SDL_Quit. Puisque l'initialisation s'est bien passée et que tout notre programme se trouve en mémoire, avec SDL_Quit on libère tout lors de l'arrêt du programme.

Maintenant, donnons le paramétrage vidéo avec la fonction SDL_Surface *SDL_SetVideoMode(int width, int height, int bpp, Uint32 flags) qui prend comme paramètres la largeur de l'écran, sa hauteur, son nombre de bits par pixel et les flags.
Les trois premiers devraient normalement être bien connus, tandis que le quatrième est tout nouveau, puisqu'il n'accepte pas du tout les mêmes flags que SDL_Init. Ne nous attardons pas trop là-dessus pour l'instant.

Comme vous avez pu le constater dans le prototype de la fonction SDL_SetVideoMode, celle-ci retourne un pointeur qui pointe vers l'adresse de la plate-forme vidéo. C'est d'ailleurs pour cela que nous avons mis notre pointeur screen en tant que SDL_Surface.

Nous devons encore une fois tester si notre pointeur pointe bien quelque part. Car il est bien possible que certains modes vidéo ne soient pas acceptés par certaines cartes graphiques ou système…
Quand quelque chose tourne mal avec la fonction SDL_SetVideoMode, ici screen vaudra NULL, donc aucune adresse n'est retournée et le pointeur screen reste non valide.

Si tout s'est bien passé, nous devrions maintenant avoir un écran noir qui s'ouvre lors de l'exécution de l'application, pour que cette fenêtre ne disparaisse pas tout de suite, nous utilisons la fonction void SDL_Delay(Uint32 ms) qui, dans notre cas, fait attendre 3000 millisecondes à l'application avant de quitter (soit 3 secondes).

Et voilà, maintenant vous savez créer une fenêtre avec SDL.

Maintenant que nous savons faire ça, on va essayer de mettre quelques trucs dans notre fenêtre comme des images par exemple.
C'est l'objet de la partie ci-dessous.

II-B. Hello World - Blitting

Dans cette partie, à travers un exemple de programme simple (Hello World), nous allons apprendre à charger des surfaces et à les appliquer les unes sur les autres (Blitting).
Une surface est juste un mot extravagant pour une image. Blitter une surface signifie juste d'appliquer une surface sur une autre.

Voila en gros ce que nous cherchons à obtenir :

Image non disponible

Nous allons commencer par include les en-têtes

header
Sélectionnez
#include "SDL/SDL.h"
#include <string>

SDL.h est inclu parce que bien sur nous allons utiliser des fonctions SDL ^^
string quand à lui ben... c'est juste que je préfère std::string plutôt que char*.

Ensuite nous allons définir les différents attributs de notre écran.

attributs de l'écran
Sélectionnez
//Les attributs de notre écran
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
const int SCREEN_BPP = 32;

Je pense que vous comprenez ce que définissent SCREEN_WIDTH et SCREEN_HEIGHT.
SCREEN_BPP définit le nombre de bits par pixel. Dans cet exemple, nous utilisons des couleurs 32 bits.

Après avoir défini les attributs pour notre écran, il nous faut définir les surfaces que nous allons utiliser.
Nous allons avoir besoin de 3 surfaces :

  • la surface de l'écran qu'on appellera screen
  • la surface qui servira de fond d'écran (background)
  • la surface qui servira à afficher notre "Hello World" qui sera une image bitmap avec écrit Hello World dessus

Voici la définition de ces 3 surfaces :

surfaces
Sélectionnez
//Les surfaces que nous allons utiliser
SDL_Surface *message = NULL;
SDL_Surface *background = NULL;
SDL_Surface *screen = NULL;

Remarque : c'est une bonne idée de toujours mettre ses pointeurs à NULL s'ils pointent sur rien.

Nous avons terminé la partie définition des différentes variables dont nous aurons besoin, maintenant passons dans le vif du sujet : le chargement d'image.
Nous allons construire une fonction qui chargera une image à partir de son nom.
Voici le prototype de la fonction :

protoype fonction load_image
Sélectionnez
SDL_Surface *load_image( std::string filename )

Cette fonction retourne un pointeur vers la version optimisé de l'image chargée.
Pourquoi la version optimisée ?
Comme nous avons définit notre écran en 32bits, si le bitmap est en 24bits, il va nous falloir faire quelques petits changements sur l'image.
En effet, bien que SDL soit capable d'appliquer une image d'un format différent sur une autre, il faut pour cela qu'il change de lui même le format "à l'arrache" ce qui peut causer quelques ralentissements.
Comme vous le voyez, ce n'est pas une bonne idée d'essayer de blitter une surface sur une autre si les 2 sont dans des formats différents.

Voici comment nous allons donc procéder :
Tout d'abord il va nous falloir définir 2 variables, une variable qui servira de tampon pour stocker l'image chargée temporairement et une autre où on mettra l'image optimisée.
Il va nous falloir charger l'image en utilisant la fonction SDL_LoadBMP(char *) qui s'occupera de charger l'image au format Bitmap et de la mettre dans notre variable tampon.
Si le chargement s'est bien passé, il nous faut créer l'image optimisée.
La fonction SDL_DisplayFormat(SDL_Surface *) va nous permettre de créer une image dans le même format que notre écran.

Image non disponible

Nous allons donc, comme le montre le petit schéma ci-dessus, nous retrouver avec 2 surfaces : l'ancienne surface et la version optimisée.
Il va donc nous falloir faire un peu de ménage.
On va se servir de la fonction SDL_FreeSurface(SDL_Surface *) afin de libérer proprement l'ancienne surface

Image non disponible

pour finir, comme notre fonction retourne la version optimisée de l'image, il ne faut pas oublier... de la retourner ^^.

Voici le code de notre fonction de chargement d'image :

protoype fonction load_image
SDL_Surface *load_image( std::string filename ) {
    //Surface tampon qui nous servira pour charger l'image
    SDL_Surface* loadedImage = NULL;

    //L'image optimisée qu'on va utiliser
    SDL_Surface* optimizedImage = NULL;

    //Chargement de l'image
    loadedImage = SDL_LoadBMP( filename.c_str() );

    //Si le chargement se passe bien
    if( loadedImage != NULL ) {
        //Création de l'image optimisée
        optimizedImage = SDL_DisplayFormat( loadedImage );

        //Libération de l'ancienne image
        SDL_FreeSurface( loadedImage );
    }

    //On retourne l'image optimisée
    return optimizedImage;
}

Par la suite, il nous faut blitter la surface, nous allons donc créer une nouvelle fonction qui se chargera de cette tâche.
Notre fonction prendra en paramètre les coordonnées où l'on veut blitter notre surface, la surface que nous allons appliquer ainsi que la surface sur laquelle nous allons appliquer notre surface. Voici à quoi cela va ressembler au début :

debut fonction apply_surface
void apply_surface( int x, int y, SDL_Surface* source, SDL_Surface* destination ) {
    SDL_Rect offset;

    offset.x = x;
    offset.y = y;

Voila donc le début de notre fonction de blitting.
Nous faisons ainsi car SDL_BlitSurface() accepte seulement les offsets dans un SDL_Rect.

Un SDL_Rect est un type de donnée représentant un rectangle. Il possède 4 membres représentant les offsets X et Y, la largeur (Width) et la hauteur (Height) du rectangle.
Ici nous avons seulement besoin des données membres X et Y.

Ensuite il nous faut donc blitter la surface (c'est tout de même la finalité de cette fonction), pour cela nous allons utiliser la fonction SDL_BlitSurface() de cette façon :

SDL_BlitSurface
    //On blitte la surface
    SDL_BlitSurface( source, NULL, destination, &offset );
}

Comme vous pouvez le remarquer, ce bout de code est la suite directe, et la fin, de celui mit juste avant.
Notre fonction est donc déjà terminé.

Le premier argument de SDL_BlitSurface est la surface que nous allons utiliser.
Ne vous souciez pas encore du second argument, laissez le à NULL pour le moment.
Le troisième argument est la surface sur laquelle nous allons appliquer la surface que nous allons utiliser.
Le quatrième et dernier argument contient l'offset d'où notre surface sera appliqué sur la surface de destination.

Bon nous avons fini avec les fonctions dont nous allons avoir besoin pour faire notre petit programme, maintenant nous allons passer au main().
Tout d'abord, lorsque vous utilisez SDL, vous devrez toujours utiliser :

main avec SDL
int main( int argc, char* args[] )

ou

int main( int argc, char** args )

Sous windows, n'utilisez jamais int main(), void main(), ou autre chose qui ne marchera pas.

Bien, maintenant que l'on sait tout ça, on va se lancer dans le main en commençant par initialiser SDL :

init SDL
int main( int argc, char* args[] ) {
    //Initialisation de tous les sous-systèmes de SDL
    if( SDL_Init( SDL_INIT_EVERYTHING ) == -1 ) {
        return EXIT_FAILURE;
    }

Ici nous initialisons SDL en utilisant SDL_Init().
Nous donnons comme paramètre à SDL_Init() SDL_INIT_EVERYTHING, qui démarre tous les sous-systèmes SDL(Vidéo, Audio, etc...).
Les sous-systèmes SDL sont les moteurs individuels utilisés pour fabriquer un jeu.
Nous n'allons pas utiliser tous ces sous-systèmes, mais cela ne nous génera pas si nous les initialisons tout de même.

Si SDL ne peut pas s'initialiser, il retourne -1. Dans ce cas, nous attrapons l'erreur en retournant EXIT_FAILURE, qui termine le programme.

Après l'initialisation de SDL, il est temps de fabriquer notre écran et de récupérer un pointeur sur cet écran afin de pouvoir blitter des surfaces dessus.

SDL_SetVideoMode
    //Mise en place de l'écran
    screen = SDL_SetVideoMode( SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP, SDL_SWSURFACE );

Vous connaissez déjà les 3 premiers arguments si vous avez suivi normalement. Le quatrième argument crée la surface écran dans la mémoire système.
Il nous faut ensuite vérifier si notre pointeur sur surface screen n'est pas NULL, ce qui voudrait dire que ça s'est mal passé.

test ecran
    //S'il y a une erreur dans la création de l'écran
    if( screen == NULL ) {
        return EXIT_FAILURE;
    }

On va agrémenter notre fenêtre en ajoutant une légende sur le haut de la fenêtre :

legende ecran
    //Mise en place de la barre caption
    SDL_WM_SetCaption( "Hello World", NULL );

Ici la légende indiquera "Hello World" comme sur le schéma ci-dessous

Image non disponible

Ensuite nous allons charger les images :

chargement images
    //Chargement des images
    message = load_image( "hello_world.bmp" );
    background = load_image( "background.bmp" );

Nous utilisons comme vous le voyez notre fonction load_image().

il est maintenant temps d'utiliser notre deuxième fonction qui va se charger d'appliquer l'image de fond sur l'écran :

background
    //On applique le fond sur l'écran
    apply_surface( 0, 0, background, screen );

Avant d'appliquer notre fond, l'écran ressemblait à ça :

Image non disponible

Maintenant que nous avons appliquer notre surface, il ressemble à ça en mémoire :

Image non disponible

Quand vous appliquez une surface, vous copiez les pixels de l'un sur l'autre surface.
Donc maintenant l'écran ressemble à l'image background que nous venions de charger.

Nous allons donc continuer en appliquant maintenant la surface qui contient notre message "Hello World"

message
    //On applique le message sur l'écran
    apply_surface( 180, 140, message, screen );

Comme vous le voyez sur le code ci-dessus, nous appliquons notre surface sur l'écran en X=180 et Y=140.
Ce qu'il faut comprendre c'est que le système de coordonnées de SDL ne fonctionne pas ainsi :

Image non disponible

Le système de coordonnées de SDL se présente comme cela :

Image non disponible

Donc l'origine se trouve dans le coin en haut à gauche de l'écran.

Ainsi quand vous appliquez la surface message, celle-ci va se placer à 180 pixels sur la droite et 140 pixels vers le bas par rapport au coin haut gauche.
Voici une illustration afin de mieux comprendre :

Image non disponible

Le système de coordonnées de SDL peut vous paraître effrayant au début, mais vous vous y ferez vite.
Bien que nous avons appliquer nos surfaces, dans l'état actuel, l'écran restera noir.
En effet, il nous faut mettre à jour l'écran en utilisant la fonction SDL_Flip() pour que l'écran ressemble à celui que nous avons en mémoire.

SDL_Flip
    //Mise à jour de l'écran
    if( SDL_Flip( screen ) == -1 ) {
        return EXIT_FAILURE;
    }

Si il y a une erreur, la fonction retourne -1.
Nous pouvons presque dire que le travail est fini, il nous reste encore 2 choses à faire : laisser l'écran s'afficher quelques secondes et terminer notre application correctement.
En effet, si nous lançons notre programme, celui-ci disparaîtra de votre écran si rapidement que vous n'aurez même pas le temps d'admirer votre travail...
Pour maintenir l'écran quelques temps, nous allons utiliser la fonction SDL_Delay() qui prend en paramètre un temps en millisecondes

SDL_Delay
    //On attend 2 secondes
    SDL_Delay( 2000 );

Dans l'exemple ci-dessus, l'écran restera affiché pendant 2000 millisecondes soit 2 secondes.
Pour finir, nous allons terminer notre programme proprement.

SDL_Delay
    //Libération des surfaces
    SDL_FreeSurface( message );
    SDL_FreeSurface( background );

    //On quitte SDL
    SDL_Quit();
}

SDL_FreeSurface() est utilisé pour libérer la surface que nous avons chargé et que nous n'utiliserons plus.
Si vous ne libérez pas la mémoire utilisé, cela provoquera une fuite mémoire.
Ensuite nous appelons SDL_Quit() pour quitter SDL. Il nous reste à retourner 0 pour terminer l'application.

Voila ce que nous obtenons à la fin :

Image non disponible

Remarque :

Si vous lancez le programme, que les images ne se voient pas, que l'écran de l'application n'apparaît que très brièvement et que vous trouvez dans le fichier stderr.txt : Fatal signal: Segmentation Fault (SDL Parachute Deployed), c'est parce que le programme tente d'accéder à un espace mémoire qui n'est pas supposé être.
Ce phénomène étrange est du au fait qu'il essaye d'accéder à NULL quand apply_surface() est appelé.
Ce qui veut dire que vous devez vérifier que vos fichiers image Bitmap soient bien dans le même répertoire que votre programme.


Tutoriel SDL

Chapitre III : SDL_TTF

III. SDL_TTF

SDL_TTF est une extension de SDL qui permet l'emploie des polices true type (*.ttf) et leur affichage.
Tout d'abord il vous faudra installer cette extension, il vous suffit pour cela de le télécharger ici.
Si des demandes se font pour l'installation de cette extension (voir des autres extensions telles que sdl_image, sdl_net, etc...), je ferais un petit tutoriel afin de vous expliquez comment on fait sur les differents IDE.

Bien passons aux choses sérieuses.
SDL_TTF est assez simple d'utilisation, dans ce tutoriel, nous allons voir les bases d'utilisation de cette extension.

Voici le résultat de ce que nous allons chercher à obtenir :

Image non disponible

Comme dans tout programme, il faut d'abord déclarer voire initialiser nos variables.

Déclaration et initialisation
//Les surfaces
SDL_Surface *background = NULL;
SDL_Surface *message = NULL;
SDL_Surface *screen = NULL;

//La structure d'événement
SDL_Event event;

//Le Font qu'on va utiliser
TTF_Font *font;

//La couleur du Font
SDL_Color textColor = { 255, 255, 255 };

Nous avons donc comme avant nos surfaces background et screen ainsi que notre structure d'événement event.
Apparaît en plus une surface message qui sera la surface qui contiendra notre texte.
Il y a aussi de nouvelles données de type TTF_Font qui sera le font (police de caractère) qu'on va utiliser et SDL_Color qui sera la couleur du texte.
Dans ce cas là, le texte sera blanc.

Nous allons maintenant reprendre notre fonction init() afin d'initialiser SDL_TTF

Initialisation
bool init() {
    //Initialisation de tous les sous-systèmes de SDL
    if( SDL_Init( SDL_INIT_EVERYTHING ) == -1 ) {
        return false;
    }

    //Mise en place de l'écran
    screen = SDL_SetVideoMode( SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP, SDL_SWSURFACE );

    //S'il y a une erreur dans la mise en place de l'écran
    if( screen == NULL ) {
        return false;
    }

    //Initialisation de SDL_TTF
    if( TTF_Init() == -1 ) {
        return false;
    }

    //Mise en place de la barre caption
    SDL_WM_SetCaption( "TTF Test", NULL );

    //Si tout s'est bien passé
    return true;
}

SDL_TTF est initialisé en faisant appel à TTF_init().
TTF_init() retourne -1 quand il y a une erreur.

TTF_init() doit être appelé avant d'utiliser n'importe quelle fonction de SDL_TTF.

Nous allons maintenant voir ce que nous avons à changer dans notre fonction de chargement load_files() afin de charger la police de caractère que nous allons utiliser.

load_files
bool load_files() {
    //Chargement de l'image de fond
    background = load_image( "background.png" ); // utilisation de la librairie SDL_image

    //Ouverture du Font
    font = TTF_OpenFont( "CaslonBold.ttf", 28 );

    //S'il y a un problème au chargement du fond
    if( background == NULL ) {
        return false;
    }

    //S'il y a une erreur dans le chargement du Font
    if( font == NULL ) {
        return false;
    }

    //Si tout s'est bien chargé
    return true;
}

Le premier argument de TTF_OpenFont est le nom du fichier *.ttf que nous avons besoin d'ouvrir, le second argument est la taille à laquelle vous souhaitez initialiser le Font quand vous l'ouvrez.

Quand il y a une erreur dans le chargement du Font, TTF_OpenFont() retourne NULL.

Après avoir initialisé SDL_TTF et ouvert le font dont on a besoin, il nous reste alors à le "poser" sur l'écran.
Pour cela, nous allons utiliser la façon la plus rapide de rendre le texte en utilisant TTF_RenderText_Solid().

TTF_RenderText_Solid
//Mise en place du texte sur la surface message
message = TTF_RenderText_Solid( font, "Test pour sdl_ttf", textColor );

//S'il y a une erreur dans la mise en place du texte
if( message == NULL ) {
    return 1;
}

//Application des images sur l'écran
apply_surface( 0, 0, background, screen );
apply_surface( 0, 200, message, screen );

//Mise à jour de l'écran
if( SDL_Flip( screen ) == -1 ) {
    return 1;
}

TTF_RenderText_Solid() prend le font dans le premier argument, et crée une surface avec le texte du second argument avec la couleur du troisième argument.
TTF_RenderText_Solid() retourne NULL quand il y a une erreur qui s'est produite.

Il y a bien évidemment bien d'autres façons de mettre en place le texte, vous pouvez allez chercher ça dans la documentation de SDL_TTF

Voilà, notre programme est maintenant censé afficher ce que nous souhaitons, il nous reste plus qu'à le quitter proprement.

Netoyage
void clean_up() { //Libération des surfaces
    SDL_FreeSurface( background );
    SDL_FreeSurface( message );

    //Fermeture des Fonts qu'on a utilisé
    TTF_CloseFont( font );

    //On quitte SDL_ttf
    TTF_Quit();

    //On quitte SDL
    SDL_Quit();
}

Voici notre fonction clean_up() retravaillé afin de permettre le nettoyage de sdl_ttf.
Premièrement nous libérons les surfaces utilisées, donc la surface message générée aussi.
Nous fermons aussi le Font en utilisant TTF_CloseFont() et quittons SDL_TTF grâce à TTF_Quit().
Il nous reste plus qu'à quitter SDL comme d'habitude.


Tutoriel SDL

Chapitre IV : Gestion des événements avec SDL

  • IV. Gestion des événements
    • IV-A. Principe
    • IV-B. Evénement clavier
    • IV-C. Statut d'une touche
  • Liens

La gestion des événement est un passage obligatoire pour qui veut faire des applications SDL.

IV. Gestion des événements

Un événement est simplement quelque chose qui se passe. Cela peut être la touche qu'on presse, le mouvement de la souris, le redimensionnement de la fenêtre ou la fermeture de la fenêtre par le X.

IV-A. Principe

Cette partie vous apprendra comment vérifier et attraper un événement, spécialement quand l'utilisateur souhaites quitter sa fenêtre avec le X.

Nous allons donc construire une application qui ferme notre fenêtre quand l'utilisateur clique sur le X de notre fenêtre.
Commençons donc :

header, variables et constantes
//Les fichiers d'entête dont on a besoin
#include "SDL/SDL.h"
#include "SDL/SDL_image.h"
#include <string>

//Les attributs de l'écran
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
const int SCREEN_BPP = 32;

//Les surfaces
SDL_Surface *image = NULL;
SDL_Surface *screen = NULL;

Je ne pense pas avoir besoin de vous expliquer cette partie, elle est la même que dans les chapitres d'avant.
Par contre une nouvelle variable va venir se faufiler dans tout ça :

SDL_Event
//La structure d'événements qu'on va utiliser
SDL_Event event;

Une structure SDL_Event stocke toutes les données d'événement pour nous afin que nous puissions les utiliser.
Nous ne présenterons pas les fonctions de chargement d'images, d'initialisation de SDL, de chargement de surfaces et de "nettoyage" car ce sont exactement les mêmes que dans les chapitres précédents.
Nous allons donc nous attarder plus sur le main, qui va traiter les événements.

debut main
int main( int argc, char* args[] ) {
    //Un moyen de s'assurer que le programme va nous attendre pour quitter
    bool quit = false;

Vous vous demandez sûrement ce que fait ici ce booléen, la réponse se trouve à la suite de ce tutorial.

La suite du main est la même que les fois d'avant avec l'appel d'init(), de load_file(), de apply_surface() et de la fonction SDL_Flip().
Ce qui nous intéresse se trouve juste après :

    //Tant que l'utilisateur ne veut pas quitter
    while( quit == false ) {

Notre booléen sert à ce niveau là.
Cette boucle, qui sera notre boucle principale, tournera jusqu'à ce que l'utilisateur souhaite quitter notre fenêtre.
Il faut donc, dans cette boucle, lorsque l'utilisateur click sur le X de notre fenêtre, qu'on passe notre booléen à true.
Ainsi nous quitterons notre boucle principale et nous exécuterons la suite de notre programme qui quittera l'application.

Voyons voir ça de plus près :

SDL_PollEvent
    //Tant qu'il y a un événement à traiter
    while( SDL_PollEvent( &event ) ) {

Dans SDL, quand un événement arrive, celui-ci est mis dans une file d'événements de type FIFO (First In, First Out).
Cette file d'événements contient les données de chaque événement qui s'est passé.

Donc si vous pressez sur le bouton de la souris, bougez la souris, puis appuyez sur une touche de votre clavier, la file d'événements devrait ressembler à ceci :

Image non disponible

Ce que fait SDL_PollEvent() c'est de prendre un événement de la file et de coller ces données dans notre structure d'événement :

Image non disponible

Cette partie du code permet donc de mettre les données des événements dans notre structure tant qu'il y a des événements dans notre file d'événements.
Cela nous permet donc de pouvoir les manipuler par la suite.

Il nous faut maintenant vérifier la nature de l'événement.
Ce que nous cherchons ici est l'événement de fermeture de fenêtre à partir du X, cet événement a pour nom SDL_QUIT.

SDL_Quit
            //Si l'utilisateur a cliqué sur le X de la fenêtre
            if( event.type == SDL_QUIT ) {
                //On quitte le programme
                quit = true;
            }
        }
    }

Quand l'utilisateur va cliquer sur le X de notre fenêtre, le type de l'événement sera donc SDL_QUIT comme nous l'avons vu plus haut.
Seulement cela ne fermera pas notre fenêtre pour autant, tout ce que ça fait c'est de nous informer que l'utilisateur souhaite quitter le programme.
Nous voulons donc quitter le programme quand l'utilisateur click sur le X de notre fenêtre, nous mettons donc notre variable booléenne à true, ce qui arrête la boucle dans laquelle nous somme.
il nous suffit ensuite de nettoyer et de faire un return pour quitter.

Netoyage
    //On libère les surfaces et on quitte sdl
    clean_up();

    return 0;
}

Voila, si vous souhaitez récupérer le code source en entier c'est ici (156 ko)

IV-B. Evénement clavier

Vous savez maintenant comment marche un événement en SDL et vous en avez fait l'expérience en manipulant l'événement SDL_Quit.
Maintenant, dans cette partie, nous allons apprendre à détecter quand une touche est pressée ainsi que comment vérifier quelle touche a été pressée en particulier.
Afin de nous avertir qu'une touche particulière a été pressée, nous allons utiliser créer des surfaces (messages qui nous indiqueront quelle touche à été pressée).

generation des surfaces de message
//Génération des surfaces de message
up = TTF_RenderText_Solid( font, "Haut a été pressé.", textColor );
down = TTF_RenderText_Solid( font, "Bas a été pressé.", textColor );
left = TTF_RenderText_Solid( font, "Gauche a été pressé", textColor );
right = TTF_RenderText_Solid( font, "Droite a été pressé", textColor );

Comme vous le voyez, ici nous allons utiliser SDL_ttf, donc n'oubliez pas de l'inclure dans les fichiers d'entête.

Ces 4 messages sont générés après avoir tout initialisé et chargé.
Si vous voulez faire ça correctement, vous devrez vérifier s'il y a une erreur à chaque rendu de texte.

Maintenant nous allons vérifier si une touche est pressée, nous vérifions cela grâce à l'événement SDL_KEYDOWN :

SDL_PollEvent
//S'il y a un événement à manipuler
if( SDL_PollEvent( &event ) ) {
    //Si une touche est pressée
    if( event.type == SDL_KEYDOWN ) {

Maintenant si une touche est pressée, il nous faut savoir laquelle en particulier afin d'afficher le bon message.

SDL_Poll_event() met les données de SDL_KEYDOWN dans la structure d'événement comme étant un SDL_KeyboardEvent nommé key

Image non disponible

Et dans key il y a une structure keysym

Image non disponible

Et dans la structure keysym se trouve enfin le SDL_Key nommé sym, qui est la touche qui a été pressée.

Si la touche flèche-haut a été pressée, le sym sera SDLK_UP et nous afficherons le message up, si c'est la touche fleche-bas qui est pressée, le sym sera SDLK_DOWN et nous afficherons le message down, etc...
Dans la source, nous vérifions aussi si l'utilisateur ferme la fenêtre avec le X et quittons le programme proprement le cas échéant.
Il vous faudra toujours prévoir une façon de quitter votre application proprement.

Pour revenir à l'affichage de nos messages, nous allons les afficher au centre de notre fenêtre :

affichage message s'il y en a un
 //Si un message a besoin d'être affiché
if( message != NULL ) {
    //Application de l'image sur la l'écran
    apply_surface( 0, 0, background, screen );
    apply_surface( ( SCREEN_WIDTH - message->w ) / 2, ( SCREEN_HEIGHT - message->h ) / 2, message, screen );

    //On met à NULL le pointeur vers la surface message
    message = NULL;
}
//Mise à jour de l'écran
if( SDL_Flip( screen ) == -1 ) {
    return 1;
}

Quand la surface message pointe sur rien , elle sera NULL et rien ne sera blitté.
Quand la surface message est non nulle, on applique les surfaces background et le message centré sur l'écran.
Nous appliquons de nouveau la surface background car quand on met à jour l'écran, il faut tout "reconstruire", c'est à dire qu'il nous faut remettre la surface du fond

La façon de centrer une surface est premièrement de soustraire les hauteur/largeur de la surface qu'on veut blitter à la hauteur/largeur de l'écran sur lequel on veut blitter la surface.
Ainsi, quand la surface est centrée le pas entre les deux côtés est égal, vous divisez la distance restante en deux moitiés égales

Il ne nous reste plus qu'à mettre la surface à NULL et de mettre à jour l'écran.

Si vous appuyez sur droite, vous obtiendrez ceci :

Image non disponible

IV-C. Statut d'une touche

Dans cette partie, nous allons vérifier si une touche est pressée sans surveiller dans les événements principaux.

evenements
//Tant que l'utilisateur ne quitte pas
while( quit == false ) {
    //Tant qu'il y a un événement à manipuler
    while( SDL_PollEvent( &event ) ) {
        //Si l'utilisateur ferme la fenêtre avec le X
        if( event.type == SDL_QUIT ) {
            //On quitte le programme
            quit = true; }
        }

Comme vous pouvez le constater, Bien que nous cherchons à afficher un message basé sur la touche pressé comme dans la partie précédente, nous ne vérifions aucun événement lié aux touches.
A la place nous allons utiliser SDL_GetKeyState() :

SDL_GetKeyState
//Récupération du keystates
Uint8 *keystates = SDL_GetKeyState( NULL );

Ce que fait SDL_GetKeyState() est de nous donner un tableau avec l'état des touches.
Ce tableau est une liste de toutes les touches en précisant si une touche est pressée ou non, comme ceci :

Image non disponible

Maintenant nous pouvons donc dire quelle touche est pressée ou non.

Juste pour information, l'argument que nous donnons à SDL_GetKeyState() obtient le nombre de clefs disponible.
Puisque nous ne nous inquiétons pas de combien de combien de touches il y a, nous le plaçons juste à la NULL.

Maintenant il nous faut afficher les messages, pour cela de simples conditions if suffisent :

keystates
//Si Haut est pressé
if( keystates[ SDLK_UP ] ) {
    apply_surface( ( SCREEN_WIDTH - up->w ) / 2, ( SCREEN_HEIGHT / 2 - up->h ) / 2, up, screen );
}
//Si Bas est pressé
if( keystates[ SDLK_DOWN ] ) {
    apply_surface( ( SCREEN_WIDTH - down->w ) / 2, ( SCREEN_HEIGHT / 2 - down->h ) / 2 + ( SCREEN_HEIGHT / 2 ), down, screen );
}
//Si Gauche est pressé
if( keystates[ SDLK_LEFT ] ) {
    apply_surface( ( SCREEN_WIDTH / 2 - left->w ) / 2, ( SCREEN_HEIGHT - left->h ) / 2, left, screen );
}
//Si Droite est pressé
if( keystates[ SDLK_RIGHT ] ) {
    apply_surface( ( SCREEN_WIDTH / 2 - right->w ) / 2 + ( SCREEN_WIDTH / 2 ), ( SCREEN_HEIGHT - right->h ) / 2, right, screen );
}
//Mise à jour de l'écran
if( SDL_Flip( screen ) == -1 ) {
    return 1;
}

Voila le résultat de notre petit application lorsqu'on appuie simultanément sur les flèches bas, gauche et droite :

Image non disponible

SDL_GetKeyState() et d'autres fonctions d'état comme SDL_GetModState(), SDL_GetMouseState(), SDL_JoystickGetAxis() et d'autres peuvent être incroyablement utiles.
Apprenez en plus au sujet d'elles dans la référence de l'api SDL, que vous devriez avoir récupéré sur votre ordinateur.

Liens

Version pdf (685 ko - 12 pages)
Télécharger les sources des 2 parties (IV-B et IV-C : 2214 ko)
Télécharger les sources du IV-A (156 ko)
Télécharger les sources du IV-B (1120 ko)
Télécharger les sources du IV-C (1094 ko)


Tutoriel SDL

Chapitre V : Transparence

V. Transparence

La surface SDL possède un élément nommé color key.
Le color key est la couleur que vous ne souhaitez pas afficher lorsque vous blittez une surface.
Ce tutorial a pour but de vous montrer comment s'en servir.

Nous souhaitons par exemple mettre une image d'un personnage sur un paysage.
Prenons ce mignon petit tux assis comme personnage :

Image non disponible

Ce tux, nous souhaitons le poser dans ce paysage :

Image non disponible

Comme vous pouvez le voir, la couleur de fond du tux va nous gêner.
Ce que nous souhaitons obtenir n'est pas l'affichage du tux tel quel avec son fond cyan sur le paysage, mais seulement le tux sans son fond.
Et donc pour cela il nous faut rendre le fond du tux transparent afin d'obtenir comme résultat :

Image non disponible

Il nous faut donc dire que la couleur qu'on ne souhaite pas voir pour l'image du tux est le cyan (soit Red = 0, Green = FF et Blue = FF) afin qu'il soit le color key.
Le color key est typiquement mis en place lors du chargement de l'image.

Load_image
SDL_Surface *load_image( std::string filename )
{
    //L'image qui est chargée
    SDL_Surface* loadedImage = NULL;

    //L'image optimisée que nous utiliserons par la suite
    SDL_Surface* optimizedImage = NULL;

    //Chargement de l'image
    loadedImage = IMG_Load( filename.c_str() );

    //Si l'image est chargée
    if( loadedImage != NULL )
    {
        //Création de l'image optimisée
        optimizedImage = SDL_DisplayFormat( loadedImage );

        //Libération de l'ancienne image
        SDL_FreeSurface( loadedImage );

Voici donc la fonction de chargement d'images, rien de changé par rapport à avant, ce qui est intéressant est ce qui va suivre.

SDL_MapRGB
    //Si l'image optimisée créée est bonne
        if( optimizedImage != NULL )
        {
            Uint32 colorkey = SDL_MapRGB( optimizedImage->format, 0, 0xFF, 0xFF );

Après avoir vérifié si l'image a bien été optimisée, nous définissons la couleur dont on souhaite qu'elle devienne le color key (et donc la couleur transparente).
Nous appelons SDL_MapRGB() pour prendre les valeurs de rouge (Red), de vert (Green) et de bleu (Blue) et il nous retourne la valeur du pixel dans le même format que la surface.
Maintenant il est temps d'utiliser cette valeur obtenue afin de rendre transparente la partie qu'on souhaite rendre transparente de l'image (ici le fond).

SDL_SetColorKey
        //On met tous les pixels de couleur R 0, G 0xFF, B 0xFF transparent
        SDL_SetColorKey( optimizedImage, SDL_RLEACCEL | SDL_SRCCOLORKEY, colorkey );

Nous utilisons pour cela la fonction SDL_SetColorKey().
Le premier argument est le pointeur vers la surface que nous allons utiliser pour le color key.

Le second argument englobe les flags utilisés. Ici nous choisissons SDL_RLEACCEL qui permet l'accélération RLE qui nous permet d'être sûr lorsque nous blittons notre surface, il n'y aura pas de ralentissements.
Le flag SDL_SRCCOLORKEY lui nous permet d'être sûr que nous utilisons bien le color key quand on blitte une surface sur une autre.

Le troisième et dernier argument est la couleur que nous posons comme étant le color key.
Comme vous pouvez le voir c'est la couleur mappée juste avant.

Ensuite il nous reste plus qu'à retourner l'image optimisée obtenue.

return
    //On retourne l'image optimisée
    return optimizedImage;
}

Maintenant que nous avons obtenu, normalement, notre surface avec le color key défini, il nous reste plus qu'à poser nos deux surfaces :
le fond en premier puis notre surface avec le color key par dessus.

apply_surface
    //Application des surfaces sur l'écran
    apply_surface( 0, 0, background, screen );
    apply_surface( 270, 190, tux, screen );

    //Mise à jour de l'écran
    if( SDL_Flip( screen ) == -1 )
    {
        return 1;
    }

Vous devriez ainsi obtenir le résultat voulu au début, sans le fond cyan du tux pour gêner.


Tutoriel SDL

VI. Sprites

VI-A. Introduction

Qu'est ce qu'un sprite ?

Un sprite désigne une image qui peut être déplacée par rapport au fond de l'écran.
L'usage des sprites est une technique fondamentale dans bien des jeux vidéo en 2 dimensions (par exemple, Mario, Arkanoid, Pac-Man, etc.).
En général, un sprite est une image rectangulaire qui comprend une couleur transparente.
Lorsque le sprite est affiché à l'écran, tout ce qui est derrière un point du sprite de couleur transparente reste visible et les autres sont masqués. Ainsi, le sprite peut avoir une silhouette bien détaillée qui se découpe bien sur le fond de l'écran.

Un exemple de sprite particulièrement courant : le pointeur de la souris de votre ordinateur personnel.

VI-B. Feuille de sprite

Dans cette partie, nous allons voir comment, à partir d'une feuille de sprites, couper un sprite afin de pouvoir l'utiliser individuellement.

Nous allons utiliser une feuille composée de quatre sprites :

Image non disponible

Afin d'illustrer ce chapitre, nous allons tenter de découper chaque sprite et de les disposer dans chacun des quatre coins de notre fenêtre SDL.
Le résultat final devra ressembler à ceci :

Image non disponible

Comme à chaque fois, nous allons commencer par déclarer nos variables :

variables
Sélectionnez
//Les attributs de l'écran (640 * 480)
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
const int SCREEN_BPP = 32;

//Les attributs de la feuille de sprites (200 * 200)
const int SHEET_WIDTH = 200;
const int SHEET_HEIGHT = 200;

//Les surfaces
SDL_Surface *faces = NULL;
SDL_Surface *screen = NULL;

//La structure d'événements
SDL_Event event;

//Les différentes parties de la feuille de sprites qui vont être blittés
SDL_Rect clip[ 4 ];

Nous utilisons, comme d'habitude, des variables globales pour les attributs de l'écran.
De nouvelles variables globales font leur apparition, ce sont les dimensions de notre feuille de sprites.

Pour les surfaces, il y a comme toujours la surface screen, mais nous avons en plus une surface faces qui va nous permettre de charger et de "manipuler" notre feuille de sprites.

Comme toujours, nous retrouvons notre structure d'événements event.

En dernier, nous avons un tableau de quatre SDL_Rect. Il va garder les positions et les dimensions de chacun des quatre sprites.

Nous allons maintenant légèrement modifier notre fonction apply_surface() afin de pouvoir gérer les sprites qu'on a découpé.
Voyons voir ça de plus près :

apply_surface
Sélectionnez
void apply_surface( int x, int y, SDL_Surface* source, SDL_Surface* destination, SDL_Rect* clip = NULL )
{
    SDL_Rect offset;

    offset.x = x;
    offset.y = y;

    //On blitte la surface
    SDL_BlitSurface( source, clip, destination, &offset );
}

Voici donc notre nouvelle fonction apply_surface().
Le nouvel argument est le SDL_Rect nommé clip qui définit la partie rectangulaire de la surface que nous souhaitons blitter.
Nous mettons par défaut cet argument à NULL afin de permettre l'utilisation de cette fonction comme avant (c'est à dire qu'on a la possibilité de ne pas mettre de cinquième argument).

Nous changeons aussi la façon dont est appelé SDL_BlitSurface().
Nous ne mettons plus le second argument à NULL mais nous y mettons l'argument clip.
Maintenant SDL_BlitSurface() va blitter la région de la surface source définie par clip.
Si l'argument est à NULL, SDL_BlitSurface() va blitter la surface entière de la source.

Passons au découpage de notre feuille de sprites.
Nous allons définir nos quatre régions rectangulaires ainsi :

clipping
Sélectionnez
    //On coupe la partie en haut à gauche (premier sprite)
    clip[ 0 ].x = 0;
    clip[ 0 ].y = 0;
    clip[ 0 ].w = SHEET_WIDTH/2;
    clip[ 0 ].h = SHEET_HEIGHT/2;

    //On coupe la partie en haut à droite (second sprite)
    clip[ 1 ].x = SHEET_WIDTH/2;
    clip[ 1 ].y = 0;
    clip[ 1 ].w = SHEET_WIDTH/2;
    clip[ 1 ].h = SHEET_HEIGHT/2;

    //On coupe la partie en bas à gauche (troisième sprite)
    clip[ 2 ].x = 0;
    clip[ 2 ].y = SHEET_HEIGHT/2;
    clip[ 2 ].w = SHEET_WIDTH/2;
    clip[ 2 ].h = SHEET_HEIGHT/2;

    //On coupe la partie en bas à droite (quatrième sprite)
    clip[ 3 ].x = SHEET_WIDTH/2;
    clip[ 3 ].y = SHEET_HEIGHT/2;
    clip[ 3 ].w = SHEET_WIDTH/2;
    clip[ 3 ].h = SHEET_HEIGHT/2;

Cette partie se fait après avoir tout initialisé.
Nous divisons par 2 car nous avons une feuille de sprites carré possédant quatre sprites (chaque sprite étant de même dimension), deux par ligne et donc il y a aussi deux lignes.
Donc au final, notre découpage définit les régions ainsi :

Image non disponible

Nous séparons donc les sprites individuellement depuis la feuille de sprites.

Commençons par rendre l'écran blanc :

Fill screen
Sélectionnez
    //On remplit l'écran de blanc
    SDL_FillRect( screen, &screen->clip_rect, SDL_MapRGB( screen->format, 0xFF, 0xFF, 0xFF ) );

Ce que fait SDL_FillRect est de prendre la surface dans le premier argument, et de remplir la région dans le second argument avec la couleur du troisième argument.
La région dans le second argument est la surface clip_rect ou la région entière de la surface.

Plaçons maintenant nos sprites dans les quatre coins de notre écran :

application des sprites
Sélectionnez
    //On applique les sprites sur l'écran
    apply_surface( 0, 0, faces, screen, &clip[ 0 ] );
    apply_surface( SCREEN_WIDTH-(SHEET_WIDTH/2), 0, faces, screen, &clip[ 1 ] );
    apply_surface( 0, SCREEN_HEIGHT-(SHEET_HEIGHT/2), faces, screen, &clip[ 2 ] );
    apply_surface( SCREEN_WIDTH-(SHEET_WIDTH/2), SCREEN_HEIGHT-(SHEET_HEIGHT/2), faces, screen, &clip[ 3 ] );

Comme vous pouvez le constater, nous blittons toujours la même surface, la seule différence étant que nous blittons une partie différente de la surface à chaque fois.
Je pense qu'il n'y a pas besoin d'expliquer le reste, si vous avez du mal avec les positions par rapport aux variables globales, changer les valeurs et faîtes les calculs, vous verrez qu'il n'y a rien de sorcier.

Maintenant, quand vous aurez beaucoup d'images que vous souhaitez utiliser, il vous suffira de créer des feuilles de sprites et de blitter la portion que vous souhaitez à chaque fois.
Cela permet aussi de regrouper les images d'un même groupe, par exemple un même sprite sous différents angles de vue, etc... Voici un exemple de feuille de sprite, avec un personnage unique (en deux "couleurs"):

Image non disponible

Cette feuille de sprite permet de simuler le déplacement de ce personnage dans toutes les directions (en ne considérant que quatre directions).

Nous reviendrons sur le déplacement d'un perso dans les parties mouvement (Chapitre X) et animation (Chapitre XII) de ce tutoriel.


Tutoriel SDL

Chapitre VII : Jouer du son

  • VII. Jouer du son
    • VII-A. Première approche
      • VII-A-1. Initialisation audio de SDL
      • VII-A-2. Initilaliser la couche audio
      • VII-A-3. Charger un échantillon au format WAV
      • VII-A-4. Jouer un échantillon audio au format WAV
      • VII-A-5. Convertir des échantillons
    • VII-B. Premier programme avec SDL_mixer
    • VII-C. Jouer les pistes Audio d'un cd
      • VII-C-1. Accéder et utiliser un lecteur de cd-rom
      • VII-C-2. Jouer les pistes d'un cd audio
  • Téléchargements

VII. Jouer du son

VII-A. Première approche

La bibliothèque de base SDL permet de lire facilement les fichiers au format WAV, mais uniquement ceux-ci.
Afin de jouer d'autres formats de son avec SDL, il existe deux extensions :

  • SDL_sound
  • SDL_mixer

Les deux permettent à peu près la même chose.
L'avantage de SDL_mixer sur SDL_sound est le fait de pouvoir facilement jouer plusieurs sons en même temps.

SDL_mixer est un module de plus haut niveau que SDL pour le son, cependant à première vue ce module n'est pas d'une utilisation très simple.
La raison est simple, SDL_mixer utilise deux approches différentes pour travailler sur les fichiers sonores : music et mixer.
En fait dans le premier cas, vous chargez un fichier audio qui occupera le périphérique à lui seul tant qu'il sera actif, ce qui peut-être dérangeant lorsque l'on désire jouer plusieurs sons en même temps.
C'est là que la partie mixer intervient, dans ce cas vous définissez un format (fréquence d'échantillonnage...) pour le périphérique, puis vous pouvez charger et jouer plusieurs sons simultanément sur celui-ci, par le système des canaux.
Attention si le format est trop différent du fichier original, vous obtiendrez des effets étranges et nuisibles.

Donc pour résumer, on va faire un peu d'anglais et lire le Readme de SDL_mixer : "Due to popular demand, here is a simple multi-channel audio mixer. It supports 8 channels of 16 bit stereo audio, plus a single channel of music, mixed by the popular MikMod MOD, Timidity MIDI and SMPEG MP3 libraries".
En gros cela dit que SDL_mixer supporte 8 canaux sur 16 bits et un canal pour le type music.
Donc on peut mélanger simultanément mixer et music.

Nous allons dans cette partie apprendre à utiliser les fonctions définies dans SDL_audio.h.

VII-A-1. Initialisation audio de SDL

Afin d'initialiser correctement la bibliothéque, il faudra tout d'abord veiller à appeler la fonction SDL_Init() avec le flag SDL_INIT_AUDIO :

SDL_Init
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);

Ceci est fait bien évidemment lorsque nous appelons SDL_Init avec le flag SDL_INIT_EVERYTHING qui nous permet d'initialiser tous les sous-systèmes de SDL.

L'initialisation de SDL avec le flag SDL_INIT_AUDIO a pour effet de démarrer le thread chargé de la gestion audio.
Pour ouvrir le périphérique proprement dit, il faut recourir à la fonction :

SDL_OpenAudio(SDL_AudioSpec *desired, SDL_AudioSpec *obtained);

Cette fonction prend en argument deux structures SDL_AudioSpec qui permettent de décrire le format audio désiré.
La structure pointée par desired doit être remplie de la manière suivante :

SDL_AudioSpec
typedef struct {
    int freq;
    Uint16 format;
    Uint8  channels;
    Uint8  silence;
    Uint16 samples;
    Uint32 size;
    void (*callback)(void *userdata, Uint8 *stream, int len);
    void  *userdata;
} SDL_AudioSpec;

- freq indique la fréquence en échantillons par seconde (par exemple, 44100 pour une qualité CD).

- format indique le format des échantillons, parmi les valeurs décrites ci-dessous :

  • AUDIO_U8 : Echantillons 8 bits non signés,
  • AUDIO_S8 : Echantillons 8 bits signés,
  • AUDIO_U16LSB : Echantillons 16 bits non signés (non encore supportés, car peu répandus),
  • AUDIO_S16LSB : Echantillons 16 bits signés (les plus répandus),
  • AUDIO_U16MSB, AUDIO_S16MSB : idem que ci-dessus pour les architectures big endian (pratiquement tout ce qui n'est pas Intel, comme les PowerPC ou les Alpha).

- channels le nombre de canaux : 1 pour un son mono, 2 pour la stéréo.

- silence la valeur de silence pour le tampon (attention, il est calculé par SDL, vous ne devez pas le remplir !).

- sample la taille du tampon audio ; il ne s'agit pas de la taille maximale des échantillons que vous pourrez charger par la suite, mais plutôt de la taille du tampon utilisé de manière interne. D'une façon générale, un grand tampon demandera moins de temps CPU mais introduira un décalage plus grand entre l'arrivée des échantillons et la sortie sur les haut-parleurs. D'un autre côté une taille de tampon trop petite peut entraîner des déformations du signal audio sur les machines peu puissantes, qui auraient du mal à suivre.

- size la taille en octets du tampon, calculée automatiquement par SLD_OpenAudio().

- *callback : l'adresse d'une fonction appelée automatiquement par SDL à chaque fois que le tampon est vide. Cette fonction est chargée de remplir à nouveau le tampon.

- *userdata : cette valeur sera passée comme premier argument de la fonction callback décrite ci-dessus. Sa signification est complètement laissée à la charge de l'utilisateur.

Il est possible que le format audio obtenu soit différent de celui demandé, notamment en cas de contraintes matérielles.
C'est pourquoi il faut toujours vérifier le contenu de la structure pointée par obtained qui décrit les caractéristiques du format audio effectivement alloué.
Il est également possible de forcer un format audio, en appelant la fonction avec un second argument NULL.
Dans ce cas, SDL fera son possible pour émuler le format audio en fonction des contraintes matérielles, si cela s'avère nécessaire.

Il est important de retenir que l'architecture audio de SDL repose sur des fonctions callbacks.
Il n'y a pas de fonction simple pour jouer un son dans l'API (bien que cela puisse être implanté relativement aisément).
Au lieu de cela, il faut écrire une fonction chargée de fournir à la demande de SDL les échantillons du son, par tranche correspondant à la taille du tampon audio alloué (le champ samples de la structure SDL_AudioSpec)...

Pour permettre à l'utilisateur d'initialiser correctement toutes les structures avant que SDL ne se mette à appeler des fonctions callback, l'appel à SDL_OpenAudio() met initialement SDL en mode "pause".
La restitution et donc le remplissage des tampons audio ne commencent que lorsque le programme appelle explicitement SDL_PauseAudio(0), ce qui a pour effet de sortir du mode pause.

Il est possible d'utiliser les fonctions SDL_LockAudio() et SDL_UnlockAudio() pour garantir que la fonction callback n'est pas appelée dans la thread audio (exclusion mutuelle).
Cela peut être utile si vous devez fournir des données audio depuis une thread séparée (i.e. en dehors de la fonction callback qui est toujours exécutée dans la thread audio de SDL)...

VII-A-2. Initilaliser la couche audio

Pour initialiser la couche audio de SDL, il va vous falloir renseigner les propriétés de l'objet SDL_AudioSpec vu ci-dessus.

Créons donc une fonction d'initialisation de la couche sonore :

audio_Init
SDL_AudioSpec audioSortie;
void audioCallback(void *udata, Uint8 *stream, int len);

int audio_Init(void)
{
    // Définition des propriétés audio
    audioSortie.freq = 22050;
    audioSortie.format = AUDIO_S16;
    audioSortie.channels = 2;
    audioSortie.samples = 1024;
    audioSortie.callback = audioCallback;
    audioSortie.userdata = NULL;

    // Initialisation de la couche audio
    if (SDL_OpenAudio(&audio, NULL) < 0)
    {
        fprintf(stderr, "Erreur d'ouverture audio: %s\n", SDL_GetError());
        return (-1);
    }

    return 0;
}

On commence par déclarer l'objet SDL_AudioSpec qui servira de conteneur à toutes les propriétés utilisées pour le son.
On déclare également une fonction dite "callback" (nous verrons cette partie plus en détail par la suite), après quoi on attaque la fonction d'initialisation.
Dans cette fonction on se contente de définir les propriétés audio de l'objet audioSortie, puis d'initialiser la couche audio avec SDL_OpenAudio() qui prend en argument les spécifications audio désirées et une adresse vers celles réellement allouées (nous n'utiliserons pas ce dernier argument ici).

Les différentes propriétés de audioSortie possibles sont expliquées dans la partie 1 ci-dessus.

VII-A-3. Charger un échantillon au format WAV

SDL fournit une fonction pour charger un fichier son au format WAV en mémoire.
Actuellement, seuls les fichiers WAV de base sont supportés (données brutes ou au format compressé ADPCM).

Voici la fonction en question :

SDL_AudioSpec *SDL_LoadWAV(const char *file, SDL_AudioSpec *spec, Uint8 **audio_buf, Uint32 *audio_len);

Son utilisation est la suivante :

file est le chemin d'accès du fichier à charger.
On fournit l'adresse d'une structure SDL_AudioSpec qui sera remplie par la fonction avec les données décrivant le format du fichier chargé (cette adresse sera également retournée par la fonction).
La fonction se charge d'allouer un tampon en mémoire et d'y charger le son.

Les arguments audio_buf et audio_len sont retournés par SDL, afin d'indiquer la longueur et l'adresse dudit tampon.

En cas d'erreur, la fonction renvoie NULL.

Le tampon alloué doit, par la suite, être explicitement libéré par l'utilisateur à l'aide de la fonction SDL_FreeWAV().

Voici un exemple de chargement d'échantillon au format WAV :

chargement WAV
SDL_AudioSpec audioBufferSpec;
Uint8 *audioBuffer;
Uint32 audioBufferLen;

// A inclure dans votre code pour charger un fichier sonore dans audioBuffer
if(!SDL_LoadWAV("essai.wav", &audioBufferSpec,
    &audioBuffer, &audioBufferLen))
{
    printf("Erreur lors du chargement du fichier WAV.\n");
    return 1;
}
    ...
SDL_FreeWAV(&audioBuffer);

Dans ce code nous utilisons un tampon Uint8 *audioBuffer et sa taille Uint32 audioBufferLen.

Maintenant que nous savons charger un fichier WAV, il va nous falloir apprendre à le jouer, ceci est le sujet de la prochaine partie.

VII-A-4. Jouer un échantillon audio au format WAV

La fonction callback, que nous avons décrite légérement plus haut, est la fonction qui est appellée à chaque cycle de votre programme pour remplir le buffer audio.
En résumé, elle contient toutes les instructions à effectuer à chaque passage dans votre code pour gérer le son.

Une fois le son initialisé avec le code de la partie ci-dessus, et un fichier WAV chargé dans le buffer audio, nous allons utiliser le code ci-dessous pour lire le fichier :

audioCallBack
Uint32 audioLen, audioPos;

void audioCallBack(void *udata, Uint8 *stream, int len)
{
    // On ne lit que s'il reste des données à jouer
    if ( audioLen == 0 )
        return;

    // Remise à zéro du tampon de sortie
    memset(stream, 0, len);

    // Lecture du buffer audio
    if (audioPos < audioBufferSpec.len) {
        if (audioPos+len > audioBufferSpec.len)
            len = audioBufferSpec.len = audioPos;
        SDL_MixAudio(stream, audioBuffer + audioPos,
            len, SDL_MIX_MAXVOLUME);
        audioPos += len;
    }

    // Décrémentation de ce qu'il reste à lire
    audioLen -= len;
}

A savoir tout d'abord, la fonction SDL_MixAudio() est la fonction qui permet de lire vos buffers audios.
Ici, audioLen correspond à la longueur de piste restante à jouer (s'il ne reste rien à jouer on quitte).
stream correspond au tampon audio de sortie (ce qui sera envoyé à la carte son en gros) et len la taille du tampon.
On remet ce tampon à zéro avant de le remplir avec les données sonores à jouer.
En effet, à chaque cycle de votre programme seul un octet (Uint8) est ici envoyé au matériel audio, et la suite de ces octets joués par la carte formera le son entendu par l'utilisateur.
Donc, si audioLen est inférieure à la longueur du buffer à jouer on envoie le son (si ce n'est pas le cas il y a un problème), et ce avec la fonction SDL_MixAudio().

Cette fonction prend en argument le tampon de sortie, l'octet à envoyer (audioBuffer est un pointeur sur un Uint8, décalé de audioPos pour avoir l'octet actuel à jouer), la longueur du buffer et le volume.
Ce dernier est compris entre 0 et SDL_MIX_MAXVOLUME, et vous pourrez le faire varier, par exemple, en pourcentage :

75% du volume = (0.75 * SDL_MIX_MAXVOLUME)

Une fois le son joué on incrémente la position dans le buffer audio de la taille de la séquence jouée (qui est celle du tampon).
Tout ceci est assez complexe c'est vrai mais dites vous que ça reste plus simple que du directMusic.
Une fois que le buffer audio est lu, on décrémente la longueur d'échantillon à jouer de la longueur jouée dernièrement.
Tout cela est bien joli, on a maintenant un module de son initialisé, un fichier WAV chargé et la fonction adéquate pour lire ce fichier, il nous reste donc à lancer la lecture.

lecture
// Lancement de la lecture
SDL_PauseAudio(0);

// Attendre que la lecture du son soit terminée
while ( audioLen > 0 )
    SDL_Delay(100);

// Fermeture du module
SDL_CloseAudio();

SDL_PauseAudio() est la fonction servant à mettre ou à enlever la pause. En effet, de base le son est sur pause, passer 0 en argument à cette fonction relance la lecture, 1 réenclenchant la pause.
N'oubliez pas de terminer le processus audio par SDL_CloseAudio() à la fin de votre code.

VII-A-5. Convertir des échantillons

L'un des aspects les plus puissants de la partie audio de SDL est la possibilité de convertir aisément des échantillons d'un format à l'autre, par l'intermédiaire de blocs de conversion audio.
Concrètement, un bloc de conversion audio est un objet SDL_AudioCVT, dont la structure est la suivante :

SDL_AudioCVT
typedef struct SDL_AudioCVT {
         int needed;        // Valeur non-nulle si la conversion est possible
         Uint16 src_format; // Format audio source
         Uint16 dst_format; // Format audio cible
         double rate_incr;  // Rapport de conversion pour la fréquence
         Uint8 *buf;        // Tampon contenant les données audio
         int    len;        // Longueur du tampon d'origine ci-dessus
         int    len_cvt;    // Longueur du tampon de destination
         int    len_mult;   // Coefficient: le tampon doit avoir une taille de len*len_mult
         double len_ratio;  // Rapport de taille entre le tampon original et le nouveau
         void (*filters[10])(struct SDL_AudioCVT *cvt, Uint16 format);
         int filter_index;  // Filtre de conversion en cours
 } SDL_AudioCVT;

Rassurez-vous, une bonne partie des champs de cette structure est remplie pour vous par SDL.
Connaissant les caractéristiques des formats d'origine et de destination, il faut faire appel à la fonction SDL_BuildAudioCVT() afin de préparer le bloc de conversion :

int SDL_BuildAudioCVT(SDL_AudioCVT *cvt, Uint16 src_format, Uint8 src_channels,
                      int src_rate, Uint16 dst_format, Uint8 dst_channels, int dst_rate);

Son utilisation est simple :

On fournit l'adresse d'un objet SDL_AudioCVT non initialisé, ainsi que les caractéristiques des formats audio : format (voir les constantes définies dans la partie 1), nombre de canaux (1 ou 2 pour mono ou stéréo) et fréquence d'échantillonnage.
La fonction renvoie une valeur nulle, si tout s'est bien déroulé.

Le bloc de conversion ainsi obtenu n'est cependant pas complet.
Avant de pouvoir effectuer la conversion par le biais de la fonction SDL_ConvertAudio(), il faut remplir les champs buf et len de la structure.
Attention, la taille effective du tampon est souvent supérieure à la taille des données d'origine, SDL effectuant la conversion in situ.
La marche à suivre est donc d'allouer un tampon (par exemple, à l'aide de malloc) d'une taille de len (la taille des données d'origine) multipliée par len_mult (fourni par SDL_BuildAudioCVT), puis de charger les données au début du tampon ainsi alloué.
Il est à noter que la valeur de len doit bien être la taille des données d'origine, sans tenir compte du facteur len_mult.

La conversion des données proprement dite est ensuite fort simple.
Il suffit de passer l'objet SDL_AudioCVT ainsi construit à la fonction SDL_ConvertAudio().

Maintenant vous souhaitez pouvoir jouer des échantillons sonore d'un autre format, nous allons voir cela dans la partie suivante en utilisant l'extension SDL : SDL_mixer

VII-B. Premier programme avec SDL_mixer

Comme nous l'avons vu, SDL_mixer est une extension de SDL permettant une utilisation plus aisé des fichiers sonores ainsi que la lecture de formats audio autre que WAV.

Vous pouvez télécharger SDL_mixer ici

L'installation est très simple, n'oubliez pas de rajouter la librairie SDL_mixer dans votre linker.

Nous apprendrons à jouer des effets sonores, jouer une musique et avoir la possibilité de la mettre en pause voir de la stopper en utilisant SDL_mixer.

Au final nous aurons quelquechose de ce genre :

Image non disponible

Commençons notre programme par la déclaration de nos variables :

music variables
//La musique qui sera jouée
Mix_Music *music = NULL;

//Les effets sonores que nous allons utiliser
Mix_Chunk *scratch = NULL;
Mix_Chunk *high = NULL;
Mix_Chunk *med = NULL;
Mix_Chunk *low = NULL;

Voici donc les nouvelle données avec lesquelles nous allons travailler.

Mix_Music est le type de données pour la musique et Mix_Chunk celui pour les effets sonores.

Nous allons maintenant retoucher notre fonction d'initialisation init() afin d'initialiser les fonctions audio de SDL_mixer.

init
bool init()
{
    //Initialisation de tous les sous-systèmes de SDL
    if( SDL_Init( SDL_INIT_EVERYTHING ) == -1 )
    {
        return false;
    }

    //Mise en place de l'écran
    screen = SDL_SetVideoMode( SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP, SDL_SWSURFACE );

    //S'il y a eu une erreur lors de la mise en place d el'écran
    if( screen == NULL )
    {
        return false;
    }

    //Initialisation de SDL_TTF
    if( TTF_Init() == -1 )
    {
        return false;
    }

    //Initialisation de SDL_mixer
    if( Mix_OpenAudio( 22050, MIX_DEFAULT_FORMAT, 2, 4096 ) == -1 )
    {
        return false;
    }

    //Mise en place de la barre caption
    SDL_WM_SetCaption( "Musique", NULL );

    //Si tout s'est bien passé
    return true;
}

Comme vous le voyez, nous appelons Mix_OpenAudio() pour initialiser les fonctions audio de SDL_mixer.

Le premier argument de Mix_OpenAudio() est la fréquence du son que nous allons utiliser.
Le second argument est le format du son utilisé que nous mettons par défaut (MIX_DEFAULT_FORMAT).
Le troisième argument est le nombre de channels que nous souhaitons utiliser (ici 2 : stéréo).
Le dernier argument est une taille d'échantillon, que nous mettons à 4096.

Après avoir changer notre fonction init(), il va nous falloir changer notre fonction load_files() afin de charger la musique et les effets sonores.

load_files
bool load_files()
{
    //Chargement du fond
    background = load_image( "background.png" );

    //Ouverture du Font
    font = TTF_OpenFont( "CaslonBold.ttf", 30 );

    //S'il y a eu un problème au chargement du fond
    if( background == NULL )
    {
        return false;
    }

    //S'il y a eu un problème à l'ouverture du Font
    if( font == NULL )
    {
        return false;
    }

    //Chargement de la musique
    music = Mix_LoadMUS( "beat.wav" );

    //S'il y a eu une erreur au chargement de la musique
    if( music == NULL )
    {
        return false;
    }

    //Chargement des effets sonores
    scratch = Mix_LoadWAV( "scratch.wav" );
    high = Mix_LoadWAV( "high.wav" );;
    med = Mix_LoadWAV( "medium.wav" );;
    low = Mix_LoadWAV( "low.wav" );;

    //S'il y a eu un problème au chargement des effets sonore
    if( ( scratch == NULL ) || ( high == NULL ) || ( med == NULL ) || ( low == NULL ) )
    {
        return false;
    }

    //Si tout s'est bien chargé
    return true;
}

Pour charger la musique, nous utilisons Mix_LoadMUS().
Mix_LoadMUS() prend le nom du fichier de musique et retourne les données de la musique.
La fonction retourne NULL s'il y a une erreur.

Pour charger les effets sonores, nous utilisons Mix_LoadWAV().
Cette fonction charge le fichier son (*.wav) dont le nom est passé en argument et retourne une donnée de type Mix_Chunk ou NULL s'il y a une erreur.

Il nous reste à changer notre fonction de nettoyage et nous pourrons passer au main.

nettoyage
void clean_up()
{
    //Libération des surfaces
    SDL_FreeSurface( background );

    //Libération des effets sonores
    Mix_FreeChunk( scratch );
    Mix_FreeChunk( high );
    Mix_FreeChunk( med );
    Mix_FreeChunk( low );

    //Libération de la musique
    Mix_FreeMusic( music );

    //Fermeture du Font
    TTF_CloseFont( font );

    //On quitte SDL_mixer
    Mix_CloseAudio();

    //On quitte SDL_ttf
    TTF_Quit();

    //On quitte SDL
    SDL_Quit();
}

Nous faisons appel à Mix_FreeChunk() pour se débarrasser des effets sonores et Mix_FreeMusic() pour libérer la musique.

Ensuite, une fois que nous en avons fini avec SDL_mixer, nous appelons Mix_CloseAudio().

Dans notre fonction main, après avoir initialisé, chargé les fichiers, que le fond et les messages ont bien été blittés, nous entrons dans notre boucle principale.

effets sonores
    //Tant que l'utilisateur n'a pas quitté
    while( quit == false )
    {
        //Tant qu'il y a un événement
        while( SDL_PollEvent( &event ) )
        {
            //Si une touche a été pressée
            if( event.type == SDL_KEYDOWN )
            {
                //Si 1 a été préssé
                if( event.key.keysym.sym == SDLK_1 )
                {
                    //On joue l'effet scratch
                    if( Mix_PlayChannel( -1, scratch, 0 ) == -1 )
                    {
                        return 1;
                    }
                }
                //Si 2 a été préssé
                else if( event.key.keysym.sym == SDLK_2 )
                {
                    //On joue l'effet hight
                    if( Mix_PlayChannel( -1, high, 0 ) == -1 )
                    {
                        return 1;
                    }
                }
                //Si 3 a été préssé
                else if( event.key.keysym.sym == SDLK_3 )
                {
                    //On joue l'effet med
                    if( Mix_PlayChannel( -1, med, 0 ) == -1 )
                    {
                        return 1;
                    }
                }
                //Si 4 a été préssé
                else if( event.key.keysym.sym == SDLK_4 )
                {
                    //On joue l'effet low
                    if( Mix_PlayChannel( -1, low, 0 ) == -1 )
                    {
                        return 1;
                    }
                }

Lorsque nous vérifions si une touche a été pressée, nous vérifions dans un premier temps si 1,2,3 ou 4 ont été préssé.
Ce sont les touches pour jouer les effets sonores dans notre programme.

Si une de ces touches a été pressée, nous faisons appel à Mix_PlayChannel() pour jouer l'effet sonore associé à la touche pressée.

Le premier argument de Mix_PlayChannel() est le canal dans lequel nous allons jouer le son.
Vu que nous l'avons mis à -1, Mix_PlayChannel() regardera juste le premier channel valide qu'il trouvera et jouera le son.

Le second argument est le Mix_Chunk que l'on va jouer.
Le troisième argument est le nombre de fois que l'effet sonore sera joué en boucle.
Donc si nous le mettons à 0, l'effet sonore sera joué qu'une seule fois.

Quand il y a un problème, Mix_PlayChannel() retourne -1.

Passons à la musique maintenant :

musique non jouée
                //Si 9 a été pressé
                else if( event.key.keysym.sym == SDLK_9 )
                {
                    //S'il n'y a pas de musique jouée
                    if( Mix_PlayingMusic() == 0 )
                    {
                        //On lance la musique
                        if( Mix_PlayMusic( music, -1 ) == -1 )
                        {
                            return 1;
                        }
                    }

Nous vérifions si la touche 9 a été pressée, ce qui devrait avoir pour effet de jouer/mettre en pause la musique.

Premièremement nous vérifions si la musique est jouée avec Mix_PlayingMusic().
Si la musique n'est pas jouée, nous appelons Mix_PlayMusic() afin de la jouer.

Le premier argument de Mix_PlayMusic() est la musique que nous allons jouer.
Le second argument est le nombre de fois que la musique sera jouée en boucle.
Ici, le fait de mettre -1 aura l'effet de jouer la musique en boucle jusqu'à ce qu'elle soit stoppée manuellement.

S'il y a un problème, Mix_PlayMusic() retourne -1.

Le code suivant va nous permettre de réagir si la musique est déjà jouée :

musique jouée
                    //Si la musique est déjà lancée
                    else
                    {
                        //Si la musique est en pause
                        if( Mix_PausedMusic() == 1 )
                        {
                            //On enlève la pause (la musique repart où elle en était)
                            Mix_ResumeMusic();
                        }
                        //Si la musique est en train de jouer
                        else
                        {
                            //On met en pause la musique
                            Mix_PauseMusic();
                        }
                    }
                }

Nous vérifions si la musique est en pause grâce à Mix_PausedMusic().
Si c'est le cas, nous relançons la musique avec Mix_ResumeMusic() (la musique sera jouée non pas depuis le debut, mais depuis le moment où elle s'est "pausée").
Si la musique n'est pas en pause, alors nous la mettons en pause avec Mix_PauseMusic().

Il nous reste la possibilité de stopper la musique :

stop music
                //Si 0 a été pressé
                else if( event.key.keysym.sym == SDLK_0 )
                {
                    //On stoppe la musique
                    Mix_HaltMusic();
                }

Nous vérifions donc si 0 a été pressé.
Si c'est le cas, la musique est stoppée avec l'utilisation de Mix_HaltMusic().

Pour conclure cette partie, je vous recommande de télécharger et de garder avec vous cette documentation : SDL_mixer documentation

VII-C. Jouer les pistes Audio d'un cd

L'accès aux périphériques manque cruellement dans la plupart des bibliothèques multimedia disponibles.
Qu'à cela ne tienne, SDL vous propose d'accéder sans problème au(x) lecteur(s) de CD-ROM.

VII-C-1. Accéder et utiliser un lecteur de cd-rom

Vous pouvez savoir combien de lecteurs de CD-ROM existent sur les systèmes grâce à la fonction SDL_CDNumDrives(), et accéder à l'un d'entre eux avec SDL_CDOpen().
Le lecteur 0 est le lecteur par défaut sur le système. Le lecteur de CD-ROM peut être ouvert même s'il ne contient aucun disque.
La fonction SDL_CDStatus() permet de déterminer l'état du lecteur de CD-ROM. Après avoir utilisé le lecteur vous devez le fermer avec SDL_CDClose().
Vous pouvez récupérer le nom du lecteur de CD-ROM au sein du système, en utilisant le fonction SDL_CDName().

Voici un exemple de code permettant d'accéder à un lecteur cd-rom :

Accés lecteur cd-rom
    SDL_CD *cdrom;

    if ( SDL_CDNumDrives() > 0 ) {
        cdrom = SDL_CDOpen(0);
        if ( cdrom == NULL ) {
            fprintf(stderr, "Impossible d'ouvrir le lecteur de CD-ROM par défaut %s\n" SDL_GetError());
            return;
        }

        ...

        SDL_CDClose(cdrom);
    }

Comme vous le voyez, c'est plutôt simple.
Nous allons donc voir un programme complet permettant de lister et d'ouvrir les lecteurs de cd-rom.

Pour accéder aux lecteurs CD avec SDL, on passe par les objets SDL_CD.

lister et ouvrir les lecteurs cd-rom
#include <SDL.h>

// Objet CD Audio
SDL_CD *AudioCD = NULL;

int main(void)
{
    int iNbrLecteursCD = 0;

    // Initialisation de SDL_CDROM
    if (SDL_Init(SDL_INIT_CDROM) < 0)
        return 0;

    // Récupération du nombre de lecteurs
    iNbrLecteursCD = SDL_CDNumDrives();
    printf("Nombre de lecteurs CD : %d\n",iNbrLecteursCD);

    // Nom du lecteur 0
    printf("Ouverture du lecteur : '%s'\n",
        SDL_CDName(0));

    // Ouverture du lecteur 0
    AudioCD = SDL_CDOpen(0);
    if (AudioCD == NULL) {
        printf("Echec de l'ouverture du lecteur : %s\n",
            SDL_GetError());
        return 0;
    }

    // Lecture des pistes

    // Fermeture du CD
    if (AudioCD!=NULL)
        SDL_CDClose(AudioCD);

    return 0;
}

Dans la fonction principale, on déclare une variable iNbrLecteursCD qui contiendra le nombre de lecteurs CD disponibles.
A partir de cela vous pourrez, plus tard, faire choisir à l'utilisateur quel lecteur il compte utiliser.

On initialise ensuite la partie CD-ROM de SDL et avec la partie TIMER (que nous allons voir au chapitre suivant).
En effet, la lecture de CD Audio se fait en arrière plan, d'où le besoin d'utiliser les timers (chapitre VIII) et le multithreading (chapitre XVIII).

Ceci fait, on récupère le nombre de lecteurs disponibles dans iNbrLecteursCD grâce à la fonction SDL_CDNumDrives() qui retourne un entier.
On considère ensuite que l'utilisateur a choisi d'ouvrir le lecteur 0.
Tout d'abord on récupère son nom, ce qui est fait facilement via la fonction const char* SDL_CDName() qui prend en paramètre le numéro du lecteur et qui retourne une chaîne de caractères (le nom du lecteur).

Une fois le nom affiché en sortie on ouvre le lecteur CD dans l'objet AudioCD via la fonction SDL_CD SDL_CDOpen() qui prend en paramètre le numéro du lecteur.
Désormais, le lecteur CD est ouvert et on peut accèder aux pistes audio de celui-ci via l'objet AudioCD (que nous verrons plus en détail dans la partie suivante).
Ne pas oublier de fermer le lecteur CD avec la fonction void SDL_CDClose(SDL_CD lecteur_a_fermer) avant la fin de l'exécution du programme.
Maintenant que le lecteur est ouvert et initialisé nous allons apprendre à l'utiliser.

VII-C-2. Jouer les pistes d'un cd audio

Avant de commencer il nous faut connaître la structure d'une piste audio dans SDL, nommée SDL_CDtrack.
Elle comporte les champs suivants :

struct CDtrack
typedef struct {
    Uint8 id;
    Uint8 type;
    Uint32 lenght;
    Uint32 offset;
} SDL_CDtrack;

id contient le numéro de la piste et le type de données (audio, data...).
Dans notre cas il faudra que le type soit égal à SDL_AUDIO_TRACK, dans le cas contraire le type serait SDL_DATA_TRACK.

lenght contient la longueur de la piste en frames.
En gros, une frame fait 2 ko, mais pour obtenir la longueur de la piste en secondes vous pouvez utiliser la constante CD_FPS de SDL.
Ainsi, la durée d'une piste en secondes est égale à la longueur de la piste en frame divisé par la constante CD_FPS (DureePisteSecondes = SDL_CDtrack.lenght/CD_FPS;).

offset contient le numéro de frame où est positionné le début de la piste, ce qui nous servira à déterminer le début et la fin de chaque piste.

Nous allons maintenant tenter de lire une piste audio, mais avant toute chose il faudra s'assurer que le lecteur est prêt à la lire.
Pour cela on utilise les objets de type CDstatus et la fonction CDStatus SDL_CDStatus(SDL_CD *cdrom), qui renvoie les status suivants :

  • CD_TRAYEMPTY : pas de CD dans le lecteur
  • CD_STOPPED : aucune lecture en cours, il y a un CD dans le lecteur
  • CD_PLAYING : lecture en cours
  • CD_PAUSED : lecture en cours et mise en pause
  • CD_ERROR : erreur de lecture du status

Vous pouvez aussi utiliser la macro CD_INDRIVE qui prend comme paramètre le status du lecteur CD renvoyé par SDL_CDStatus.
Elle renvoie 0 si le lecteur est vide.

Désormais nous pouvons lire une piste, voyons voir la fonction suivante :

PlayCDTrack
void PlayCDTrack(SDL_CD *lecteur, int piste)
{
    if (CD_INDRIVE(SDL_CDStatus(lecteur)) == 0)
        printf("Le lecteur %s est vide.\n",SDL_CDName(lecteur->id));

    else if (lecteur->track[piste].type != SDL_AUDIO_TRACK)
        printf("La piste n'est pas une piste audio!\n");

    else if (SDL_CDPlay(lecteur, lecteur->track[piste].offset, lecteur->track[piste].length) == 0)
        printf("Lecture de la piste %d en cours sur le lecteur %s.\n",
            piste, SDL_CDName(lecteur->id));

    else {
        printf("Erreur de lecture: %s",SDL_GetError());
        exit(0);
    }
}

La fonction prend comme argument l'objet SDL_CD à lire et le numéro de la piste à lire.
Pour lire une piste on utilise la fonction SDL_CDPlay() qui prend comme arguments l'objet, l'offset de la piste à lire et sa longueur length (en CD_FPS, pour lire 10 secondes entrez CD_FPS*10 par exemple).

Notez que SDL possède deux fonctions pour jouer un CD.
Vous pouvez soit utiliser SDL_CDPlayTracks() pour jouer une piste précise, soit utiliser SDL_CDPlay() et jouer l'ensemble du CD.

Ici on lit toute la piste en partant du début.

Si le lecteur est vide on affiche un message d'erreur.
Sinon on joue la piste, et si la fonction SDL_CDPlay() ne renvoie pas la valeur 0 (en cas d'erreur) on affiche un message d'erreur avant de quitter.

Je précise que si la longueur de la piste à lire est supérieure à la longueur réelle de cette piste la piste suivante sera jouée.
Par exemple, si la piste 2 dure 2min30 et que vous entrez 2min50 en paramètre length à SDL_CDPlay() 20 secondes de la piste 3 seront jouées.

Il existe également cinq autres fonctions à connaître pour utiliser des CD Audio avec SDL :

  • SDL_CDStop(SDL_CD *lecteur) Stoppe la lecture en cours sur le lecteur passé en argument.
  • SDL_CDPause(SDL_CD *lecteur) Met la lecture du lecteur passé en argument en pause.
  • SDL_CDResume(SDL_CD *lecteur) Reprend la lecture sur le lecteur passé en argument.
  • SDL_CDEject(SDL_CD *lecteur) Ouvre le cadie du lecteur CD.
  • SDL_CDClose(SDL_CD *lecteur) Ferme l'accès et libère les ressources allouées du descripteur de CD passé en argument.

Voilà, vous en savez désormais assez pour commencer à lire des CD AUdio dans vos programmes avec SDL.
N'oubliez pas de libérer les ressources allouées pour vos objets SDL_CD avec la fonction SDL_CDClose().

Téléchargements

Version pdf ( 366 ko - 21 pages)
Télécharger les sources (553 ko)


Tutoriel SDL

Chapitre VIII : Timer

  • VIII. Timer
    • VIII-A. Introduction
    • VIII-B. La classe Timer
    • VIII-C. Les méthodes de la classe Timer
    • VIII-D. Première application : un chronomètre
  • Liens

VIII. Timer

VIII-A. Introduction

Disons que vous avez besoin d'attendre 30 secondes exactement mais que vous n'avez pas de chronomètre.
S'il y a une pendule sur le mur avec une aiguille pour les secondes, vous regardez le point de départ de l'aiguille des secondes :

Image non disponible

Ensuite, il vous suffit d'attendre jusqu'à ce que 30 secondes se soient écoulées depuis le point de départ :

Image non disponible

La classe Timer que je vais vous présenter fonctionne sur le même principe.
SDL a déjà lancé un timer et vous pouvez connaître le temps en millisecondes en utilisant la fonction SDL_GetTicks().
Si vous devez chronométrer quelque chose pendant 1000 millisecondes, vous gardez en mémoire le temps de départ (le temps à partir duquel vous avez commencé à attendre) et vous attendez jusqu'à ce que la différence entre le temps de départ (gardé en mémoire) et le temps courant soit de 1000 millisecondes.

VIII-B. La classe Timer

Voici tout d'abord la classe Timer que nous allons utiliser :

classe Timer
//Le Timer
class Timer
{
    private:
    //Le temps quand le timer est lancé
    int startTicks;

    //Le temps enregistré quand le Timer a été mis en pause
    int pausedTicks;

    //Le status du Timer
    bool paused;
    bool started;

    public:
    //Initialise les variables
    Timer();

    //Les différentes actions du timer
    void start();
    void stop();
    void pause();
    void unpause();

    //recupére le nombre de millisecondes depuis que le timer a été lancé
    //ou récupére le nombre de millisecondes depuis que le timer a été mis en pause
    int get_ticks();

    //Fonctions de vérification du status du timer
    bool is_started();
    bool is_paused();
};

Cette classe possède les variables et les méthodes nécessaires au bon fonctionnement d'un Timer SDL.
Nous allons voir en détail chacune des méthodes qui la composent dans la prochaine section (VIII-C). Avant cela nous allons tout de même voir le constructeur de cette classe :

constructeur
Timer::Timer()
{
    //Initialisation des variables
    startTicks = 0;
    pausedTicks = 0;
    paused = false;
    started = false;
}

Comme vous pouvez le voir, le constructeur sert uniquement à initialiser les différentes variables, rien de plus.

VIII-C. Les méthodes de la classe Timer

Nous allons commencer par la méthode void start() :

start
void Timer::start()
{
    //On demarre le timer
    started = true;

    //On enlève la pause du timer
    paused = false;

    //On récupére le temps courant
    startTicks = SDL_GetTicks();
}

Nous démarrons donc le timer, nous mettons le statut started à true, nous enlevons la pause (même si celle-ci n'était pas activé) et ensuite nous récupérons le temps courant qui sera notre point de départ, que nous sauvegardons dans la variable startTicks.
Voici donc pour la méthode start de la classe Timer, nous allons donc continuer avec la méthode void stop() :

stop
void Timer::stop()
{
    //On stoppe le timer
    started = false;

    //On enlève la pause
    paused = false;
}

Lorsque nous stoppons notre timer, il suffit de mettre le statut started à false et d'enlever la pause.
Toujours simple, nous allons donc voir la méthode int get_ticks() :

get_ticks
int Timer::get_ticks()
{
    //Si le timer est en marche
    if( started == true )
    {
        //Si le timer est en pause
        if( paused == true )
        {
            //On retourne le nombre de ticks quand le timer a été mis en pause
            return pausedTicks;
        }
        else
        {
            //On retourne le temps courant moins le temps quand il a démarré
            return SDL_GetTicks() - startTicks;
        }
    }

    //Si le timer n'est pas en marche
    return 0;
}

Nous voici avec la fonction qui nous permet de récupérer le temps du timer.

Premièrement nous vérifions si le timer est en marche. Si c'est le cas, nous vérifions alors si celui-ci est en pause.

Si le timer est en pause (paused == true), nous retournons le temps que le timer avait lorsqu'il a été mis en pause. Nous verrons plus tard comment mettre en pause ou d'enlever la pause.

Si le timer n'est pas en pause, nous retournons la différence dans le temps depuis que le timer est lancé et le temps courrant.

Donc, si vous avez lancé le timer quand SDL_GetTicks() était à 10 000 et que maintenant SDL_GetTicks() est à 20 000, il retournera 10 000, c'est à dire que 10 secondes sont passés depuis que le timer a été lancé.

Si le timer n'a jamais été lancé, la fonction retourne 0.

Maintenant que nous avons vu une des fonctions les plus importante de notre classe, nous allons voir comment mettre en pause avec la méthode void pause() :

pause
void Timer::pause()
{
    //Si le timer est en marche et pas encore en pause
    if( ( started == true ) && ( paused == false ) )
    {
        //On met en pause le timer
        paused = true;

        //On calcul le pausedTicks
        pausedTicks = SDL_GetTicks() - startTicks;
    }
}

Maintenant si nous souhaitons mettre en pause le timer, nous vérifions d'abord si celui-ci est lancé mais aussi si celui-ci n'est pas déjà en pause.
Si nous pouvons mettre en pause le timer, nous mettons le statut paused à true, et nous gardons en mémoire le temps du timer dans pausedTicks (le temps exact où le timer à été mit en pause).

Donc si vous avez démarré quand SDL_GetTicks() était à 5000, et que vous mettez en pause lorsqu'il est à 7000, pausedTicks sera égal à 2000.

Maintenant que nous savons mettre notre timer en pause, nous allons voir comment enlever la pause grâce à la méthode void unpause() :

unpause
void Timer::unpause()
{
    //Si le timer est en pause
    if( paused == true )
    {
        //on enlève la pause du timer
        paused = false;

        //On remet à zero le startTicks
        startTicks = SDL_GetTicks() - pausedTicks;

        //Remise à zero du pausedTicks
        pausedTicks = 0;
    }
}

Lorsque nous voulons enlever la pause, nous vérifions d'abord si le timer est bien en pause.

Si c'est le cas, nous mettons le statut paused à false, puis nous mettons le temps de départ (symbolisé par la variable de classe startTicks) égal au temps courant moins le temps du timer quand celui-ci a été mis en pause.
Finalement , nous mettons la variable de classe pausedTicks à 0 car je préfère faire ainsi (il n'y a pas de vraies raisons).

Il nous reste à voir les méthodes bool is_started() et bool is_paused() qui nous permettent de vérifier, respectivement, si le timer est démarré et en pause :

is_started
bool Timer::is_started()
{
    return started;
}
is_paused
bool Timer::is_paused()
{
    return paused;
}

Je pense que vous n'avez pas vraiment besoin de moi pour voir comment ces deux méthodes fonctionnent.

VIII-D. Première application : un chronomètre

Afin d'illustrer l'utilisation de notre classe Timer, nous allons "construire" un chronomètre.

Ce chronomètre devra comporter les même fonctions qu'un chronomètre de base, c'est à dire pouvoir le demarrer (on lance le chronomètre), le stopper (on remet le temps à 0), le mettre en pause (garde le temps au moment de la mise en pause) et le relancer/enlever la pause (le chronomètre repart à partir du temps de pause).

Par défaut le chronomètre sera lancé au lancement de l'application.
Voici à quoi celui-ci ressemblera lorsque nous stopperons le chronomètre :

Image non disponible

Et voici à quoi il ressemblera si nous le lançons, puis le mettons en pause au bout de 1,725 secondes :

Image non disponible

Commençons donc notre programme, dans notre fonction main(), après initialisation de SDL et le chargement des différents fichiers dont on aura besoin, nous déclarons un objet de type Timer et nous mettons en place nos surfaces de message :

main
//On construit le timer
    Timer myTimer;

    //On génére la surface de message
    startStop = TTF_RenderText_Solid( font, "Appuyer sur S pour demarrer ou stopper le timer", textColor );
    pause = TTF_RenderText_Solid( font, "Appuyer sur P pour mettre/enlever la pause du timer", textColor );

Avant d'entrer dans la boucle principale, il nous faut lancer le timer :

main
//Mise en route du timer
    myTimer.start();

Ensuite vient donc la boucle principale avec la gestion du clavier, tout d'abord nous allons voir quoi faire quand la touche "s" a été pressée (la touche "s" sert démarrer ou stopper le timer) :

main
//Tant que l'utilisateur n'a pas quitter
    while( quit == false )
    {
        //Tant qu'il y a un événement
        while( SDL_PollEvent( &event ) )
        {
            //Si une touche a été préssée
            if( event.type == SDL_KEYDOWN )
            {
                //Si "s" a été préssé
                if( event.key.keysym.sym == SDLK_s )
                {
                    //Si le timer est en marche
                    if( myTimer.is_started() == true )
                    {
                        //On stoppe le timer
                        myTimer.stop();
                    }
                    else
                    {
                        //Mise en marche du timer
                        myTimer.start();
                    }
                }

Donc, lorsque "s" est pressée, si le timer est en marche, on le stoppe, autrement on le démarre.

Pour la gestion de la pause, la touche associée est la touche "p", voici le code associé à la touche "p" :

main
//Si "p" a été préssé
                if( event.key.keysym.sym == SDLK_p )
                {
                    //Si le timer est en pause
                    if( myTimer.is_paused() == true )
                    {
                        //On enlève la pause
                        myTimer.unpause();
                    }
                    else
                    {
                        //On met le timer en pause
                        myTimer.pause();
                    }
                }

Donc lorsque la touche "p" est pressée, si le timer est en pause, on enlève la pause, autrement on met le timer en pause.

Pour la gestion des événements, il nous reste juste la fermeture de l'application qui se passe comme d'habitude.

nous allons terminer ce tutorial par l'application des différentes surfaces et l'affichage du temps.

Commençons par l'application des surfaces de fond et des messages :

main
//On pose le fond
        apply_surface( 0, 0, background, screen );

        //On pose les surfaces
        apply_surface( ( SCREEN_WIDTH - startStop->w ) / 2, 200, startStop, screen );
        apply_surface( ( SCREEN_WIDTH - pause->w ) / 2, 250, pause, screen );

Si vous avez suivi les tutoriaux précèdent, vous devriez comprendre cette étape sans problèmes.

Voyons maintenant l'affichage du temps :

main
//Une chaîne temporaire
        char time[ 64 ];

        //On convertis time en une chaîne de caractère (on le tronque aussi à trois chiffres après la virgule)
        sprintf( time, "%.3f", (float)myTimer.get_ticks() / 1000 );

Nous créons donc une chaîne temporaire, et nous mettons le temps du timer dans cette chaîne.

Vu que nous voulons le temps en secondes, nous convertissons le temps du timer dans en nombre flottant (float), puis nous divisons par 1000 (1000 millisecondes par secondes).

Maintenant il nous suffit de créer la surface depuis la chaîne qui garde le temps du timer :

main
//mise en place de la surface time en texte
        seconds = TTF_RenderText_Solid( font, time, textColor );

        //On pose la surface
        apply_surface( ( SCREEN_WIDTH - seconds->w ) / 2, 0, seconds, screen );

        //On libère la surface de temps
        SDL_FreeSurface( seconds );

        //Mise à jour de l'écran
        if( SDL_Flip( screen ) == -1 )
        {
            return 1;
        }

La nouvelle surface montrant le temps du timer est posée sur l'écran, puis elle est libérée puisque nous en avons fini avec.
Après ça l'écran est mis à jour, et nous continuons la boucle principale, le temps est reposé puis libéré et ainsi de suite.

J'espère que vous avez compris le concept du timer et que vous entrevoyez les possibilités qu'il peut vous offrir, nous en verrons quelques unes dans les prochains tutoriaux en application avec de nouveaux concepts.


Tutoriel SDL

Chapitre IX : Frame Rate

  • IX. Frame Rate
    • IX-A. Calculer le nombre de Frames par seconde
    • IX-B. Réguler le nombre de Frames par seconde
    • IX-C. Utilisation d'une librairie spécialisée dans le contrôle du taux de rafraîchissement
    • IX-D. Ressources et autres exemples
  • Liens

IX. Frame Rate

Frame Rate veut dire littéralement taux d'images.
C'est le nombre d'images affichées en une seconde.

Plus le nombre est élevé, plus l'animation est fluide (à priori, la sensibilité et la persistance rétinienne impose une cadence d'au moins 24 images par seconde pour imposer une véritable impression de fluidité).

Nous allons voir dans un premier temps comment calculer ce nombre pour ensuite voir comment et pourquoi le réguler dans certains cas.

IX-A. Calculer le nombre de Frames par seconde

Si vous devez savoir à quelle nombre de fps(frames per second) votre jeu peut fonctionner, ou si vous vous demandez juste ce que vous pouvez tirer sur un rendement SDL, savoir comment calculer le nombre de frame par seconde est une compétence très utile.
Cette partie du tutoriel vous enseignera comment construire un simple test de calcul de fps.
Nous aurons quelque chose qui ressemble à ça :

image test maximum fps

Présenté comme dans ce tutoriel, le calcul ne sert pas à grand chose.
Pour le rendre plus intéressant, vous devrez, par vous même, ajouter quelques instructions et autres morceaux de programme que vous souhaitez tester afin de voir comment le nombre de fps évolue et ainsi en tirer les bonnes conclusions.

Nous allons utiliser la classe Timer que nous avons vu dans le tuto précèdent, je ne ferais donc pas de rappels sur cette partie.
Si vous avez des doutes sur le fonctionnement de certaines instructions propres au Timer, reportez vous simplement au chapitre VIII sur les timers.

Commençons donc par la déclaration des variables dont nous aurons besoin :

variables
int frame = 0;

//Timer utilise pour calculer le nombre de frames par seconde
Timer fps;

//Timer utilise pour mettre à jour la barre de caption
Timer update;

Voici donc les trois variables clefs de notre test de calcul du nombre de fps.
La variable frame garde la trace du nombre de frames qui ont été rendu.
La variable fps est le timer qui garde la trace du temps qui a été perdu pendant le rendu.
La variable update est le timer que nous utilisons pour mettre à jour la barre de caption, c'est à dire l'endroit où nous afficherons le nombre de frames par seconde.

Après que tout a été initialisé et chargé, il nous faut mettre en route les timers et entrer dans la boucle principale :

Lancement des timers
//On demarre le timer update
update.start();

//On demarre le timer fps
fps.start();

//Tant que l'utilisateur n'a pas quitte
while( quit == false ) {

Il ne faut pas oublier de lancer les timers.

Par souci de simplicité, nous n'allons nous occuper d'aucun événement à part celui de la fermeture de la fenêtre SDL.

boucle événement
//Tant qu'il y a un événement
        while( SDL_PollEvent( &event ) )
        {
            //Si l'utilisateur a clique sur le X de la fenetre
            if( event.type == SDL_QUIT )
            {
                //On quitte le programme
                quit = true;
            }
        }

Après cette partie, nous appliquons la surface (le fond), mettons à jour la fenêtre, puis incrémentons le compteur frame.

après la boucle événement
//On pose le fond
        apply_surface( 0, 0, image, screen );

        //Mise à jour de l'écran
        if( SDL_Flip( screen ) == -1 )
        {
            return 1;
        }

        //On incremente le compteur de frames
        frame++;

Comme vu sur l'image de présentation de notre programme, le résultat du calcul du nombre de frames par seconde sera affiché sur la barre caption.
Voici le code correspondant :

calcul frame
//Si une seconde est passee depuis la derniere mise à jour de la barre caption
        if( update.get_ticks() > 1000 )
        {
            //Une chaine de caracteres temporaire
            char caption[ 64 ];

            //On calcule le nombre de frames par seconde et on cree la chaine de caracteres
            sprintf( caption, "Frames Par Seconde: %f", (float)frame / ( fps.get_ticks() / 1000.f ) );

            //On remet à zero la barre caption
            SDL_WM_SetCaption( caption, NULL );

            //On relance le timer update
            update.start();
        }

On souhaite ici mettre à jour le calcul toutes les secondes, on vérifie donc si une seconde s'est passée depuis la dernière mise à jour de la barre caption.
Je vous incite à vous amusez à changer cette valeur (par exemple 100 au lieu de 1000 afin de rafraîchir le calcul tout les dixième de seconde) et de voir le résultat.
Afin d'afficher le résultat du calcul, nous avons besoin d'une chaîne de caractères, dans notre code cette chaîne se nomme caption.
Notre calcul est donc ensuite recopié dans cette chaîne.
Le nombre de frames par seconde est calculé en prenant la quantité de frames rendu divisé par le temps mis afin de les rendre (en seconde, c'est pourquoi nous divisons par 1000, le temps étant en millisecondes).
On remet la barre caption à zéro puis nous relançons le timer update afin de permettre à nouveau une vérification correcte.

Voilà qui est fini, vous pouvez maintenant afficher le nombre de frames par seconde sur vos programmes.
Télécharger les sources du IX-A (161 ko)

IX-B. Réguler le nombre de Frames par seconde

La régulation du nombre de frames par seconde est un élément important dans le développement de certains types de jeux vidéo ou d'applications.
En effet, si un jeu tourne trop rapidement, bien plus que le concepteur l'aurait voulu, celui-ci peut devenir totalement injouable.
En régulant le nombre de frames par seconde, vous évitez ainsi que le jeu ou l'application tourne trop rapidement, voir ceux-ci aient des variations de vitesse trop importantes.
Ce chapitre va vous apprendre comment faire ça en vous donnant un exemple concret.

Dans cet exemple, il y aura simplement un message qui traversera la fenêtre SDL de haut en bas comme sur l'image ci-dessous.

image test maximum fps

Le texte descendra à une certaine vitesse, régulière, lorsque le régulateur sera actif.
Vous pourrez alors appuyer sur la touche Entrée afin de désactiver le régulateur et voir ce qui se passe.
Le retour en vitesse réguler est bien évidemment aussi possible.

Tout d'abord nous allons définir notre nombre de frames par seconde "idéal" comme une constante globale.

variable global
//Le nombre de frames par seconde
const int FRAMES_PER_SECOND = 20;

Comme vous pouvez le voir, nous avons choisi 20 pour cet exemple.
Cette valeur a été décidé comme étant très bonne, cependant je ne dis pas qu'elle est la meilleure, tout dépend de votre jeu ou de votre application.
Encore une fois, je vous incite à changer cette valeur afin de trouver celle qui vous convient.

Nous allons définir encore quelques variables, cette fois-ci dans notre main :

main variables
int frame = 0;

//Ce qui va nous servir a activer/desactiver le regulateur
bool cap = true;

//Le regulateur
Timer fps;

frame sera notre compteur de frame, ce qui sera important pour savoir où poser notre surface message.
cap sera notre variable booléenne qui nous dira si le régulateur est activé ou non.
Enfin, fps sera notre timer régulateur.

Après initialisation de SDL et le chargement des fichiers, nous allons créer notre message qui traversera notre écran.

surface message
//Generation de la surface message
message = TTF_RenderText_Solid( font, "Appuyer sur Entrée sur mettre sur le regulateur sur OFF", textColor );

Au départ, la variable cap étant à true, nous gênérons le bon message. Ensuite nous allons entrer dans la boucle principale.
Au début de chaque frame, nous devons lancer notre timer fps.

lancement fps
//Tant que l'utilisateur n'a pas quitte
while( quit == false )
{
    //On demarre le timer fps
    fps.start();

Ce qui va suivre est assez simple.
Nous allons vérifier si la touche entrée à été pressée et si c'est le cas, nous allons changer le statut de la variable cap.
Ensuite, nous allons vérifier la valeur de cette variable cap afin de générer le bon message traversant la fenêtre.

generation message
//Tant qu'il y a un evenement
        while( SDL_PollEvent( &event ) )
        {
            //Si une touche est pressee
            if( event.type == SDL_KEYDOWN )
            {
                //Si la touche Entrée est pressee
                if( event.key.keysym.sym == SDLK_RETURN )
                {
                    //On tourne le regulateur sur on/off
                    cap = ( !cap );

                    if (cap == true) {
                        //Generation de la surface message
                        message = TTF_RenderText_Solid( font, "Appuyer sur Entrée sur mettre sur le regulateur sur OFF", textColor );
                    }
                    else {
                        //Generation de la surface message
                        message = TTF_RenderText_Solid( font, "Appuyer sur Entrée sur mettre sur le regulateur sur ON", textColor );
                    }
                }
            }

            //Si l'utilisateur a clique sur le X de la fenetre
            else if( event.type == SDL_QUIT )
            {
                //On quitte the programme
                quit = true;
            }
        }

Nous vérifions comme d'habitude aussi la fermeture de la fenêtre SDL par le bouton 'X' de la fenêtre.

Ensuite il nous faut poser la surface message sur l'écran.

surface message
//On pose le message
apply_surface( ( SCREEN_WIDTH - message->w) / 2,( ( SCREEN_HEIGHT + message->h * 2) / FRAMES_PER_SECOND) *
         ( frame % FRAMES_PER_SECOND ) - message->h, message, screen );

Ne vous inquiétez pas sur tout ce code afin de simplement poser le message au bon endroit sur l'écran.
C'est juste une façon plus courte de faire ainsi :

if( frame % FRAMES_PER_SECOND == 0 )
{
//poser ici
}
if( frame % FRAMES_PER_SECOND == 1 )
{
//poser ici
}

etc, etc.

Ensuite, il nous faut mettre à jour l'écran et incrémenter le compteur de frames.

maj écran + incrementer frame
//Mise à jour de l'ecran
        if( SDL_Flip( screen ) == -1 )
        {
            return EXIT_FAILURE;
        }

        //On incremente le compteur de frames
        frame++;

Il nous reste encore une chose à faire avant de finir :
La partie de code nous permettant de réguler le nombre de frames par seconde.

maj écran + incrementer frame
//Si nous voulons reguler le frame rate
        if( cap == true )
        {
            //Tant que le timer fps n'est pas assez haut
            while( fps.get_ticks() < 1000 / FRAMES_PER_SECOND )
            {
                //On attend...
            }
        }

Quand nous démarrons une frame, nous démarrons un timer pour garder la trace du temps mit afin de rendre cette frame.
Afin que ce programme n'aille pas trop vite, chaque frame doit mettre une certaine quantité de temps.
Lorsque 20 frames ont été montrées par seconde, chaque frame doit mettre pas moins que 1/20ème d'une seconde.
Si le frame rate est à 60fps, chaque frame doit donc mettre pas moins de 1/60ème d'une seconde.

Finalement, ce que cette partie de code effectue est une attente de 1/20ème de seconde.

Télécharger les sources du IX-B (361 ko)
Version pdf (338 ko - 11 pages)

IX-C. Utilisation d'une librairie spécialisée dans le contrôle du taux de rafraîchissement

Il existe en effet une librairie SDL qui permet de contrôler le taux de rafraîchissement très simplement.
Cette librairie se nomme SDL_gfx
Les composants actuels de cette librairie sont :

  • SDL_gfxPrimitves.h qui sont des primitives graphique
  • SDL_rotozoom.h qui permet d'effectuer des zooms et des rotations
  • SDL_imageFilter.h qui contient des filtres d'images
  • SDL_framerate.h qui nous intéresse car c'est cette partie qui permet de contrôler le taux de rafraîchissement

Pour voir comment utiliser SDL_framerate.h, je vous conseille de suivre le tutoriel de fearyourself qui y consacre une partie ici

IX-D. Ressources et autres exemples

vitesse d'affichage - FAQ SDL
Programmation d'un Pong en SDL/OpenGL par Jean Christophe Beyler
Ce dernier nous montres ici la gestion de la fréquence d'affichage dans un programme plus complexe ( un Pong ).
Je vous invite à lire entièrement son tuto car j'ai eu plaisir à le lire.


Tutoriel SDL

Chapitre X : Mouvement

  • X. Mouvement
    • X-1. La classe point
    • X-2. Mouvements du point
    • X-3. Affichage du mouvement
  • Téléchargements

X. Mouvement

Il est maintenant temps de faire un programme un petit peu plus interactif.
Ce tutoriel a pour but de couvrir les concepts de base du mouvement d'un objet sur un écran.

X-1. La classe point

Afin d'illustrer ce tutoriel, j'ai décidé d'utiliser comme objet à déplacer un simple point :

Ce point est un objet comme un autre, j'aurais très bien pu choisir un bonhomme ou une voiture...

Nous allons maintenant construire notre classe point.
Afin d'afficher notre objet point sur l'écran, nous allons avoir besoin de ses coordonnées et d'une méthode pour afficher notre image.
J'ai aussi décidé que le point se déplacera grâce à la pression de certaines touches.
Afin de déplacer notre objet point, il nous faut donc récupérer aussi les événements clavier (pression des touches associés aux déplacements).
Nous avons donc aussi besoin d'une méthode qui nous permet de récupérer ces événements qui affectent notre point.

Voila donc à quoi va ressembler notre classe point :

classe Point
class Point
{
    private:
    //Les coordonnées x et y du point
    int x, y;

    //La vitesse du point
    int xVel, yVel;

    public:
    //Initialisation des variables
    Point();

    //Recupere la touche pressee et ajuste la vitesse du point
    void handle_input();

    //Montre le point sur l'ecran
    void show();
};

Comme vous le voyez, il n'y a rien de compliqué dans notre classe point.
Vous pouvez aussi tout aussi bien appeler cette classe bonhomme et charger une image de bonhomme au lieu de notre image de point, cela marche tout aussi bien.
Je donnerai à la fin de ce tutoriel, en plus des sources pour déplacer le point sur l'écran, des sources pour déplacer un personnage sur de l'herbe.

Revenons à notre classe Point.
Nous avons donc les coordonnées x et y en variable privée ainsi que les vitesses xVel et yVel qui nous permettrons de déplacer le point comme vous allez le voir dans la prochaine partie.
Le constructeur nous sert à initialiser les variables, la méthode handle_input() à récupérer les événements et la méthode show() à afficher notre objet.

X-2. Mouvements du point

Avant de déplacer notre point, il nous faut le placer.
Le constructeur nous permet d'initialiser la position du point sur l'écran ainsi que ses vitesses de déplacement horizontales et verticales (xVel et yVel).

constructeur
Point::Point()
{
    //Initialisation des coordonnees
    x = 0;
    y = 0;

    //Initialisation de la vitesse
    xVel = 0;
    yVel = 0;
}

Comme vous le voyez, ce que fais simplement le constructeur est de placer notre objet point dans le coin haut gauche de notre écran SDL.

Maintenant, afin que notre objet point puisse se mouvoir, il nous faut récupérer les événements clavier.
Nous allons donc voir à quoi ressemble notre méthode handle_input()

handle_input : touche pressée
void Point::handle_input()
{
    //Si une touche a ete pressee
    if( event.type == SDL_KEYDOWN )
    {
        //ajustement de la vitesse
        switch( event.key.keysym.sym )
        {
            case SDLK_UP: yVel -= POINT_HEIGHT / 2; break;
            case SDLK_DOWN: yVel += POINT_HEIGHT / 2; break;
            case SDLK_LEFT: xVel -= POINT_WIDTH / 2; break;
            case SDLK_RIGHT: xVel += POINT_WIDTH / 2; break;
            default: break;
        }
    }

Voici la partie de code de la méthode handle_input() correspondant à la pression d'une touche.

Vous pensez sans doute que tout ce qu'il y aurait eu à faire c'est x++(ou x--) ou y++(ou y--) à la pression d'une touche.
Le problème avec ça c'est que le point bougera seulement quand une touche sera pressée.
Ce qui veut dire que vous devez presser la touche, relâcher la touche, puis presser à nouveau la touche encore afin qu'il continu de bouger.

Une façon pas très intelligente de faire serait d'appeler SDL_EnableKeyRepeat car ça posserai plusieurs problèmes : impossibilité de donner une vitesse à notre objet, surcharge de la gestion des événements, etc.

Donc ce que nous faisons à la place est de changer la vitesse (horizontale ou verticale) de notre point.
Quand la touche droite est pressée, nous augmentons la vitesse horizontale (xVel) de la moitié de la longueur de notre objet point (donc de 10),
ce qui a pour effet d'augmenter sa position X de 10 à chaque rafraîchissement de l'écran (à chaque frame).
De même, lorsque la touche gauche est pressée, nous décrémentons la vitesse horizontale de 10 ce qui a pour effet de diminuer la position X de
notre objet de 10 à chaque frame.
Le même principe est appliqué pour les coordonnées Y de notre point.
Cependant, souvenez-vous comment marche les coordonnées Y dans une fenêtre SDL : première application avec SDL
Donc augmenter les coordonnées Y de notre point le fait descendre, et décrémenter le fait monter.

Il nous faut aussi traiter quand la touche est relâchée afin de stopper le mouvement du point :

handle_input : touche relachée
    //Si une touche a ete relachee
    else if( event.type == SDL_KEYUP )
    {
        //ajustement de la vitesse
        switch( event.key.keysym.sym )
        {
            case SDLK_UP: yVel += POINT_HEIGHT / 2; break;
            case SDLK_DOWN: yVel -= POINT_HEIGHT / 2; break;
            case SDLK_LEFT: xVel += POINT_WIDTH / 2; break;
            case SDLK_RIGHT: xVel -= POINT_WIDTH / 2; break;
            default: break;
        }
    }
}

Quand on relâche la touche, il nous suffit d'annuler l'effet de la pression de la touche.
Quand on appuie sur la touche droite, on augmente sa vitesse xVel de 10, donc quand on la relâche, il suffit de décrémenter de 10.

Il est maintenant temps d'appliquer ces vitesses et d'afficher notre point et ses mouvements.

X-3. Affichage du mouvement

L'affichage et les mouvements de notre point sont gérés par la méthode show() que voici :

show()
void Point::show()
{
    //Bouge le point à gauche ou à droite
    x += xVel;

    //Si le point se rapproche trop des limites(gauche ou droite) de l'ecran
    if( ( x < 0 ) || ( x + POINT_WIDTH > SCREEN_WIDTH ) )
    {
        //On revient
        x -= xVel;
    }

    //Bouge le point en haut ou en bas
    y += yVel;

    //Si le point se rapproche trop des limites(haute ou basse) de l'ecran
    if( ( y < 0 ) || ( y + POINT_HEIGHT > SCREEN_HEIGHT ) )
    {
        //On revient
        y -= yVel;
    }

    //Affiche le point
    apply_surface( x, y, point, screen );
}

Tout d'abord, nous déplaçons notre point en ajoutant la vitesse à ses coordonnées.
Nous vérifions, à chaque mouvement, si le point arrive aux limites de notre écran SDL.
Si c'est le cas, nous annulons le mouvement en soustrayant sa vitesse à ses coordonnées (comme lors du relâchement de la touche).
En faisant ainsi, le point ne peut donc pas sortir de l'écran.

Cependant, en faisant ainsi, il se peut que vous vous retrouviez avec une situation comme celle-ci :
Image non disponible
Ceci arrive lorsque vous affectez la vitesse à quelque chose qui n'est pas divisible par les dimensions de l'écran.
Donc le point ne bouge pas jusqu'au mur, il bouge seulement jusqu'à sa position juste avant.

Une meilleure façon de faire serait de mettre les coordonnées du point égale aux dimensions de l'écran moins les
dimensions du point lorsqu'on arrive en collision avec les parois de l'écran.
Je vous laisse essayer.

A la fin de la méthode show() on affiche finalement notre point grâce à notre fonction apply_surface()

Il ne nous reste plus grand chose à faire, voici donc à quoi ressemble notre boucle principale dans le main :

boucle principale
    //Tant que l'utilisateur n'a pas quitter
    while( quit == false )
    {
        //On demarre le timer fps
        fps.start();

        //Tant qu'il y a un événement
        while( SDL_PollEvent( &event ) )
        {
            //On recupere l'evenement pour le point
            monPoint.handle_input();

            //Si l'utilisateur a cliqué sur le X de la fenêtre
            if( event.type == SDL_QUIT )
            {
                //On quitte the programme
                quit = true;
            }
        }

        //On remplit l'ecran de blanc
        SDL_FillRect( screen, &screen->clip_rect, SDL_MapRGB( screen->format, 0xFF, 0xFF, 0xFF ) );

        //On affiche le point sur l'ecran
        monPoint.show();

        //Mise à jour de l'écran
        if( SDL_Flip( screen ) == -1 )
        {
            return EXIT_FAILURE;
        }

        //Tant que le timer fps n'est pas assez haut
        while( fps.get_ticks() < 1000 / FRAMES_PER_SECOND )
        {
            //On attend...
        }
    }

Rien de nouveau par rapport aux tutoriels précédents.
Nous vérifions s'il y a un événement pour notre point, puis nous vérifions si l'utilisateur souhaite quitter.
Ensuite nous affichons le fond et nous bougeons et affichons notre point.
Finalement nous mettons à jour notre écran.

A noter que j'ai fait une attente active mais qu'il aurait été plus judicieux d'utiliser SDL_framerate comme on peut le voir dans le tutoriel de fearyourself ici

Téléchargements

Télécharger les sources du X (172 ko)
Télécharger un autre exemple dans lequel on peut déplacer un Tux ou un personnage (232 ko)

Version pdf (72 ko - 8 pages)


Tutoriel SDL

Chapitre XI : Détection et gestion des collisions avec SDL

  • XI. Détection et gestion des collisions avec SDL
    • XI-1. Détection de collision
    • XI-2. Collisions avec des boîtes de collision
    • XI-3. Collisions circulaires
    • Sources et pdf

XI. Détection et gestion des collisions avec SDL

Dans ce tutoriel, nous allons essayer de voir comment gérer la collision entre deux ou plusieurs éléments.
Certains éléments peuvent être statiques (fixés à des coordonnées précises), d'autres dynamiques (qui bougent soit par l'action de la personne derrière l'écran, soit en suivant un "chemin" déterministe).
Ces éléments peuvent aussi prendre différentes formes qui peuvent être plus ou moins complexes.

Nous allons dans un premier temps nous occuper du cas le plus simple : la collision entre un carré dirigé par le clavier et un "mur" (de type rectangulaire) statique que l'on va placer.

XI-1. Détection de collision

Pour cette première partie, nous avons donc un carré et un mur. Il va falloir qu'on fasse en sorte que le carré ne traverse pas le mur.
Voici à quoi ressemblera notre premier programme :

programme collision 1

Pour ce faire, il nous faut vérifier si le carré et le mur sont en collision.

Construisons d'abord notre mur et notre carré :

mur
//Le mur
SDL_Rect wall;
carré
//La classe Square (carre)
class Square
{
    private:
    //la boite de collision du carre
    SDL_Rect box;

    //La vitesse du point
    int xVel, yVel;

    public:
    //Initialisation des variables
    Square();

    //Recupere la touche pressee et ajuste la vitesse du carre
    void handle_input();

    //Montre le carré sur l'ecran
    void show();
};

La classe Square représente le carré qu'on va déplacer dans la fenêtre SDL.
Comme vous pouvez le remarquer, cette classe est quasi-identique à notre classe Point vue dans les chapitres précédents.
La seule différence notable est que les coordonnées X et Y de notre carré sont dans une structure SDL_Rect qui contient aussi les dimensions de notre carré.

Nous avons donc deux SDL_Rect et il nous faut déterminer quand il y a collision.
La fonction suivante va prendre en paramètre nos deux SDL_Rect, calculer les côtés de nos rectangles (le carré et le mur) afin d'ensuite faire les tests de collision.

fonction check_collision : determination des cotes
bool check_collision( SDL_Rect &A, SDL_Rect &B )
{
    //Les cotes des rectangles
    int leftA, leftB;
    int rightA, rightB;
    int topA, topB;
    int bottomA, bottomB;

    //Calcul les cotes du rectangle A
    leftA = A.x;
    rightA = A.x + A.w;
    topA = A.y;
    bottomA = A.y + A.h;

    //Calcul les cotes du rectangle B
    leftB = B.x;
    rightB = B.x + B.w;
    topB = B.y;
    bottomB = B.y + B.h;

Comme vous le voyez, il est facile à partir d'un SDL_Rect de calculer les côtés des rectangles car nous possédons ses coordonnées X et Y ainsi que sa hauteur H et sa largeur W.

Si vous avez du mal à voir comment les côtés sont déterminés, voici un petit schéma :

Maintenant que nous avons ce qu'il nous faut, nous pouvons tester si les deux rectangles sont en collision.
Il existe des cas, faciles à déterminer, où la collision entre les deux rectangles est impossible :
cas 1 - La partie basse (bottom) du rectangle A se retrouve au-dessus de la partie haute (Top) du rectangle B.
cas 2 - La partie haute (Top) du rectangle A se retrouve en dessous de la partie basse (bottom) du rectangle B.
cas 3 - La partie gauche (Left) du rectangle A se retrouve plus à droite que la partie droite (Right) du rectangle B.
cas 4 - La partie droite (Right) du rectangle A se retrouve plus à gauche que la partie gauche (Left) du rectangle B.

Un schéma valant mieux qu'une explication :

Dans les autres cas, il y a collision.
Il est donc facile de vérifier s'il y a collision entre nos deux rectangles, il suffit de vérifier qu'ils ne sont pas dans l'un des quatre cas présentés ci-dessus.

fonction check_collision : verification des collisions
Sélectionnez
    //Tests de collision
    if( bottomA <= topB )
    {
        return false;
    }

    if( topA >= bottomB )
    {
        return false;
    }

    if( rightA <= leftB )
    {
        return false;
    }

    if( leftA >= rightB )
    {
        return false;
    }

    //Si conditions collision detectee
    return true;
}

Notez que j'ai utilisé les opérateurs égal dans les tests, j'ai juste décidé qu'il n'y avait pas collision lorsque les deux rectangles étaient collés.
Vous pouvez très bien enlever les opérateurs égal, dans ce cas quand les deux rectangles seront côte à côte, il y aura collision (ce qui peut poser problème, nous le verrons par la suite).

Continuons ce programme par l'affichage du carré.
C'est dans cette fonction d'affichage et de rendu que l'on va utiliser notre fonction de collision et faire le traitement le cas échéant.

methode d'affichage du carré
Sélectionnez
void Square::show() {
    //Bouge le carre a droite ou a gauche
    box.x += xVel;

    //Si collision avec les cotes de l'ecran (droite ou gauche) ou collision avec le mur
    if( ( box.x < 0 ) || ( box.x + SQUARE_WIDTH > SCREEN_WIDTH ) || ( check_collision( box, wall ) ) ) {
        //Annulation du dernier deplacement
        box.x -= xVel;
    }

    //Bouge le carre vers le haut ou vers le bas
    box.y += yVel;
    //Si collision avec les cotes de l'ecran (haut ou bas) ou collision avec le mur
    if( ( box.y < 0 ) || ( box.y + SQUARE_HEIGHT > SCREEN_HEIGHT ) || ( check_collision( box, wall ) ) ) {
        //Annulation du dernier deplacement
        box.y -= yVel;
    }
    //Affiche le carre
    apply_surface( box.x, box.y, square, screen );
}

Dans cette fonction, nous bougeons notre carré.
Nous vérifions que celui-ci ne sort pas de l'écran ou qu'il ne traverse pas le mur.
Si notre carré est en collision, que ce soit avec les bords de l'écran ou avec notre mur, on annule le déplacement.
L'algorithme utilisé est donc celui-ci :

algorithme de collision
On bouge le carre
Si il y a une collision dans cette nouvelle position
    On annule le mouvement
Fin Si

Un petit problème peut apparaître selon la taille de notre fenêtre SDL et la vitesse de déplacement de notre carré.
En effet, si ces trois valeurs (hauteur de l'écran, largeur de l'écran et vitesse de déplacement du carré), ne sont pas multiples, il peut y avoir un problème de ce type :

Comme vous pouvez le voir, le carré reste bloqué sans pouvoir se coller au mur, ce qui n'est pas très réaliste comme collision.
Pour combler ce défaut, nous allons utiliser un autre algorithme :

algorithme de collision version 2
On bouge le carré
Si il y a une collision dans cette nouvelle position
    On colle le carré contre l'élément en collision
Fin Si

Cet algorithme est préférable au premier lors de collisions avec des éléments statiques mais à éviter dans le cas de collisions avec des éléments dynamiques.
Voici donc une autre version de la fonction d'affichage du carré :

methode d'affichage du carré v2
void Square::show()
{
    //Bouge le carre a droite ou a gauche
    box.x += xVel;

    int leftW, leftB;
    int rightW, rightB;
    int topW, topB;
    int bottomW, bottomB;

    leftB = box.x;
    rightB = box.x + box.w;
    topB = box.y;
    bottomB = box.y + box.h;

    leftW = wall.x;
    rightW = wall.x + wall.w;
    topW = wall.y;
    bottomW = wall.y + wall.h;

    //Si collision avec les cotes de l'ecran (droite ou gauche)
    if( ( box.x < 0 ) ) {
        //On colle le carre contre l'ecran
        box.x = 0;
    }
    if ( box.x + SQUARE_WIDTH > SCREEN_WIDTH ) {
        //On colle le carre contre l'ecran
        box.x = SCREEN_WIDTH - SQUARE_WIDTH;
    }
    //Si collision avec le mur
    if( ( check_collision( box, wall )) ) {
        // collision droite
        if(box.x + box.w > wall.x + wall.w) {
            box.x = wall.x + wall.w;
        }
        // collision gauche
        if(box.x < wall.x) {
            box.x = wall.x - box.w;
        }
    }

    //Bouge le carre vers le haut ou vers le bas
    box.y += yVel;

    //Si collision avec les cotes de l'ecran (haut ou bas)
    if( ( box.y < 0 ) ) {
        //On colle le carre contre l'ecran
        box.y = 0;
    }
    if ( box.y + SQUARE_HEIGHT > SCREEN_HEIGHT ) {
        //On colle le carre contre l'ecran
        box.y = SCREEN_HEIGHT - SQUARE_HEIGHT;
    }
    //Si collision avec le mur
    if( ( check_collision( box, wall )) ) {
        // collision haut
        if(box.y < wall.y){
            box.y = wall.y - box.h;
        }

        // collision bas
        if(box.y + box.h > wall.y + wall.h) {
            box.y = wall.y + wall.h;
        }
    }

    //Affiche le carre
    apply_surface( box.x, box.y, square, screen );
}

Cette version est plus lourde que la précédente car il nous faut récupérer les côtés de nos deux rectangles afin de pouvoir les coller lors d'une collision.
Dans le cas où nous aurions mis un opérateur égal dans les tests de collision dans notre fonction check_collision plus haut, il y aurait eu une boucle infinie entre la détection de la collision et le placement du carré.

Enfin pour terminer cette fonction, on affiche bien sûr le carré à l'écran.

Les sources de ce programme sont téléchargeables ici :
Télécharger les sources du chapitre XI-1 version 1 (100 ko)
Télécharger les sources du chapitre XI-1 version 2 (101 ko)

XI-2. Collisions avec des boîtes de collision

Une des méthodes les plus utilisées dans le monde du jeu vidéo est la gestion des collisions avec des boîtes de collision.
Cette partie du tutoriel va vous apprendre à créer et utiliser ces boîtes afin de détecter les collisions.

Avec les boîtes de collision, vous pouvez vérifier les collisions pour n'importe quel type de figure géométrique car tout peut se rapporter à un ensemble de boîtes de collision.
Prenons l'exemple d'un point :

Celui-ci peut se représenter avec un ensemble de boîtes de collision comme celles-ci :

Chaque image sur un ordinateur est composée de pixels, et les pixels sont des carrés qui composent des rectangles qui seront nos boîtes de collision.
Ainsi, lorsque l'on souhaite vérifier une collision, il suffit de vérifier si les deux groupes de rectangles entrent en collision.

Pour gérer ces groupes de rectangle qui nous servirons de boîtes de collision, nous allons utiliser des vecteurs.

include
#include "SDL/SDL.h"
#include "SDL/SDL_image.h"
#include <string>
#include <vector>

Les vecteurs sont des sortes de tableaux, mais sont plus facile à gérer. Si vous ne connaissez pas, je vous conseille d'aller ici :
FAQ vector

Nous allons devoir revoir un peu la classe Point que nous avions définie dans les chapitres précédents car il va nous falloir ajouter les boîtes de collision.

Classe Point
//La classe Point
class Point
{
    private:
    //Les coordonnees du point
    int x, y;

    //Les boites de collision du point
    std::vector<SDL_Rect> box;

    //La velocite du point
    int xVel, yVel;

    //fonction pour les boites de collision
    void shift_boxes();

    public:
    //Initialisation des variables
    Point( int X, int Y );

    //Prend les touches pressees et ajuste la vélocite du point
    void handle_input();

    //Bouge le point
    void move( std::vector<SDL_Rect> &rects );

    //Montre le point à l'écran
    void show();

    //Recuperation des boites de collision
    std::vector<SDL_Rect> &get_rects();
};

Nous avons les coordonnées et la vélocité du point comme avant, mais maintenant on a en plus un vecteur de SDL_Rect pour contenir les boîtes de collision.

Au niveau des fonctions, on voit apparaître la fonction shift_boxes() qui bouge les boîtes de collision en relation avec les coordonnées.

Il y a aussi un constructeur qui initialise le Point aux coordonnées passées en paramètre ainsi que la fonction de récupération des événements déjà présente avant.
Cette fois-ci on va séparer la partie mouvement du Point de la partie affichage de celui-ci.
Nous avons aussi une fonction get_rects() qui va nous permettre de récupérer les boîtes de collision.

Passons à la fonction de vérification des collisions.
Cette fois-ci on ne va pas se contenter de vérifier la collision entre deux rectangles, mais entre deux groupes de rectangles qui sont nos boîtes de collisions de nos deux éléments.

check collision
bool check_collision( std::vector<SDL_Rect> &A, std::vector<SDL_Rect> &B )
{
    //Les cotes du rectangle
    int leftA, leftB;
    int rightA, rightB;
    int topA, topB;
    int bottomA, bottomB;

    //On va dans la boite A
    for( int Abox = 0; Abox < A.size(); Abox++ )
    {
        //Calcul des cotes du rectangle A
        leftA = A[ Abox ].x;
        rightA = A[ Abox ].x + A[ Abox ].w;
        topA = A[ Abox ].y;
        bottomA = A[ Abox ].y + A[ Abox ].h;

        //On va dans la boite B
        for( int Bbox = 0; Bbox < B.size(); Bbox++ )
        {
            //Calcul des cotes du rectangle B
            leftB = B[ Bbox ].x;
            rightB = B[ Bbox ].x + B[ Bbox ].w;
            topB = B[ Bbox ].y;
            bottomB = B[ Bbox ].y + B[ Bbox ].h;

            //Si les cotes de A sont en dehors de B
            if( ( ( bottomA <= topB ) || ( topA >= bottomB ) || ( rightA <= leftB ) || ( leftA >= rightB ) ) == false )
            {
                //Une collision est detectee
                return true;
            }
        }
    }

    //Si aucune des boites de collision ne se touchent
    return false;
}

Cette fonction prend un rectangle du vecteur A, puis vérifie s'il y a collision avec tous les rectangles du vecteur B, ensuite on prend le rectangle A suivant et on recommence l'opération.
Ce travail est effectué jusqu'à ce qu'on trouve une collision ou qu'on ait testé tous les rectangles ensemble.

Nous allons maintenant nous occuper de la création de notre point et de ses boîtes de collision.

constructeur Point
Point::Point( int X, int Y )
{
    //Initialisation des coordonnées
    x = X;
    y = Y;

    //Initialisation de la velocite
    xVel = 0;
    yVel = 0;

    //Creation des SDL_Rects necessaires
    box.resize( 11 );

    //Initialisation des hauteurs et largeurs des boites de collision
    box[ 0 ].w = 6;
    box[ 0 ].h = 1;

    box[ 1 ].w = 10;
    box[ 1 ].h = 1;

    box[ 2 ].w = 14;
    box[ 2 ].h = 1;

    box[ 3 ].w = 16;
    box[ 3 ].h = 2;

    box[ 4 ].w = 18;
    box[ 4 ].h = 2;

    box[ 5 ].w = 20;
    box[ 5 ].h = 6;

    box[ 6 ].w = 18;
    box[ 6 ].h = 2;

    box[ 7 ].w = 16;
    box[ 7 ].h = 2;

    box[ 8 ].w = 14;
    box[ 8 ].h = 1;

    box[ 9 ].w = 10;
    box[ 9 ].h = 1;

    box[ 10 ].w = 6;
    box[ 10 ].h = 1;

    //On bouge les boites de collision
    shift_boxes();
}

On met le point aux coordonnées données en argument et on initialise sa vélocité.
Ensuite nous créons onze boîtes de collision dans le vecteur dont nous initialisons la largeur et la hauteur.
Il nous faut ensuite les disposer comme sur l'image du point plus haut (rectangles noirs et bleus).

Cette dernière chose est faite grâce à la fonction shift_boxes() :

shift_boxes()
void Point::shift_boxes()
{
    //Initialisation
    int r = 0;

    //On va dans les boites de collision du point
    for( int set = 0; set < box.size(); set++ )
    {
        //On centre la boite de collision
        box[ set ].x = x + ( POINT_WIDTH - box[ set ].w ) / 2;

        //On met la boite de collision à sa place
        box[ set ].y = y + r;

        //On bouge la position en dessous de la hauteur de la boite de collision
        r += box[ set ].h;
    }
}

Cette fonction va placer nos boîtes de collision sur le point et faire en sorte que si on bouge notre point, les boîtes de collision le suivront disposées de la même façon.
Pour mieux comprendre cette fonction, je vous conseille de la faire tourner sur un papier par exemple.

Nous allons nous occuper du mouvement de notre point et faire le nécessaire lors de collisions, tout ceci dans la fonction move().

mouvement du point : fonction move()
void Point::move( std::vector<SDL_Rect> &rects )
{
    //Bouge le point a droite ou a gauche
    x += xVel;

    //Bouge la boite de collision
    shift_boxes();

    //Si le point a ete trop loin a droite ou a gauche ou est en collision avec un autre objet
    if( ( x < 0 ) || ( x + POINT_WIDTH > SCREEN_WIDTH ) || ( check_collision( box, rects ) ) )
    {
        //On revient en arriere (annulation du mouvement)
        x -= xVel;
        shift_boxes();
    }

    //Bouge le point vers le haut ou le bas
    y += yVel;

    //Bouge la boite de collision
    shift_boxes();

    //Si le point a ete trop loin a droite ou a gauche ou est en collision avec un autre objet
    if( ( y < 0 ) || ( y + POINT_HEIGHT > SCREEN_HEIGHT ) || ( check_collision( box, rects ) ) )
    {
        //On revient en arriere (annulation du mouvement)
        y -= yVel;
        shift_boxes();
    }
}

C'est à peu près la même chose qu'avant.
Nous bougeons le point, et si le point va en dehors de l'écran ou qu'une collision est détectée entre deux groupes de rectangles, on annule le mouvement.
La seule différence réside dans le déplacement des boîtes de collision avec le point avec la fonction shift_boxes().

Il nous manque aussi l'affichage du point que voici :

affichage du point : fonction show()
void Point::show()
{
    //Affiche le point
    apply_surface( x, y, point, screen );
}

J'ai volontairement porté l'utilisation des boîtes de collision à outrance pour montrer qu'on peut faire des collisions "pixel-perfect" (au pixel près) en les utilisant.
Cependant, il n'est pas nécessaire d'en faire autant pour créer des collisions réalistes et la plupart des jeux vidéos utilisent des boîtes de collision grossières loin du pixel avec beaucoup moins de précision.
La précision au pixel est rarement nécessaire pour faire quelque chose de réaliste, c'est à vous de décider de la précision que vous souhaitez.

Les sources de cette partie du tutoriel sont disponibles ici : Télécharger les sources du chapitre XI-2 (106 ko)

XI-3. Collisions circulaires

Dans la partie précédente, nous avons utilisé onze boîtes de collision pour un cercle (notre point).
Dans cette partie, on va apprendre une méthode plus efficace pour gérer la détection des collisions avec des cercles.

Nous allons avoir besoin d'un peu de math cette fois-ci, donc nous allons inclure une librairie supplémentaire, la librairie cmath.

include
//Les fichiers d'entete
#include <SDL/SDL.h>
#include <SDL/SDL_image.h>
#include <string>
#include <vector>
#include <cmath>

Nous allons aussi devoir créer notre propre structure de cercle pour ce programme :

cercle
//Une structure cercle
struct Circle
{
    int x, y;
    int r;
};

Les variables x et y sont les coordonnées du cercle et r est le rayon.

Nous allons une fois de plus revoir notre classe Point :

classe Point
///La classe Point
class Point
{
    private:
    //La zone du point
    Circle c;

    //La velocite du point
    int xVel, yVel;

    public:
    //Initialisation des variables
    Point();

    //Prend les touches pressees et ajuste la velocite du point
    void handle_input();

    //Bouge le point
    void move( std::vector<SDL_Rect> &rects, Circle &circle );

    //Montre le point à l'ecran
    void show();
};

Nous avons deux différences par rapport à avant : cette fois nous utilisons notre structure cercle au lieu d'un vecteur de SDL_Rect et dans la fonction move() nous vérifions la collision entre un cercle et un vecteur de SDL_Rect.

Pour vérifier les collisions, nous allons avoir besoin de quelque chose en plus : connaître la distance entre deux points :

distance
double distance( int x1, int y1, int x2, int y2 )
{
    //Retourne la distance entre deux points
    return sqrt( pow( x2 - x1, 2 ) + pow( y2 - y1, 2 ) );
}

Je ne pense pas avoir à expliquer cette formule, il s'agit simplement de mathématiques.

Nous allons utiliser deux fonctions pour vérifier les collisions, une entre deux cercles et l'autre entre un cercle et un vecteur de rectangles.
Vérifier les collisions entre deux cercles est assez simple, tout ce qu'on a à faire c'est de vérifier si oui ou non la distance entre les centres des deux cercles est inférieure à la somme de leurs rayons :

check_collision : entre deux cercles
Sélectionnez
bool check_collision( Circle &A, Circle &B )
{
    //Si la distance entre le centre des cercles est inferieure à la somme de leurs radian
    if( distance( A.x, A.y, B.x, B.y ) < ( A.r + B.r ) )
    {
        //Le cercle est en collision
        return true;
    }

    //S'il ne l'est pas
    return false;
}

La fonction de vérification des collisions entre un cercle et un groupe de rectangles est un peu plus complexe.
Il y a deux types de collision possibles comme montrés sur cette image:

Le cercle peut toucher un côté ou un coin du rectangle.
Donc nous avons à prendre en compte les côtés du rectangle, les "côtés" du cercle et les coins du rectangle.
Pour calculer les "côtés" du cercle, nous allons utiliser son rayon et déterminer deux segments qui nous serviront de base pour les collisions comme sur l'image suivante :

Voici le code permettant de déterminer ses "côtés" :

cotes du cercle
    //Calcul des cotes de A
    leftAv = A.x;
    rightAv = A.x;
    topAv = A.y - A.r;
    bottomAv = A.y + A.r;

    leftAh = A.x - A.r;
    rightAh = A.x + A.r;
    topAh = A.y;
    bottomAh = A.y;

Les quatre premières lignes définissent le "côté" vertical, les quatre suivantes le "côté" horizontal du cercle.

Ainsi quand nous vérifions les collisions avec les côtés du cercle, nous vérifions s'il y a collision entre les rectangles et les lignes formant les deux côtés du cercle.

Une ligne est juste un rectangle avec une épaisseur de un, donc la vérification de collision est la même qu'avant.

check_collision
    //On va dans la boite B
    for( int Bbox = 0; Bbox < B.size(); Bbox++ )
    {
        //Calcul des cotes de B
        leftB = B[ Bbox ].x;
        rightB = B[ Bbox ].x + B[ Bbox ].w;
        topB = B[ Bbox ].y;
        bottomB = B[ Bbox ].y + B[ Bbox ].h;

        //Calcul des angles de B
        Bx1 = B[ Bbox ].x, By1 = B[ Bbox ].y;
        Bx2 = B[ Bbox ].x + B[ Bbox ].w, By2 = B[ Bbox ].y;
        Bx3 = B[ Bbox ].x, By3 = B[ Bbox ].y + B[ Bbox ].h;
        Bx4 = B[ Bbox ].x + B[ Bbox ].w, By4 = B[ Bbox ].y + B[ Bbox ].h;

        //Si aucun cote de la verticale A est en dehors de B
        if( ( ( bottomAv <= topB ) || ( topAv >= bottomB ) || ( rightAv <= leftB ) || ( leftAv >= rightB ) ) == false )
        {
            //Une collision est detectee
            return true;
        }

        //Si aucun cote de l'horizontale A est en dehors de B
        if( ( ( bottomAh <= topB ) || ( topAh >= bottomB ) || ( rightAh <= leftB ) || ( leftAh >= rightB ) ) == false )
        {
            //Une collision est detectee
            return true;
        }

Nous calculons les côtés et les angles des rectangles de la boîte B.
Ensuite on vérifie la collision rectangle-rectangle comme vue dans les parties précédentes.
La partie qu'il nous reste est la vérification des collisions avec les coins des rectangles.

colision avec les coins
        //Si un des coins de B est dans A
        if( ( distance( A.x, A.y, Bx1, By1 ) < A.r ) || ( distance( A.x, A.y, Bx2, By2 ) < A.r ) || ( distance( A.x, A.y, Bx3, By3 ) < A.r ) || ( distance( A.x, A.y, Bx4, By4 ) < A.r ) )
        {
            //Une collision est detectee
            return true;
        }
    }

    //Pas de collision
    return false;
}

Pour cela on regarde si les coins se trouvent à l'intérieur du cercle tout simplement.

A la fin, s'il n'y a pas eu de collisions, la fonction retourne faux.

Les sources de cette partie du tutoriel peuvent être récupérées ici :
Télécharger les sources du chapitre XI-3 (107 ko)

Sources et pdf

Télécharger les sources du chapitre XI-1 version 1 (100 ko)
Télécharger les sources du chapitre XI-1 version 2 (101 ko)
Télécharger les sources du chapitre XI-2 (106 ko)
Télécharger les sources du chapitre XI-3 (107 ko)

Version pdf (141 ko - 21 pages)


Tutoriel SDL

Chapitre XII : Animation

  • XII. Mouvement
    • XII-A. Introduction
    • XII-B. Découpage
    • XII-C. La classe Cat
      • XII-C-1. Le constructeur
      • XII-C-2. La méthode handle_events
      • XII-C-3. La méthode show
      • XII-C-4. La boucle principale
    • XII-D. Améliorations
  • Téléchargements

XII. Mouvement

Jusqu'à maintenant, nous avons toujours travaillé juste avec des images fixes.
Ce tutoriel va vous apprendre à mettre en place un petit moteur d'animations basiques.

XII-A. Introduction

Le principe basique d'une animation est de prendre une série d'images qui se suivent comme sur cette feuille de sprites :

feuille de sprites personnage

Afficher ces images les unes après les autres dans l'ordre créé une illusion de mouvements (si vous ne voyez pas l'animation, cliquez Ici) :

personnage qui marche

Donc quand vous animez des mouvements en SDL, vous affichez simplement une séquence de SDL_surface.

Pour les besoins de ce tutoriel, nous allons utiliser cette feuille de sprite libre de droits :

feuille de sprite chat

Cette feuille de sprite représente la décomposition en 3 phases du mouvement du personnage chat vers la droite et vers la gauche.
Une animation de mouvement peut être décomposée en autant de phases que vous en avez besoin afin d'avoir un mouvement plus naturel ou plus fluide.
Ici nous nous contenterons de 3 phases.

XII-B. Découpage

Nous avons à notre disposition une feuille de sprites contenant les différentes phases du mouvement de notre chat pour l'animation.
Pour l'utiliser correctement, il va nous falloir la découper.

Voici le code de la fonction qui nous permet de découper la feuille de sprites en sprites individuels :

decoupage
void set_clips()
{
    //On coupe la feuille de sprite
    clipsRight[ 0 ].x = 0;
    clipsRight[ 0 ].y = 0;
    clipsRight[ 0 ].w = CAT_WIDTH;
    clipsRight[ 0 ].h = CAT_HEIGHT;

    clipsRight[ 1 ].x = CAT_WIDTH;
    clipsRight[ 1 ].y = 0;
    clipsRight[ 1 ].w = CAT_WIDTH;
    clipsRight[ 1 ].h = CAT_HEIGHT;

    clipsRight[ 2 ].x = CAT_WIDTH * 2;
    clipsRight[ 2 ].y = 0;
    clipsRight[ 2 ].w = CAT_WIDTH;
    clipsRight[ 2 ].h = CAT_HEIGHT;

    clipsLeft[ 0 ].x = 0;
    clipsLeft[ 0 ].y = CAT_HEIGHT;
    clipsLeft[ 0 ].w = CAT_WIDTH;
    clipsLeft[ 0 ].h = CAT_HEIGHT;

    clipsLeft[ 1 ].x = CAT_WIDTH;
    clipsLeft[ 1 ].y = CAT_HEIGHT;
    clipsLeft[ 1 ].w = CAT_WIDTH;
    clipsLeft[ 1 ].h = CAT_HEIGHT;

    clipsLeft[ 2 ].x = CAT_WIDTH * 2;
    clipsLeft[ 2 ].y = CAT_HEIGHT;
    clipsLeft[ 2 ].w = CAT_WIDTH;
    clipsLeft[ 2 ].h = CAT_HEIGHT;
}

Pour mieux comprendre ce code, je vous invite à revoir ce chapitre :
Chapitre VI : Sprites

Il faut voir qu'on a ici deux sets de sprites, les sprites pour l'animation du mouvement vers la droite et les sprites pour l'animation du mouvement vers la gauche.
Remarquez qu'il y a une façon plus simple de gérer le découpage ici :

decoupage amélioré
void set_clips()
{
    //On coupe la feuille de sprite
    for(i=0;i<3;i++) {
        clipsRight[ i ].x = CAT_WIDTH * i;
        clipsRight[ i ].y = 0;
        clipsRight[ i ].w = CAT_WIDTH;
        clipsRight[ i ].h = CAT_HEIGHT;

        clipsLeft[ i ].x = CAT_WIDTH * i;
        clipsLeft[ i ].y = CAT_HEIGHT;
        clipsLeft[ i ].w = CAT_WIDTH;
        clipsLeft[ i ].h = CAT_HEIGHT;
}

XII-C. La classe Cat

Notre feuille de sprites contenant un chat, j'ai nommé ma classe Cat mais j'aurais très bien pu la nommer Personnage vu qu'elle peut être utilisée pour n'importe quelle autre feuille de sprites (avec quelques légères modifications).
Voici donc notre classe Cat qui va nous permettre de faire bouger notre chat tout en donnant l'illusion de mouvements :

classe Cat
//Notre personnage
class Cat
{
    private:
    //coordonnées
    int offSet;

    //sa vitesse de deplacement
    int velocity;

    //sa frame courante
    int frame;

    //Son statut d'animation
    int status;

    public:
    //Le constructeur permettant l'initialisation des variables
    Cat();

    void handle_events();

    //montrer le personnage
    void show();
};

Je vais maintenant vous expliquer un peu plus en détail ce code.
Premièrement nous avons les variables offset et velocity représentant respectivement les coordonnées de notre chat et la vitesse de notre chat.
Vu qu'on va simplement bouger notre chat de droite à gauche, nous avons juste besoin de la composante x des coordonnées.

Ensuite nous avons les variables frame et status.
frame permet de garder la trace de l'image de l'animation du mouvement à montrer.
status permet de garder la trace du sens du mouvement de notre chat (gauche ou droite).

Ensuite, bien sûr, nous avons le construteur, le gestionnaire d'événements et la fonction qui permet de bouger et d'afficher notre chat.

XII-C-1. Le constructeur

Voici le code, très simple, du constructeur de notre classe Cat :

constructeur
Cat::Cat()
{
    //Initialisation des variables de mouvement
    offSet = 0;
    velocity = 0;

    //Initialisation des variables d'animation
    frame = 0;
    status = CAT_RIGHT;
}

Dans le constructeur, on initialise les differentes variables.
On met la coordonnée x à 0, donc le premier sprite de notre chat va se positionner à la position 0 de l'écran.
On met ensuite la vitesse à 0, donc notre chat sera immobile.
Ensuite nous initialisons la variable frame à 0 pour signifier qu'on part de l'animation 0.
Pour finir, nous initialisons la variable status à CAT_RIGHT, ainsi l'animation par defaut est notre chat marchant vers la droite.

XII-C-2. La méthode handle_events

Voici notre méthode pour récupérer les événements clavier affectant notre chat :

handle_events()
void Cat::handle_events()
{
    //Si une touche est préssée
    if( event.type == SDL_KEYDOWN )
    {
        //Mise à jour de la velocité
        switch( event.key.keysym.sym )
        {
            case SDLK_RIGHT: velocity += CAT_WIDTH / 4; break;
            case SDLK_LEFT: velocity -= CAT_WIDTH / 4; break;
            default: break;
        }
    }
    //Si une touche est relachée
    else if( event.type == SDL_KEYUP )
    {
        //Mise à jour de la velocité
        switch( event.key.keysym.sym )
        {
            case SDLK_RIGHT: velocity -= CAT_WIDTH / 4; break;
            case SDLK_LEFT: velocity += CAT_WIDTH / 4; break;
            default: break;
        }
    }
}

Pour mieux comprendre ce code, je vous invite à revoir ce chapitre :
Chapitre X : Mouvements

Tout est expliqué dans le chapitre dont le lien se trouve ci-dessus.
Il faut comprendre qu'une vitesse négative va faire déplacer notre chat vers la gauche et qu'une vitesse positive le fera déplacer vers la droite.

XII-C-3. La méthode show

Voici la fonction qui va nous permettre de déplacer et d'afficher notre chat.
Tout d'abord il faut penser à garder notre chat dans l'écran afin de l'avoir toujours à l'oeil (un chat c'est malicieux...).

show
void Cat::show()
{
    //Mouvement
    offSet += velocity;

    //On garde le personnage dans les limites de la fenêtre SDL
    if( ( offSet < 0 ) || ( offSet + CAT_WIDTH > SCREEN_WIDTH ) )
    {
        offSet -= velocity;
    }

Le code n'est pas très difficile à comprendre, si on atteint le bord gauche (coordonnée X égal à zero), on annule la vitesse. De même, si on arrive sur le bord droit, on fait de même.
Une autre chose à voir c'est que la coordonnée x du sprite qu'on traite est le bord haut gauche du sprite. C'est pour cela qu'on est obligé d'ajouter la taille du sprite pour le test avec le bord droit et non avec le bord gauche.
Une autre façon de faire serait de mettre simplement la position du personnage à jour (qu'on colle donc aux bords de l'écran) :

show - autre façon de faire
void Cat::show()
{
    //Mouvement
    offSet += velocity;

    //On garde le personnage dans les limites de la fenêtre SDL
    if( offSet < 0 )
    {
        offSet = 0;
    }
    if( offSet + CAT_WIDTH > SCREEN_WIDTH )
    {
        offset = SCREEN_WIDTH - CAT_WIDTH;
    }

Vous pouvez utiliser les deux façons, elles marchent aussi bien l'une que l'autre bien que la deuxième est plus souvent utilisée

Après avoir bougé le sprite et vérifié la collision avec les bords de l'écran, il est temps de changer de phase du mouvement.
En effet, à chaque déplacement, il va falloir changer le sprite pour simuler le mouvement.

show - suite
    //Si Cat bouge à gauche
    if( velocity < 0 )
    {
        //On prend le personnage de profil gauche
        status = CAT_LEFT;

        //On bouge à la prochaine image de l'animation
        frame++;
    }
    //Si Cat bouge à droite
    else if( velocity > 0 )
    {
        //On prend le personnage de profil droit
        status = CAT_RIGHT;

        //On bouge à la prochaine image de l'animation
        frame++;
    }
    //Si Cat ne bouge plus
    else
    {
        //Restart the animation
        frame = 1;
    }

La première chose à faire est de vérifier de quel côté notre chat est en train de bouger en testant tout simplement le signe de la vitesse.
Comme dit précédemment, si la vitesse est négative alors notre chat bouge à gauche et si la vitesse est positive alors notre chat bouge à droite.

Ensuite, si notre chat bouge à gauche, il faut mettre la variable status à CAT_LEFT, ce qui nous servira pour l'affichage plus tard, et incrémenter notre compteur frame afin que le sprite suivant soit affiché.
De même, si notre chat bouge à droite, il faut mettre la variable status à CAT_RIGHT et incrémenter le compteur frame.

Si notre chat ne bouge pas, on met la variable frame à 1 afin de redémarrer l'animation depuis le sprite où le chat est immobile (image du milieu sur la feuille de sprite selon notre découpage).
Nous aurions très bien pu mettre la variable frame à 0 mais notre chat se serait trouvé avec une patte en l'air lorsqu'il est immobile...

Il faut aussi penser à faire boucler l'animation, en effet notre compteur ne doit ici pas depasser 3 (on incremente le compteur avant le test) car il n'y a que 3 phases du mouvement (0, 1 et 2) :

show - suite
    //Boucle l'animation
    if( frame >= 3 )
    {
        frame = 0;
    }

Donc si on arrive à une valeur du compteur frame égale à 3, on le met à 0 afin que l'animation boucle.
Ainsi on aura une suite du genre :
0, 1, 2, 0, 1, 2, ...

Enfin, il nous reste à afficher le bon sprite sur l'écran.
Pour cela, on va utiliser ce code que je vais expliquer par la suite :

show - suite
    //Affichage
    if( status == CAT_RIGHT )
    {
        apply_surface( offSet, SCREEN_HEIGHT - CAT_HEIGHT, cat, screen, &clipsRight[ frame ] );
    }
    else if( status == CAT_LEFT )
    {
        apply_surface( offSet, SCREEN_HEIGHT - CAT_HEIGHT, cat, screen, &clipsLeft[ frame ] );
    }

Si notre chat bouge vers la droite (CAT_RIGHT), on affiche le bon sprite du chat marchant vers la droite.
De même, si notre chat bouge vers la gauche (CAT_LEFT), on affiche le bon sprite du chat marchant vers la gauche.

Plus en détail, on va afficher à l'écran screen (4ème argument) à la position x offSet (1er argument) et à la position y = SCREEN_HEIGHT - CAT_HEIGHT (2ème argument), n'oublions pas que le point que nous traitons pour les coordonnées du sprite est son bord haut gauche, notre chat cat (3ème argument, c'est notre feuille de sprite entière).
Le dernier argument nous permet de déterminer quelle partie découpée de la feuille de sprite on va afficher.

Une meilleure façon de faire qui nous aurait évité de faire ce test aurait été de faire un tableau à deux dimensions avec CAT_RIGHT = 0 et CAT_LEFT = 1.
En ajoutant les variables CAT_DIRECTIONS et CAT_NBR_FRAMES (respectivement égales à 2 et 3 dans notre cas), nous aurions ainsi pu définir un tableau de clips de cette façon :

tableau de clips
Sélectionnez
SDL_Rect clips[CAT_DIRECTIONS][CAT_NBR_FRAMES];

XII-C-4. La boucle principale

Le gros du travail est fait maintenant, il ne nous reste plus qu'à assembler tout ça.
Comme dans tout programme SDL, avant la boucle principale, on va commencer par initialiser SDL, charger les fichiers puis ici on va avoir besoin de découper notre feuille de sprites ainsi que de "construire" notre chat.
Viens donc ensuite la boucle principale où tout va se passer :

boucle principale
    //Tant que l'utilisateur n'a pas quitte
    while( quit == false )
    {
        //Mise en route du timer
        fps.start();

        //Tant qu'il y a un evenement
        while( SDL_PollEvent( &event ) )
        {
            //Recuperation des evenements pour notre "Cat"
            walk.handle_events();

            //Si l'utilisateur a clique sur le X de la fenetre
            if( event.type == SDL_QUIT )
            {
                //On quitte le programme
                quit = true;
            }
        }

        //Remplissage de l'ecran avec du blanc
        SDL_FillRect( screen, &screen->clip_rect, SDL_MapRGB( screen->format, 0xFF, 0xFF, 0xFF ) );

        //Affichage du "Cat" sur l'ecran
        walk.show();

        //Mise a jour de l'ecran
        if( SDL_Flip( screen ) == -1 )
        {
            return 1;
        }

        //On rend la main tant qu'on en a pas besoin
        while( fps.get_ticks() < 1000 / FRAMES_PER_SECOND )
        {
            //Attente...
        }
    }

C'est à peu près toujours la même chose que sur les tutoriels précédents, notamment celui sur le mouvement (Chapitre X : Mouvements).

Donc comme vous pouvez le voir pour un petit moteur d'animation, tout ce dont on a besoin c'est de savoir quelle animation on va utiliser et quelle image, quelle phase de l'animation, on va afficher.

Je rajouterais que j'ai fait une attente active mais qu'il aurait été plus judicieux d'utiliser SDL_framerate comme on peut le voir dans le tutoriel de fearyourself ici

XII-D. Améliorations

Dans l'état actuel de notre programme, notre variable FRAMES_PER_SECOND agit sur le comportement du jeu lui même, ce qu'il ne devrait pas faire car comme son nom l'indique, il s'agit seulement du nombre d'images à afficher par secondes.
En effet, si vous changez la valeur de cette variable dans le programme actuel, vous remarquerez que la vitesse de déplacement de notre chat baissera ou augmentera selon que vous aurez baissé ou augmenté la valeur.
Dans un jeu, ce type de comportement ne doit pas être influencé par le nombre d'images par seconde qu'on affiche.

Pour bien faire, le déplacement de notre chat doit se faire en pixel par unité de temps et non pas en pixel par frame comme c'est le cas ici.
Afin de remedier à ce problème, on va creer une nouvelle variable qu'on va nommer CAT_VITESSE qui contiendra la vitesse de déplacement de notre chat en pixel par secondes :

CAT_VITESSE
//Vitesse de marche du chat (en pixel par seconde)
const int CAT_VITESSE = 120;

Il nous suffit ensuite de changer le code de notre fonction handle_events() :

handle_events() amélioré
void Cat::handle_events()
{
    //Si une touche est préssée
    if( event.type == SDL_KEYDOWN )
    {
        //mise à jour de la velocité
        switch( event.key.keysym.sym )
        {
            case SDLK_RIGHT: velocity += (CAT_VITESSE / FRAMES_PER_SECOND); break;
            case SDLK_LEFT: velocity -= (CAT_VITESSE / FRAMES_PER_SECOND); break;
            default: break;
        }
    }
    //si une touche est relachée
    else if( event.type == SDL_KEYUP )
    {
        //Mise à jour de la velocité
        switch( event.key.keysym.sym )
        {
            case SDLK_RIGHT: velocity -= (CAT_VITESSE / FRAMES_PER_SECOND); break;
            case SDLK_LEFT: velocity += (CAT_VITESSE / FRAMES_PER_SECOND); break;
            default: break;
        }
    }
}

Ainsi la vitesse de notre chat sera maintenant constante et ne dépendra plus du nombre de frames par secondes.

Il subsiste un autre problème, la vitesse d'animation du personnage change aussi en fonction de la valeur de notre variable FRAMES_PER_SECOND.
En effet, on voit bien dans le code de la méthode show() qu'elle est lié directement au nombre de frames par seconde.
Pour remedier à ce problème, on va ajouter un timer pour régler la vitesse d'animation du personnage et ne plus le lier au nombre de frames par seconde.

Commençons par définir notre vitesse d'animation :

nouvelles variables
const int PERSO_DELAI_FRAME = 120; // délais entre deux frames (en ms)
const int PERSO_NB_ANIM = 3; // Nombre de frame d'animation

Ensuite, il nous faut donc notre timer, sur notre personnage donc on va ajouter celui-ci dans notre classe Cat :

Classe Cat avec timer
class Cat
{
    private:
    int offSet;
    Timer anim;

La suite de notre classe reste la même.
Attention à bien définir la classe Timer avant la classe Cat, sinon votre compilateur ne va pas aimer.

On va démarrer ce timer lorsqu'on va créer notre chat, donc dans le constructeur :

construteur amélioré
Cat::Cat()
{
    //Initialisation des variables de mouvement
    offSet = 0;
    velocity = 0;

    //Initialisation des variables d'animation
    frame = 0;
    status = CAT_RIGHT;

    //Départ du timer
    anim.start();
}

Il nous reste à changer l'animation lorsqu'il est temps de le faire, donc lorsque notre timer atteint la valeur qu'on a fixé au début :

show() amélioré
void Cat::show()
{
    [...]

    //Si Cat bouge à gauche
    if( velocity < 0 )
    {
        //On prend le personnage de profil gauche
        status = CAT_LEFT;

        //S'il est l'heure, on change l'animation
        if( this->anim.get_ticks() >= CAT_DELAI_FRAME ) {

            // On remet le timer à 0
            anim.start();

            // On passe à la frame suivante
            frame++;
        }
    }
    //Si Cat bouge à droite
    else if( velocity > 0 )
    {
        //On prend le personnage de profil droit
        status = CAT_RIGHT;

        //S'il est l'heure, on change l'animation
        if( this->anim.get_ticks() >= CAT_DELAI_FRAME ) {

            // On remet le timer à 0
            anim.start();

            // On passe à la frame suivante
            frame++;
        }
    }
[...]

Le début du code ne change pas, et la fin non plus. Le code change lorsqu'on incrémentait le nombre de frame.
Maintenant, avant d'incrémenter ce nombre, on va vérifier si il est temps de changer l'animation du personnage.
Si c'est le cas, on change l'animation, on redémarre notre timer anim et on incrémente le nombre de frame.

Voila, cette fois le comportement de notre programme est conforme au comportement d'un jeu dans la gestion des animations et des mouvements.

Vous pouvez aussi essayer de modifier le code du Chapitre X afin qu'il ait un comportement similaire.

Téléchargements

Télécharger les sources du chapitre XII (112 ko)
Télécharger les sources du chapitre XII - version améliorée par FabaCoeur (101 ko)
Version pdf (149 ko - 16 pages)


Tutoriel SDL

Chapitre XIII : Sauvegarde

  • XIII. Sauvegarde
    • XIII-A. Introduction
      • XIII-A-1. Entrées/Sorties
      • XIII-A-2. Structure du fichier de sauvegarde
    • XIII-B. Chargement des données
    • XIII-C. Sauvegarde des données
    • XIII-D. Main et boucle principale

XIII. Sauvegarde

Que vous souhaitiez sauvegarder des paramétres ou progresser dans un jeu, vous aurez besoin de savoir manipuler les entrées/sorties.
Dans ce tutoriel, on va sauvegarder un type de fond d'écran ainsi que la position (les coordonnées) d'un point.
Ce que nous souhaitons ici, c'est que quand nous lançerons le programme, le fond d'écran et la position de notre point sera la même que lorsqu'on aura fermé le programme.

Ce tutoriel va vous apprendre les bases de la sauvegarde et du chargement de données dans un fichier.

XIII-A. Introduction

Pour les besoins de ce tutoriel, nous allons réutiliser notre classe Point du chapitre X : Mouvements.
Nous allons ajouter deux méthodes très classiques à cette classe : des accesseurs get_x et get_y pour récuperer les coordonnées x et y du point.

accesseurs
Sélectionnez
    //Recupere la coordonnees x du point
    int &get_x();

    //Recupere la coordonnees y du point
    int &get_y();

Il n'y a besoin d'ajouter rien d'autre dans cette classe, elle est maintenant fonctionnelle pour les sauvegardes.

XIII-A-1. Entrées/Sorties

Comme dis précédemment, nous allons travailler avec les entrées/sorties.
Nous allons donc inclure une bibliothèque d'entrées/sorties : fstream

entete
Sélectionnez
//Les fichiers d'entete
#include <SDL/SDL.h>
#include <SDL/SDL_image.h>
#include <string>
#include <fstream>

fstream va nous permettre de lire/écrire un fichier.
C'est une bibliothèque standard C++ et non une partie de SDL, tout comme string, c'est pourquoi je ne décrirais pas plus son utilisation.

XIII-A-2. Structure du fichier de sauvegarde

Pour sauvegarder et charger des données, nous allons utiliser un fichier qui nous servira de fichier de sauvegarde.
Avant de se lancer dans le code, il nous faut savoir ce qu'on va stocker dans ce fichier.
Ici, ce dont on aura besoin de savoir est la position de notre point, les coordonnées donc qu'on peut maintenant récupérer grace aux accesseurs, ainsi que la couleur du fond d'écran de notre fenêtre SDL.
Pour les coordonnées, on va les mettre sur la première ligne de notre fichier.
Pour la couleur du fond, plutôt que de mettre les composante rgb (ce qu'on aurait très bien pu faire), on va plutot mettre la couleur de ce qu'on va appeler le niveau (white level, green level, etc.).

Au final, notre fichier texte contiendra des données telles que celle-ci :

données fichier de sauvegarde

XIII-B. Chargement des données

Dans ce tutoriel, le chargement des données se fera au lancement du programme.
Nous allons donc faire ce chargement au moment du chargement des fichiers et donc dans la méthode load_files(), après chargement des images.
Nous allons, pour charger un fichier, créer un objet ifstream.

load_file
bool load_files( Point &thisPoint, Uint32 &bg )
{
    //Chargement du point
    point = load_image( "dot.png" );

    //S'il y a eu un probleme au chargement du point
    if( point == NULL )
    {
        return false;
    }

    //Ouverture d'un fichier a lire
    std::ifstream load( "game_save" );

ifstream vous permet de récupérer ce qui est dans un fichier. Lorsque vous passez un nom de fichier au constructeur, il l'ouvre pour le lire.
Comme vous pouvez le voir, on passe en paramètre notre point thisPoint et la couleur du fond bg car on va en avoir besoin dans cette fonction.

Le code qui suit va nous permettre de récupérer les coordonnées de notre point dans le fichier :

load_file
    //Si le fichier est charge
    if( load != NULL )
    {
        //Le nom du niveau
        std::string level;

        //Recuperation des coordonnees
        load >> thisPoint.get_x();
        load >> thisPoint.get_y();

        //Si la coordonnee x est invalide
        if( ( thisPoint.get_x() < 0 ) || ( thisPoint.get_x() > SCREEN_WIDTH - POINT_WIDTH ) )
        {
            return false;
        }

        //Si la coordonnee y est invalide
        if( ( thisPoint.get_y() < 0 ) || ( thisPoint.get_y() > SCREEN_HEIGHT - POINT_HEIGHT ) )
        {
            return false;
        }

Lorsqu'il y a un problème lors du chargement du fichier, l'objet ifstream est NULL, c'est pourquoi nous testons la valeur de retour de celui-ci.

Si le fichier est bien chargé, nous déclarons notre niveau (level) qui va nous servir pour mettre le fond d'écran de notre fenêtre SDL à la bonne couleur.

Ensuite, nous récupérons les deux entiers correspondants aux coordonnées x et y de notre point depuis le fichier de sauvegarde.
Comme vous pouvez le voir, nous récupérons ces entiers avec ifstream comme nous le ferions en utilisant cin.
C'est parce que ce sont tous les 2 des istream (input stream).

Nous vérifions aussi si les coordonnées qu'on récupère ont un sens, si elles sont valides.
En effet, l'utilisateur peut facilement altérer le fichier de sauvegarde.

Maintenant qu'on a récupéré les coordonnées de notre point, on va récupérer la couleur du fond d'écran de notre fenêtre SDL.

load_file
        //On passe le caractere suivant ("\n")
        load.ignore();

        //On recupere la ligne suivante
        getline( load, level );

        //Si une erreur se produit pendant la recuperation des donnees
        if( load.fail() == true )
        {
            return false;
        }

Nous passons à la ligne suivante contenant la couleur du niveau avec la fonction ignore(), qui nous permet de passer le caractère suivant qui est ici un '\n'.
Nous récupérons donc la ligne suivante avec la fonction getline() cette fois ci.
La fonction getline() est différente de l'utilisation de '>>' comme fait précédemment pour la récupération des coordonnées. Cette fonction permet de récupérer tout jusqu'à la fin de la ligne.

Ensuite nous vérifions juste si il n'y a pas de problèmes durant la lecture du fichier.
La fonction fail() retourne true si il y a un problème.

Il nous reste maintenant à affecter la bonne couleur à notre variable bg représentant la couleur du fond d'écran de notre fenêtre SDL :

load_file
        //Si le niveau etait blanc
        if( level == "White Level" )
        {
            //On met le fond en blanc
            bg = SDL_MapRGB( screen->format, 0xFF, 0xFF, 0xFF );
        }
        //Si le niveau etait rouge
        else if( level == "Red Level" )
        {
            //On met le fond en rouge
            bg = SDL_MapRGB( screen->format, 0xFF, 0x00, 0x00 );
        }
        //Si le niveau etait vert
        else if( level == "Green Level" )
        {
            //On met le fond en vert
            bg = SDL_MapRGB( screen->format, 0x00, 0xFF, 0x00 );
        }
        //Si le niveau etait bleu
        else if( level == "Blue Level" )
        {
            //On met le fond en bleu
            bg = SDL_MapRGB( screen->format, 0x00, 0x00, 0xFF );
        }
        else{
            //Pas de correspondance
            return false
        }

Maintenant qu'on a récupéré notre niveau, il est facile d'affecter la bonne couleur à notre variable bg comme vous pouvez le voir.

On a fini de récupérer ce dont on avait besoin, il nous reste à fermer proprement le fichier :

load_file
        //Fermeture du fichier
        load.close();
    }

    //Si tout s'est bien passé
    return true;
}

Voilà pour le chargement des données depuis notre fichier de sauvegarde.
Ce système ne sert pas que pour les sauvegardes dans un jeu, il peut aussi servir pour pleins de choses telles que les fichiers de configuration par exemple.

Maintenant qu'on a fini la partie sur le chargement des données, on va attaquer la partie sur la sauvegarde des données.

XIII-C. Sauvegarde des données

Dans ce tutoriel, la sauvegarde des données se fera à la fermeture du programme, donc automatiquement.
C'est pourquoi nous allons implémenter ce mécanisme dans notre fonction de nettoyage clean_up qui est appelé à la fermeture de notre fenêtre SDL.

clean_up
void clean_up( Point &thisPoint, Uint32 &bg )
{
    //Liberation de la surface
    SDL_FreeSurface( point );

    //Ouverture d'un fichier pour ecrire
    std::ofstream save( "game_save" );

     if (!save) {
        std::cout << "Erreur d'ouverture du fichier de sauvegarde : sauvegarde échouée" << std::endl;
    }
    else {

        //Ecriture des coordonnees dans le fichier
        save << thisPoint.get_x();
        save << " ";
        save << thisPoint.get_y();
        save << "\n";

Dans cette fonction, on va créer un objet ofstream pour écrire dans le fichier.
Comme vous pouvez le voir, puisque l'utilisation de ifstream était similaire à celle de cin, il est logique que l'utilisation de ofstream soit similaire à celle de cout

Si l'ouverture de save échoue, la sauvegarde échoue aussi.

Dans cette partie du code, nous écrivons dans le fichier les coordonnées x et y de notre point, récupérées grâce aux accesseurs.

Il nous faut maintenant enregistrer la couleur de fond.

clean_up
        //Les valeurs rgb de la couleur du fond
        Uint8 r, g, b;

        //Recuperation des valeurs rgb de la couleur du fond
        SDL_GetRGB( bg, screen->format, &r, &g, &b );

Nous récupérons donc les valeurs rgb de notre fond en utilisant la fonction SDL_GetRGB().

Il nous reste à écrire dans le fichier la bonne couleur de niveau :

clean_up
        //Si le fond etait blanc
        if( ( r == 0xFF ) && ( g == 0xFF ) && ( b == 0xFF ) )
        {
            //On ecrit le type du niveau dans le fichier (blanc)
            save << "White Level";
        }
        //Si le fond etait rouge
        if( r == 0xFF && ( g != 0xFF ) )
        {
            //On ecrit le type du niveau dans le fichier (rouge)
            save << "Red Level";
        }
        //Si le fond etait vert
        if( g == 0xFF && ( b != 0xFF ) )
        {
            //On ecrit le type du niveau dans le fichier (vert)
            save << "Green Level";
        }
        //Si le fond etait bleu
        if( b == 0xFF && ( g != 0xFF ) )
        {
            //On ecrit le type du niveau dans le fichier (bleu)
            save << "Blue Level";
        }

Ce qui se fait très naturellement comme vous pouvez le constater.

Il nous reste à fermer proprement le fichier une fois encore ainsi que de quitter SDL proprement aussi :

clean_up
        //Fermeture du fichier
        save.close();
    }

    //On quitte SDL
    SDL_Quit();
}

Voilà pour la sauvegarde des données dans un fichier.

XIII-D. Main et boucle principale

Il nous reste maintenant à assembler tout ça dans la fonction principale, le main.

main
int main( int argc, char* args[] )
{
    //Ce qui va nous permettre de quitter
    bool quit = false;

    //Initialisation
    if( init() == false )
    {
        return EXIT_FAILURE;
    }

    //Le point qu'on va utiliser
    Point monPoint;

    //La couleur du fond
    Uint32 background = SDL_MapRGB( screen->format, 0xFF, 0xFF, 0xFF );

    //Le regulateur
    Timer fps;

    //Chargement des fichiers
    if( load_files( monPoint, background ) == false )
    {
        return EXIT_FAILURE;
    }

Comme vous pouvez le voir, nous utilisons notre fonction load_files().
Je ne pense pas avoir besoin de vous expliquer ce morceau de code, donc nous allons passer à la boucle principale :

boucle principale
    //Tant que l'utilisateur n'a pas quitte
    while( quit == false )
    {
        //On demarre le timer fps
        fps.start();

        //Tant qu'il y a un evenement
        while( SDL_PollEvent( &event ) )
        {
            //On recupere l'evenement pour le point
            monPoint.handle_input();

            //Si l'utilisateur a appuye sur une touche
            if( event.type == SDL_KEYDOWN )
            {
                //On change le fond selon la touche pressee
                switch( event.key.keysym.sym )
                {
                    case SDLK_1: background = SDL_MapRGB( screen->format, 0xFF, 0xFF, 0xFF ); break;
                    case SDLK_2: background = SDL_MapRGB( screen->format, 0xFF, 0x00, 0x00 ); break;
                    case SDLK_3: background = SDL_MapRGB( screen->format, 0x00, 0xFF, 0x00 ); break;
                    case SDLK_4: background = SDL_MapRGB( screen->format, 0x00, 0x00, 0xFF ); break;
                }
            }

            //Si l'utilisateur a clique sur le X de la fenetre
            if( event.type == SDL_QUIT )
            {
                //On quitte le programme
                quit = true;
            }
        }

        //On remplit l'ecran de blanc
        SDL_FillRect( screen, &screen->clip_rect, background );

        //On affiche le point sur l'ecran
        monPoint.show();

        //Mise à jour de l'ecran
        if( SDL_Flip( screen ) == -1 )
        {
            return EXIT_FAILURE;
        }

        //Tant que le timer fps n'est pas assez haut
        while( fps.get_ticks() < 1000 / FRAMES_PER_SECOND )
        {
            //On attend...
        }
    }

Voici donc la boucle principale.
Pour changer la couleur du fond d'écran de notre fenêtre SDL, vous avez donc simplement à appuyer sur les touches 1,2,3 et 4.
Le code reste semblable à ce que vous avez déjà pu voir dans mes tutoriels précédents.

A la fin du main, il nous reste plus qu'à lancer notre fonction de nettoyage et de sauvegarde :

boucle principale
    //Nettoyage et sauvegarde
    clean_up( monPoint, background );

    return EXIT_SUCCESS;
}

Ainsi, à la fin du programme SDL, nous sauvegardons nos données en plus de nettoyer ce qu'on a utilisé.

Télécharger les sources du chapitre XIII (192 ko)
Version pdf (83 ko - 11 pages)


Tutoriel SDL

Chapitre XIV : Scrolling avec SDL

  • XIV. Scrolling
    • XIV-1. Scrolling de la caméra
    • XIV-2. Scrolling du background
    • Sources et pdf
  • Remerciements

XIV. Scrolling

Jusqu'à maintenant, nous avons travaillé avec des environnements en 640 par 480 (de la taille de notre fenêtre SDL).
Ce tutoriel va vous apprendre à vous déplacer à travers des environnements de n'importe quelle taille en utilisant le principe du scrolling (défilement).

XIV-1. Scrolling de la caméra

Quand on fait un jeu qui requiert du scrolling, la seule différence est qu'il nous faut ce qu'on appelle une caméra.
Vu qu'on ne peut pas afficher le niveau en entier à l'écran qui est trop petit, il nous suffit de prendre la partie du niveau qu'on veut afficher (généralement celle contenant notre élément actif).
Par exemple sur l'image suivante, la partie qu'on va afficher est entourée en rouge :

Il faut penser aussi que la caméra doit suivre notre point sur l'image si celui se met à bouger.
Dans cette partie du tutoriel, nous allons créer ce programme.

Dans la déclaration des variables, on va voir apparaître quelques nouveautés.

variables
//Les attributs de l'écran (640 * 480)
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
const int SCREEN_BPP = 32;

//Le nombre de frame par seconde
const int FRAMES_PER_SECOND = 30;

//Les dimensions du point (l'image qu'on va deplacer)
const int POINT_WIDTH = 20;
const int POINT_HEIGHT = 20;

//Vitesse de deplacement du point (en pixel par seconde)
const int POINT_VITESSE = 200;

//Les dimensions du niveau
const int LEVEL_WIDTH = 1280;
const int LEVEL_HEIGHT = 960;

//Les surfaces
SDL_Surface *point = NULL;
SDL_Surface *background = NULL;
SDL_Surface *screen = NULL;

//La structure d'événements que nous allons utiliser
SDL_Event event;

//La camera
SDL_Rect camera = { 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT };

Comme vous le voyez, on a définie les dimensions de notre niveau en plus de celles de la fenêtre SDL, celle-ci étant plus petite que le niveau.
On voit aussi apparaître la caméra qui est tout simplement un SDL_Rect car de forme rectangulaire.
On initialise cette caméra à la taille de notre fenêtre SDL car ce que l'on va afficher sera de la taille de notre fenêtre SDL.

Encore une fois, nous allons devoir revoir notre classe Point afin d'y ajouter la caméra :

classe Point
class Point
{
    private:
    //Les coordonnées x et y du point
    int x, y;

    //La vitesse du point
    int xVel, yVel;

    public:
    //Initialisation des variables
    Point();

    //Recupere la touche pressee et ajuste la vitesse du point
    void handle_input();

    //Bouge le point
    void move();

    //Montre le point sur l'ecran
    void show();

    //Met la camera sur le point
    void set_camera();
};

C'est la même chose qu'avant, sauf qu'on ajoute une fonction set_camera() pour centrer la caméra sur notre point.

Autre chose que l'on va retoucher légèrement est la fonction move() de notre Point.
En effet, on ne va plus tester les collisions avec les bords de notre fenêtre SDL et donc de l'écran mais avec les bords du niveau.

move()
void Point::move()
{
    //Bouge le point à gauche ou à droite
    x += xVel;

    //Si le point se rapproche trop des limites(gauche ou droite) de l'ecran
    if( ( x < 0 ) || ( x + POINT_WIDTH > LEVEL_WIDTH ) )
    {
        //On revient
        x -= xVel;
    }

    //Bouge le point en haut ou en bas
    y += yVel;

    //Si le point se rapproche trop des limites(haute ou basse) de l'ecran
    if( ( y < 0 ) || ( y + POINT_HEIGHT > LEVEL_HEIGHT ) )
    {
        //On revient
        y -= yVel;
    }
}

Comme vous pouvez le voir, la différence par rapport à avant est minime.
Nous ne gardons donc plus notre point dans l'écran mais dans notre niveau.

Nous allons maintenant regarder la fonction set_camera().

set_camera()
void Point::set_camera()
{
    //Centre la camera sur le point
    camera.x = ( x + POINT_WIDTH / 2 ) - SCREEN_WIDTH / 2;
    camera.y = ( y + POINT_HEIGHT / 2 ) - SCREEN_HEIGHT / 2;

Quand nous mettons en place la caméra, dans un premier temps nous la centrons autour du point (le point étant le centre de la caméra).
Cependant si nous ne faisons que ça, quand le point va se rapprocher des bords du niveau, on va voir apparaître du vide comme c'est le cas sur l'image ci-dessous :

Pour éviter cela, on va arrêter le mouvement de la caméra lorsque le point arrive vers les extrémités du niveau.
Cela aura pour conséquence de ne plus voir de vide et de voir le point continuer à bouger alors que la caméra s'est arrêtée.

set_camera() : suite
    //Garde la camera sur les bords
    if( camera.x < 0 )
    {
        camera.x = 0;
    }
    if( camera.y < 0 )
    {
        camera.y = 0;
    }
    if( camera.x > LEVEL_WIDTH - camera.w )
    {
        camera.x = LEVEL_WIDTH - camera.w;
    }
    if( camera.y > LEVEL_HEIGHT - camera.h )
    {
        camera.y = LEVEL_HEIGHT - camera.h;
    }
}

Si une partie de la caméra va en dehors du niveau, nous la remettons tout simplement dans le niveau, c'est le même principe de collision avec un objet statique vu dans le chapitre XI.

La fonction pour afficher le point va aussi changer un peu.
En effet, maintenant on va l'afficher sur l'écran relativement à la caméra.

show()
void Point::show()
{
    //Affiche le point
    apply_surface( x - camera.x, y - camera.y, point, screen );
}

Dans notre boucle principale du main, voici comment ça se déroule :

boucle principale
//Tant que l'utilisateur n'a pas quitter
    while( quit == false )
    {
        //On demarre le timer fps
        fps.start();

        //Tant qu'il y a un événement
        while( SDL_PollEvent( &event ) )
        {
            //On recupere l'evenement pour le point
            monPoint.handle_input();

            //Si l'utilisateur a cliqué sur le X de la fenêtre
            if( event.type == SDL_QUIT )
            {
                //On quitte the programme
                quit = true;
            }
        }

        //Bouge le point
        monPoint.move();

        //Met la camera
        monPoint.set_camera();

        //Affiche le fond
        apply_surface( 0, 0, background, screen, &camera );

        //Affiche le point sur l'écran
        monPoint.show();

On bouge le point, on met la caméra en place, on affiche la partie du fond dans la caméra puis on affiche le point.
Rien de bien sorcier.

Vous pouvez retrouver les sources du programme de cette partie du tutoriel ici :
Télécharger les sources du chapitre XIV-1 (854 ko)

XIV-2. Scrolling du background

Il peut arriver qu'on ait besoin d'un niveau de taille infinie, dans ce cas il serait impossible de passer le niveau comme précédemment.
Une solution est de faire défiler un fond en boucle.

Il peut aussi arriver que vous vouliez faire un jeu avec un niveau prédéfinit particulièrement long et que ce niveau avance tout seul, comme le célèbre vieux jeu R-Type.

Cette partie du tutoriel a pour but de créer un programme capable de faire un défilement du niveau.

Le défilement horizontal d'un niveau ressemble à quelque chose comme ça :

Il est bien sûr possible de faire un défilement vertical avec le même principe.

En réalité, le fond est blitté de multiples fois comme si une caméra avançait dessus.
Quand on arrive au bout du fond, on le remet au début, ce qui nous donne une impression d'infinie.

Pour débuter, il nous faut donc mettre le fond au départ :

coordonnees de depart du fond
//Les coordonnées du fond
    int bgX = 0, bgY = 0;

Vu que le fond bouge, nous devons garder en mémoire ses coordonnées.

Dans la boucle principale du main, on va bouger ce fond :

boucle principale : deplacement du fond
Sélectionnez
//Tant que l'utilisateur n'a pas quitter
    while( quit == false )
    {
        //On demarre le timer fps
        fps.start();

        //Tant qu'il y a un événement
        while( SDL_PollEvent( &event ) )
        {
            //Si l'utilisateur a cliqué sur le X de la fenêtre
            if( event.type == SDL_QUIT )
            {
                //On quitte the programme
                quit = true;
            }
        }

        //Defilement du fond
        bgX -= 2;

        //Si le fond est allé trop loin
        if( bgX <= -background->w )
        {
            //On le remet à zero
            bgX = 0;
        }

Après la récupération des événements, nous déplaçons le fond vers la gauche.
Ensuite nous vérifions si celui-ci est allé trop loin et nous le remettons au début le cas échéant.

Il nous reste à l'afficher :

boucle principale : affichage
//Affichage du fond
        apply_surface( bgX, bgY, background, screen );
        apply_surface( bgX + background->w, bgY, background, screen );

        //Affcihage du point
        apply_surface( 310, 230, point, screen );

Nous affichons le fond sur l'écran placé avec ses coordonnées.
Pour mieux voir le rendu de ce défilement, nous ajoutons un point statique (je vous laisse le soin de le rendre dynamique).

Les sources de ce programme sont téléchargeable ici :
Télécharger les sources du chapitre XIV-2 (306 ko)

Sources et pdf

Télécharger les sources du chapitre XIV-1 (854 ko)
Télécharger les sources du chapitre XIV-2 (306 ko)

Version pdf (310 ko - 11 pages)


Tutoriel SDL

Chapitre XV : Gestion des événements avancés avec SDL

  • XV. Gestion des événements avancés
    • XV-1. Événements souris
    • XV-2. Événements joysticks
    • XV-3. Événements fenêtre
  • Sources et pdf

XV. Gestion des événements avancés

Dans ce tutoriel, nous allons apprendre à gérer les événements autre que les événements claviers vu dans le chapitre IV : Gestion des événements avec SDL.

Nous allons voir en particulier la gestion des événements suivant :
- Les événements souris.
- Les événements joysticks.
- Les événements fenêtre.

XV-1. Événements souris

Pour apprendre à gérer les événements souris, on va faire un programme d'exemple contenant un simple bouton cliquable.
Donc, pour les besoins de ce tutoriel, on va créer un bouton :

Comme vous pouvez le voir, les différents états de notre bouton sont décomposés dans une feuille de sprite.
Il va donc nous falloir la découper :

set_clips()
void set_clips()
{
    //On coupe la partie en haut à gauche (premier sprite)
    clips[ CLIP_MOUSEOVER ].x = 0;
    clips[ CLIP_MOUSEOVER ].y = 0;
    clips[ CLIP_MOUSEOVER ].w = 320;
    clips[ CLIP_MOUSEOVER ].h = 240;

    //On coupe la partie en haut à droite (second sprite)
    clips[ CLIP_MOUSEOUT ].x = 320;
    clips[ CLIP_MOUSEOUT ].y = 0;
    clips[ CLIP_MOUSEOUT ].w = 320;
    clips[ CLIP_MOUSEOUT ].h = 240;

    //On coupe la partie en bas à gauche (troisième sprite)
    clips[ CLIP_MOUSEDOWN ].x = 0;
    clips[ CLIP_MOUSEDOWN ].y = 240;
    clips[ CLIP_MOUSEDOWN ].w = 320;
    clips[ CLIP_MOUSEDOWN ].h = 240;

    //On coupe la partie en bas à droite (quatrième sprite)
    clips[ CLIP_MOUSEUP ].x = 320;
    clips[ CLIP_MOUSEUP ].y = 240;
    clips[ CLIP_MOUSEUP ].w = 320;
    clips[ CLIP_MOUSEUP ].h = 240;
}

Les variables CLIP_MOUSEOVER, CLIP_MOUSEOUT, CLIP_MOUSEDOWN et CLIP_MOUSEUP sont définies plus haut dans notre code avec pour valeurs respectives 0, 1, 2 et 3.
Si vous ne comprenez pas ce code, je vous renvoi au Chapitre VI : Sprites.

Pour bien travailler sur notre bouton, nous allons créer une classe bouton :

classe bouton
//La classe Button
class Button
{
    private:
    //Les attributs du bouton
    SDL_Rect box;

    //La partie de la feuille de sprite qui va être affichée
    SDL_Rect* clip;

    public:
    //Initialisation des variables
    Button( int x, int y, int w, int h );

    //Recuperation des événements et mise en place du bouton
    void handle_events();

    //Affiche le bouton à l'écran
    void show();
};

Dans cette classe, nous avons un rectangle qui définie la position et les dimensions du bouton.
Nous avons aussi un pointeur vers la feuille de sprite qu'on va utiliser.

Ensuite nous avons le constructeur qui nous permet d'initialiser le bouton avec les arguments en paramètre.
Puis nous avons notre fonction de gestion des événements que nous verrons plus en détail par la suite.

Pour finir nous avons la fonction show() qui nous permet tout simplement d'afficher le bouton.

Nous allons commencer par voir le constructeur :

constructeur bouton
Button::Button( int x, int y, int w, int h )
{
    //Initialisation des variables du bouton
    box.x = x;
    box.y = y;
    box.w = w;
    box.h = h;

    //Initialisation du sprite par defaut pour le bouton
    clip = &clips[ CLIP_MOUSEOUT ];
}

Le constructeur est assez simple.
Il initialise les coordonnées x et y du bouton ainsi que sa hauteur et sa largeur (h et w).
Il initialise en plus le sprite par défaut pour le bouton venant de la feuille de sprite qu'on a découpé précédemment.

Nous allons passer à une partie importante : la récupération des événements sur le bouton.
Comme nous nous occupons de récupérer les événements souris sur ce bouton, il nous faut dans un premier temps vérifier si la souris bouge ou non.

handle_events() : mouvement souris
Sélectionnez
void Button::handle_events()
{
    //Les coordonnees de la souris
    int x = 0, y = 0;

    //Si la souris a bougee
    if( event.type == SDL_MOUSEMOTION )
    {

Pour vérifier si la souris bouge, il existe un événement SDL approprié : SDL_MOUSEMOTION.
Une autre chose intéressante à récupérer de la souris, ce sont ses coordonnées. On va initialiser les variables qui nous serviront pour les coordonnées à 0 comme fait sur le code ci-dessus.
Encore une fois, SDL nous simplifie la tache car une fois qu'on a capturé l'événement de mouvement de la souris, il est très simple de récupérer les coordonnées de la souris en mouvement, ce qui est fait dans cette partie du code :

handle_events() : récupération coordonnées souris
        //Recuperation des coordonnees de la souris
        x = event.motion.x;
        y = event.motion.y;

Maintenant que nous savons quand la souris bouge et comment récupérer ses coordonnées, nous pouvons par exemple vérifier si la souris se trouve ou non dans la zone de notre bouton, chose intéressante pour, par exemple, faire un menu de jeu.
Pour faire cette partie, il suffit de vérifier si les coordonnées de la souris se trouvent bien dans le bouton ou non.

handle_events() : tests souris - bouton
        //Si la souris est dans le bouton
        if( ( x > box.x ) && ( x < box.x + box.w ) && ( y > box.y ) && ( y < box.y + box.h ) )
        {
            //Mise à jour du sprite du bouton
            clip = &clips[ CLIP_MOUSEOVER ];
        }
        //Si non
        else
        {
            //Mise à jour du sprite du bouton
            clip = &clips[ CLIP_MOUSEOUT ];
        }
    }

x et y sont les coordonnées de la souris, on vérifie que box.x < x < box.x + box.w et que box.y < y < box.y + box.h. box étant notre bouton bien entendu.
Pour mieux comprendre les tests effectués, je vous invite à regarder le schéma ci-dessous :

Voila, nous sommes capable de savoir quand la souris est dans le bouton et quand elle n'y est pas.
Pour bien montrer dans notre programme que nous savons le faire, on va changer l'affichage du bouton en changeant de sprite comme c'est écrit dans le code ci-dessus.

Une autre chose intéressante lorsqu'on veut faire un menu de jeu par exemple, est de savoir quand est-ce qu'on clique sur un bouton du menu.
Cette partie est faites dans le code ci-dessous :

handle_events() : test clique souris - bouton
    //Si un bouton de la souris est pressee
    if( event.type == SDL_MOUSEBUTTONDOWN )
    {
        //Si le bouton gauche de la souris est pressee
        if( event.button.button == SDL_BUTTON_LEFT )
        {
            //Recuperation des coordonnees de la souris
            x = event.button.x;
            y = event.button.y;

            //Si la souris est dans le bouton
            if( ( x > box.x ) && ( x < box.x + box.w ) && ( y > box.y ) && ( y < box.y + box.h ) )
            {
                //Mise à jour du sprite du bouton
                clip = &clips[ CLIP_MOUSEDOWN ];
            }
        }
    }
    //Si un bouton de la souris est relache
    if( event.type == SDL_MOUSEBUTTONUP )
    {
        //si c'est le bouton gauche de la souris qui est relache
        if( event.button.button == SDL_BUTTON_LEFT )
        {
            //Recuperation des coordonnees de la souris
            x = event.button.x;
            y = event.button.y;

            //Si la souris est dans le bouton
            if( ( x > box.x ) && ( x < box.x + box.w ) && ( y > box.y ) && ( y < box.y + box.h ) )
            {
                //Mise à jour du sprite du bouton
                clip = &clips[ CLIP_MOUSEUP ];
            }
        }
    }
}

Pour savoir si un bouton de la souris est appuyé, SDL nous aide grâce à SDL_MOUSEBUTTONDOWN.
De même pour savoir si un bouton de la souris est relâche avec SDL_MOUSEBUTTONUP.
Une fois qu'on sait si un bouton est appuyé, on peut aussi vérifier lequel est appuyé. Dans notre cas, on va vérifier le clique gauche de la souris et donc vérifier si l'événement clique de la souris est bien SDL_BUTTON_LEFT.
Pour savoir si on a bien fais un clique gauche sur le bouton, on vérifie donc si on clique sur un bouton de la souris, si c'est le clique gauche de la souris qui a été pressé et si on se trouve bien dans le bouton.
De même qu'avant, on va changer le sprite du bouton pour montrer qu'on a cliqué dessus.

Le traitement est assez similaire pour le relâchement du bouton gauche de la souris.

Voila, vous êtes maintenant capable de capter les événements de votre souris.

On aurait pu traiter cette partie de code plus efficacement ou plus intelligemment, par exemple en utilisant SDL_GetMouseState() et en ne refaisant pas les tests pour savoir si la souris est bien dans le bouton ou non lors du clique (avec par exemple l'enregistrement des différents états par lequel passe le bouton).

Je vous laisse vous débrouiller pour améliorer ce code, vous devriez en être capable.

Il existe bien évidemment d'autres événements souris, comme la gestion de la molette de la souris avec les événements SDL_BUTTON_WHEELDOWN et SDL_BUTTON_WHEELUP.
Attention, car ces deux événements se récupèrent avec l'événement SDL_MOUSEBUTTONDOWN comme le suggère le code suivant :

molette souris
if( event.type == SDL_MOUSEBUTTONDOWN ){
    if( event.button.button == SDL_BUTTON_WHEELDOWN ){
        ...
    }
}

On pourrait envisager par exemple de gérer un zoom/dezoom avec la molette de la souris en utilisant la librairie SDL_gfx.

Il existe aussi de nombreuses fonctions sympathiques comme la fonction SDL_ShowCursor(bool show) qui permet de masquer ou non le curseur de la souris (ce qui peut être pratique).

Télécharger les sources du chapitre XV-1 (110 ko)

XV-2. Événements joysticks

Dans cette partie, nous allons voir comment gérer les événements joystick.
Pour illustrer la capture et l'utilisation des événements joystick, nous allons revenir sur le chapitre X et utiliser le joystick pour bouger le point au lieu du clavier.
Gérer les événements joystick est légèrement différent de la gestion des autres événements.

Tout d'abord, les joysticks ont leur propres types de données dans SDL qui est SDL_Joystick. Il nous faut donc déclarer notre joystick au début de notre programme :

déclaration du joystick
//Le joystick qu'on va utiliser
    SDL_Joystick *stick = NULL;

La classe Point ne va pas changer, elle reste exactement la même, la seule chose qui change c'est la façon dont on va récupérer les événements et donc le contenu de la fonction handle_input().

Cependant, avant de plonger dans le vif du sujet, il nous faut apprendre à mettre en place correctement notre joystick à l'initialisation :

init()
bool init()
{
    //Initialisation de tous les sous-système de SDL
    if( SDL_Init( SDL_INIT_EVERYTHING ) == -1 )
    {
        return false;
    }

    //Mise en place de l'écran
    screen = SDL_SetVideoMode( SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP, SDL_SWSURFACE );

    //S'il y a eu une erreur lors de la mise en place de l'écran
    if( screen == NULL )
    {
        return false;
    }

    //Verification s'il y a un Joystick
    if( SDL_NumJoysticks() < 1 )
    {
        std::cout << "Erreur : pas de joystick branché" << std::endl;
        return false;
    }

    //Ouverture du Joystick
    stick = SDL_JoystickOpen( 0 );

    //S'il y a un problème à l'ouverture du Joystick
    if( stick == NULL )
    {
        return false;
    }

    //Mise en place de la barre caption
    SDL_WM_SetCaption( "bouge le Point", NULL );

    //Si tout s'est bien passé
    return true;
}

En effet, ce qui change entre récupérer des événements clavier par exemple et récupérer des événements joystick, c'est qu'il nous faut initialiser le joystick.

Dans un premier temps, il nous faut vérifier s'il y a bien au moins un joystick de branché grâce à la fonction SDL_NumJoysticks().
Si effectivement il y a bien au moins un joystick de branché, nous ouvrons le premier disponible en utilisant SDL_JoystickOpen().
Le premier joystick disponible est le joystick 0.
S'il y a un problème pour l'ouverture du joystick, notre variable stick retourne NULL.

De même que l'initialisation change avec l'ajout du joystick, notre fonction de nettoyage change dans le même sens :

clean_up()
void clean_up()
{
    //Libèration des surfaces
    SDL_FreeSurface( point );

    //Fermeture du Joystick
    SDL_JoystickClose( stick );

    //On quitte SDL
    SDL_Quit();
}

Il nous faut fermer le joystick qu'on a ouvert grâce à la fonction SDL_JoystickClose().

Maintenant que nous savons comment bien initialiser notre joystick, nous allons entrer dans le vif du sujet avec la fonction handle_input() :

handle_input() : partie 1
void Point::handle_input()
{
    //Si un axe a changé
    if( event.type == SDL_JOYAXISMOTION )
    {
        //Si le joystick 0 a bougé
        if( event.jaxis.which == 0 )
        {
            //Si l'axe X a changé
            if( event.jaxis.axis == 0 )
            {
                //Si l'axe X est neutre
                if( ( event.jaxis.value > -8000 ) && ( event.jaxis.value < 8000 ) )
                {
                    xVel = 0;
                }
                //Si non
                else
                {
                    //Ajustement de la vitesse
                    if( event.jaxis.value < 0 )
                    {
                        xVel = -POINT_WIDTH / 2;
                    }
                    else
                    {
                        xVel = POINT_WIDTH / 2;
                    }
                }
            }

Lorsque le joystick bouge, un événement SDL_JOYAXISMOTION apparaît.

Nous vérifions dans un premier temps que le joystick qui a bougé est bien le joystick 0. Vous allez sans doute trouver ça pointilleux sachant que le seul joystick initialisé ici est le 0, mais c'est une bonne habitude à prendre de vérifier quoi qu'il arrive.

Ensuite, nous vérifions sur quel axe il a bougé. Sur la plupart des joysticks moderne, l'axe X est 0 et l'axe Y est 1.

Enfin, nous vérifions si la valeur de l'axe X du joystick est entre -8000 et +8000, dans ce cas, nous considérons qu'il est neutre et que donc le point ne doit pas bouger.

Vous devez penser que c'est une plage très large pour le considérer comme neutre, il faut cependant savoir que la valeur de l'axe du joystick se trouve entre les valeurs -32768 et +32768.
Essayez avec des valeurs plus petites pour vous faire une idée.

Si le joystick n'est pas considéré comme neutre, alors on change la vitesse du point.

Il en est de même pour l'axe Y :

handle_input() : partie 2
            //Si l'axe Y a changé
            else if( event.jaxis.axis == 1 )
            {
                //Si l'axe Y est neutre
                if( ( event.jaxis.value > -8000 ) && ( event.jaxis.value < 8000 ) )
                {
                    yVel = 0;
                }
                //Si non
                else
                {
                    //Ajustement de la vitesse
                    if( event.jaxis.value < 0 )
                    {
                        yVel = -POINT_HEIGHT / 2;
                    }
                    else
                    {
                        yVel = POINT_HEIGHT / 2;
                    }
                }
            }
        }
    }
}

Vous voila prêt à gérer les événements joystick, le reste ne changeant pas.

Télécharger les sources du chapitre XV-2 (195 ko)

Gérer les événements SDL_JoyAxisEvent est ce qu'il y a de plus difficile à gérer lorsqu'on gère les événements d'un joystick.
Récupérer les autres événements comme SDL_JoyBallEvent, SDL_JoyHatEvent ou encore SDL_JoyButtonEvent est assez facile à faire en regardant rapidement la documentation de SDL.
Je vais cependant en parler rapidement.

Pour ce qui est des boutons, après avoir vérifié que l'événement SDL_JoyButtonEvent est récupéré, on peut utiliser les événements SDL_JOYBUTTONDOWN ou SDL_JOYBUTTONUP afin de savoir si le bouton est pressé ou relâché.
Ensuite pour savoir quel bouton est pressé (ou relâche), on utilise event.jbutton.button comme ceci :

bouton 0 pressé
// Boutons du Joystick
case SDL_JOYBUTTONDOWN:
    if ( event.jbutton.button == 0 )
    {
        // Code pour le bouton 0
    }
    break;

Pour ce qui est des chapeaux, après avoir vérifié que l'événement SDL_JoyHatEvent est récupéré, on peut connaître la direction en utilisant les événements suivant :

événements direction chapeau
SDL_HAT_CENTERED
SDL_HAT_UP
SDL_HAT_RIGHT
SDL_HAT_DOWN
SDL_HAT_LEFT

SDL_HAT_RIGHTUP
SDL_HAT_RIGHTDOWN
SDL_HAT_LEFTUP
SDL_HAT_LEFTDOWN

En effet, les chapeaux ne peuvent être dirigés que dans une seule direction à la fois et pour savoir si le chapeau a bougé, on utilise l'événement SDL_JOYHATMOTION :

coordonnées de depart du fond
// Mouvement du chapeau
case SDL_JOYHATMOTION:
    if ( event.jhat.hat & SDL_HAT_UP )
    {
        // Vers le haut
    }
    else if ( event.jhat.hat & SDL_HAT_RIGHTDOWN )
    {
        // Vers la diagonale droit-bas
    }
    break;

Pour le mouvement de la boule, après avoir vérifié que l'événement SDL_JoyBallEvent est récupéré, on utilise SDL_JOYBALLMOTION. La récupération du mouvement en x et y se fait à l'aide de event.jball.xrel et event.jball.yrel.
Ces derniers nous donnent une position relative et non absolue contrairement à la souris.

SDL_JOYBALLMOTION : tiré de la doc SDL
case SDL_JOYBALLMOTION:
    printf ("Trackball axis %i ",event.jball.ball);
    printf ("on joystick %i ", event.jball.which);
    printf ("moved to (%i,%i)\n", event.jball.xrel, event.jball.yrel);
    break;

La librairie SDL nous permet aussi de connaître les caractéristiques du matériel, surtout pour les joysticks, grâce aux fonctions suivantes : SDL_JoystickNumAxes , SDL_JoystickNumButtons, SDL_JoystickNumBalls, SDL_JoystickNumHats... qui retournent des int et prennent l'élément SDL_Joystick comme argument.

XV-3. Événements fenêtre

Jusqu'à présent, nos fenêtres SDL étaient de taille fixe, n'étaient pas redimensionnables et n'étaient pas forcement bien adaptés à l'écran.
Le but de cette partie du tutoriel est de pouvoir capturer les événements fenêtre afin de gérer le redimensionnement, le passage en mode plein écran, et d'autres choses que vous pourriez trouver utiles dans le futur.

Commençons par construire une classe fenêtre (Window en anglais) :

classe Window
// Notre classe fenetre
class Window
{
    private:
    //variable qui indique si on est en mode fenétré ou non
    bool windowed;

    //variable qui dit si notre fenêtre est ok
    bool windowOK;

    public:
    //Constructeur
    Window();

    //Recuperation des événements de la fenêtre
    void handle_events();

    //plein écran ou non
    void toggle_fullscreen();

    //Verifie si quelque chose ne va pas avec la fenetre
    bool error();
};

Nous avons tout d'abord deux variables booléennes.
La première va nous permettre de savoir si la fenêtre est en mode fenêtré ou en mode plein écran.
La seconde nous indique si la fenêtre est opérationnelle tout simplement.

Ensuite nous avons un constructeur, un récupérateur d'événements sur notre fenêtre, une fonction pour le passage en mode plein écran ou en mode fenêtré et pour finir une fonction de vérification d'erreurs.

Nous allons revoir notre fonction d'initialisation car nous allons devoir mettre tout ce qui concerne la fenêtre dans le constructeur de la fenêtre.

fonction init()
bool init()
{
    //Initialisation de tout les sous-systemes
    if( SDL_Init( SDL_INIT_EVERYTHING ) == -1 )
    {
        return false;
    }

    //si tout s'est bien passé
    return true;
}

Ainsi, maintenant dans la fonction init(), on ne s'occupe que d'initialiser SDL.

Toute la mise en place de la fenêtre se passe dans le constructeur que voici :

constructeur de la classe Window
Window::Window()
{
    //Mise en place de l'écran
    screen = SDL_SetVideoMode( SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP, SDL_SWSURFACE | SDL_RESIZABLE );

    //S'il y a une erreur
    if( screen == NULL )
    {
        windowOK = false;
        return;
    }
    else
    {
        windowOK = true;
    }

    //on met en place la barre caption de la fenetre
    SDL_WM_SetCaption( "Test Evenements Fenetre", NULL );

    windowed = true;
}

Dans cette classe, nous créons une fenêtre qui peut être redimensionnable.
Ainsi, vous pouvez voir apparaître le SDL_RESIZABLE en paramètre à la fonction SDL_SetVideoMode().

Ensuite vient le test habituel afin de savoir si la fenêtre a bien été créée ou non, cependant on met à jour la variable windowOK qui va nous dire qu'il y a un problème.

Enfin, si tout se passe bien, nous mettons cette variable windowOK à true, ainsi que la variable windowed qui nous indique qu'on démarre en mode fenêtré.

Le changement de mode va se faire avec la fonction toggle_fullscreen() :

Fonction toggle_fullscreen()
void Window::toggle_fullscreen()
{
    //Si on est en mode fenetré
    if( windowed == true )
    {
        //On met en plein écran
        screen = SDL_SetVideoMode( SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP, SDL_SWSURFACE | SDL_RESIZABLE | SDL_FULLSCREEN );

        //S'il y a une erreur
        if( screen == NULL )
        {
            windowOK = false;
            return;
        }

        //Mise à jour de la variable
        windowed = false;
    }
    //Si on est en plein écran
    else if( windowed == false )
    {
        //On passe en mode fenetré
        screen = SDL_SetVideoMode( SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP, SDL_SWSURFACE | SDL_RESIZABLE );

        //S'il y a une erreur
        if( screen == NULL )
        {
            windowOK = false;
            return;
        }

        //Mise à jour de la variable
        windowed = true;
    }
}

La possibilité de passer en mode plein écran ou en mode fenêtré n'est possible seulement grâce au fait qu'on est avec une fenêtre redimensionnable.

Pour le passage en mode plein écran ou en mode fenêtré, on vérifie dans un premier temps dans quel mode nous nous trouvons grâce à la variable windowed qui est tenu à jours.

Si on est en mode fenêtré, alors on passe en mode plein écran en appelant la fonction SDL_SetVideoMode() avec comme paramètre supplémentaire SDL_FULLSCREEN.
Si on est déjà en mode plein écran, on revient en mode fenêtré en faisant appel à la même fonction SDL_SetVideoMode() sans le paramètre SDL_FULLSCREEN.

Le passage d'un mode à l'autre va se faire en capturant un événement clavier (la pression de la touche entrée) comme nous allons le voir dans la fonction de récupération des événements de la fenêtre.

handle_events : redimmension
void Window::handle_events()
{
    //Si quelque chose ne va pas avec notre fenetre
    if( windowOK == false )
    {
        return;
    }

    //Si la fenetre est redimmensionnee
    if( event.type == SDL_VIDEORESIZE )
    {
        //Redimmensionnement de la fenetre
        screen = SDL_SetVideoMode( event.resize.w, event.resize.h, SCREEN_BPP, SDL_SWSURFACE | SDL_RESIZABLE );

        //S'il y a une erreur
        if( screen == NULL )
        {
            windowOK = false;
            return;
        }
    }

Voici le début de notre fonction handle_events().
Dans un premier temps, on vérifie si notre fenêtre est bien opérationnelle.

Nous allons ensuite attraper et gérer la redimension de la fenêtre.
L'événement SDL correspondant au redimensionnement de la fenêtre est SDL_VIDEORESIZE. Si la fenêtre est redimensionnée, on récupère les nouvelles dimensions grâce à event.resize.w pour obtenir la nouvelle largeur de la fenêtre et event.resize.h pour la hauteur.
Comme vous pouvez le voir, quand nous redimensionnons la fenêtre, on fait de nouveau appel à SDL_SetVideoMode(), il nous faut donc de nouveau vérifier si notre fenêtre est bien construite.

Dans la suite de notre fonction, on va faire le passage en mode plein écran ou en mode fenêtré en récupérant un événement clavier (la pression de la touche entrée) :

handle_events : toggle_fullscreen()
    //Si la touche entrée a été préssée
    else if( ( event.type == SDL_KEYDOWN ) && ( event.key.keysym.sym == SDLK_RETURN ) )
    {
        //mode plein écran on/off
        toggle_fullscreen();
    }

Nous avons déjà vu les événements clavier, donc si quelque chose pose problème, vous pouvez toujours revenir au chapitre IV : gestion des événements avec SDL.

Un autre type d'événement fenêtre fourni par SDL est SDL_ACTIVEEVENT.
Cet événement va nous indiquer s'il y a eu un changement sur le focus. Cet événement arrive lorsque l'écran, la souris ou le clavier deviennent actif ou inactif.

handle_events : changement de focus sur la fenêtre
    //Si le focus de la fenetre a changé
    else if( event.type == SDL_ACTIVEEVENT )
    {
        //Si la fenetre a été réduite ou remise en place
        if( event.active.state & SDL_APPACTIVE )
        {
            //Si l'application n'est plus active
            if( event.active.gain == 0 )
            {
                SDL_WM_SetCaption( "Test Evenements Fenetre : Reduite", NULL );
            }
            else
            {
                SDL_WM_SetCaption( "Test Evenements Fenetre", NULL );
            }
        }

Dans un premier temps, on va regarder comment gérer le changement de focus sur la fenêtre.
On va vérifier si la fenêtre devient active ou inactive en passant du mode réduit ou non. L'événement SDL qui nous indique le changement de focus sur la fenêtre est SDL_APPACTIVE.
Lorsque la fenêtre est réduite, gain dans la structure SDL_ActiveEvent est à 0. Dans le cas contraire (où la fenêtre n'est pas réduite), il est à 1.

Dans ce programme, nous changeons la barre de caption afin de notifier à l'utilisateur le changement.

Toujours dans cette partie, nous allons vérifier s'il y a eu un changement de focus au niveau du clavier ou de la souris :

handle_events : changement de focus sur le clavier ou la souris
        //Si quelque chose se passe avec le focus du clavier
        else if( event.active.state & SDL_APPINPUTFOCUS )
        {
            //Si l'application perd le focus du clavier
            if( event.active.gain == 0 )
            {
                SDL_WM_SetCaption( "Test Evenements Fenetre : Focus du clavier perdu", NULL );
            }
            else
            {
                SDL_WM_SetCaption( "Test Evenements Fenetre", NULL );
            }
        }
        //Si quelque chose se passe avec le focus de la souris
        else if( event.active.state & SDL_APPMOUSEFOCUS )
        {
            //Si l'application perd le focus de la souris
            if( event.active.gain == 0 )
            {
                SDL_WM_SetCaption( "Test Evenements Fenetre : Focus de la souris perdu", NULL );
            }
            else
            {
                SDL_WM_SetCaption( "Test Evenements Fenetre", NULL );
            }
        }
    }

SDL_APPINPUTFOCUS nous indique si il y a eu un changement dans le focus pour le clavier.
SDL_APPMOUSEFOCUS fais de même pour la souris.

Dans le cas de la souris, lorsqu'on perd le focus, gain est à 0 et il passe à 1 lorsque celui-ci est restauré.

Nous allons terminer cette fonction par un autre événement SDL sur la fenêtre : SDL_VIDEOEXPOSE
SDL_VIDEOEXPOSE est un événement qui se passe lorsque l'écran est altéré par quelque chose d'extérieur au programme. Nous allons le récupérer et mettre à jour l'écran si ça arrive :

handle_events : altération de l'écran
    //Si l'écran de la fenetre a été altéré
    else if( event.type == SDL_VIDEOEXPOSE )
    {
        //Mise à jour de l'ecran
        if( SDL_Flip( screen ) == -1 )
        {
            //S'il y a une erreur
            windowOK = false;
            return;
        }
    }
}

Voila pour la fonction de récupération des événements sur notre fenêtre.
Il ne reste plus grand chose à voir sur cette partie, je vous montre tout de même la fonction error() de notre fenêtre :

fonction error()
bool Window::error()
{
    return !windowOK;
}

Comme vous le voyez, c'est tout ce qu'il y a de plus simple en utilisant notre variable windowOK.

La création de la fenêtre se fait dans le main(), comme ceci :

main : création de la fenêtre
int main( int argc, char* args[] )
{
    //ce qui va nous permettre de quitter
    bool quit = false;

    //Initialisation
    if( init() == false )
    {
        return 1;
    }

    //Creation d'une fenetre
    Window myWindow;

    //Si la creation échoue
    if( myWindow.error() == true )
    {
        return 1;
    }

Rien de compliqué là dedans, on créé la fenêtre et on vérifie s'il y a un problème à la création.
La boucle principale de notre programme est à peu près la même que dans les autres tutoriaux, je vous laisse le soin de regarder le code de plus près dans les sources téléchargeables.

Télécharger les sources du chapitre XV-3 (100 ko)

Sources et pdf

Télécharger les sources du chapitre XV-1 (110 ko)
Télécharger les sources du chapitre XV-2 (100 ko)
Télécharger les sources du chapitre XV-3 (195 ko)

Version pdf (134 ko - 21 pages)


Tutoriel SDL

Chapitre XVI : Alpha Blending avec SDL

  • XVI. Alpha Blending
    • XVI-1. Définition
    • XVI-2. Programme exemple

XVI. Alpha Blending

XVI-1. Définition

L'alpha blending est une technique consistant à ajouter de la transparence à des objets dont les surfaces.
Les surfaces sont généralement caractérisées par une texture ou par une couleur. La technique d'alpha blending consiste à ajouter à la surface, dont on souhaite donner la possibilité de devenir transparente, un octet (nombre de 0 à 255) définissant le caractère translucide de la surface.

Lorsque la valeur alpha est à 255, l'objet est totalement opaque et au contraire, si la valeur alpha est à 0, l'objet est complètement invisible.

XVI-2. Programme exemple

Vous savez déjà utiliser la transparence en utilisant le color key comme sur le Chapitre V : Transparence.

Dans ce tutoriel, nous allons poser deux surfaces dont celle de devant sera capable de s'effacer/revenir en changeant simplement sa valeur alpha. Cela aura pour effet de donner un fondu entre les deux images.

Voici une image du passage d'une image à l'autre sur notre programme :

La transparence alpha SDL est de notoriété lente car elle n'utilise aucune accélération matérielle. Quelque soit votre carte graphique, ça ne changera rien.
Si vous souhaitez avoir une accélération matérielle, il vous faudra passer par OpenGL. Sachez que SDL marche très bien en compagnie d'OpenGL.

Commençons notre programme exemple par l'initialisation de notre valeur alpha dans le main() :

initialisation valeur alpha
//La valeur alpha de la surface de devant
        int alpha = SDL_ALPHA_OPAQUE;

SDL nous permet d'utiliser une variable globale nommé SDL_ALPHA_OPAQUE qui est la valeur maximale que peu prendre une valeur alpha.
Cette variable globale est définie comme suit :

SDL_ALPHA_OPAQUE
#define SDL_ALPHA_OPAQUE 255

De même, il existe une autre variable globale pour définir la valeur minimale que peut prendre la valeur alpha :

SDL_ALPHA_TRANSPARENT
#define SDL_ALPHA_TRANSPARENT 0

Ces deux variables sont définies dans le fichier SDL_video.h.

Pour gérer le passage d'une image à l'autre, nous allons utiliser les flèches haut et bas du clavier.
Lorsque la flèche haut sera pressée, nous allons augmenter la valeur alpha jusqu'à ce qu'elle atteigne son maximum. De même, lorsque la flèche bas sera pressée, nous allons baisser la valeur alpha jusqu'à son minimum.

changement de la valeur alpha
        //Recuperation du keystates
            Uint8 *keystates = SDL_GetKeyState( NULL );

            //Si la fleche haut a ete pressee
            if( keystates[ SDLK_UP ] )
            {
                //Si la valeur alpha n'est pas à son maximum
                if( alpha < SDL_ALPHA_OPAQUE )
                {
                    //Augmentation de la valeur alpha
                    alpha += 5;
                }
            }

            //Si la fleche bas a ete pressee
            if( keystates[ SDLK_DOWN ] )
            {
                //Si la valeur alpha n'est pas à son minimum
                if( alpha > SDL_ALPHA_TRANSPARENT )
                {
                    //Diminution de la valeur alpha
                    alpha -= 5;
                }
            }

La valeur alpha va donc de 0 à 255 (ou inversement) et l'image passe de l'opacité à la transparence complète.

Pour appliquer la valeur alpha a notre surface, il va falloir passer par la fonction SDL_SetAlpha() :

SDL_SetAlpha()
//Mise en place de la surface alpha
            SDL_SetAlpha( front, SDL_SRCALPHA | SDL_RLEACCEL, alpha );

La variable alpha est donc la valeur alpha pour notre surface front. SDL_SRCALPHA doit être passé en paramètre, sinon toutes les informations alpha sont ignorées. Pour le paramètre SDL_RLEACCEL, il permet d'avoir l'accélération RLE (voir Chapitre V).

La suite, vous la connaissez, on applique les surfaces et on met à jour l'écran :

application des surfaces
//On applique la surface de derriere
            apply_surface( 0, 0, back, screen );

            //On applique la surface de devant
            apply_surface( 0, 0, front, screen );

            //Mise a jour de l'ecran
            if( SDL_Flip( screen ) == -1 )
            {
                return 1;
            }

Voila, vous devriez obtenir un beau fondu entre les deux images en appuyant sur les flèches haut et bas de votre clavier.

Télécharger les sources du chapitre XVI (110 ko)
Version pdf (85 ko - 7 pages)


Tutoriel SDL

Chapitre XVII : Utilisation d'un Font Bitmap avec SDL

  • XVII. Utilisation d'un Font Bitmap
    • XVII-1. Création d'un font bitmap
    • XVII-2. Utilisation d'un font bitmap avec SDL

XVII. Utilisation d'un Font Bitmap

Un font bitmap consiste en une série de pixels représentant une image de chaque caractères.

Les fonts bitmap sont utilisées lorsque les bibliothèques de font existantes (comme SDL_ttf) ne sont simplement pas assez flexible.
Ce tutoriel va vous apprendre comment créer un font bitmap et comment s'en servir sous SDL à travers un programme exemple.

XVII-1. Création d'un font bitmap

Un font bitmap est simplement une feuille de sprites.

Par exemple, si vous souhaitez faire un "HELLO WORLD", il suffit de piocher les lettres qu'on a besoin sur la feuille de sprite pour les afficher comme suit :

Il existe de multiples façons de créer un font bitmap et je vais en citer seulement 2 :

- Utiliser un logiciel spécialisé dans la création de font bitmap (gratuit ou non). J'ai découvert récemment un créateur de font bitmap en ligne et gratuit : Bitfontmaker. L'inconvénient avec ce dernier, c'est qu'il restreint la taille de chaque font (caractère) à 10 x 10 pixels.
- Créer le font bitmap à la main en utilisant un logiciel de dessin. C'est cette dernière possibilité que j'ai utilisée pour réaliser la font bitmap pour ce tutoriel.

La première chose à penser lorsqu'on créé un font bitmap est la norme qu'on va choisir. Je vous conseil la norme ASCII dans tous les cas.
Si vous souhaitez avoir plus de caractères, il vous faudra regarder du coté des normes ISO comme par exemple ISO-8859-1 plus connu sous le nom Latin-1 qui est utilisée dans les pays occidentaux.
Si vous souhaitez des caractères plus exotiques, vous pouvez chercher dans les normes ISO-8859. Par exemple, pour l'arabe c'est la norme ISO-8859-6.

Pour ma part, je me suis limité aux 95 caractères de la norme ASCII. Cependant, j'ai fais une feuille de sprite pouvant contenir autant de caractères que dans les normes ISO-8859, vous pourrez donc à loisir changer / compléter mon font bitmap comme bon vous semble, celle-ci sera fournie avec le programme exemple.

Pour construire le font bitmap manuellement, je vous conseil, dans un premier temps, de définir la taille de vos caractères afin de connaître la taille de vos cellules qui contiendront vos caractères.
Dans mon cas, chaque cellule fait une taille de 39 x 55 pixels, ce qui est assez large. Comme j'ai fait une feuille assez grande pour contenir autant de caractères que dans les normes ISO-8859, j'ai 256 cellules et donc une feuille d'une taille de 624 x 880 (16 cellules par ligne et 16 cellules par colonnes).

Il vous reste plus qu'à faire une jolie grille pour que vos cellules apparaissent clairement et dessiner vos caractères à l'intérieur de chaque cellule à l'endroit correspondant à la norme choisie.

Voila le résultat de ce que ça peut donner quand c'est fait rapidement à la souris :

XVII-2. Utilisation d'un font bitmap avec SDL

Maintenant que vous savez comment créer un font bitmap ainsi que son principe d'utilisation, il est temps de passer à son utilisation avec SDL.

On va commencer par créer une classe BitmapFont :

classe BitmapFont
//Notre classe BitmapFont
class BitmapFont
{
    private:
    //La surface du font
    SDL_Surface *bitmap;

    //Les caractères individuels dans le font
    SDL_Rect chars[ 256 ];

    public:
    //Le constructeur par defaut
    BitmapFont();

    //Le constructeur avec un argument, genere le font quand l'objet est construit
    BitmapFont( SDL_Surface *surface );

    //Genere le font
    void build_font( SDL_Surface *surface );

    //Affiche le texte
    void show_text( int x, int y, std::string text, SDL_Surface *surface );
};

La première chose que nous avons dans cette classe est la surface du font bitmap. Ensuite nous avons un tableau de SDL_Rect pour découper les lettres depuis le font bitmap. Nous en avons mit 256 pour pouvoir gérer les normes vues plus haut.

Ensuite nous avons nos constructeurs, une fonction build_font() afin d'initialiser le font et pour finir une fonction show_text() afin d'afficher le texte à l'écran.

Pour les besoins du programme, nous allons avoir besoin d'une fonction qui nous permet de récupérer un pixel individuel depuis une surface :

get_pixel32()
Uint32 get_pixel32( int x, int y, SDL_Surface *surface )
{
    //Convertie les pixels en 32 bit
    Uint32 *pixels = (Uint32 *)surface->pixels;

    //Recupere le pixel demande
    return pixels[ ( y * surface->w ) + x ];
}

La première chose que nous faisons dans cette fonction est de convertir le pointeur vers le pixel depuis un type void vers un type entier non signé 32 bits (Uint32) afin de pouvoir y accéder proprement par la suite.

Ensuite, nous récupérons le pixel demandé en argument. Vous vous demandez sûrement pourquoi nous n'avons tout simplement pas récupéré le pixel en retournant juste pixels[x][y].

En fait, les pixels ne sont pas stockés de cette façon :

Ils sont stockés ainsi :

Dans un tableau d'une seule dimension.

Donc, pour récupérer le pixel qu'on souhaite depuis le tableau, on multiplie la coordonnées y par la taille et on ajoute la coordonnées x.

Ces fonctions ne marchent qu'avec des surfaces 32 bits. Vous allez devoir faire vos propres fonctions si vous utilisez un format différent.

Commençons par voir nos constructeurs :

Constructeurs
BitmapFont::BitmapFont()
{
    //Met bitmap a NULL
    bitmap = NULL;
}

BitmapFont::BitmapFont( SDL_Surface *surface )
{
    //Construit le font
    build_font( surface );
}

Le premier, sans paramètres, met simplement le pointeur vers le bitmap à NULL.
Le second construit notre font avec les variables passées en paramètres en utilisant la méthode build_font().

Pour construire notre font, on n'a pas besoin de beaucoup de chose : une surface. On pourrait éventuellement passer aussi la taille d'une cellule (hauteur et largeur).

build_font()
void BitmapFont::build_font( SDL_Surface *surface )
{
    //Si la surface est NULL
    if( surface == NULL )
    {
        return;
    }

    //Recupere le bitmap
    bitmap = surface;

    //Mise en place de la couleur du background (fond)
    Uint32 bgColor = SDL_MapRGB( bitmap->format, 0, 0xFF, 0xFF );

    //Dimension des cellules
    int cellW = bitmap->w / 16;
    int cellH = bitmap->h / 16;

La première chose à faire est de vérifier la validité de la surface passée en paramètre.

La suite est très simple : on assigne la surface comme étant notre bitmap, on définit la couleur de fond ainsi que la taille des cellules.

Une feuille de font bitmap typique fait 256 caractères dans l'ordre ASCII. Chaque cellule contenant une lettre faisant la même taille. Nous avons donc 16 lignes et 16 colonnes et donc la hauteur d'une cellule est égale à la hauteur de notre surface divisé par 16 (de même pour la largeur d'une cellule).

Maintenant qu'on a notre bitmap et qu'on a définit les cellules, il est temps de découper individuellement les lettres.

Découpage - partie 1
    //Le caractere courant
    int currentChar = 0;

    //On parcours les lignes des cellules
    for( int rows = 0; rows < 16; rows++ )
    {
        //On parcours les colonnes des cellules
        for( int cols = 0; cols < 16; cols++ )
        {

Dans un premier temps, on définit un entier qui nous permettra de garder une trace du caractère qu'on est en train de construire. Ensuite on commence à parcourir notre font bitmap cellule par cellule.

Découpage - partie 2
            //caractere courant
            chars[ currentChar ].x = cellW * cols;
            chars[ currentChar ].y = cellH * rows;

            //Mise en place des dimensions du caractere
            chars[ currentChar ].w = cellW;
            chars[ currentChar ].h = cellH;

Le découpage de chaque caractère est classique avec la définition de la position en x/y et la taille en hauteur/largeur.

Là où ça va se compliquer un petit peu c'est qu'on va essayer de découper le caractère au plus proche plutôt que de le découper avec la taille de la cellule. En effet, chaque caractère ne faisant pas la même taille, en découpant avec la taille de la cellule, on se retrouvera rapidement avec des espaces entre chaque lettre complètement chaotiques.
On va donc découper plus proprement chaque caractère en parcourant les pixels de chacune de nos cellules.

Découpage - partie 3
            //On parcours les colonnes des pixels
            for( int pCol = 0; pCol < cellW; pCol++ )
            {
                //On parcours les lignes des pixels
                for( int pRow = 0; pRow < cellH; pRow++ )
                {
                    //Recupere les coordonnees du pixel
                    int pX = ( cellW * cols ) + pCol;
                    int pY = ( cellH * rows ) + pRow;

On commence par le parcours des colonnes afin de couper en largeur. On calcul les coordonnées en ajoutant les coordonnées relatives du pixel aux coordonnées de la cellule courante.

Découpage - partie 4
                    //Si un pixel non "colorkey" est trouve
                    if( get_pixel32( pX, pY, bitmap ) != bgColor )
                    {
                        //Coordonnee x
                        chars[ currentChar ].x = pX;

                        //On arrete la boucle
                        pCol = cellW;
                        pRow = cellH;
                    }
                }
            }

Le scan de notre cellule ressemble donc à ça :
scan

On récupère la couleur du pixel et si cette couleur est différente du fond définit, alors on a trouvé le début de notre caractère.
On met donc à jour la position x de notre caractère avec la position du pixel.
On fait la même chose de l'autre côté de la cellule afin de récupérer la largeur de notre caractère.

Découpage - partie 5
            //On parcours les colonnes des pixels
            for( int pCol_w = cellW - 1; pCol_w >= 0; pCol_w-- )
            {
                //On parcours les lignes des pixels
                for( int pRow_w = 0; pRow_w < cellH; pRow_w++ )
                {
                    //Recupere les coordonnees du pixel
                    int pX = ( cellW * cols ) + pCol_w;
                    int pY = ( cellH * rows ) + pRow_w;

                    //Si un pixel non "colorkey" est trouve
                    if( get_pixel32( pX, pY, bitmap ) != bgColor )
                    {
                        //longueur du caractere courant
                        chars[ currentChar ].w = ( pX - chars[ currentChar ].x ) + 1;

                        //On arrete la boucle
                        pCol_w = -1;
                        pRow_w = cellH;
                    }
                }
            }

On démarre donc cette fois ci de la droite de la cellule et on remonte en arrière colonne par colonne.
Une fois un des pixels du caractère trouvé, la taille est calculée en soustrayant à la coordonnée x de notre pixel la position x de notre caractère déjà défini. On fini par ajouter 1 pour prendre en compte le pixel.

Voila, on a défini la taille d'un caractère, on passe donc au suivant et ainsi de suite.

N'oubliez pas que ce qu'on a fait ici ne fonctionne qu'avec un font bitmap ayant 256 caractères dans l'ordre ASCII placés dans 16 lignes et 16 colonnes. Il faudra faire autrement pour d'autres styles de font bitmap.

Vous remarquerez que nous n'avons pas découpé plus en détail la hauteur de nos caractères. C'est parce que nous avons besoin, pour la hauteur, d'une taille uniforme déjà offerte par la hauteur de la cellule afin d'assurer les espaces entre deux lignes.

Il nous reste à voir la méthode show_text avant de s'attaquer à l'application de notre classe.

show_text()
void BitmapFont::show_text( int x, int y, std::string text, SDL_Surface *surface )
{
    //coordonnees temporaires
    int X = x, Y = y;

Cette fonction va nous permettre d'afficher le texte.

La première chose qu'on fait est de conserver les coordonnées passées en paramètre dans des variables temporaires. Les coordonnées passées en paramètre vont nous servir de coordonnées de base et les coordonnées temporaires vont nous servir pour déterminer où sera posé le prochain caractère.

show_text()
    //verification que le font a bien ete construit
    if( bitmap != NULL )
    {
        //On parcours le texte
        for( int show = 0; text[ show ] != '\0'; show++ )
        {

On vérifie que le bitmap a bien été construit puis on va parcourir le texte.

show_text()
            //Si le caractere courant est un espace
            if( text[ show ] == ' ' )
            {
                //On bouge de la taille d'un caractere
                X += bitmap->w / 32;
            }
            //Si le caractere courant est un "newline"
            else if( text[ show ] == '\n' )
            {
                //On descent
                Y += bitmap->h / 16;

                //On revient en arriere
                X = x;
            }

On vérifie si le caractère est un espace ou un newline (nouvelle ligne).

Si le caractère est un espace, on bouge la coordonnée temporaire vers la droite. On a choisi ici de bouger de la taille d'une demi cellule.

Si le caractère est un newline, la coordonnée temporaire descend de la taille d'une cellule et on revient à la coordonnée x de base. On revient donc à la ligne.

show_text()
            else
            {
                //recupere la valeur ASCII du caractere
                int ascii = (int)text[ show ];

                //Affiche le caractere
                apply_surface( X, Y, bitmap, surface, &chars[ ascii ] );

                //On bouge de la longueur du caractere + un pas de un pixel
                X += chars[ ascii ].w + 1;
            }
        }
    }
}

Si le caractère courant n'est pas un caractère spécial, on applique le caractère sur notre fenêtre SDL.

Pour afficher le caractère, on récupère la valeur ASCII en castant notre caractère en integer. Ensuite on applique notre caractère à l'écran. C'est là l'intérêt de mettre nos caractères dans l'ordre ASCII.

Ensuite la coordonnée temporaire est bougée de la taille du caractère plus un. Ainsi, la caractère suivant sera appliqué juste à la suite de celui-ci avec seulement un espace d'un pixel.

La boucle continue jusqu'à ce qu'on ai plus aucun caractère à afficher.

Nous allons finir ce tutoriel en utilisant notre classe.

main
//Creation de notre font
    BitmapFont font( bitmapFont );

    //On remplit l'ecran de blanc
    SDL_FillRect( screen, &screen->clip_rect, SDL_MapRGB( screen->format, 0xFF, 0xFF, 0xFF ) );

    //Affiche le texte
    font.show_text( 50, 50, "Bienvenue sur Developpez.com\n\nLoka", screen );

    //Mise à jour de l'écran
    if( SDL_Flip( screen ) == -1 )
    {
        return 1;
    }

Voici ce que ça donne au final (je rappelle que j'ai fais les caractères rapidement à la souris) :

final

Télécharger les sources du chapitre XVII (113 ko)
Version pdf (126 ko - 14 pages)


Tutoriel SDL

Chapitre XVIII : Thread avec SDL

  • XVIII. Thread
  • Téléchargements
  • Remerciements

Jusqu'à présent, nous avons construit nos programmes en utilisant qu'un seul processus à la fois. Avec le multithreading, nous pouvons construire des programmes qui peuvent utiliser plusieurs processus en même temps.

Ce tutoriel va introduire les capacités multithreading compatible avec toute plateforme de SDL.

XVIII. Thread

Tout comme à notre habitude, nous allons construire ce tutoriel autour d'un programme d'exemple. Ce programme va utiliser un thread pour animer la barre caption.

La première chose à faire pour utiliser les threads avec SDL est d'utiliser l'include correspondant :

fichiers d'entete
//Les fichiers d'entête
#include "SDL/SDL.h"
#include "SDL/SDL_image.h"
#include "SDL/SDL_thread.h"
#include <string>

Le fichier d'entête correspondant à l'utilisation des threads est donc... SDL_thread.h

Nous avons ensuite deux variables importantes :

variables
//Le thread qui sera utiliser
SDL_Thread *thread = NULL;

//Ce qui va nous permettre de quitter
bool quit = false;

La première variable sera notre thread que nous allons lancer et qui est donc un SDL_Thread. La seconde est notre variable quit qui nous avons pour habitude d'avoir en local dans le main() mais cette fois ci on en a besoin en tant que variable globale afin d'être utilisée aussi avec notre thread.

Nous allons maintenant passer à la partie importante de notre programme : la fonction qui va s'occuper d'être notre thread.

thread
int my_thread( void *data )
{
    //Tant que le programme n'est pas fini
    while( quit == false )
    {
        //On anime la barre caption
        SDL_WM_SetCaption( "Thread is running", NULL );
        SDL_Delay( 250 );

        SDL_WM_SetCaption( "Thread is running.", NULL );
        SDL_Delay( 250 );

        SDL_WM_SetCaption( "Thread is running..", NULL );
        SDL_Delay( 250 );

        SDL_WM_SetCaption( "Thread is running...", NULL );
        SDL_Delay( 250 );
    }

    return 0;
}

Voici donc ce qui sera notre thread. Tout ce que cette fonction fait est de mettre un message évolutif sur la barre de caption et ainsi créer une animation texte. Plus exactement, chaque quart de seconde, le message change tant que l'utilisateur n'a pas quitté.
Maintenant vous comprenez pourquoi notre variable quit est devenue globale.

Afin que la fonction soit utilisée comme un thread par SDL, il faut respecter deux choses :

  • Premièrement, elle doit retourner un int
  • Deuxièmement, elle doit avoir en argument qui est un pointeur vers un type de données void

Si la fonction ne respecte pas ces deux conditions, elle ne pourra pas être utilisée comme un thread.

Maintenant qu'on a fais notre fonction, il faut l'utiliser dans notre main() :

Création et lancement du thread
//Creation et lancement du thread
    thread = SDL_CreateThread( my_thread, NULL );

Dans notre fonction main, après que tout a été initialisé et chargé, nous appelons SDL_CreateThread().

SDL_CreateThread() prend en premier argument la fonction, la transforme en thread et lance ce nouveau thread construit. La fonction retourne un pointeur vers le thread, ainsi on peut garder une trace de ce dernier.

La suite reste classique, on affiche une image à l'écran et on attend que l'utilisateur quitte le programme.

main
//On applique l'image sur l'écran
    apply_surface( 0, 0, image, screen );

    //Mise à jour de l'écran
    if( SDL_Flip( screen ) == -1 )
    {
        return 1;
    }

    //Tant que l'utilisateur n'a pas quitté
    while( quit == false )
    {
        //Tant qu'il y a un événement
        while( SDL_PollEvent( &event ) )
        {
            //Si l'utilisateur a cliqué sur le X de la fenêtre
            if( event.type == SDL_QUIT )
            {
                //On quitte the programme
                quit = true;
            }
        }
    }

Tant que c'est en marche, la barre caption change grâce à notre thread qui est lancé en parallèle de ce que nous sommes en train de faire. Grâce aux possibilités multithreading de SDL, nous sommes maintenant capable de faire deux choses en même temps.

Il ne faudra pas oublier de bien stopper le thread quand on quitte le programme

netoyage
void clean_up()
{
    //On stop le thread
    SDL_KillThread( thread );

    //On libère la surface
    SDL_FreeSurface( image );

    //On Quitte SDL
    SDL_Quit();
}

Pour stopper le thread, on appel la fonction SDL_KillThread() qui va stopper instantanément le thread.
Une meilleure façon de faire serait d'attendre que le thread finisse son travail, mais dans ce cas il n'y a aucun incident à simplement le stopper.
Ensuite on libère la surface de notre image et on quitte SDL, comme d'habitude.

Bien qu'il y ai beaucoup de cas où l'utilisation des threads peut se révéler intéressante dans les jeux vidéos, si vous êtes nouveau dans la programmation de jeux, ne les utilisez pas jusqu'à ce que vous soyez plus expérimentés.
Le multithreading peut vite devenir une prise de tête et son apport pas vraiment significatif dans le cadre de petits jeux. Vous avez à connaitre la synchronisation des threads, ainsi que comment gérer la concurrence.

Je recommande leur utilisation seulement si vous êtes devenu à l'aise avec l'architecture logicielle et seulement dans ce cas cet outil deviendra un puissant ami.

Téléchargements

Télécharger les sources du chapitre XVIII (102 ko)
Version pdf (84 ko - 6 pages)


Accueil

Accueil CHM


Apprenez à programmer en C

Installation de la SDL

À partir de maintenant, fini la théorie : nous allons enfin passer au concret ! Dans cette nouvelle et importante partie, nous allons nous faire plaisir et pratiquer grâce à une bibliothèque que l'on appelle la SDL.

Vous avez déjà découvert la plupart des fonctionnalités du langage C, bien qu'il y ait toujours des petits détails complexes et croustillants à découvrir. Ce livre pourrait donc s'arrêter là en annonçant fièrement : « C'est bon, vous avez appris à programmer en C ! ». Pourtant, quand on débute, on n'a en général pas le sentiment de savoir programmer tant qu'on n'est pas « sorti » de la console.

La SDL est une bibliothèque particulièrement utilisée pour créer des jeux en 2D. Nous allons dans ce premier chapitre en apprendre plus sur cette bibliothèque et découvrir comment l'installer.

On dit que la SDL est une « bibliothèque tierce ». Il faut savoir qu'il existe deux types de bibliothèques.

    • La bibliothèque standard : c'est la bibliothèque de base qui fonctionne sur tous les OS (d'où le mot « standard ») et qui permet de faire des choses très basiques comme des printf. Elle a été automatiquement installée lorsque vous avez téléchargé votre IDE et votre compilateur.

Au long des parties I et II, nous avons uniquement utilisé la bibliothèque standard (stdlib.h, stdio.h, string.h, time.h…). Nous ne l'avons pas étudiée dans son intégralité mais nous en avons vu un assez gros morceau. Si vous voulez tout savoir sur la bibliothèque standard, faites une recherche sur Google, par exemple, en tapant « C standard library », et vous aurez la liste des prototypes ainsi qu'une brève explication de chacune des fonctions.

    • Les bibliothèques tierces : ce sont des bibliothèques qui ne sont pas installées par défaut. Vous devez les télécharger sur Internet et les installer sur votre ordinateur.

Contrairement à la bibliothèque standard, qui est relativement simple et qui contient assez peu de fonctions, il existe des milliers de bibliothèques tierces écrites par d'autres programmeurs. Certaines sont bonnes, d'autres moins, certaines sont payantes, d'autres gratuites, etc. l'idéal étant de trouver des bibliothèques de bonne qualité et gratuites à la fois !

Je ne peux pas faire un cours pour toutes les bibliothèques tierces qui existent. Même en y passant toute ma vie 24h / 24, je ne pourrais pas !
J'ai donc fait le choix de vous présenter une et une seule bibliothèque écrite en C et donc utilisable par des programmeurs en langage C tels que vous.

Cette bibliothèque a pour nom SDL. Pourquoi ai-je choisi cette bibliothèque plutôt qu'une autre ? Que permet-elle de faire ?
Autant de questions auxquelles je vais commencer par répondre.

Pourquoi avoir choisi la SDL ?

Choisir une bibliothèque : pas facile !

Comme je vous l'ai dit à l'instant, il existe des milliers et des milliers de bibliothèques à télécharger.
Certaines d'entre elles sont simples, d'autres plus complexes. Certaines sont tellement grosses que même tout un cours comme celui que vous êtes en train de lire ne suffirait pas !

Faire un choix est donc dur. De plus, c'est la première bibliothèque que vous allez apprendre à utiliser (si on ne compte pas la bibliothèque standard), il vaut donc mieux commencer par une bibliothèque simple.

J'ai rapidement constaté que la majorité de mes lecteurs souhaitait découvrir comment ouvrir des fenêtres, créer des jeux, etc. Enfin, si vous aimez la console on peut continuer longtemps, si vous voulez… Non ? Ah bon, tiens c'est curieux !
Quant à moi, non seulement j'ai bien envie de vous montrer comment on peut faire tout ça, mais en plus je tiens absolument à vous faire pratiquer. En effet, nous avons bien fait quelques TP dans les parties I et II, mais ce n'est pas assez. C'est en forgeant que l'on devient forgeron, et c'est en programmant que euh… Bref, vous m'avez compris !

Je suis donc parti pour vous à la recherche d'une bibliothèque à la fois simple et puissante pour que vous puissiez rapidement réaliser vos rêves les plus fous (presque) sans douleur (tout est relatif, bien sûr !).

La SDL est un bon choix !

Nous allons étudier la bibliothèque SDL (fig. suivante). Pourquoi celle-ci et non pas une autre ?

Logo de la SDL

  • C'est une bibliothèque écrite en C, elle peut donc être utilisée par des programmeurs en C tels que vous. Notez que comme la plupart des bibliothèques écrites en C, il est possible de les utiliser en C++ ainsi que dans d'autres langages.

  • C'est une bibliothèque libre et gratuite : cela vous évitera d'avoir à investir pour lire la suite du livre. Contrairement à ce que l'on pourrait penser, trouver des bibliothèques libres et gratuites n'est pas très difficile, il en existe beaucoup aujourd'hui. Une bibliothèque libre est tout simplement une bibliothèque dont vous pouvez obtenir le code source. En ce qui nous concerne, voir le code source de la SDL ne nous intéressera pas. Toutefois, le fait que la bibliothèque soit libre vous garantit plusieurs choses, notamment sa pérennité (si le développeur principal arrête de s'en occuper, d'autres personnes pourront la continuer à sa place) ainsi que sa gratuité le plus souvent. La bibliothèque ne risque donc pas de disparaître du jour au lendemain.

  • Vous pouvez réaliser des programmes commerciaux et propriétaires avec. Certes, c'est peut-être un peu trop vouloir anticiper, mais autant choisir une bibliothèque gratuite qui vous laisse un maximum de libertés. En effet, il existe deux types de bibliothèques libres :

    • les bibliothèques sous license GPL : elles sont gratuites et vous pouvez avoir le code source, mais vous êtes obligés en contrepartie de fournir le code source des programmes que vous réalisez avec ;

    • les bibliothèques sous license LGPL : c'est la même chose, sauf que cette fois vous n'êtes pas obligés de fournir le code source de vos programmes. Vous pouvez donc réaliser des programmes propriétaires avec.

    Bien qu'il soit possible juridiquement de ne pas diffuser votre code source, je vous invite à le faire quand même. Vous pourrez ainsi obtenir des conseils de programmeurs plus expérimentés que vous. Cela vous permettra de vous améliorer.
    Après, c'est vous qui choisirez de réaliser des programmes libres ou propriétaires, c'est surtout une question de mentalité. Je ne rentrerai pas dans le débat ici, pas plus que je ne prendrai position : on peut tirer du bon comme du mauvais dans chacun de ces deux types de programmes.

  • C'est une bibliothèque multi-plates-formes. Que vous soyez sous Windows, Mac ou Linux, la SDL fonctionnera chez vous. C'est même d'ailleurs ce qui fait que cette bibliothèque est impressionnante aux yeux des programmeurs : elle fonctionne sur un très grand nombre de systèmes d'exploitation. Il y a Windows, Mac et Linux certes, mais elle peut aussi fonctionner sur Atari, Amiga, Symbian, Dreamcast, etc. En clair, vos programmes pourraient très bien fonctionner sur de vieilles machines comme l'Atari ! Il faudrait néanmoins faire quelques petites adaptations et peut-être utiliser un compilateur spécial. Nous n'en parlerons pas ici.

  • Enfin, la bibliothèque permet de faire des choses amusantes. Je ne dis pas qu'une bibliothèque mathématique capable de résoudre des équations du quatrième degré n'est pas intéressante, mais je tiens à ce que ce cours soit ludique autant que possible afin de vous motiver à programmer.

La SDL n'est pas une bibliothèque spécialement conçue pour créer des jeux vidéo. Je l'admets, la plupart des programmes utilisant la SDL sont des jeux vidéo, mais cela ne veut pas dire que vous êtes forcément obligés d'en créer. A priori, tout est possible avec plus ou moins de travail, j'ai déjà eu l'occasion de voir des éditeurs de texte développés à l'aide de la SDL, bien qu'il y ait plus adapté. Si vous souhaitez développer des interfaces graphiques classiques sous forme de fenêtres (boutons, menus, etc.), je vous invite à vous renseigner plutôt sur GTK+.

Les possibilités offertes par la SDL

La SDL est une bibliothèque bas niveau. Vous vous souvenez de ce que je vous avais dit au tout début du cours à propos des langages haut niveau et bas niveau ? Eh bien ça s'applique aussi aux bibliothèques.

    • Une bibliothèque bas niveau : c'est une bibliothèque disposant de fonctions très basiques. Il y a en général peu de fonctions car on peut tout faire avec elles. Comme les fonctions restent basiques, elles sont très rapides. Les programmes réalisés à l'aide d'une telle bibliothèque sont donc en général ce qui se fait de plus rapide.

    • Une bibliothèque haut niveau : elle possède beaucoup de fonctions capables d'exécuter de nombreuses actions. Cela la rend plus simple d'utilisation.

Toutefois, une bibliothèque de ce genre est généralement « grosse », donc plus difficile à étudier et à connaître intégralement. En outre, elle est souvent plus lente qu'une bibliothèque bas niveau (bien que parfois, ça ne soit pas vraiment visible).

Bien entendu, il faut nuancer. On ne peut pas dire « une bibliothèque bas niveau est mieux qu'une bibliothèque haut niveau » ou l'inverse. Chacun des deux types présente des qualités et des défauts. La SDL que nous allons étudier fait plutôt partie des bibliothèques bas niveau.

Il faut donc retenir que la SDL propose surtout des fonctions basiques. Vous avez par exemple la possibilité de dessiner pixel par pixel, de dessiner des rectangles ou encore d'afficher des images. C'est tout, et c'est suffisant.

  • En faisant bouger une image, vous pouvez faire se déplacer un personnage.

  • En affichant plusieurs images d'affilée, vous pouvez créer une animation.

  • En combinant plusieurs images côte à côte, vous pouvez créer un véritable jeu.

Pour vous donner une idée de jeu réalisable avec la SDL, sachez que l'excellent « Civilization : Call to power » a été adapté pour Linux à l'aide de la bibliothèque SDL (fig. suivante).

Le jeu Civilization : Call to power utilise la SDL

Ce qu'il faut bien comprendre, c'est qu'en fait tout dépend de vous et éventuellement de votre équipe. Vous pouvez faire des jeux encore plus beaux si vous avez un graphiste doué sous la main.

La seule limite de la SDL, c'est la 2D. Elle n'est en effet pas conçue pour la 3D. Voici une liste de jeux que l'on peut parfaitement concevoir en SDL (ce n'est qu'une petite liste, tout est possible a priori tant que ça reste de la 2D) :

  • Casse-briques ;

  • Bomberman ;

  • Tetris ;

  • jeux de plate-forme : Super Mario Bros, Sonic, Rayman…

  • RPG 2D : Zelda, les premiers Final Fantasy, etc.

Il m'est impossible de faire une liste complète, la seule limite ici étant l'imagination. J'ai d'ailleurs vu un des lecteurs de ce cours réaliser un croisement osé entre un casse-briques et un Tetris.

Redescendons sur Terre et reprenons le fil de ce cours. Nous allons maintenant installer la SDL sur notre ordinateur avant d'aller plus loin.

Téléchargement de la SDL

Le site de la SDL www.libsdl.org devrait bientôt devenir incontournable pour vous. Là-bas, vous trouverez tout ce dont vous avez besoin, en particulier la bibliothèque elle-même à télécharger ainsi que sa documentation.

Sur le site de la SDL, rendez-vous dans le menu à gauche, section Download.
Téléchargez la version de la SDL la plus récente que vous voyez (SDL 1.2 au moment où j'écris ces lignes).

La page de téléchargement est séparée en plusieurs parties.

    • Source code : vous pouvez télécharger le code source de la SDL. Comme je vous l'ai dit, le code source ne nous intéresse pas. Je sais que vous êtes curieux et que vous voudriez savoir comment c'est fait à l'intérieur, mais actuellement ça ne vous apportera rien. Pire, ça vous embrouillera et ce n'est pas le but.

    • Runtime libraries : ce sont les fichiers que vous aurez besoin de distribuer en même temps que votre exécutable lorsque vous donnerez votre programme à d'autres personnes. Sous Windows, il s'agit tout simplement d'un fichier SDL.dll. Celui-ci devra se trouver :

      • soit dans le même dossier que l'exécutable (ce que je recommande). L'idéal est de toujours donner la DLL avec votre exécutable et de la laisser dans le même dossier. Si vous placez la DLL dans le dossier de Windows, vous n'aurez plus besoin de joindre une DLL dans chaque dossier contenant un programme SDL. Toutefois, cela peut poser des problèmes de conflits de version si vous écrasez une DLL plus récente ;

      • soit dans le dossier c:\Windows.

    • Development libraries : ce sont les fichiers .a (ou .lib sous Visual) et .h vous permettant de créer des programmes SDL. Ces fichiers ne sont nécessaires que pour vous, le programmeur. Vous n'aurez donc pas à les distribuer avec votre programme une fois qu'il sera fini.

Si vous êtes sous Windows, on vous propose trois versions dépendant de votre compilateur :

    • VC6 : pour ceux qui utilisent Visual Studio payant dans une vieille version (ce qui a peu de chances de vous concerner) ; vous y trouverez des fichiers .lib ;

    • VC8 : pour ceux qui utilisent Visual Studio 2005 Express ou ultérieur ; vous y trouverez des fichiers .lib ;

    • mingw32 : pour ceux qui utilisent Code::Blocks (il y aura donc des fichiers .a).

La particularité, c'est que les « Development libraries » contiennent tout ce qu'il faut : les .h et .a (ou .lib) bien sûr, mais aussi la SDL.dll à distribuer avec votre application ainsi que la documentation de la SDL !
Bref, tout ce que vous avez à faire au final est de télécharger les « Development libraries ». Tout ce dont vous avez besoin s'y trouve.

Ne vous trompez pas de lien ! Prenez bien la SDL dans la section « Development libraries » et non le code source de la section « Source code » !

Qu'est-ce que la documentation ?

Une documentation, c'est la liste complète des fonctions d'une bibliothèque. Toutes les documentations sont écrites en anglais (oui, même les bibliothèques écrites par des Français ont leur documentation en anglais). Voilà une raison de plus pour progresser dans la langue de Shakespeare !

La documentation n'est pas un cours, elle est en général assez austère. L'avantage par rapport à un cours, c'est qu'elle est complète. Elle contient la liste de toutes les fonctions, c'est donc LA référence du programmeur.
Bien souvent, vous rencontrerez des bibliothèques pour lesquelles il n'y a pas de cours. Vous aurez uniquement la « doc' » comme on l'appelle, et vous devrez être capables de vous débrouiller avec seulement ça (même si parfois c'est un peu dur de démarrer sans aide). Un vrai bon programmeur peut donc découvrir le fonctionnement d'une bibliothèque uniquement en lisant sa doc'.

A priori, vous n'aurez pas besoin de la doc' de la SDL de suite car je vais moi-même vous expliquer comment elle fonctionne. Toutefois, c'est comme pour la bibliothèque standard : je ne pourrai pas vous parler de toutes les fonctions. Vous aurez donc certainement besoin de lire la doc' plus tard.

La documentation se trouve déjà dans le package « Development libraries », mais si vous le voulez vous pouvez la télécharger à part en vous rendant dans le menu Documentation / Downloadable.
Je vous recommande de placer les fichiers HTML de la documentation dans un dossier spécial (intitulé par exemple Doc SDL) et de faire un raccourci vers le sommaire index.html. Le but est que vous puissiez accéder rapidement à la documentation lorsque vous en avez besoin.

Créer un projet SDL : Windows

L'installation d'une bibliothèque est en général un petit peu plus compliquée que les installations dont vous avez l'habitude. Ici, il n'y a pas d'installeur automatique qui vous demande simplement de cliquer sur Suivant - Suivant - Suivant - Terminer.

En général, installer une bibliothèque est assez difficile pour un débutant. Pourtant, si ça peut vous remonter le moral, l'installation de la SDL est beaucoup plus simple que bien d'autres bibliothèques que j'ai eu l'occasion d'utiliser (en général on ne vous donne que le code source de la bibliothèque, et c'est à vous de la recompiler !).

En fait, le mot « installer » n'est peut-être pas celui qui convient le mieux. Nous n'allons rien installer du tout : nous voulons simplement arriver à créer un nouveau projet de type SDL avec notre IDE.
Or, selon l'IDE que vous utilisez la manipulation sera un peu différente. Je vais présenter la manipulation pour chacun des IDE que je vous ai présentés au début du cours pour que tout le monde puisse suivre.

Je vais maintenant vous montrer comment créer un projet SDL sous chacun de ces trois IDE.

Création d'un projet SDL sous Code::Blocks

1/ Extraction des fichiers de la SDL

Ouvrez le fichier compressé de « Development Libraries » que vous avez téléchargé.
Ce fichier est un .zip pour Visual et un .tar.gz pour mingw32 (il vous faudra un logiciel comme Winrar ou 7-Zip pour décompresser le .tar.gz).

Le fichier compressé contient plusieurs sous-dossiers. Ceux qui nous intéressent sont les suivants :

  • bin : contient la .dll de la SDL ;

  • docs : contient la documentation de la SDL ;

  • include : contient les .h ;

  • lib : contient les .a (ou .lib pour Visual).

Vous devez extraire tous ces fichiers et dossiers quelque part sur votre disque dur. Vous pouvez par exemple les placer dans le dossier de Code::Blocks, dans un sous-dossier SDL (fig. suivante).

Le dossier de la SDL dézippé

Dans mon cas, la SDL sera installée dans le dossier :

C:\Program Files\CodeBlocks\SDL-1.2.13

Retenez bien le nom du dossier dans lequel vous l'avez installée, vous allez en avoir besoin pour configurer Code::Blocks.

Maintenant, il va falloir faire une petite manipulation pour simplifier la suite. Allez dans le sous-dossier include/SDL (dans mon cas, il se trouve dans C:\Program Files\CodeBlocks\SDL-1.2.13\include\SDL). Vous devriez y voir de nombreux petits fichiers .h. Copiez-les dans le dossier parent, c'est-à-dire dans :

C:\Program Files\CodeBlocks\SDL-1.2.13\include

La SDL est installée ! Il faut maintenant configurer Code::Blocks.

2/ Création du projet SDL

Ouvrez maintenant Code::Blocks et demandez à créer un nouveau projet.

Au lieu de créer un projet Console Application comme vous aviez l'habitude de faire, vous allez demander à créer un projet de type SDL project.

La première fenêtre de l'assistant qui apparaît ne sert à rien, cliquez sur Next.
On vous demande ensuite le nom de votre projet et le dossier dans lequel il doit être placé, comme vous l'avez toujours fait (fig. suivante).

Assistant SDL et nom de projet

Vient ensuite la partie où vous devez indiquer où se trouve installée la SDL (fig. suivante).

Assistant SDL et configuration

Cliquez sur le bouton … à droite. Une nouvelle fenêtre un peu complexe s'ouvre (fig. suivante).

Localisation de la SDL

Vous devez simplement remplir le champ nommé base. Indiquez le dossier où vous avez décompressé la SDL. Dans mon cas, c'est :

C:\Program Files\CodeBlocks\SDL-1.2.13

Cliquez sur Close. Une nouvelle fenêtre apparaît. C'est une fenêtre-piège (dont je n'ai toujours pas saisi l'intérêt). Elle vous demande un dossier. Cliquez sur Annuler pour ne rien faire.

Cliquez ensuite sur Next dans l'assistant, puis choisissez de compiler en mode Release ou Debug (peu importe) et enfin, choisissez Finish.

Code::Blocks va créer un petit projet SDL de test comprenant un main.c et un fichier .bmp. Avant d'essayer de le compiler, copiez la DLL de la SDL (vous devriez l'avoir copiée dans C:\Program Files\CodeBlocks\SDL-1.2.13\bin\SDL.dll) dans le dossier de votre projet.

Essayez ensuite de compiler : une fenêtre avec une image devrait s'afficher. Bravo, ça fonctionne !

Si on vous dit « Cette application n'a pas pu démarrer car SDL.dll est introuvable », c'est que vous n'avez pas copié le fichier SDL.dll dans le dossier de votre projet !
Il faudra penser à fournir cette .dll en plus de votre .exe à vos amis si vous voulez qu'ils puissent eux aussi exécuter le programme. En revanche, vous n'avez pas besoin de leur joindre les .h et .a qui n'intéressent que vous.

Vous pouvez supprimer le .bmp du programme, on n'en aura pas besoin.
Quant au fichier main.c, il est un peu long, on ne va pas démarrer avec ça. Supprimez tout son contenu et remplacez-le par :

#include <stdlib.h>
#include <stdio.h>
#include <SDL/SDL.h>

int main(int argc, char *argv[])
{

    return 0;
}

C'est en fait un code de base très similaire à ceux que l'on connaît (un include de stdlib, un autre de stdio, un main…).
La seule chose qui change, c'est le include d'un fichier SDL.h. C'est le fichier .h de base de la SDL qui se chargera d'inclure tous les autres fichiers .h de la SDL.

Création d'un projet SDL sous Visual C++

1/ Extraction des fichiers de la SDL

Sur le site de la SDL, téléchargez la dernière version de la SDL. Dans la section « Development Libraries », prenez la version pour Visual C++ 2005 Service Pack 1.

Ouvrez le fichier zip.
Il contient la doc' (dossier docs), les .h (dossier include), et les .lib (dossier lib) qui sont l'équivalent des .a pour le compilateur de Visual. Vous trouverez aussi le fichier SDL.dll dans ce dossier lib.

    • Copiez SDL.dll dans le dossier de votre projet.

    • Copiez les .lib dans le dossier lib de Visual C++. Par exemple chez moi il s'agit du dossier :

C:\Program Files\Microsoft Visual Studio 8\VC\lib.

    • Copiez les .h dans le dossier includes de Visual C++. Créez un dossier SDL dans ce dossier includes pour regrouper les .h de la SDL entre eux.

Chez moi, je mets donc les .h dans :C:\Program Files\Microsoft Visual Studio 8\VC\include\SDL.

2/ Création d'un nouveau projet SDL

Sous Visual C++, créez un nouveau projet de type Application console Win32. Appelez votre projet testsdl par exemple. Cliquez sur OK.

Un assistant va s'ouvrir. Allez dans Paramètres de l'application et vérifiez que Projet vide est coché (fig. suivante).

Cochez Projet vide

Le projet est alors créé. Il est vide. Ajoutez-y un nouveau fichier en faisant un clic droit sur Fichiers sources, Ajouter / Nouvel élément (fig. suivante).

Ajout d'un nouveau fichier

Dans la fenêtre qui s'ouvre, demandez à créer un nouveau fichier de type Fichier C++ (.cpp) que vous appellerez main.c. En utilisant l'extension .c dans le nom du fichier, Visual créera un fichier .c et non un fichier .cpp.

Écrivez (ou copiez-collez) le code « de base » mentionné précédemment dans votre nouveau fichier vide.

3/ Configuration du projet SDL sous Visual C++

La configuration du projet est un peu plus délicate que pour Code::Blocks, mais elle reste humainement faisable. Allez dans les propriétés de votre projet : Projet / Propriétés de testsdl.

  • Dans la section C / C++ => Génération de code, mettez le champ Bibliothèque runtime à DLL multithread (/MD).

  • Dans la section C/C++ => Avancé, sélectionnez Compilation sous et optez pour la valeur Compiler comme code C (/TC) (sinon Visual vous compilera votre projet comme étant du C++).

  • Dans la section Éditeur de liens => Entrée, modifiez la valeur de Dépendances supplémentaires pour y ajouter SDL.lib SDLmain.lib.

  • Dans la section Éditeur de liens => Système, modifiez la valeur de Sous-système et mettez-la à Windows (fig. suivante).

Configuration de Visual pour la SDL

Validez ensuite vos modifications en cliquant sur OK et enregistrez le tout.
Vous pouvez maintenant compiler en allant dans le menu Générer / Générer la solution.

Rendez-vous dans le dossier de votre projet pour y trouver votre exécutable (il sera peut-être dans un sous-dossier Debug). N'oubliez pas que le fichier SDL.dll doit se trouver dans le même dossier que l'exécutable. Double-cliquez sur votre .exe : si tout va bien, il ne devrait rien se passer. Sinon, s'il y a une erreur c'est probablement que le fichier SDL.dll ne se trouve pas dans le même dossier.

Créer un projet SDL : Mac OS (Xcode)

Cette section a été à l'origine rédigée par Pouet_forever du Site du Zéro, que je remercie à nouveau au passage.

Tout d'abord, rendez-vous sur le site de la SDL pour télécharger la version 1.2 pour Mac OS. Dans l'arborescence de gauche, cliquez sur SDL 1.2 dans la partie Download, comme indiqué à la figure suivante.

Cliquez sur SDL 1.2

En bas de la page, vous trouverez une partie Runtime Libraries. Téléchargez le fichier concernant votre architecture (Intel ou PowerPC), comme sur la figure suivante. Pour connaître votre architecture, il suffit d'aller dans le menu Pomme en haut à gauche et de cliquer sur À propos de ce Mac. Sur la ligne Processeur, vous verrez soit Intel, soit PowerPC.

Choisissez le type d'architecture

Une fois le fichier téléchargé, montez-le (double-cliquez dessus) ; il devrait alors s'ouvrir tout seul. Dans ce dossier, vous trouverez un dossier SDL.framework. Copiez-le et collez-le dans le dossier /Library/Frameworks (en français /Bibliothèque/Frameworks).

Ça y est, notre bibliothèque est installée !
Vous trouverez un autre dossier qui se nomme devel-lite ; gardez-le ouvert, nous nous en servirons par la suite.

Maintenant, créez un nouveau projet Cocoa Application, puis cliquez sur Next. Dans Product Name, nommez votre projet (« SDL » par exemple) et dans Company Identifier, mettez ce que vous voulez (votre pseudo par exemple). Laissez le reste tel quel puis cliquez sur Next. Choisissez où vous souhaitez mettre votre projet. Un dossier sera créé, donc pas besoin d'en créer un et d'y placer votre projet.

Une fois votre projet créé, vous pouvez supprimer tous les fichiers qui ne nous serviront pas : AppDelegate.h, AppDelegate.m, MainMenu.xib, InfoPlist.strings, main.m et Credits.rtf (figure suivante).

Vous pouvez supprimer les fichiers inutiles

Sélectionnez votre projet dans l'arborescence de gauche (Install SDL sur la figure suivante). Dans la 2$^e$ arborescence, sélectionnez le nom de votre projet sous PROJECT (et pas TARGETS) (figure suivante).

Sélectionnez votre projet

Vous pouvez éventuellement changer la localisation de English à French. Sélectionnez English, cliquez sur le - pour le supprimer et cliquez sur le + pour ajouter French (si vous ne le faites pas, ça n'aura aucune incidence).

On va maintenant configurer notre projet en 32bits (la SDL ne fonctionnant pas en 64 bits) et ajouter les chemins pour les frameworks ainsi que les différents headers. Cliquez sur l'onglet Build Settings, puis All. Ensuite dans Architectures, cliquez sur 64-bit Intel et sélectionnez 32-bit Intel, comme à la figure suivante.

Il faut configurer le projet en 32 bits

Une fois que c'est fait, sélectionnez LLVM GCC 4.2 sur la ligne Compiler for C/C++/Objective-C.

Allez dans le champ de recherche en haut à droite et tapez « search paths » ; vous devriez trouver les 2 lignes qui nous intéressent : Header search paths et Framework search paths. Double cliquez sur la zone à droite de la ligne Framework search paths, cliquez sur le + et ajoutez le chemin /Library/Frameworks. Pour Header Search paths ajoutez le chemin /Library/Frameworks/SDL.framework/Headers. Vous devriez avoir le même résultat qu'à la figure suivante.

Remplissez les champs correspondants

Sélectionnez maintenant votre « cible » (en dessous de TARGETS cette fois-ci), comme à la figure suivante.

Sélectionnez la cible

Allez dans l'onglet Summary. Dans Application Category, vous pouvez mettre ce que vous voulez, mais ça n'apporte pas grand chose, c'est pour l'AppStore. Modifiez la ligne Main Interface et mettez « SDLMain ». Le cadre App Icon, comme son nom l'indique, est pour définir une icône à votre programme. Il suffit de faire glisser l'image souhaitée dans le cadre (avec un drag & drop). Dans la partie Linked Frameworks and Libraries, on va ajouter notre framework : SDL.framework. Cliquez sur le + et dans le champ de recherche, tapez « SDL ». Quand vous avez repéré le framework , cliquez sur « Add ». Si vous ne trouvez pas le framework dans la liste déroulante, c'est que vous ne l'avez pas placé dans le bon dossier (/Library/Frameworks ou /Bibliothèque/Frameworks en français).

Les icônes sous Mac OS sont de type .icns. Si vous mettez un autre format, vous remarquerez que votre icône n'apparaît pas. Il faut donc que vous utilisiez une icône au format .icns. Si vous voulez convertir une image en icône, vous pouvez utiliser le logiciel Icon Composer qui se situe dans le dossier /Developer/Applications/Utilities. Il vous suffit simplement de glisser votre image dans le cadre souhaité et d'enregistrer votre image.

Dans l'onglet Info, on peut indiquer beaucoup d'informations concernant le programme ; je ne m'attarderai pas dessus, si vous voulez des informations, je vous conseille de lire la doc d'Apple à ce sujet. Les deux seules choses que vous pouvez éventuellement modifier sont la Localization, de en en fr, ainsi que le Copyright pour y mettre ce que vous voulez (le __MyCompanyName__ n'est pas génial).

Allez ensuite dans l'onglet Build Phases et cliquez sur Add Build Phase > Copy Files en bas à droite de la fenêtre. Double cliquez sur le nom Copy Files et renommez le en « Copy frameworks into app ». Dans Destination, sélectionnez Frameworks. Pour ajouter vos frameworks, glissez-les de l'arborescence de gauche dans votre Build phase, comme à la figure suivante.

Glissez vos frameworks dans le Build phase

Je vous conseille de ranger tous vos frameworks dans le dossier Framework. Cela vous permettra de vous y retrouver plus facilement.
De même pour les fichiers sources, je vous conseille de créer des dossiers pour les ranger correctement. Pour créer un dossier, il faut faire un clic droit et New Group, ensuite vous glissez vos fichiers dedans.

Nous allons maintenant ajouter les fichiers SDLMain.h et SDLMain.m. Allez dans le dossier devel-lite ouvert précédemment et ajoutez les 2 fichiers au projet. Si une fenêtre vous demandant de choisir des options pour la copie s'affiche, cochez la case Copy items into destination group's folder (if needed).

Dernière ligne droite : créez un fichier main.c. Allez dans le menu File > New > New File, puis dans C and C++. Sélectionnez C File puis Next. Nommez votre fichier et le tour est joué !

Créer un projet SDL : Linux

Si vous compilez sous Linux avec un IDE, il faudra modifier les propriétés du projet (la manipulation sera quasiment la même). Si vous utilisez Code::Blocks (qui existe aussi en version Linux) vous pouvez suivre la même procédure que celle décrite dans la partie Windows.

Et pour ceux qui compilent à la main ?

Il y en a peut-être parmi vous qui ont pris l'habitude de compiler à la main sous Linux à l'aide d'un Makefile (fichier commandant la compilation).
Si c'est votre cas, je vous invite à télécharger un Makefile que vous pouvez utiliser pour compiler des projets SDL.

Télécharger le Makefile

La seule chose un peu particulière, c'est l'ajout de la bibliothèque SDL pour le linker (LDFLAGS). Il faudra que vous ayez téléchargé la SDL version Linux et que vous l'ayez installée dans le dossier de votre compilateur, de la même manière qu'on le fait sous Windows (dossiers include/SDL et lib)

Ensuite, vous pourrez utiliser les commandes suivantes dans la console :

make          #Pour compiler le projet
make clean    #Pour effacer les fichiers de compilation (.o inutiles)
make mrproper #Pour tout supprimer sauf les fichiers source

En résumé

  • La SDL est une bibliothèque de bas niveau qui permet d'ouvrir des fenêtres et d'y faire des manipulations graphiques en 2D.

  • Elle n'est pas installée par défaut, il faut la télécharger et configurer votre IDE pour l'utiliser.

  • Elle est libre et gratuite, ce qui vous permet de l'utiliser rapidement et vous garantit sa pérennité.

  • Il existe des milliers d'autres bibliothèques dont beaucoup sont de très bonne qualité. C'est la SDL qui a été sélectionnée pour la suite de ce cours pour son aspect ludique. Si vous souhaitez développer des interfaces complètes avec menus et boutons par la suite, je vous invite à vous pencher sur la bibliothèque GTK+ par exemple.


Apprenez à programmer en C

Création d'une fenêtre et de surfaces

Dans le chapitre précédent, nous avons fait un petit tour d'horizon de la SDL pour découvrir les possibilités que cette bibliothèque nous offre. Vous l'avez normalement téléchargée et vous êtes capables de créer un nouveau projet SDL valide sans aucun problème. Celui-ci est toutefois encore très vide.

Nous attaquons le vif du sujet dès ce chapitre. Nous allons poser les bases de la programmation en C avec la SDL. Comment charger la SDL ? Comment ouvrir une fenêtre aux dimensions désirées ? Comment dessiner à l'intérieur de la fenêtre ?

Nous avons du pain sur la planche. Allons-y !

Charger et arrêter la SDL

Un grand nombre de bibliothèques écrites en C nécessitent d'être initialisées et fermées par des appels à des fonctions. La SDL n'échappe pas à la règle.

En effet, la bibliothèque doit charger un certain nombre d'informations dans la mémoire pour pouvoir fonctionner correctement. Ces informations sont chargées en mémoire dynamiquement par des malloc (ils sont très utiles ici !). Or, comme vous le savez, qui dit malloc dit… free !
Vous devez libérer la mémoire que vous avez allouée manuellement et dont vous n'avez plus besoin. Si vous ne le faites pas, votre programme va prendre plus de place en mémoire que nécessaire, et dans certains cas ça peut être carrément catastrophique. Imaginez que vous fassiez une boucle infinie de malloc sans le faire exprès : en quelques secondes, vous saturerez toute votre mémoire !

Voici donc les deux premières fonctions de la SDL à connaître :

  • SDL_Init : charge la SDL en mémoire (des malloc y sont faits) ;

  • SDL_Quit : libère la SDL de la mémoire (des free y sont faits).

La toute première chose que vous devrez faire dans votre programme sera donc un appel à SDL_Init, et la dernière un appel à SDL_Quit.

SDL_Init : chargement de la SDL

La fonction SDL_Init prend un paramètre. Vous devez indiquer quelles parties de la SDL vous souhaitez charger.

Ah bon, la SDL est composée de plusieurs parties ?

Eh oui ! Il y a une partie de la SDL qui gère l'affichage à l'écran, une autre qui gère le son, etc.

La SDL met à votre disposition plusieurs constantes pour que vous puissiez indiquer quelle partie vous avez besoin d'utiliser dans votre programme (tab. suivante).

Constante | Description
SDL_INIT_VIDEO | Charge le système d'affichage (vidéo). C'est la partie que nous chargerons le plus souvent.
SDL_INIT_AUDIO | Charge le système de son. Vous permettra donc par exemple de jouer de la musique.
SDL_INIT_CDROM | Charge le système de CD-ROM. Vous permettra de manipuler votre lecteur de CD-ROM
SDL_INIT_JOYSTICK | Charge le système de gestion du joystick.
SDL_INIT_EVERYTHING | Charge tous les systèmes listés ci-dessus à la fois.

Si vous appelez la fonction comme ceci :

SDL_Init(SDL_INIT_VIDEO);

… alors le système vidéo sera chargé et vous pourrez ouvrir une fenêtre, y dessiner, etc.
En fait, tout ce que vous faites c'est envoyer un nombre à SDL_Init à l'aide d'une constante. Vous ne savez pas de quel nombre il s'agit, et justement c'est ça qui est bien. Vous avez juste besoin d'écrire la constante, c'est plus facile à lire et à retenir.

La fonction SDL_Init regardera le nombre qu'elle reçoit et en fonction de cela, elle saura quels systèmes elle doit charger.

Maintenant, si vous faites :

SDL_Init(SDL_INIT_EVERYTHING);

… vous chargez tous les sytèmes de la SDL. Ne faites cela que si vous avez vraiment besoin de tout, il est inutile de surcharger votre ordinateur en chargeant des modules dont vous ne vous servirez pas.

Supposons que je veuille charger l'audio et la vidéo seulement. Dois-je utiliser SDL_INIT_EVERYTHING ?

Vous n'allez pas utiliser SDL_INIT_EVERYTHING juste parce que vous avez besoin de deux modules, pauvres fous ! Heureusement, on peut combiner les options à l'aide du symbole | (la barre verticale).

// Chargement de la vidéo et de l'audio
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);

Vous pouvez aussi en combiner trois sans problème :

// Chargement de la vidéo, de l'audio et du timer
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER);

Ces « options » que l'on envoie à SDL_Init sont aussi appelées flags. C'est quelque chose que vous rencontrerez assez souvent.
Retenez bien qu'on utilise la barre verticale | pour combiner les options. Ça agit un peu comme une addition.

SDL_Quit : arrêt de la SDL

La fonction SDL_Quit est très simple à utiliser vu qu'elle ne prend pas de paramètre :

SDL_Quit();

Tous les systèmes initialisés seront arrêtés et libérés de la mémoire.
Bref, c'est un moyen de quitter la SDL proprement, à faire à la fin de votre programme.

Canevas de programme SDL

En résumé, voici à quoi va ressembler votre programme SDL dans sa version la plus simple :

#include <stdlib.h>
#include <stdio.h>
#include <SDL/SDL.h>

int main(int argc, char *argv[])
{
    SDL_Init(SDL_INIT_VIDEO); // Démarrage de la SDL (ici : chargement du système vidéo)

    /*
    La SDL est chargée.
    Vous pouvez mettre ici le contenu de votre programme
    */

    SDL_Quit(); // Arrêt de la SDL (libération de la mémoire).

    return 0;
}

Bien entendu, aucun programme « sérieux » ne tiendra dans le main. Ce que je fais là est schématique. Dans la réalité, votre main contiendra certainement de nombreux appels à des fonctions qui feront elles aussi plusieurs appels à d'autres fonctions.
Ce qui compte au final, c'est que la SDL soit chargée au début et qu'elle soit fermée à la fin quand vous n'en avez plus besoin.

Gérer les erreurs

La fonction SDL_Init renvoie une valeur :

  • -1 en cas d'erreur ;

  • 0 si tout s'est bien passé.

Vous n'y êtes pas obligés, mais vous pouvez vérifier la valeur retournée par SDL_Init. Ça peut être un bon moyen de traiter les erreurs de votre programme et donc de vous aider à les résoudre.

Mais comment afficher l'erreur qui s'est produite ?

Excellente question ! On n'a plus de console maintenant, donc comment faire pour stocker et afficher des messages d'erreurs ?

Deux possibilités :

  • soit on modifie les options du projet pour qu'il affiche à nouveau la console. On pourra alors faire des printf ;

  • soit on écrit dans un fichier les erreurs. On utilisera fprintf.

J'ai choisi d'écrire dans un fichier. Cependant, écrire dans un fichier implique de faire un fopen, un fclose… bref, c'est un peu moins facile qu'un printf.
Heureusement, il y a une solution plus simple : utiliser la sortie d'erreur standard.

Il y a une variable stderr qui est définie par stdio.h et qui pointe vers un endroit où l'erreur peut être écrite. Généralement sous Windows, ce sera un fichier stderr.txt tandis que sous Linux, l'erreur apparaîtra le plus souvent dans la console. Cette variable est automatiquement créée au début du programme et supprimée à la fin. Vous n'avez donc pas besoin de faire de fopen ou de fclose.
Vous pouvez donc faire un fprintf sur stderr sans utiliser fopen ou fclose :

#include <stdlib.h>
#include <stdio.h>
#include <SDL/SDL.h>

int main(int argc, char *argv[])
{
    if (SDL_Init(SDL_INIT_VIDEO) == -1) // Démarrage de la SDL. Si erreur :
    {
        fprintf(stderr, "Erreur d'initialisation de la SDL : %s\n", SDL_GetError()); // Écriture de l'erreur
        exit(EXIT_FAILURE); // On quitte le programme
    }

    SDL_Quit();

    return EXIT_SUCCESS;
}

Quoi de neuf dans ce code ?

  • On écrit dans stderr notre erreur. Le %s permet de laisser la SDL indiquer les détails de l'erreur : la fonction SDL_GetError() renvoie en effet la dernière erreur de la SDL.

  • On quitte en utilisant exit(). Jusque-là, rien de nouveau. Toutefois, vous aurez remarqué que j'utilise une constante (EXIT_FAILURE) pour indiquer la valeur que renvoie le programme. De plus, à la fin j'utilise EXIT_SUCCESS au lieu de 0.
    Qu'est-ce que j'ai changé ? En fait j'améliore petit à petit nos codes source. En effet, le nombre qui signifie « erreur » par exemple peut être différent selon les ordinateurs ! Cela dépend là encore de l'OS.
    Pour pallier ce problème, stdlib.h nous fournit deux constantes (des define) :

    • EXIT_FAILURE : valeur à renvoyer en cas d'échec du programme ;

    • EXIT_SUCCESS : valeur à renvoyer en cas de réussite du programme.

    En utilisant ces constantes au lieu de nombres, vous êtes certains de renvoyer une valeur correcte quel que soit l'OS.
    Pourquoi ? Parce que le fichier stdlib.h change selon l'OS sur lequel vous êtes, donc les valeurs des constantes sont adaptées. Votre code source, lui, n'a pas besoin d'être modifié ! C'est ce qui rend le langage C compatible avec tous les OS (pour peu que vous programmiez correctement en utilisant les outils fournis, comme ici).

Cela n'a pas de grandes conséquences pour nous pour le moment, mais c'est plus sérieux d'utiliser ces constantes. C'est donc ce que nous ferons à partir de maintenant.

Ouverture d'une fenêtre

Bon : la SDL est initialisée et fermée correctement, maintenant. La prochaine étape, si vous le voulez bien, et je suis sûr que vous le voulez bien, c'est l'ouverture d'une fenêtre !

Pour commencer déjà, assurez-vous d'avoir un main qui ressemble à ceci :

int main(int argc, char *argv[])
{
    if (SDL_Init(SDL_INIT_VIDEO) == -1)
    {
        fprintf(stderr, "Erreur d'initialisation de la SDL");
        exit(EXIT_FAILURE);
    }

    SDL_Quit();

    return EXIT_SUCCESS;
}

Cela devrait être le cas si vous avez bien suivi le début du chapitre. Pour le moment donc, on initialise juste la vidéo (SDL_INIT_VIDEO), c'est tout ce qui nous intéresse.

Choix du mode vidéo

La première chose à faire après SDL_Init(), c'est indiquer le mode vidéo que vous voulez utiliser, c'est-à-dire la résolution, le nombre de couleurs et quelques autres options.

On va utiliser pour cela la fonction SDL_SetVideoMode() qui prend quatre paramètres :

  • la largeur de la fenêtre désirée (en pixels) ;

  • la hauteur de la fenêtre désirée (en pixels) ;

  • le nombre de couleurs affichables (en bits / pixel) ;

  • des options (des flags).

Pour la largeur et la hauteur de la fenêtre, je crois que je ne vais pas vous faire l'affront de vous expliquer ce que c'est. Par contre, les deux paramètres suivants sont plus intéressants.

  • Le nombre de couleurs : c'est le nombre maximal de couleurs affichables dans votre fenêtre. Si vous jouez aux jeux vidéo, vous devriez avoir l'habitude de cela. Une valeur de 32 bits / pixel permet d'afficher des milliards de couleurs (c'est le maximum). C'est cette valeur que nous utiliserons le plus souvent car désormais tous les ordinateurs gèrent les couleurs en 32 bits. Sachez aussi que vous pouvez mettre des valeurs plus faibles comme 16 bits / pixel (65536 couleurs), ou 8 bits / pixel (256 couleurs). Cela peut être utile surtout si vous faites un programme pour un petit appareil genre PDA ou téléphone portable.

  • Les options : comme pour SDL_Init on doit utiliser des flags pour définir des options. Voici les principaux flags que vous pouvez utiliser (et combiner avec le symbole |).

    • SDL_HWSURFACE : les données seront chargées dans la mémoire vidéo, c'est-à-dire dans la mémoire de votre carte 3D. Avantage : cette mémoire est plus rapide. Défaut : il y a en général moins d'espace dans cette mémoire que dans l'autre (SDL_SWSURFACE).

    • SDL_SWSURFACE : les données seront chargées dans la mémoire système (c'est-à-dire la RAM, a priori). Avantage : il y a beaucoup d'espace dans cette mémoire. Défaut : c'est moins rapide et moins optimisé.

    • SDL_RESIZABLE : la fenêtre sera redimensionnable. Par défaut elle ne l'est pas.

    • SDL_NOFRAME : la fenêtre n'aura pas de barre de titre ni de bordure.

    • SDL_FULLSCREEN : mode plein écran. Dans ce mode, aucune fenêtre n'est ouverte. Votre programme prendra toute la place à l'écran, en changeant automatiquement la résolution de celui-ci au besoin.

    • SDL_DOUBLEBUF : mode double buffering. C'est une technique très utilisée dans les jeux 2D, et qui permet de faire en sorte que les déplacements des objets à l'écran soient fluides, sinon ça scintille et c'est assez laid. Je vous expliquerai les détails de cette technique très intéressante plus loin.

Donc, si je fais :

SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE);

… cela ouvre une fenêtre de taille 640 x 480 en 32 bits / pixel (milliards de couleurs) qui sera chargée en mémoire vidéo (c'est la plus rapide, on préfèrera utiliser celle-là).

Autre exemple, si je fais :

SDL_SetVideoMode(400, 300, 32, SDL_HWSURFACE | SDL_RESIZABLE | SDL_DOUBLEBUF);

… cela ouvre une fenêtre redimensionnable de taille initiale 400 x 300 (32 bits / pixel) en mémoire vidéo, avec le double buffering activé.

Voici un premier code source très simple que vous pouvez essayer :

#include <stdlib.h>
#include <stdio.h>
#include <SDL/SDL.h>

int main(int argc, char *argv[])
{
    SDL_Init(SDL_INIT_VIDEO);

    SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE);

    SDL_Quit();

    return EXIT_SUCCESS;
}

J'ai volontairement retiré la gestion d'erreur pour rendre le code plus lisible et plus court, mais vous devriez dans un vrai programme prendre toutes les précautions nécessaires et gérer les erreurs.

Testez.
Que se passe-t-il ? La fenêtre apparaît et disparaît à la vitesse de la lumière. En effet, la fonction SDL_SetVideoMode est immédiatement suivie de SDL_Quit, donc tout s'arrête immédiatement.

Mettre en pause le programme

Comment faire en sorte que la fenêtre se maintienne ?

Il faut faire comme le font tous les programmes, que ce soit des jeux vidéo ou autre : une boucle infinie. En effet, à l'aide d'une bête boucle infinie on empêche notre programme de s'arrêter. Le problème est que cette solution est trop efficace car du coup, il n'y a pas de moyen d'arrêter le programme (à part un bon vieux CTRL + ALT + SUPPR à la rigueur mais c'est… brutal).

Voici un code qui fonctionne mais à ne pas tester, je vous le donne juste à titre explicatif :

int main(int argc, char *argv[])
{
    SDL_Init(SDL_INIT_VIDEO);

    SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE);

    while(1);

    SDL_Quit();

    return EXIT_SUCCESS;
}

Vous reconnaissez le while(1); : c'est la boucle infinie. Comme 1 signifie « vrai » (rappelez-vous les booléens), la boucle est toujours vraie et tourne en rond indéfiniment sans qu'il y ait moyen de l'arrêter. Ce n'est donc pas une très bonne solution.

Pour mettre en pause notre programme afin de pouvoir admirer notre belle fenêtre sans faire de boucle interminable, on va utiliser une petite fonction à moi, la fonction pause() :

void pause()
{
    int continuer = 1;
    SDL_Event event;

    while (continuer)
    {
        SDL_WaitEvent(&event);
        switch(event.type)
        {
            case SDL_QUIT:
                continuer = 0;
        }
    }
}

Je ne vous explique pas le détail de cette fonction pour le moment. C'est volontaire, car cela fait appel à la gestion des événements que je vous expliquerai seulement dans un prochain chapitre. Si je vous explique tout à la fois maintenant, vous risquez de tout mélanger ! Faites donc pour l'instant confiance à ma fonction de pause, nous ne tarderons pas à l'expliquer.

Voici un code source complet et correct que vous pouvez (enfin !) tester :

#include <stdlib.h>
#include <stdio.h>
#include <SDL/SDL.h>

void pause();

int main(int argc, char *argv[])
{
    SDL_Init(SDL_INIT_VIDEO); // Initialisation de la SDL

    SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE); // Ouverture de la fenêtre

    pause(); // Mise en pause du programme

    SDL_Quit(); // Arrêt de la SDL

    return EXIT_SUCCESS; // Fermeture du programme
}

void pause()
{
    int continuer = 1;
    SDL_Event event;

    while (continuer)
    {
        SDL_WaitEvent(&event);
        switch(event.type)
        {
            case SDL_QUIT:
                continuer = 0;
        }
    }
}

Vous remarquerez que j'ai mis le prototype de ma fonction pause() en haut pour tout vous présenter sur un seul fichier.
Je fais appel à la fonction pause() qui fait une boucle infinie un peu plus intelligente que tout à l'heure. Cette boucle s'arrêtera en effet si vous cliquez sur la croix pour fermer la fenêtre !

La fig. suivante vous donne une idée de ce à quoi devrait ressembler la fenêtre que vous avez sous les yeux (ici, une fenêtre 640 x 480).

Une fenêtre vide en 640 x 480

Pfiou ! Nous y sommes enfin arrivés !

Si vous voulez, vous pouvez mettre le flag « redimensionnable » pour autoriser le redimensionnement de votre fenêtre. Toutefois, dans la plupart des jeux on préfère avoir une fenêtre de taille fixe (c'est plus simple à gérer !), nous garderons donc notre fenêtre fixe pour le moment.

Attention au mode plein écran (SDL_FULLSCREEN) et au mode sans bordure (SDL_NOFRAME). Comme il n'y a pas de barre de titre dans ces deux modes, vous ne pourrez pas fermer le programme et vous serez alors obligés d'utiliser la commande CTRL + ALT + SUPPR.
Attendez d'apprendre à manipuler les événements SDL (dans quelques chapitres) et vous pourrez alors coder un moyen de sortir de votre programme avec une technique un peu moins brutale que le CTRL + ALT + SUPPR.

Changer le titre de la fenêtre

Pour le moment, notre fenêtre a un titre par défaut : (SDL_app sur la fig. suivante).
Que diriez-vous de changer cela ?

C'est extrêmement simple, il suffit d'utiliser la fonction SDL_WM_SetCaption.
Cette fonction prend deux paramètres. Le premier est le titre que vous voulez donner à la fenêtre, le second est le titre que vous voulez donner à l'icône.

Contrairement à ce qu'on pourrait croire, donner un titre à l'icône ne correspond pas à charger une icône qui s'afficherait dans la barre de titre en haut à gauche. Cela ne fonctionne pas partout (à ma connaissance, cela donne un résultat sous Gnome sous Linux). Personnellement, j'envoie la valeur NULL à la fonction. Sachez qu'il est possible de changer l'icône de la fenêtre, mais nous verrons comment le faire dans le chapitre suivant seulement, car ce n'est pas encore de notre niveau.

Voici donc le même main que tout à l'heure avec la fonction SDL_WM_SetCaption en plus :

int main(int argc, char *argv[])
{
    SDL_Init(SDL_INIT_VIDEO);

    SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE);
    SDL_WM_SetCaption("Ma super fenêtre SDL !", NULL);

    pause();

    SDL_Quit();

    return EXIT_SUCCESS;
}

Vous aurez remarqué que j'ai mis NULL pour le fameux second paramètre peu utile. En C, il est obligatoire de renseigner tous les paramètres même si certains ne vous intéressent pas, quitte à envoyer NULL comme je l'ai fait ici. Le C++ permet, lui, de ne pas renseigner certains paramètres facultatifs lors des appels de fonctions.

La fenêtre a maintenant un titre (cf fig. suivante).

Une fenêtre avec un titre

Manipulation des surfaces

Pour le moment nous avons une fenêtre avec un fond noir. C'est la fenêtre de base. Ce qu'on veut faire, c'est la remplir, c'est-à-dire « dessiner » dedans.

La SDL, je vous l'ai dit dans le chapitre précédent, est une bibliothèque bas niveau. Cela veut dire qu'elle ne nous propose que des fonctions de base, très simples.
Et en effet, la seule forme que la SDL nous permet de dessiner, c'est le rectangle ! Tout ce que vous allez faire, c'est assembler des rectangles dans la fenêtre. On appelle ces rectangles des surfaces. La surface est la brique de base de la SDL.

Il est bien sûr possible de dessiner d'autres formes, comme des cercles, des triangles, etc. Mais pour le faire, il faudra écrire nous-mêmes des fonctions, en dessinant pixel par pixel la forme, ou bien utiliser une bibliothèque supplémentaire avec la SDL. C'est un peu compliqué et de toute manière, vous verrez que nous n'en aurons pas vraiment besoin dans la pratique.

Votre première surface : l'écran

Dans tout programme SDL, il y a au moins une surface que l'on appelle généralement ecran (ou screen en anglais). C'est une surface qui correspond à toute la fenêtre, c'est-à-dire à toute la zone noire de la fenêtre que vous voyez.

Dans notre code source, chaque surface sera mémorisée dans une variable de type SDL_Surface. Oui, c'est un type de variable créé par la SDL (une structure, en l'occurrence).

Comme la première surface que nous devons créer est l'écran, allons-y :

SDL_Surface *ecran = NULL;

Vous remarquerez que je crée un pointeur. Pourquoi je fais ça ? Parce que c'est la SDL qui va allouer de l'espace en mémoire pour notre surface. Une surface n'a en effet pas toujours la même taille et la SDL est obligée de faire une allocation dynamique pour nous (ici, ça dépendra de la taille de la fenêtre que vous avez ouverte).

Je ne vous l'ai pas dit tout à l'heure, mais la fonction SDL_SetVideoMode renvoie une valeur ! Elle renvoie un pointeur sur la surface de l'écran qu'elle a créée en mémoire pour nous.
Parfait, on va donc pouvoir récupérer ce pointeur dans ecran :

ecran = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE);

Notre pointeur peut maintenant valoir :

  • NULL : ecran vaut NULL si la SDL_SetVideoMode n'a pas réussi à charger le mode vidéo demandé. Cela arrive si vous demandez une trop grande résolution ou un trop grand nombre de couleurs que ne supporte pas votre ordinateur ;

  • une autre valeur : si la valeur est différente de NULL, c'est que la SDL a pu allouer la surface en mémoire, donc que tout est bon !

Il serait bien ici de gérer les erreurs, comme on a appris à le faire tout à l'heure pour l'initialisation de la SDL. Voici donc notre main avec la gestion de l'erreur pour SDL_SetVideoMode :

int main(int argc, char *argv[])
{
    SDL_Surface *ecran = NULL; // Le pointeur qui va stocker la surface de l'écran

    SDL_Init(SDL_INIT_VIDEO);

    ecran = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE); // On tente d'ouvrir une fenêtre
    if (ecran == NULL) // Si l'ouverture a échoué, on le note et on arrête
    {
        fprintf(stderr, "Impossible de charger le mode vidéo : %s\n", SDL_GetError());
        exit(EXIT_FAILURE);
    }

    SDL_WM_SetCaption("Ma super fenêtre SDL !", NULL);

    pause();

    SDL_Quit();

    return EXIT_SUCCESS;
}

Le message que nous laissera SDL_GetError() nous sera très utile pour savoir ce qui n'a pas marché.

Petite anecdote : une fois, je me suis trompé en voulant faire du plein écran. Au lieu de demander une résolution de 1024 * 768, j'ai écrit 10244 * 768. Je ne comprenais pas au départ pourquoi ça ne voulait pas charger, car je ne voyais pas le double 4 dans mon code (oui, je devais être un peu fatigué). Un petit coup d'œil au fichier stderr.txt qui contenait l'erreur et j'ai tout de suite compris que c'était ma résolution qui avait été rejetée (tiens, comme c'est curieux !).

Colorer une surface

Il n'y a pas 36 façons de remplir une surface… En fait, il y en a deux :

  • soit vous remplissez la surface avec une couleur unie ;

  • soit vous remplissez la surface en chargeant une image.

Il est aussi possible de dessiner pixel par pixel dans la surface mais c'est assez compliqué, nous ne le verrons pas ici.

Nous allons dans un premier temps voir comment remplir une surface avec une couleur unie. Dans le chapitre suivant, nous apprendrons à charger une image.

La fonction qui permet de colorer une surface avec une couleur unie s'appelle SDL_FillRect (FillRect = « remplir rectangle » en anglais). Elle prend trois paramètres, dans l'ordre :

  • un pointeur sur la surface dans laquelle on doit dessiner (par exemple ecran) ;

  • la partie de la surface qui doit être remplie. Si vous voulez remplir toute la surface (et c'est ce qu'on veut faire), envoyez NULL ;

  • la couleur à utiliser pour remplir la surface.

En résumé :

SDL_FillRect(surface, NULL, couleur);
La gestion des couleurs en SDL

En SDL, une couleur est stockée dans un nombre de type Uint32.

Si c'est un nombre, pourquoi ne pas avoir utilisé le type de variable int ou long, tout simplement ?

La SDL est une bibliothèque multi-plates-formes. Or, comme vous le savez maintenant, la taille occupée par un int ou un long peut varier selon votre OS. Pour s'assurer que le nombre occupera toujours la même taille en mémoire, la SDL a donc « inventé » des types pour stocker des entiers qui ont la même taille sur tous les systèmes.
Il y a par exemple :

  • Uint32 : un entier de longueur 32 bits, soit 4 octets (je rappelle que 1 octet = 8 bits) ;

  • Uint16 : un entier codé sur 16 bits (2 octets) ;

  • Uint8 : un entier codé sur 8 bits (1 octet).

La SDL ne fait qu'un simple typedef qui changera selon l'OS que vous utilisez. Regardez de plus près le fichier SDL_types.h si vous êtes curieux.

On ne va pas s'attarder là-dessus plus longtemps car les détails de tout cela ne sont pas importants. Vous avez juste besoin de retenir que Uint32 est un type qui stocke un entier, comme un int.

D'accord, mais comment je sais quel nombre je dois mettre pour utiliser la couleur verte, azur, gris foncé ou encore jaune pâle à points roses avec des petites fleurs violettes (bien entendu, cette dernière couleur n'existe pas) ?

Il existe une fonction qui sert à ça : SDL_MapRGB. Elle prend quatre paramètres :

  • le format des couleurs. Ce format dépend du nombre de bits / pixel que vous avez demandé avec SDL_SetVideoMode. Vous pouvez le récupérer, il est stocké dans la sous-variable ecran->format ;

  • la quantité de rouge de la couleur ;

  • la quantité de vert de la couleur ;

  • la quantité de bleu de la couleur.

Certains d'entre vous ne le savent peut-être pas, mais toute couleur sur un ordinateur est construite à partir de trois couleurs de base : le rouge, le vert et le bleu.
Chaque quantité peut varier de 0 (pas de couleur) à 255 (toute la couleur).
Donc, si on écrit :

SDL_MapRGB(ecran->format, 255, 0, 0)

… on crée une couleur rouge. Il n'y a pas de vert ni de bleu.

Autre exemple, si on écrit :

SDL_MapRGB(ecran->format, 0, 0, 255)

… cette fois, c'est une couleur bleue.

SDL_MapRGB(ecran->format, 255, 255, 255)

… là, il s'agit d'une couleur blanche (toutes les couleurs s'additionnent). Si vous voulez du noir, il faut donc écrire 0, 0, 0.

On ne peut que mettre du rouge, du vert, du bleu, du noir et du blanc ?

Non, c'est à vous de combiner intelligemment les quantités de couleurs. Pour vous aider, ouvrez par exemple le logiciel Paint. Allez dans le menu Couleurs / Modifier les couleurs. Cliquez sur le bouton Définir les couleurs personnalisées.
Là, choisissez la couleur qui vous intéresse (fig. suivante).

Sélection d'une couleur et de ses composantes

Les composantes de la couleur sont affichées en bas à droite. Ici, j'ai sélectionné un bleu-vert. Comme l'indique la fenêtre, il se crée à l'aide de 17 de rouge, 206 de vert et 112 de bleu.

Coloration de l'écran

La fonction SDL_MapRGB renvoie un Uint32 qui correspond à la couleur demandée.
On peut donc créer une variable bleuVert qui contiendra le code de la couleur bleu-vert :

Uint32 bleuVert = SDL_MapRGB(ecran->format, 17, 206, 112);

Toutefois, ce n'est pas toujours la peine de passer par une variable pour stocker la couleur (à moins que vous en ayez besoin très souvent dans votre programme).
Vous pouvez tout simplement envoyer directement la couleur donnée par SDL_MapRGB à SDL_FillRect.

Si on veut remplir notre écran de bleu-vert, on peut donc écrire :

SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 17, 206, 112));

On combine ici deux fonctions, mais comme vous devriez maintenant le savoir, ça ne pose aucun problème au langage C.

Mise à jour de l'écran

Nous y sommes presque.
Toutefois il manque encore une petite chose : demander la mise à jour de l'écran. En effet, l'instruction SDL_FillRect colorie bien l'écran mais cela ne se fait que dans la mémoire. Il faut ensuite demander à l'ordinateur de mettre à jour l'écran avec les nouvelles données.

Pour cela, on va utiliser la fonction SDL_Flip, dont nous reparlerons plus longuement plus loin dans le cours.
Cette fonction prend un paramètre : l'écran.

SDL_Flip(ecran); /* Mise à jour de l'écran */
On résume !

Voici une fonction main() qui crée une fenêtre avec un fond bleu-vert :

int main(int argc, char *argv[])
{
    SDL_Surface *ecran = NULL;

    SDL_Init(SDL_INIT_VIDEO);

    ecran = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE);
    SDL_WM_SetCaption("Ma super fenêtre SDL !", NULL);

    // Coloration de la surface ecran en bleu-vert
    SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 17, 206, 112));

    SDL_Flip(ecran); /* Mise à jour de l'écran avec sa nouvelle couleur */

    pause();

    SDL_Quit();

    return EXIT_SUCCESS;
}

Le résultat est présenté sur la fig. suivante.

La fenêtre est remplie d'un fond bleu-vert

Dessiner une nouvelle surface à l'écran

C'est bien, mais ne nous arrêtons pas en si bon chemin. Pour le moment on n'a qu'une seule surface, c'est-à-dire l'écran. On aimerait pouvoir y dessiner, c'est-à-dire « coller » des surfaces avec une autre couleur par-dessus.

Pour commencer, nous allons avoir besoin de créer une variable de type SDL_Surface pour cette nouvelle surface :

SDL_Surface *rectangle = NULL;

Nous devons ensuite demander à la SDL de nous allouer de l'espace en mémoire pour cette nouvelle surface.
Pour l'écran, nous avons utilisé SDL_SetVideoMode. Toutefois, cette fonction ne marche que pour l'écran (la surface globale), on ne va pas créer une fenêtre différente pour chaque rectangle que l'on veut dessiner !

Il existe donc une autre fonction pour créer une surface : SDL_CreateRGBSurface. C'est celle que nous utiliserons à chaque fois que nous voudrons créer une surface unie comme ici.

Cette fonction prend… beaucoup de paramètres (huit !). D'ailleurs, peu d'entre eux nous intéressent pour l'instant, je vais donc éviter de vous détailler ceux qui ne nous serviront pas de suite.
Comme en C nous sommes obligés d'indiquer tous les paramètres, nous enverrons la valeur 0 quand le paramètre ne nous intéresse pas.

Regardons de plus près les quatre premiers paramètres, les plus intéressants (ils devraient vous rappeler la création de l'écran).

  • Une liste de flags (des options). Vous avez le choix entre :

    • SDL_HWSURFACE : la surface sera chargée en mémoire vidéo. Il y a moins d'espace dans cette mémoire que dans la mémoire système (quoique, avec les cartes 3D qu'on sort de nos jours, il y a de quoi se poser des questions…), mais cette mémoire est plus optimisée et accélérée ;

    • SDL_SWSURFACE : la surface sera chargée en mémoire système où il y a beaucoup de place, mais cela obligera votre processeur à faire plus de calculs. Si vous aviez chargé la surface en mémoire vidéo, c'est la carte 3D qui aurait fait la plupart des calculs.

  • La largeur de la surface (en pixels).

  • La hauteur de la surface (en pixels).

  • Le nombre de couleurs (en bits / pixel).

Voici donc comment on alloue notre nouvelle surface en mémoire :

rectangle = SDL_CreateRGBSurface(SDL_HWSURFACE, 220, 180, 32, 0, 0, 0, 0);

Les quatre derniers paramètres sont mis à 0, comme je vous l'ai dit, car ils ne nous intéressent pas.

Comme notre surface a été allouée manuellement, il faudra penser à la libérer de la mémoire avec la fonction SDL_FreeSurface(), à utiliser juste avant SDL_Quit() :

SDL_FreeSurface(rectangle);
SDL_Quit();

La surface ecran n'a pas besoin d'être libérée avec SDL_FreeSurface(), cela est fait automatiquement lors de SDL_Quit().

On peut maintenant colorer notre nouvelle surface en la remplissant par exemple de blanc :

SDL_FillRect(rectangle, NULL, SDL_MapRGB(ecran->format, 255, 255, 255));
Coller la surface à l'écran

Allez, c'est presque fini, courage ! Notre surface est prête, mais si vous testez le programme vous verrez qu'elle ne s'affichera pas ! En effet, la SDL n'affiche à l'écran que la surface ecran. Pour que l'on puisse voir notre nouvelle surface, il va falloir blitter la surface, c'est-à-dire la coller sur l'écran. On utilisera pour cela la fonction SDL_BlitSurface.

Cette fonction attend :

  • la surface à coller (ici, ce sera rectangle) ;

  • une information sur la partie de la surface à coller (facultative). Ça ne nous intéresse pas car on veut coller toute la surface, donc on enverra NULL ;

  • la surface sur laquelle on doit coller, c'est-à-dire ecran dans notre cas ;

  • un pointeur sur une variable contenant des coordonnées. Ces coordonnées indiquent où devra être collée notre surface sur l'écran, c'est-à-dire sa position.

Pour indiquer les coordonnées, on doit utiliser une variable de type SDL_Rect.
C'est une structure qui contient plusieurs sous-variables, dont deux qui nous intéressent ici :

  • x : l'abscisse ;

  • y : l'ordonnée.

Il faut savoir que le point de coordonnées (0, 0) est situé tout en haut à gauche.
En bas à droite, le point a les coordonnées (640, 480) si vous avez ouvert une fenêtre de taille 640 x 480 comme moi.

Aidez-vous du schéma de la fig. suivante pour vous situer.

Coordonnées sur la fenêtre

Si vous avez déjà fait des maths une fois dans votre vie, vous ne devriez pas être trop perturbés. Créons donc une variable position. On va mettre x et y à 0 pour coller la surface en haut à gauche de l'écran :

SDL_Rect position;

position.x = 0;
position.y = 0;

Maintenant qu'on a notre variable position, on peut blitter notre rectangle sur l'écran :

SDL_BlitSurface(rectangle, NULL, ecran, &position);

Remarquez le symbole &, car il faut envoyer l'adresse de notre variable position.

Résumé du code source

Je crois qu'un petit code pour résumer tout ça ne sera pas superflu !

int main(int argc, char *argv[])
{
    SDL_Surface *ecran = NULL, *rectangle = NULL;
    SDL_Rect position;

    SDL_Init(SDL_INIT_VIDEO);

    ecran = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE);
    // Allocation de la surface
    rectangle = SDL_CreateRGBSurface(SDL_HWSURFACE, 220, 180, 32, 0, 0, 0, 0);
    SDL_WM_SetCaption("Ma super fenêtre SDL !", NULL);

    SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 17, 206, 112));

    position.x = 0; // Les coordonnées de la surface seront (0, 0)
    position.y = 0;
    // Remplissage de la surface avec du blanc
    SDL_FillRect(rectangle, NULL, SDL_MapRGB(ecran->format, 255, 255, 255));
    SDL_BlitSurface(rectangle, NULL, ecran, &position); // Collage de la surface sur l'écran

    SDL_Flip(ecran); // Mise à jour de l'écran

    pause();

    SDL_FreeSurface(rectangle); // Libération de la surface
    SDL_Quit();

    return EXIT_SUCCESS;
}

Et voilà le travail (fig. suivante) !

Une surface blanche en haut à gauche de la fenêtre

Centrer la surface à l'écran

On sait afficher la surface en haut à gauche. Il serait aussi facile de la placer en bas à droite. Les coordonnées seraient (640 - 220, 480 - 180), car il faut retrancher la taille de notre rectangle pour qu'il s'affiche entièrement.

Mais… comment faire pour centrer le rectangle blanc ?
Si vous réfléchissez bien deux secondes, c'est mathématique. C'est là qu'on comprend l'intérêt des maths et de la géométrie ! Et encore, tout ceci est d'un niveau très simple ici.

position.x = (640 / 2) - (220 / 2);
position.y = (480 / 2) - (180 / 2);

L'abscisse du rectangle sera la moitié de la largeur de l'écran (640 / 2). Mais, en plus de ça, il faut retrancher la moitié de la largeur du rectangle (220 / 2), car sinon ça ne sera pas parfaitement centré (essayez de ne pas le faire, vous verrez ce que je veux dire).
C'est la même chose pour l'ordonnée avec la hauteur de l'écran et du rectangle.

Résultat : la surface blanche est parfaitement centrée (fig. suivante).

Une surface blanche au centre de la fenêtre

Exercice : créer un dégradé

On va finir le chapitre par un petit exercice (corrigé) suivi d'une série d'autres exercices (non corrigés pour vous forcer à travailler !).

L'exercice corrigé n'est vraiment pas difficile : on veut créer un dégradé vertical allant du noir au blanc.
Vous allez devoir créer 255 surfaces de 1 pixel de hauteur. Chacune aura une couleur différente, de plus en plus noire.

Voici ce que vous devez arriver à obtenir au final, une image similaire à la fig. suivante.

Dégradé du noir au blanc

C'est mignon, non ? Et le pire c'est qu'il suffit de quelques petites boucles seulement pour y arriver.

Pour faire ça, on va devoir créer 256 surfaces (256 lignes) ayant les composantes rouge-vert-bleu suivantes :

(0, 0, 0) // Noir
(1, 1, 1) // Gris très très proche du noir
(2, 2, 2) // Gris très proche du noir
...
(128, 128, 128) // Gris moyen (à 50 %)
...
(253, 253, 253) // Gris très proche du blanc
(254, 254, 254) // Gris très très proche du blanc
(255, 255, 255) // Blanc

Tout le monde devrait avoir vu venir une boucle pour faire ça (on ne va pas faire 256 copier-coller !). Vous allez devoir créer un tableau de type SDL_Surface* de 256 cases pour stocker chacune des lignes.

Allez au boulot, je vous donne 5 minutes !

Correction !

D'abord, il fallait créer notre tableau de 256 SDL_Surface*. On l'initialise à NULL :

SDL_Surface *lignes[256] = {NULL};

On crée aussi une variable i dont on aura besoin pour nos for.

On change aussi la hauteur de la fenêtre pour qu'elle soit plus adaptée dans notre cas. On lui donne donc 256 pixels de hauteur, pour chacune des 256 lignes à afficher.

Ensuite, on fait un for pour allouer une à une chacune des 256 surfaces. Le tableau recevra 256 pointeurs vers chacune des surfaces créées :

for (i = 0 ; i <= 255 ; i++)
    lignes[i] = SDL_CreateRGBSurface(SDL_HWSURFACE, 640, 1, 32, 0, 0, 0, 0);

Ensuite, on remplit et on blitte chacune de ces surfaces une par une.

for (i = 0 ; i <= 255 ; i++)
{
    position.x = 0; // Les lignes sont à gauche (abscisse de 0)
    position.y = i; // La position verticale dépend du numéro de la ligne

    SDL_FillRect(lignes[i], NULL, SDL_MapRGB(ecran->format, i, i, i)); // Dessin
    SDL_BlitSurface(lignes[i], NULL, ecran, &position); // Collage
}

Notez que j'utilise la même variable position tout le temps. Pas besoin d'en créer 256 en effet, car la variable ne sert que pour être envoyée à SDL_BlitSurface. On peut donc la réutiliser sans problème.
Chaque fois, je mets à jour l'ordonnée (y) pour blitter la ligne à la bonne hauteur. La couleur à chaque passage dans la boucle dépend de i (ce sera 0, 0, 0 la première fois, et 255, 255, 255 la dernière fois).

Mais pourquoi x est toujours à 0 ?
Comment se fait-il que toute la ligne soit remplie si x est tout le temps à 0 ?

Notre variable position indique à quel endroit est placé le coin en haut à gauche de notre surface (ici, notre ligne). Elle n'indique pas la largeur de la surface, juste sa position sur l'écran.
Comme toutes nos lignes commencent à gauche de la fenêtre (le plus à gauche possible), on met une abscisse de 0. Essayez de mettre une abscisse de 50 pour voir ce que ça fait : toutes les lignes seront décalées vers la droite.
Comme la surface fait 640 pixels de largeur, la SDL dessine 640 pixels vers la droite (de la même couleur) en partant des coordonnées indiquées dans la variable position.

Sur le schéma de la fig. suivante, je vous montre les coordonnées du point en haut à gauche de l'écran (position de la première ligne) et celui du point en bas à droite de l'écran (position de la dernière ligne).

Coordonnées sur la fenêtre de dégradé

Comme vous le voyez, de haut en bas l'abscisse ne change pas (x reste égal à 0 tout le long).
C'est seulement y qui change pour chaque nouvelle ligne, d'où le position.y = i;.

Enfin, il ne faut pas oublier de libérer la mémoire pour chacune des 256 surfaces créées, le tout à l'aide d'une petite boucle bien entendu.

for (i = 0 ; i <= 255 ; i++) // N'oubliez pas de libérer les 256 surfaces
    SDL_FreeSurface(lignes[i]);
Résumé du main

Voici donc la fonction main au complet :

int main(int argc, char *argv[])
{
    SDL_Surface *ecran = NULL, *lignes[256] = {NULL};
    SDL_Rect position;
    int i = 0;

    SDL_Init(SDL_INIT_VIDEO);

    ecran = SDL_SetVideoMode(640, 256, 32, SDL_HWSURFACE);

    for (i = 0 ; i <= 255 ; i++)
        lignes[i] = SDL_CreateRGBSurface(SDL_HWSURFACE, 640, 1, 32, 0, 0, 0, 0);

    SDL_WM_SetCaption("Mon dégradé en SDL !", NULL);

    SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 0, 0, 0));

    for (i = 0 ; i <= 255 ; i++)
    {
        position.x = 0; // Les lignes sont à gauche (abscisse de 0)
        position.y = i; // La position verticale dépend du numéro de la ligne
        SDL_FillRect(lignes[i], NULL, SDL_MapRGB(ecran->format, i, i, i));
        SDL_BlitSurface(lignes[i], NULL, ecran, &position);
    }

    SDL_Flip(ecran);
    pause();

    for (i = 0 ; i <= 255 ; i++) // N'oubliez pas de libérer les 256 surfaces
        SDL_FreeSurface(lignes[i]);
    SDL_Quit();

    return EXIT_SUCCESS;
}

« Je veux des exercices pour m'entraîner ! »

Pas de problème, générateur d'exercices activé !

  • Créez le dégradé inverse, du blanc au noir. Cela ne devrait pas être trop difficile pour commencer !

  • Vous pouvez aussi faire un double dégradé, en allant du noir au blanc comme on a fait ici, puis du blanc au noir (la fenêtre fera alors le double de hauteur).

  • Guère plus difficile, vous pouvez aussi vous entraîner à faire un dégradé horizontal au lieu d'un dégradé vertical.

  • Faites des dégradés en utilisant d'autres couleurs que le blanc et le noir. Essayez pour commencer du rouge au noir, du vert au noir et du bleu au noir, puis du rouge au blanc, etc.

En résumé

  • La SDL doit être chargée avec SDL_Init au début du programme et déchargée avec SDL_Quit à la fin.

  • Les flags sont des constantes que l'on peut additionner entre elles avec le symbole « | ». Elles jouent le rôle d'options.

  • La SDL vous fait manipuler des surfaces qui ont la forme de rectangles avec le type SDL_Surface. Le dessin sur la fenêtre se fait à l'aide de ces surfaces.

  • Il y a toujours au moins une surface qui prend toute la fenêtre, que l'on appelle en général ecran.

  • Le remplissage d'une surface se fait avec SDL_FillRect et le collage sur l'écran à l'aide de SDL_BlitSurface.

  • Les couleurs sont définies à l'aide d'un mélange de rouge, de vert et de bleu.


Apprenez à programmer en C

Afficher des images

Nous venons d'apprendre à charger la SDL, à ouvrir une fenêtre et gérer des surfaces. C'est vraiment la base de ce qu'il faut connaître sur cette bibliothèque. Cependant, pour le moment nous ne pouvons créer que des surfaces unies, c'est-à-dire ayant la même couleur, ce qui est un peu monotone.

Dans ce chapitre, nous allons apprendre à charger des images dans des surfaces, que ce soit des BMP, des PNG, des GIF ou des JPG. La manipulation d'images est souvent très motivante car c'est en assemblant ces images (aussi appelées « sprites ») que l'on fabrique les premières briques d'un jeu vidéo.

Charger une image BMP

La SDL est une bibliothèque très simple. Elle ne propose à la base que le chargement d'images de type « bitmap » (extension .bmp). Ne paniquez pas pour autant, car grâce à une extension de la SDL (la bibliothèque SDL_Image), nous verrons qu'il est possible de charger de nombreux autres types.

Pour commencer, nous allons nous contenter de ce que la SDL offre à la base. Nous allons donc étudier le chargement de BMP.

Le format BMP

Un BMP (abréviation de « Bitmap ») est un format d'image.
Les images que vous voyez sur votre ordinateur sont stockées dans des fichiers. Il existe plusieurs formats d'images, c'est-à-dire plusieurs façons de coder l'image dans un fichier. Selon le format, l'image prend plus ou moins d'espace disque et se trouve être de plus ou moins bonne qualité.

Le Bitmap est un format non compressé (contrairement aux JPG, PNG, GIF, etc.).
Concrètement, cela signifie les choses suivantes :

  • le fichier est très rapide à lire, contrairement aux formats compressés qui doivent être décompressés, ce qui prend un peu plus de temps ;

  • la qualité de l'image est parfaite. Certains formats compressés (je pense au JPG plus particulièrement, car les PNG et GIF n'altèrent pas l'image) détériorent la qualité de l'image, ce n'est pas le cas du BMP ;

  • mais le fichier est aussi bien plus gros puisqu'il n'est pas compressé !

Il a donc des qualités et des défauts.
Pour la SDL, l'avantage c'est que ce type de fichier est simple et rapide à lire. Si vous avez souvent besoin de charger des images au cours de l'exécution de votre programme, il vaut mieux utiliser des BMP : certes le fichier est plus gros, mais il se chargera plus vite qu'un GIF par exemple. Cela peut se révéler utile si votre programme doit charger de très nombreuses images en peu de temps.

Charger un Bitmap

Téléchargement du pack d'images

Nous allons travailler avec plusieurs images dans ce chapitre. Si vous voulez faire les tests en même temps que vous lisez (et vous devriez !), je vous recommande de télécharger un pack qui contient toutes les images dont on va avoir besoin.

Télécharger le pack d'images (1 Mo)

Bien entendu, vous pouvez utiliser vos propres images. Il faudra en revanche adapter la taille de votre fenêtre à celles-ci.

Placez toutes les images dans le dossier de votre projet. Nous allons commencer par travailler avec le fichier lac_en_montagne.bmp. C'est une scène 3D d'exemple tirée de l'excellent logiciel de modélisation de paysages Vue d'Esprit 4, qui n'est aujourd'hui plus commercialisé. Depuis, le logiciel a été renommé en « Vue » et a beaucoup évolué. Si vous voulez en savoir plus, rendez-vous sur e-onsoftware.com.

Charger l'image dans une surface

Nous allons utiliser une fonction qui va charger l'image BMP et la mettre dans une surface.
Cette fonction a pour nom SDL_LoadBMP. Vous allez voir à quel point c'est simple :

maSurface = SDL_LoadBMP("image.bmp");

La fonction SDL_LoadBMP remplace deux fonctions que vous connaissez :

  • SDL_CreateRGBSurface : elle se chargeait d'allouer de la mémoire pour stocker une surface de la taille demandée (équivalent au malloc) ;

  • SDL_FillRect : elle remplissait la structure d'une couleur unie.

Pourquoi est-ce que ça remplace ces deux fonctions ? C'est très simple :

  • la taille à allouer en mémoire pour la surface dépend de la taille de l'image : si l'image a une taille de 250 x 300, alors votre surface aura une taille de 250 x 300 ;

  • d'autre part, votre surface sera remplie pixel par pixel par le contenu de votre image BMP.

Codons sans plus tarder :

int main(int argc, char *argv[])
{
    SDL_Surface *ecran = NULL, *imageDeFond = NULL;
    SDL_Rect positionFond;

    positionFond.x = 0;
    positionFond.y = 0;

    SDL_Init(SDL_INIT_VIDEO);

    ecran = SDL_SetVideoMode(800, 600, 32, SDL_HWSURFACE);
    SDL_WM_SetCaption("Chargement d'images en SDL", NULL);

    /* Chargement d'une image Bitmap dans une surface */
    imageDeFond = SDL_LoadBMP("lac_en_montagne.bmp");
    /* On blitte par-dessus l'écran */
    SDL_BlitSurface(imageDeFond, NULL, ecran, &positionFond);

    SDL_Flip(ecran);
    pause();

    SDL_FreeSurface(imageDeFond); /* On libère la surface */
    SDL_Quit();

    return EXIT_SUCCESS;
}

J'ai donc créé un pointeur vers une surface (imageDeFond) ainsi que les coordonnées correspondantes (positionFond).
La surface est créée en mémoire et remplie par la fonction SDL_LoadBMP.
On la blitte ensuite sur la surface ecran et c'est tout ! Admirez le résultat sur la fig. suivante.

Une image BMP chargée dans la fenêtre

Comme vous voyez ce n'était pas bien difficile !

Associer une icône à son application

Maintenant que nous savons charger des images, nous pouvons découvrir comment associer une icône à notre programme. L'icône sera affichée en haut à gauche de la fenêtre (ainsi que dans la barre des tâches). Pour le moment nous avons une icône par défaut.

Mais, les icônes des programmes ne sont-elles pas des .ico, normalement ?

Non, pas forcément ! D'ailleurs les .ico n'existent que sous Windows. La SDL réconcilie tout le monde en utilisant un système bien à elle : une surface !
Eh oui, l'icône d'un programme SDL n'est rien d'autre qu'une simple surface.

Votre icône doit normalement être de taille 16 x 16 pixels. Toutefois, sous Windows il faut que l'icône soit de taille 32 x 32 pixels, sinon elle sera déformée. Ne vous en faites pas, la SDL « réduira » les dimensions de l'image pour qu'elle rentre dans 16 x 16 pixels.

Pour ajouter l'icône à la fenêtre, on utilise la fonction SDL_WM_SetIcon.
Cette fonction prend deux paramètres : la surface qui contient l'image à afficher ainsi que des informations sur la transparence (NULL si on ne veut pas de transparence). La gestion de la transparence d'une icône est un peu compliquée (il faut préciser un à un quels sont les pixels transparents), nous ne l'étudierons donc pas.

On va combiner deux fonctions en une :

SDL_WM_SetIcon(SDL_LoadBMP("sdl_icone.bmp"), NULL);

L'image est chargée en mémoire par SDL_LoadBMP et l'adresse de la surface est directement envoyée à SDL_WM_SetIcon.

La fonction SDL_WM_SetIcon doit être appelée avant que la fenêtre ne soit ouverte, c'est-à-dire qu'elle doit se trouver avant SDL_SetVideoMode dans votre code source.

Voici le code source complet. Vous noterez que j'ai simplement ajouté le SDL_WM_SetIcon par rapport au code précédent :

int main(int argc, char *argv[])
{
    SDL_Surface *ecran = NULL, *imageDeFond = NULL;
    SDL_Rect positionFond;

    positionFond.x = 0;
    positionFond.y = 0;

    SDL_Init(SDL_INIT_VIDEO);

    /* Chargement de l'icône AVANT SDL_SetVideoMode */
    SDL_WM_SetIcon(SDL_LoadBMP("sdl_icone.bmp"), NULL);

    ecran = SDL_SetVideoMode(800, 600, 32, SDL_HWSURFACE);
    SDL_WM_SetCaption("Chargement d'images en SDL", NULL);

    imageDeFond = SDL_LoadBMP("lac_en_montagne.bmp");
    SDL_BlitSurface(imageDeFond, NULL, ecran, &positionFond);

    SDL_Flip(ecran);
    pause();

    SDL_FreeSurface(imageDeFond);
    SDL_Quit();

    return EXIT_SUCCESS;
}

Résultat, l'icône est chargée et affichée sur la fenêtre (fig. suivante).

Une icône associée au programme

Gestion de la transparence

Le problème de la transparence

Nous avons tout à l'heure chargé une image bitmap dans notre fenêtre.
Supposons que l'on veuille blitter une image par-dessus. Ça vous arrivera très fréquemment car dans un jeu, en général, le personnage que l'on déplace est un Bitmap et il se déplace sur une image de fond.

On va blitter l'image de Zozor (il s'agit de la bonne vieille mascotte du Site du Zéro pour ceux qui ne le connaîtraient pas) sur la scène :

int main(int argc, char *argv[])
{
    SDL_Surface *ecran = NULL, *imageDeFond = NULL, *zozor = NULL;
    SDL_Rect positionFond, positionZozor;

    positionFond.x = 0;
    positionFond.y = 0;
    positionZozor.x = 500;
    positionZozor.y = 260;

    SDL_Init(SDL_INIT_VIDEO);

    SDL_WM_SetIcon(SDL_LoadBMP("sdl_icone.bmp"), NULL);

    ecran = SDL_SetVideoMode(800, 600, 32, SDL_HWSURFACE);
    SDL_WM_SetCaption("Chargement d'images en SDL", NULL);

    imageDeFond = SDL_LoadBMP("lac_en_montagne.bmp");
    SDL_BlitSurface(imageDeFond, NULL, ecran, &positionFond);

    /* Chargement et blittage de Zozor sur la scène */
    zozor = SDL_LoadBMP("zozor.bmp");
    SDL_BlitSurface(zozor, NULL, ecran, &positionZozor);

    SDL_Flip(ecran);
    pause();

    SDL_FreeSurface(imageDeFond);
    SDL_FreeSurface(zozor);
    SDL_Quit();

    return EXIT_SUCCESS;
}

On a juste rajouté une surface pour y stocker Zozor, que l'on blitte ensuite à un endroit sur la scène (fig. suivante).

Zozor blitté par-dessus l'image de fond

C'est plutôt laid, non ?

Je sais pourquoi, c'est parce que tu as mis un fond bleu tout moche sur l'image de Zozor !

Parce que vous croyez qu'avec un fond noir ou un fond marron derrière Zozor, ça aurait été plus joli ? Eh bien non, le problème ici c'est que notre image est forcément rectangulaire, donc si on la colle sur la scène on voit son fond, ce qui ne rend pas très bien.

Heureusement, la SDL gère la transparence !

Rendre une image transparente

Étape 1 : préparer l'image

Pour commencer, il faut préparer l'image que vous voulez blitter sur la scène.
Le format BMP ne gère pas la transparence, contrairement aux GIF et PNG. Il va donc falloir utiliser une astuce.

Il faut mettre la même couleur de fond sur toute l'image. Celle-ci sera rendue transparente par la SDL au moment du blit. Observez à quoi ressemble mon zozor.bmp de plus près (fig. suivante).

L'image zozor.bmp a un fond bleu

Le fond bleu derrière est donc volontaire. Notez que j'ai choisi le bleu au hasard, j'aurais très bien pu mettre un fond vert ou rouge par exemple. Ce qui compte, c'est que cette couleur soit unique et unie. J'ai choisi le bleu parce qu'il n'y en avait pas dans l'image de Zozor. Si j'avais choisi le vert, j'aurais pris le risque que l'herbe que machouille Zozor (en bas à gauche de l'image) soit rendue transparente.

À vous donc de vous débrouiller avec votre logiciel de dessin (Paint, Photoshop, The Gimp, chacun ses goûts) pour donner un fond uni à votre image.

Étape 2 : indiquer la couleur transparente

Pour indiquer à la SDL la couleur qui doit être rendue transparente, vous devez utiliser la fonction SDL_SetColorKey. Cette fonction doit être appelée avant de blitter l'image.
Voici comment je m'en sers pour rendre le bleu derrière Zozor transparent :

SDL_SetColorKey(zozor, SDL_SRCCOLORKEY, SDL_MapRGB(zozor->format, 0, 0, 255));

Il y a trois paramètres :

  • la surface qui doit être rendue transparente (ici, c'est zozor) ;

  • une liste de flags : utilisez SDL_SRCCOLORKEY pour activer la transparence, 0 pour la désactiver ;

  • indiquez ensuite la couleur qui doit être rendue transparente. J'ai utilisé SDL_MapRGB pour créer la couleur au format nombre (Uint32) comme on l'a déjà fait par le passé. Comme vous le voyez, c'est le bleu pur (0, 0, 255) que je rends transparent.

En résumé, on charge d'abord l'image avec SDL_LoadBMP, on indique la couleur transparente avec SDL_SetColorKey, puis on peut blitter avec SDL_BlitSurface :

/* On charge l'image : */
zozor = SDL_LoadBMP("zozor.bmp");
/* On rend le bleu derrière Zozor transparent : */
SDL_SetColorKey(zozor, SDL_SRCCOLORKEY, SDL_MapRGB(zozor->format, 0, 0, 255));
/* On blitte l'image maintenant transparente sur le fond : */
SDL_BlitSurface(zozor, NULL, ecran, &positionZozor);

Résultat : Zozor est parfaitement intégré à la scène (fig. suivante) !

Zozor transparent sur l'image de fond

Voilà LA technique de base que vous réutiliserez tout le temps dans vos futurs programmes. Apprenez à bien manier la transparence car c'est fondamental pour réaliser un jeu un minimum réaliste.

La transparence Alpha

C'est un autre type de transparence.
Jusqu'ici, on se contentait de définir UNE couleur de transparence (par exemple le bleu). Cette couleur n'apparaissait pas une fois l'image blittée.

La transparence Alpha correspond à tout autre chose. Elle permet de réaliser un « mélange » entre une image et le fond. C'est une sorte de fondu.

La transparence Alpha d'une surface peut être activée par la fonction SDL_SetAlpha :

SDL_SetAlpha(zozor, SDL_SRCALPHA, 128);

Il y a là encore trois paramètres :

  • la surface en question (zozor) ;

  • une liste de flags : mettez SDL_SRCALPHA pour activer la transparence, 0 pour la désactiver ;

  • très important : la valeur Alpha de la transparence. C'est un nombre compris entre 0 (image totalement transparente, donc invisible) et 255 (image totalement opaque, comme s'il n'y avait pas de transparence Alpha).

Plus le nombre Alpha est petit, plus l'image est transparente et fondue.

Voici par exemple un code qui applique une transparence Alpha de 128 à notre Zozor :

zozor = SDL_LoadBMP("zozor.bmp");
SDL_SetColorKey(zozor, SDL_SRCCOLORKEY, SDL_MapRGB(zozor->format, 0, 0, 255));
/* Transparence Alpha moyenne (128) : */
SDL_SetAlpha(zozor, SDL_SRCALPHA, 128);
SDL_BlitSurface(zozor, NULL, ecran, &positionZozor);

Vous noterez que j'ai conservé la transparence de SDL_SetColorKey. Les deux types de transparence sont en effet combinables.

La fig. suivante vous montre à quoi ressemble Zozor selon la valeur Alpha.

Alpha | Aperçu
255 (entièrement opaque) |
190 |
128 (transparence moyenne) |
75 |
0 (entièrement transparente) |

La transparence Alpha 128 (transparence moyenne) est une valeur spéciale qui est optimisée par la SDL. Ce type de transparence est plus rapide à calculer pour votre ordinateur que les autres. C'est peut être bon à savoir si vous utilisez beaucoup de transparence Alpha dans votre programme.

Charger plus de formats d'image avec SDL_Image

La SDL ne gère que les Bitmap (BMP) comme on l'a vu.
A priori, ce n'est pas un très gros problème parce que la lecture des BMP est rapide pour la SDL, mais il faut reconnaître qu'aujourd'hui on a plutôt l'habitude d'utiliser d'autres formats. En particulier, nous sommes habitués aux formats d'images « compressés » comme le PNG, le GIF et le JPEG. Ça tombe bien, il existe justement une bibliothèque SDL_Image qui gère tous les formats suivants :

  • TGA ;

  • BMP ;

  • PNM ;

  • XPM ;

  • XCF ;

  • PCX ;

  • GIF ;

  • JPG ;

  • TIF ;

  • LBM ;

  • PNG.

Il est en fait possible de rajouter des extensions à la SDL. Ce sont des bibliothèques qui ont besoin de la SDL pour fonctionner. On peut voir ça comme des add-ons (on emploie aussi parfois le mot « greffon », plus français). SDL_Image est l'une d'entre elles.

Installer SDL_image sous Windows

Téléchargement

Une page spéciale du site de la SDL référence les bibliothèques utilisant la SDL. Cette page s'intitule Libraries, vous trouverez un lien dans le menu de gauche.
Vous pourrez voir qu'il y a beaucoup de bibliothèques disponibles. Celles-ci ne proviennent généralement pas des auteurs de la SDL, ce sont plutôt des utilisateurs de la SDL qui proposent leurs bibliothèques pour améliorer la SDL.

Certaines sont très bonnes et méritent le détour, d'autres sont moins bonnes et encore boguées. Il faut arriver à faire le tri.

Cherchez SDL_Image dans la liste… vous arriverez sur la page dédiée à SDL_Image.

Téléchargez la version de SDL_Image qui vous correspond dans la section Binary (ne prenez PAS la source, on n'en a pas besoin !). Si vous êtes sous Windows, téléchargez SDL_image-devel-1.2.10-VC.zip, et ce même si vous n'utilisez pas Visual C++ !

Installation

Dans ce .zip, vous trouverez :

  • SDL_image.h : le seul header dont a besoin la bibliothèque SDL_Image. Placez-le dans C:\Program Files\CodeBlocks\SDL-1.2.13\include, c'est-à-dire à côté des autres headers de la SDL ;

  • SDL_image.lib : copiez dans C:\Program Files\CodeBlocks\SDL-1.2.13\lib. Oui, je sais, je vous ai dit que normalement les .lib étaient des fichiers réservés à Visual, mais ici exceptionnellement le .lib fonctionnera même avec le compilateur mingw ;

  • plusieurs DLL : placez-les toutes dans le dossier de votre projet (à côté de SDL.dll, donc).

Ensuite, vous devez modifier les options de votre projet pour « linker » avec le fichier SDL_image.lib.

Si vous êtes sous Code::Blocks par exemple, allez dans le menu Projects / Build options. Dans l'onglet Linker, cliquez sur le bouton Add et indiquez où se trouve le fichier SDL_image.lib (fig. suivante).

Sélection de SDL_image.lib

Si on vous demande Keep as a relative path?, répondez ce que vous voulez, ça ne changera rien dans l'immédiat. Je recommande de répondre par la négative, personnellement.

Ensuite, vous n'avez plus qu'à inclure le header SDL_image.h dans votre code source. Selon l'endroit où vous avez placé le fichier SDL_image.h, vous devrez soit utiliser ce code :

#include <SDL/SDL_image.h>

… soit celui-ci :

#include <SDL_image.h>

Essayez les deux, l'un des deux devrait fonctionner.

Si vous êtes sous Visual Studio, la manipulation est quasiment la même. Si vous avez réussi à installer la SDL, vous n'aurez aucun problème pour installer SDL_image.

Installer SDL_image sous Mac OS X

Si vous utilisez Mac OS X, téléchargez le fichier .dmg sur le site de la SDL et mettez-le dans le dossier /Library/Frameworks (/Bibliothèque/Frameworks en français).

Ensuite, tapez « search paths » dans le champ de recherche de Xcode. Repérez la ligne Header search paths ; double-cliquez sur la ligne à droite, et ajoutez « /Library/Frameworks/SDL_image.framework/Headers ».

Il ne vous reste plus qu'à ajouter le framework à votre projet. La figure suivante vous montre à quoi ressemble le Header search paths du projet après avoir installé SDL_image.

Le Header search paths du projet

Il faudra en revanche inclure le fichier .h dans votre code comme ceci :

#include "SDL_image.h"

… au lieu d'utiliser des chevrons < >. Remplacez donc la ligne d'include de SDL_image dans le code qui va suivre par celle que je viens de vous donner.

Charger les images

En fait, installer SDL_image est 100 fois plus compliqué que de l'utiliser, c'est vous dire la complexité de la bibliothèque !

Il y a UNE seule fonction à connaître : IMG_Load.
Elle prend un paramètre : le nom du fichier à ouvrir.

Ce qui est pratique, c'est que cette fonction est capable d'ouvrir tous les types de fichiers que gère SDL_image (GIF, PNG, JPG, mais aussi BMP, TIF…). Elle détectera toute seule le type du fichier en fonction de son extension.

Comme SDL_image peut aussi ouvrir les BMP, vous pouvez même oublier maintenant la fonction SDL_LoadBMP et ne plus utiliser que IMG_Load pour le chargement de n'importe quelle image.

Autre bon point : si l'image que vous chargez gère la transparence (comme c'est le cas des PNG et des GIF), alors SDL_image activera automatiquement la transparence pour cette image ! Cela vous évite donc d'avoir à appeler SDL_SetColorKey.

Je vais vous présenter le code source qui charge sapin.png et l'affiche.
Notez bien que j'inclue SDL/SDL_image.h et que je ne fais pas appel à SDL_SetColorKey car mon PNG est naturellement transparent.
Vous allez voir que j'utilise IMG_Load partout dans ce code en remplacement de SDL_LoadBMP.

#include <stdlib.h>
#include <stdio.h>
#include <SDL/SDL.h>
#include <SDL/SDL_image.h> /* Inclusion du header de SDL_image (adapter le dossier au besoin) */

void pause();

int main(int argc, char *argv[])
{
    SDL_Surface *ecran = NULL, *imageDeFond = NULL, *sapin = NULL;
    SDL_Rect positionFond, positionSapin;

    positionFond.x = 0;
    positionFond.y = 0;
    positionSapin.x = 500;
    positionSapin.y = 260;

    SDL_Init(SDL_INIT_VIDEO);

    SDL_WM_SetIcon(IMG_Load("sdl_icone.bmp"), NULL);

    ecran = SDL_SetVideoMode(800, 600, 32, SDL_HWSURFACE);
    SDL_WM_SetCaption("Chargement d'images en SDL", NULL);

    imageDeFond = IMG_Load("lac_en_montagne.bmp");
    SDL_BlitSurface(imageDeFond, NULL, ecran, &positionFond);

    /* Chargement d'un PNG avec IMG_Load
    Celui-ci est automatiquement rendu transparent car les informations de
    transparence sont codées à l'intérieur du fichier PNG */
    sapin = IMG_Load("sapin.png");
    SDL_BlitSurface(sapin, NULL, ecran, &positionSapin);

    SDL_Flip(ecran);
    pause();

    SDL_FreeSurface(imageDeFond);
    SDL_FreeSurface(sapin);
    SDL_Quit();

    return EXIT_SUCCESS;
}

void pause()
{
    int continuer = 1;
    SDL_Event event;

    while (continuer)
    {
        SDL_WaitEvent(&event);
        switch(event.type)
        {
            case SDL_QUIT:
                continuer = 0;
        }
    }
}

Comme on peut le voir sur la fig. suivante, l'image PNG a été insérée avec la transparence sur l'image de fond !

Une image PNG transparente insérée à l'aide de SDL_image

En résumé

  • La SDL permet de charger des images dans des surfaces. Par défaut, elle ne gère que les BMP avec SDL_LoadBMP.

  • On peut définir une couleur transparente avec SDL_SetColorKey.

  • On peut rendre l'ensemble de l'image plus ou moins transparent avec la fonction SDL_SetAlpha .

  • La bibliothèque SDL_image permet d'insérer n'importe quel type d'image (PNG, JPEG…) avec IMG_Load. Il faut cependant l'installer en plus de la SDL.


Apprenez à programmer en C

La gestion des événements

La gestion des événements est une des fonctionnalités les plus importantes de la SDL.
C'est probablement une des sections les plus passionnantes à découvrir. C'est à partir de là que vous allez vraiment être capables de contrôler votre application.

Chacun de vos périphériques (clavier, souris…) peut produire des événements. Nous allons apprendre à intercepter ces événements et à réagir en conséquence. Votre application va donc devenir enfin réellement dynamique !

Concrètement, qu'est-ce qu'un événement ? C'est un « signal » envoyé par un périphérique (ou par le système d'exploitation) à votre application. Voici quelques exemples d'événements courants :

  • quand l'utilisateur appuie sur une touche du clavier ;

  • quand il clique avec la souris ;

  • quand il bouge la souris ;

  • quand il réduit la fenêtre ;

  • quand il demande à fermer la fenêtre ;

  • etc.

Le rôle de ce chapitre sera de vous apprendre à traiter ces événements. Vous serez capables de dire à l'ordinateur « Si l'utilisateur clique à cet endroit, fais ça, sinon fais cela… S'il bouge la souris, fais ceci. S'il appuie sur la touche Q, arrête le programme… », etc.

Le principe des événements

Pour nous habituer aux événements, nous allons apprendre à traiter le plus simple d'entre eux : la demande de fermeture du programme.
C'est un événement qui se produit lorsque l'utilisateur clique sur la croix pour fermer la fenêtre (fig. suivante).

La croix permettant de fermer la fenêtre

C'est vraiment l'événement le plus simple. En plus, c'est un événement que vous avez utilisé jusqu'ici sans vraiment le savoir, car il était situé dans la fonction pause() !
En effet, le rôle de la fonction pause était d'attendre que l'utilisateur demande à fermer le programme. Si on n'avait pas créé cette fonction, la fenêtre se serait affichée et fermée en un éclair !

À partir de maintenant, vous pouvez oublier la fonction pause. Supprimez-la de votre code source, car nous allons apprendre à la reproduire.

La variable d'événement

Pour traiter des événements, vous aurez besoin de déclarer une variable (juste une seule, rassurez-vous) de type SDL_Event. Appelez-la comme vous voulez : moi, je vais l'appeler event, ce qui signifie « événement » en anglais.

SDL_Event event;

Pour nos tests nous allons nous contenter d'un main très basique qui affiche juste une fenêtre, comme on l'a vu quelques chapitres plus tôt. Voici à quoi doit ressembler votre main :

int main(int argc, char *argv[])
{
    SDL_Surface *ecran = NULL;
    SDL_Event event; // Cette variable servira plus tard à gérer les événements

    SDL_Init(SDL_INIT_VIDEO);

    ecran = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE);
    SDL_WM_SetCaption("Gestion des événements en SDL", NULL);

    SDL_Quit();

    return EXIT_SUCCESS;
}

C'est donc un code très basique, il ne contient qu'une nouveauté : la déclaration de la variable event dont nous allons bientôt nous servir.

Testez ce code : comme prévu, la fenêtre va s'afficher et se fermer immédiatement après.

La boucle des événements

Lorsqu'on veut attendre un événement, on fait généralement une boucle. Cette boucle se répètera tant qu'on n'a pas eu l'événement voulu.
On va avoir besoin d'utiliser un booléen qui indiquera si on doit continuer la boucle ou non.
Créez donc ce booléen que vous appellerez par exemple continuer :

int continuer = 1;

Ce booléen est mis à 1 au départ car on veut que la boucle se répète TANT QUE la variable continuer vaut 1 (vrai). Dès qu'elle vaudra 0 (faux), alors on sortira de la boucle et le programme s'arrêtera.

Voici la boucle à créer :

while (continuer)
{
    /* Traitement des événements */
}

Voilà : on a pour le moment une boucle infinie qui ne s'arrêtera que si on met la variable continuer à 0. C'est ce que nous allons écrire à l'intérieur de cette boucle qui est le plus intéressant.

Récupération de l'événement

Maintenant, faisons appel à une fonction de la SDL pour demander si un événement s'est produit.
On dispose de deux fonctions qui font cela, mais d'une manière différente :

  • SDL_WaitEvent : elle attend qu'un événement se produise. Cette fonction est dite bloquante car elle suspend l'exécution du programme tant qu'aucun événement ne s'est produit ;

  • SDL_PollEvent : cette fonction fait la même chose mais n'est pas bloquante. Elle vous dit si un événement s'est produit ou non. Même si aucun événement ne s'est produit, elle rend la main à votre programme de suite.

Ces deux fonctions sont utiles, mais dans des cas différents.
Pour faire simple, si vous utilisez SDL_WaitEvent votre programme utilisera très peu de processeur car il attendra qu'un événement se produise.
En revanche, si vous utilisez SDL_PollEvent, votre programme va parcourir votre boucle while et rappeler SDL_PollEvent indéfiniment jusqu'à ce qu'un événement se soit produit. À tous les coups, vous utiliserez 100 % du processeur.

Mais alors, il faut tout le temps utiliser SDL_WaitEvent si cette fonction utilise moins le processeur, non ?

Non, car il y a des cas où SDL_PollEvent se révèle indispensable. C'est le cas des jeux dans lesquels l'écran se met à jour même quand il n'y a pas d'événement.
Prenons par exemple Tetris : les blocs descendent tout seuls, il n'y a pas besoin que l'utilisateur crée d'événement pour ça ! Si on avait utilisé SDL_WaitEvent, le programme serait resté « bloqué » dans cette fonction et vous n'auriez pas pu mettre à jour l'écran pour faire descendre les blocs !

Comment fait SDL_WaitEvent pour ne pas consommer de processeur ?
Après tout, la fonction est bien obligée de faire une boucle infinie pour tester tout le temps s'il y a un événement ou non, n'est-ce pas ?

C'est une question que je me posais il y a encore peu de temps. La réponse est un petit peu compliquée car ça concerne la façon dont l'OS gère les processus (les programmes).
Si vous voulez – mais je vous en parle rapidement –, avec SDL_WaitEvent, le processus de votre programme est mis « en pause ». Votre programme n'est donc plus traité par le processeur.
Il sera « réveillé » par l'OS au moment où il y aura un événement. Du coup, le processeur se remettra à travailler sur votre programme à ce moment-là. Cela explique pourquoi votre programme ne consomme pas de processeur pendant qu'il attend l'événement.

Je comprends que ce soit un peu abstrait pour vous pour le moment. Et à dire vrai, vous n'avez pas besoin de comprendre ça maintenant. Vous assimilerez mieux toutes les différences plus loin en pratiquant.
Pour le moment, nous allons utiliser SDL_WaitEvent car notre programme reste très simple. Ces deux fonctions s'utilisent de toute façon de la même manière.

Vous devez envoyer à la fonction l'adresse de votre variable event qui stocke l'événement.
Comme cette variable n'est pas un pointeur (regardez la déclaration à nouveau), nous allons mettre le symbole & devant le nom de la variable afin de donner l'adresse :

SDL_WaitEvent(&event);

Après appel de cette fonction, la variable event contient obligatoirement un événement.

Cela n'aurait pas forcément été le cas si on avait utilisé SDL_PollEvent : cette fonction aurait pu renvoyer « Pas d'événement ».

Analyse de l'événement

Maintenant, nous disposons d'une variable event qui contient des informations sur l'événement qui s'est produit.
Il faut regarder la sous-variable event.type et faire un test sur sa valeur. Généralement on utilise un switch pour tester l'événement.

Mais comment sait-on quelle valeur correspond à l'événement « Quitter », par exemple ?

La SDL nous fournit des constantes, ce qui simplifie grandement l'écriture du programme. Il en existe beaucoup (autant qu'il y a d'événements possibles). Nous les verrons au fur et à mesure tout au long de ce chapitre.

while (continuer)
{
    SDL_WaitEvent(&event); /* Récupération de l'événement dans event */
    switch(event.type) /* Test du type d'événement */
    {
        case SDL_QUIT: /* Si c'est un événement de type "Quitter" */
            continuer = 0;
            break;
    }
}

Voici comment ça fonctionne.

  1. Dès qu'il y a un événement, la fonction SDL_WaitEvent renvoie cet événement dans event.

  2. On analyse le type d'événement grâce à un switch. Le type de l'événement se trouve dans event.type

  3. On teste à l'aide de case dans le switch le type de l'événement. Pour le moment, on ne teste que l'événement SDL_QUIT (demande de fermeture du programme), car c'est le seul qui nous intéresse.<liste>

  4. Si c'est un événement SDL_QUIT, c'est que l'utilisateur a demandé à quitter le programme. Dans ce cas on met le booléen continuer à 0. Au prochain tour de boucle, la condition sera fausse et donc la boucle s'arrêtera. Le programme s'arrêtera ensuite.

  5. Si ce n'est pas un événement SDL_QUIT, c'est qu'il s'est passé autre chose : l'utilisateur a appuyé sur une touche, a cliqué ou tout simplement bougé la souris dans la fenêtre. Comme ces autres événements ne nous intéressent pas, on ne les traite pas. On ne fait donc rien : la boucle recommence et on attend à nouveau un événement (on repart à l'étape 1).

Ce que je viens de vous expliquer ici est extrêmement important. Si vous avez compris ce code, vous avez tout compris et le reste du chapitre sera très facile pour vous.

Le code complet

int main(int argc, char *argv[])
{
    SDL_Surface *ecran = NULL;
    SDL_Event event; /* La variable contenant l'événement */
    int continuer = 1; /* Notre booléen pour la boucle */

    SDL_Init(SDL_INIT_VIDEO);

    ecran = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE);
    SDL_WM_SetCaption("Gestion des événements en SDL", NULL);

    while (continuer) /* TANT QUE la variable ne vaut pas 0 */
    {
        SDL_WaitEvent(&event); /* On attend un événement qu'on récupère dans event */
        switch(event.type) /* On teste le type d'événement */
        {
            case SDL_QUIT: /* Si c'est un événement QUITTER */
                continuer = 0; /* On met le booléen à 0, donc la boucle va s'arrêter */
                break;
        }
    }

    SDL_Quit();

    return EXIT_SUCCESS;
}

Voilà le code complet. Il n'y a rien de bien difficile : si vous avez suivi jusqu'ici, ça ne devrait pas vous surprendre.
D'ailleurs, vous remarquerez qu'on n'a fait que reproduire ce que faisait la fonction pause. Comparez avec le code de la fonction pause : c'est le même, sauf qu'on a cette fois tout mis dans le main. Bien entendu, il est préférable de placer ce code dans une fonction à part, comme pause, car cela allège la fonction main et la rend plus lisible.

Le clavier

Nous allons maintenant étudier les événements produits par le clavier.

Si vous avez compris le début du chapitre, vous n'aurez aucun problème pour traiter les autres types d'événements. Il n'y a rien de plus facile.

Pourquoi est-ce si simple ? Parce que maintenant que vous avez compris le fonctionnement de la boucle infinie, tout ce que vous allez avoir à faire, c'est d'ajouter d'autres case dans le switch pour traiter d'autres types d'événements. Ça ne devrait pas être trop dur.

Les événements du clavier

Il existe deux événements différents qui peuvent être générés par le clavier :

  • SDL_KEYDOWN : quand une touche du clavier est enfoncée ;

  • SDL_KEYUP : quand une touche du clavier est relâchée.

Pourquoi y a-t-il ces deux événements ?
Parce que quand vous appuyez sur une touche, il se passe deux choses : vous enfoncez la touche (SDL_KEYDOWN), puis vous la relâchez (SDL_KEYUP). La SDL vous permet de traiter ces deux événements à part, ce qui sera bien pratique, vous verrez.

Pour le moment, nous allons nous contenter de traiter l'événement SDL_KEYDOWN (appui de la touche) :

while (continuer)
{
    SDL_WaitEvent(&event);
    switch(event.type)
    {
        case SDL_QUIT:
            continuer = 0;
            break;
        case SDL_KEYDOWN: /* Si appui sur une touche */
            continuer = 0;
            break;
    }
}

Si on appuie sur une touche, le programme s'arrête. Testez, vous verrez !

Récupérer la touche

Savoir qu'une touche a été enfoncée c'est bien, mais savoir laquelle, c'est quand même mieux !

On peut obtenir la nature de la touche enfoncée grâce à une sous-sous-sous-variable (ouf) qui s'appelle event.key.keysym.sym.
Cette variable contient la valeur de la touche qui a été enfoncée (elle fonctionne aussi lors d'un relâchement de la touche SDL_KEYUP).

L'avantage, c'est que la SDL permet de récupérer la valeur de toutes les touches du clavier. Il y a les lettres et les chiffres, bien sûr (ABCDE0123…), mais aussi la touche Echap, ou encore Impr. Ecran, Suppr, Entrée, etc.

Il y a une constante pour chacune des touches du clavier. Vous trouverez cette liste dans la documentation de la SDL, que vous avez très probablement téléchargée avec la bibliothèque quand vous avez dû l'installer.
Si tel n'est pas le cas, je vous recommande fortement de retourner sur le site de la SDL et d'y télécharger la documentation, car elle est très utile.

Vous trouverez la liste des touches du clavier dans la section Keysym definitions. Cette liste est trop longue pour être présentée ici, aussi je vous propose pour cela de consulter la documentation sur le web directement.

Bien entendu, la documentation est en anglais et donc… la liste aussi. Si vous voulez vraiment programmer, il est important d'être capable de lire l'anglais car toutes les documentations sont dans cette langue, et vous ne pouvez pas vous en passer !

Il y a deux tableaux dans cette liste : un grand (au début) et un petit (à la fin). Nous nous intéresserons au grand tableau.
Dans la première colonne vous avez la constante, dans la seconde la représentation équivalente en ASCII et enfin, dans la troisième colonne, vous avez une description de la touche.
Notez que certaines touches comme Maj (ou Shift) n'ont pas de valeur ASCII correspondante.

Prenons par exemple la touche Echap (« Escape » en anglais). On peut vérifier si la touche enfoncée est Echap comme ceci :

switch (event.key.keysym.sym)
{
    case SDLK_ESCAPE: /* Appui sur la touche Echap, on arrête le programme */
        continuer = 0;
        break;
}

J'utilise un switch pour faire mon test mais j'aurais aussi bien pu utiliser un if.
Toutefois, j'ai plutôt tendance à me servir des switch quand je traite les événements car je teste beaucoup de valeurs différentes (en pratique, j'ai beaucoup de case dans un switch, contrairement à ici).

Voici une boucle d'événement complète que vous pouvez tester :

while (continuer)
{
    SDL_WaitEvent(&event);
    switch(event.type)
    {
        case SDL_QUIT:
            continuer = 0;
            break;
        case SDL_KEYDOWN:
            switch (event.key.keysym.sym)
            {
                case SDLK_ESCAPE: /* Appui sur la touche Echap, on arrête le programme */
                    continuer = 0;
                    break;
            }
            break;
    }
}

Cette fois, le programme s'arrête si on appuie sur Echap ou si on clique sur la croix de la fenêtre.

Maintenant que vous savez comment arrêter le programme en appuyant sur une touche, vous êtes autorisés à faire du plein écran si ça vous amuse (Flag SDL_FULLSCREEN dans SDL_SetVideoMode, pour rappel).

Auparavant, je vous avais demandé d'éviter de le faire car on ne savait pas comment arrêter un programme en plein écran (il n'y a pas de croix sur laquelle cliquer pour arrêter !).}

Exercice : diriger Zozor au clavier

Vous êtes maintenant capables de déplacer une image dans la fenêtre à l'aide du clavier !
C'est un exercice très intéressant qui va d'ailleurs nous permettre de voir comment utiliser le double buffering et la répétition de touches.
De plus, ce que je vais vous apprendre là est la base de tous les jeux réalisés en SDL, donc ce n'est pas un exercice facultatif ! Je vous invite à le lire et à le faire très soigneusement.

Charger l'image

Pour commencer, nous allons charger une image. On va faire simple : on va reprendre l'image de Zozor utilisée dans le chapitre précédent.
Créez donc la surface zozor, chargez l'image et rendez-la transparente (c'était un BMP, je vous le rappelle).

zozor = SDL_LoadBMP("zozor.bmp");
SDL_SetColorKey(zozor, SDL_SRCCOLORKEY, SDL_MapRGB(zozor->format, 0, 0, 255));

Ensuite, et c'est certainement le plus important, vous devez créer une variable de type SDL_Rect pour retenir les coordonnées de Zozor :

SDL_Rect positionZozor;

Je vous recommande d'initialiser les coordonnées, en mettant soit x = 0 et y = 0 (position en haut à gauche de la fenêtre), soit en centrant Zozor dans la fenêtre comme vous avez appris à le faire il n'y a pas si longtemps.

/* On centre Zozor à l'écran */
positionZozor.x = ecran->w / 2 - zozor->w / 2;
positionZozor.y = ecran->h / 2 - zozor->h / 2;

Vous devez initialiser positionZozor après avoir chargé les surfaces ecran et zozor. En effet, j'utilise la largeur (w) et la hauteur (h) de ces deux surfaces pour calculer la position centrée de Zozor à l'écran, il faut donc que ces surfaces aient été initialisées auparavant.

Si vous vous êtes bien débrouillés, vous devriez avoir réussi à afficher Zozor au centre de l'écran (fig. suivante).

Zozor centré dans la fenêtre

J'ai choisi de mettre le fond en blanc cette fois (en faisant un SDL_FillRect sur ecran), mais ce n'est pas une obligation.

Schéma de la programmation événementielle

Quand vous codez un programme qui réagit aux événements (comme on va le faire ici), vous devez suivre la plupart du temps le même « schéma » de code.
Ce schéma est à connaître par cœur. Le voici :

while (continuer)
{
    SDL_WaitEvent(&event);
    switch(event.type)
    {
        case SDL_TRUC: /* Gestion des événements de type TRUC */
        case SDL_BIDULE: /* Gestion des événements de type BIDULE */
    }

    /* On efface l'écran (ici fond blanc) : */
    SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 255, 255, 255));

    /* On fait tous les SDL_BlitSurface nécessaires pour coller les surfaces à l'écran */

    /* On met à jour l'affichage : */
    SDL_Flip(ecran);
}

Voilà dans les grandes lignes la forme de la boucle principale d'un programme SDL.
On boucle tant qu'on n'a pas demandé à arrêter le programme.

    1. On attend un événement (SDL_WaitEvent) ou bien on vérifie s'il y a un événement mais on n'attend pas qu'il y en ait un (SDL_PollEvent). Pour le moment on se contente de SDL_WaitEvent.

    2. On fait un (grand) switch pour savoir de quel type d'événement il s'agit (événement de type TRUC, de type BIDULE, comme ça vous chante !). On traite l'événement qu'on a reçu : on effectue certaines actions, certains calculs.

    3. Une fois sorti du switch, on prépare un nouvel affichage :

<liste type="1">

  1. première chose à faire : on efface l'écran avec un SDL_FillRect. Si on ne le faisait pas, on aurait des « traces » de l'ancien écran qui subsisteraient, et forcément, ce ne serait pas très joli ;

  2. ensuite, on fait tous les blits nécessaires pour coller les surfaces sur l'écran ;

  3. enfin, une fois que c'est fait, on met à jour l'affichage aux yeux de l'utilisateur, en appelant la fonction SDL_Flip(ecran).

Traiter l'événement SDL_KEYDOWN

Voyons maintenant comment on va traiter l'événement SDL_KEYDOWN.
Notre but est de diriger Zozor au clavier avec les flèches directionnelles. On va donc modifier ses coordonnées à l'écran en fonction de la flèche sur laquelle on appuie :

switch(event.type)
{
    case SDL_QUIT:
        continuer = 0;
        break;
    case SDL_KEYDOWN:
        switch(event.key.keysym.sym)
        {
            case SDLK_UP: // Flèche haut
                positionZozor.y--;
                break;
            case SDLK_DOWN: // Flèche bas
                positionZozor.y++;
                break;
            case SDLK_RIGHT: // Flèche droite
                positionZozor.x++;
                break;
            case SDLK_LEFT: // Flèche gauche
                positionZozor.x--;
                break;
        }
        break;
}

Comment j'ai trouvé ces constantes ? Dans la doc' !
Je vous ai donné tout à l'heure un lien vers la page de la doc' qui liste toutes les touches du clavier : c'est là que je me suis servi.

Ce qu'on fait là est très simple :

  • si on appuie sur la flèche « haut », on diminue l'ordonnée (y) de la position de Zozor d'un pixel pour le faire « monter ». Notez que nous ne sommes pas obligés de le déplacer d'un pixel, on pourrait très bien le déplacer de 10 pixels en 10 pixels ;

  • si on va vers le bas, on doit au contraire augmenter (incrémenter) l'ordonnée de Zozor (y) ;

  • si on va vers la droite, on augmente la valeur de l'abscisse (x) ;

  • si on va vers la gauche, on doit diminuer l'abscisse (x).

Et maintenant ?
En vous aidant du schéma de code donné précédemment, vous devriez être capables de diriger Zozor au clavier !

int main(int argc, char *argv[])
{
    SDL_Surface *ecran = NULL, *zozor = NULL;
    SDL_Rect positionZozor;
    SDL_Event event;
    int continuer = 1;

    SDL_Init(SDL_INIT_VIDEO);

    ecran = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE);
    SDL_WM_SetCaption("Gestion des événements en SDL", NULL);

    /* Chargement de Zozor */
    zozor = SDL_LoadBMP("zozor.bmp");
    SDL_SetColorKey(zozor, SDL_SRCCOLORKEY, SDL_MapRGB(zozor->format, 0, 0, 255));

    /* On centre Zozor à l'écran */
    positionZozor.x = ecran->w / 2 - zozor->w / 2;
    positionZozor.y = ecran->h / 2 - zozor->h / 2;

    while (continuer)
    {
        SDL_WaitEvent(&event);
        switch(event.type)
        {
            case SDL_QUIT:
                continuer = 0;
                break;
            case SDL_KEYDOWN:
                switch(event.key.keysym.sym)
                {
                    case SDLK_UP: // Flèche haut
                        positionZozor.y--;
                        break;
                    case SDLK_DOWN: // Flèche bas
                        positionZozor.y++;
                        break;
                    case SDLK_RIGHT: // Flèche droite
                        positionZozor.x++;
                        break;
                    case SDLK_LEFT: // Flèche gauche
                        positionZozor.x--;
                        break;
                }
                break;
        }

        /* On efface l'écran */
        SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 255, 255, 255));
        /* On place Zozor à sa nouvelle position */
        SDL_BlitSurface(zozor, NULL, ecran, &positionZozor);
        /* On met à jour l'affichage */
        SDL_Flip(ecran);
    }

    SDL_FreeSurface(zozor);
    SDL_Quit();

    return EXIT_SUCCESS;
}

Il est primordial de bien comprendre comment est composée la boucle principale du programme. Il faut être capable de la refaire de tête. Relisez le schéma de code que vous avez vu plus haut, au besoin.

Donc en résumé, on a une grosse boucle appelée « Boucle principale du programme ». Elle ne s'arrêtera que si on le demande en mettant le booléen continuer à 0.
Dans cette boucle, on récupère d'abord un événement à traiter. On fait un switch pour déterminer de quel type d'événement il s'agit. En fonction de l'événement, on effectue différentes actions. Ici, je mets à jour les coordonnées de Zozor pour donner l'impression qu'on le déplace.

Ensuite, après le switch vous devez mettre à jour votre écran comme suit.

  1. Premièrement, vous effacez l'écran via un SDL_FillRect (de la couleur de fond que vous voulez).

  2. Ensuite, vous blittez vos surfaces sur l'écran. Ici, je n'ai eu besoin de blitter que Zozor car il n'y a que lui. Vous noterez, et c'est très important, que je blitte Zozor à positionZozor ! C'est là que la différence se fait : si j'ai mis à jour positionZozor auparavant, alors Zozor apparaîtra à un autre endroit et on aura l'impression qu'on l'a déplacé !

  3. Enfin, toute dernière chose à faire : SDL_Flip. Cela ordonne la mise à jour de l'écran aux yeux de l'utilisateur.

On peut donc déplacer Zozor où l'on veut sur l'écran, maintenant (fig. suivante) !

Zozor en balade

Quelques optimisations

Répétition des touches

Pour l'instant, notre programme fonctionne mais on ne peut se déplacer que d'un pixel à la fois. Nous sommes obligés d'appuyer à nouveau sur les flèches du clavier si on veut encore se déplacer d'un pixel. Je ne sais pas vous, mais moi ça m'amuse moyennement de m'exciter frénétiquement sur la même touche du clavier juste pour déplacer le personnage de 200 pixels.

Heureusement, il y a SDL_EnableKeyRepeat !
Cette fonction permet d'activer la répétition des touches. Elle fait en sorte que la SDL régénère un événement de type SDL_KEYDOWN si une touche est maintenue enfoncée un certain temps.

Cette fonction peut être appelée quand vous voulez, mais je vous conseille de l'appeler de préférence avant la boucle principale du programme. Elle prend deux paramètres :

  • la durée (en millisecondes) pendant laquelle une touche doit rester enfoncée avant d'activer la répétition des touches ;

  • le délai (en millisecondes) entre chaque génération d'un événement SDL_KEYDOWN une fois que la répétition a été activée.

Le premier paramètre indique au bout de combien de temps on génère une répétition la première fois, et le second indique le temps qu'il faut ensuite pour que l'événement se répète.
Personnellement, pour des raisons de fluidité, je mets la même valeur à ces deux paramètres, le plus souvent.

Essayez avec une répétition de 10 ms :

SDL_EnableKeyRepeat(10, 10);

Maintenant, vous pouvez laisser une touche du clavier enfoncée. Vous allez voir, c'est quand même mieux !

Travailler avec le double buffer

À partir de maintenant, il serait bon d'activer l'option de double buffering de la SDL.

Le double buffering est une technique couramment utilisée dans les jeux. Elle permet d'éviter un scintillement de l'image.
Pourquoi l'image scintillerait-elle ? Parce que quand vous dessinez à l'écran, l'utilisateur « voit » quand vous dessinez et donc quand l'écran s'efface. Même si ça va très vite, notre cerveau perçoit un clignotement et c'est très désagréable.

La technique du double buffering consiste à utiliser deux « écrans » : l'un est réel (celui que l'utilisateur est en train de voir sur son moniteur), l'autre est virtuel (c'est une image que l'ordinateur est en train de construire en mémoire).

Ces deux écrans alternent : l'écran A est affiché pendant que l'autre (l'écran B) en « arrière-plan » prépare l'image suivante (fig. suivante).

En double buffering, l'image suivante est préparée en tâche de fond

Une fois que l'image en arrière-plan (l'écran B) a été dessinée, on intervertit les deux écrans en appelant la fonction SDL_Flip (fig. suivante).

SDL_Flip intervertit les écrans pour afficher la nouvelle image

L'écran A part en arrière-plan préparer l'image suivante, tandis que l'image de l'écran B s'affiche directement et instantanément aux yeux de l'utilisateur. Résultat : aucun scintillement !

Pour réaliser cela, tout ce que vous avez à faire est de charger le mode vidéo en ajoutant le flag SDL_DOUBLEBUF :

ecran = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE | SDL_DOUBLEBUF);

Vous n'avez rien d'autre à changer dans votre code.

Le double buffering est une technique bien connue de votre carte graphique. C'est donc directement géré par le matériel et ça va très très vite.

Vous vous demandez peut-être pourquoi on a déjà utilisé SDL_Flip auparavant sans le double buffering ?
En fait, cette fonction a deux utilités :

  • si le double buffering est activé, elle sert à commander « l'échange » des écrans qu'on vient de voir ;

  • si le double buffering n'est pas activé, elle commande un rafraîchissement manuel de la fenêtre. Cette technique est valable dans le cas d'un programme qui ne bouge pas beaucoup, mais pour la plupart des jeux, je recommande de l'activer.

Dorénavant, j'aurai toujours le double buffering activé dans mes codes source (ça ne coûte pas plus cher et c'est mieux : de quoi se plaint-on ?).

Voici le code source complet qui fait usage du double buffering et de la répétition des touches. Il est très similaire à celui que l'on a vu précédemment, on y a seulement ajouté les nouvelles instructions qu'on vient d'apprendre.

int main(int argc, char *argv[])
{
    SDL_Surface *ecran = NULL, *zozor = NULL;
    SDL_Rect positionZozor;
    SDL_Event event;
    int continuer = 1;

    SDL_Init(SDL_INIT_VIDEO);

    ecran = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE | SDL_DOUBLEBUF); /* Double Buffering */
    SDL_WM_SetCaption("Gestion des évènements en SDL", NULL);

    zozor = SDL_LoadBMP("zozor.bmp");
    SDL_SetColorKey(zozor, SDL_SRCCOLORKEY, SDL_MapRGB(zozor->format, 0, 0, 255));

    positionZozor.x = ecran->w / 2 - zozor->w / 2;
    positionZozor.y = ecran->h / 2 - zozor->h / 2;

    SDL_EnableKeyRepeat(10, 10); /* Activation de la répétition des touches */

    while (continuer)
    {
        SDL_WaitEvent(&event);
        switch(event.type)
        {
            case SDL_QUIT:
                continuer = 0;
                break;
            case SDL_KEYDOWN:
                switch(event.key.keysym.sym)
                {
                    case SDLK_UP:
                        positionZozor.y--;
                        break;
                    case SDLK_DOWN:
                        positionZozor.y++;
                        break;
                    case SDLK_RIGHT:
                        positionZozor.x++;
                        break;
                    case SDLK_LEFT:
                        positionZozor.x--;
                        break;
                }
                break;
        }

        SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 255, 255, 255));
        SDL_BlitSurface(zozor, NULL, ecran, &positionZozor);
        SDL_Flip(ecran);
    }

    SDL_FreeSurface(zozor);
    SDL_Quit();

    return EXIT_SUCCESS;
}

La souris

Vous vous dites peut-être que gérer la souris est plus compliqué que le clavier ?
Que nenni ! C'est même plus simple, vous allez voir !

La souris peut générer trois types d'événements différents.

  • SDL_MOUSEBUTTONDOWN : lorsqu'on clique avec la souris. Cela correspond au moment où le bouton de la souris est enfoncé.

  • SDL_MOUSEBUTTONUP : lorsqu'on relâche le bouton de la souris. Tout cela fonctionne exactement sur le même principe que les touches du clavier : il y a d'abord un appui, puis un relâchement du bouton.

  • SDL_MOUSEMOTION : lorsqu'on déplace la souris. À chaque fois que la souris bouge dans la fenêtre (ne serait-ce que d'un pixel !), un événement SDL_MOUSEMOTION est généré !

Nous allons d'abord travailler avec les clics de la souris et plus particulièrement avec SDL_MOUSEBUTTONUP. On ne travaillera pas avec SDL_MOUSEBUTTONDOWN ici, mais vous savez de toute manière que c'est exactement pareil sauf que cela se produit plus tôt, au moment de l'enfoncement du bouton de la souris.
Nous verrons un peu plus loin comment traiter l'événement SDL_MOUSEMOTION.

Gérer les clics de la souris

Nous allons donc capturer un événement de type SDL_MOUSEBUTTONUP (clic de la souris) puis voir quelles informations on peut récupérer.
Comme d'habitude, on va devoir ajouter un case dans notre switch de test, alors allons-y gaiement :

switch(event.type)
{
    case SDL_QUIT:
        continuer = 0;
        break;
    case SDL_MOUSEBUTTONUP: /* Clic de la souris */
        break;
}

Jusque-là, pas de difficulté majeure.

Quelles informations peut-on récupérer lors d'un clic de la souris ? Il y en a deux :

  • le bouton de la souris avec lequel on a cliqué (clic gauche ? clic droit ? clic bouton du milieu ?) ;

  • les coordonnées de la souris au moment du clic (x et y).

Récupérer le bouton de la souris

On va d'abord voir avec quel bouton de la souris on a cliqué.
Pour cela, il faut analyser la sous-variable event.button.button (non, je ne bégaie pas) et comparer sa valeur avec l'une des 5 constantes suivantes :

  • SDL_BUTTON_LEFT : clic avec le bouton gauche de la souris ;

  • SDL_BUTTON_MIDDLE : clic avec le bouton du milieu de la souris (tout le monde n'en a pas forcément un, c'est en général un clic avec la molette) ;

  • SDL_BUTTON_RIGHT : clic avec le bouton droit de la souris ;

  • SDL_BUTTON_WHEELUP : molette de la souris vers le haut ;

  • SDL_BUTTON_WHEELDOWN : molette de la souris vers le bas.

Les deux dernières constantes correspondent au mouvement vers le haut ou vers le bas auquel on procède avec la molette de la souris. Elles ne correspondent pas à un « clic » sur la molette comme on pourrait le penser à tort.

On va faire un test simple pour vérifier si on a fait un clic droit avec la souris. Si on a fait un clic droit, on arrête le programme (oui, je sais, ce n'est pas très original pour le moment mais ça permet de tester) :

switch(event.type)
{
    case SDL_QUIT:
        continuer = 0;
        break;
    case SDL_MOUSEBUTTONUP:
        if (event.button.button == SDL_BUTTON_RIGHT) /* On arrête le programme si on a fait un clic droit */
            continuer = 0;
        break;
}

Vous pouvez tester, vous verrez que le programme s'arrête si on fait un clic droit.

Récupérer les coordonnées de la souris

Voilà une information très intéressante : les coordonnées de la souris au moment du clic !
On les récupère à l'aide de deux variables (pour l'abscisse et l'ordonnée) : event.button.x et event.button.y.

Amusons-nous un petit peu : on va blitter Zozor à l'endroit du clic de la souris.
Compliqué ? Pas du tout ! Essayez de le faire, c'est un jeu d'enfant !

Voici la correction :

while (continuer)
{
    SDL_WaitEvent(&event);
    switch(event.type)
    {
        case SDL_QUIT:
            continuer = 0;
            break;
        case SDL_MOUSEBUTTONUP:
            positionZozor.x = event.button.x;
            positionZozor.y = event.button.y;
            break;
    }

    SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 255, 255, 255));
    SDL_BlitSurface(zozor, NULL, ecran, &positionZozor); /* On place Zozor à sa nouvelle position */
    SDL_Flip(ecran);
}

Ça ressemble à s'y méprendre à ce que je faisais avec les touches du clavier. Là, c'est même encore plus simple : on met directement la valeur de x de la souris dans positionZozor.x, et de même pour y.
Ensuite on blitte Zozor à ces coordonnées-là, et voilà le travail (fig. suivante) !

Zozor apparaît à l'endroit du clic

Petit exercice très simple : pour le moment, on déplace Zozor quel que soit le bouton de la souris utilisé pour le clic. Essayez de ne déplacer Zozor que si on fait un clic gauche avec la souris. Si on fait un clic droit, arrêtez le programme.

Gérer le déplacement de la souris

Un déplacement de la souris génère un événement de type SDL_MOUSEMOTION.
Notez bien qu'on génère autant d'événements que l'on parcourt de pixels pour se déplacer ! Si on bouge la souris de 100 pixels (ce qui n'est pas beaucoup), il y aura donc 100 événements générés.

Mais ça ne fait pas beaucoup d'événements à gérer pour notre ordinateur, tout ça ?

Pas du tout : rassurez-vous, il en a vu d'autres !

Bon, que peut-on récupérer d'intéressant ici ?
Les coordonnées de la souris, bien sûr ! On les trouve dans event.motion.x et event.motion.y.

Faites attention : on n'utilise pas les mêmes variables que pour le clic de souris de tout à l'heure (avant, c'était event.button.x). Les variables utilisées sont différentes en SDL en fonction de l'événement.

On va placer Zozor aux mêmes coordonnées que la souris, là encore. Vous allez voir, c'est rudement efficace et toujours aussi simple !

while (continuer)
{
    SDL_WaitEvent(&event);
    switch(event.type)
    {
        case SDL_QUIT:
            continuer = 0;
            break;
        case SDL_MOUSEMOTION:
            positionZozor.x = event.motion.x;
            positionZozor.y = event.motion.y;
            break;
    }

    SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 255, 255, 255));
    SDL_BlitSurface(zozor, NULL, ecran, &positionZozor); /* On place Zozor à sa nouvelle position */
    SDL_Flip(ecran);
}

Bougez votre Zozor à l'écran. Que voyez-vous ?
Il suit naturellement la souris où que vous alliez. C'est beau, c'est rapide, c'est fluide (vive le double buffering).

Quelques autres fonctions avec la souris

Nous allons voir deux fonctions très simples en rapport avec la souris, puisque nous y sommes. Ces fonctions vous seront très probablement utiles bientôt.

Masquer la souris

On peut masquer le curseur de la souris très facilement.
Il suffit d'appeler la fonction SDL_ShowCursor et de lui envoyer un flag :

  • SDL_DISABLE : masque le curseur de la souris ;

  • SDL_ENABLE : réaffiche le curseur de la souris.

Par exemple :

SDL_ShowCursor(SDL_DISABLE);

Le curseur de la souris restera masqué tant qu'il sera à l'intérieur de la fenêtre.
Masquez de préférence le curseur avant la boucle principale du programme. Pas la peine en effet de le masquer à chaque tour de boucle, une seule fois suffit.

Placer la souris à un endroit précis

On peut placer manuellement le curseur de la souris aux coordonnées que l'on veut dans la fenêtre.
On utilise pour cela SDL_WarpMouse qui prend pour paramètres les coordonnées x et y où le curseur doit être placé.

Par exemple, le code suivant place la souris au centre de l'écran :

SDL_WarpMouse(ecran->w / 2, ecran->h / 2);

Lorsque vous faites un WarpMouse, un événement de type SDL_MOUSEMOTION sera généré. Eh oui, la souris a bougé ! Même si ce n'est pas l'utilisateur qui l'a fait, il y a quand même eu un déplacement.

Les événements de la fenêtre

La fenêtre elle-même peut générer un certain nombre d'événements :

  • lorsqu'elle est redimensionnée ;

  • lorsqu'elle est réduite en barre des tâches ou restaurée ;

  • lorsqu'elle est active (au premier plan) ou lorsqu'elle n'est plus active ;

  • lorsque le curseur de la souris se trouve à l'intérieur de la fenêtre ou lorsqu'il en sort.

Commençons par étudier le premier d'entre eux : l'événement généré lors du redimensionnement de la fenêtre.

Redimensionnement de la fenêtre

Par défaut, une fenêtre SDL ne peut pas être redimensionnée par l'utilisateur.
Je vous rappelle que pour changer ça, il faut ajouter le flag SDL_RESIZABLE dans la fonction SDL_SetVideoMode :

ecran = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE | SDL_DOUBLEBUF | SDL_RESIZABLE);

Une fois que vous avez ajouté ce flag, vous pouvez redimensionner la fenêtre. Lorsque vous faites cela, un événement de type SDL_VIDEORESIZE est généré.

Vous pouvez récupérer :

  • la nouvelle largeur dans event.resize.w ;

  • la nouvelle hauteur dans event.resize.h.

On peut utiliser ces informations pour faire en sorte que notre Zozor soit toujours centré dans la fenêtre :

case SDL_VIDEORESIZE:
    positionZozor.x = event.resize.w / 2 - zozor->w / 2;
    positionZozor.y = event.resize.h / 2 - zozor->h / 2;
    break;

Visibilité de la fenêtre

L'événement SDL_ACTIVEEVENT est généré lorsque la visibilité de la fenêtre change.
Cela peut être dû à de nombreuses choses :

  • la fenêtre est réduite en barre des tâches ou restaurée ;

  • le curseur de la souris se trouve à l'intérieur de la fenêtre ou en sort ;

  • la fenêtre est active (au premier plan) ou n'est plus active.

En programmation, on parle de focus.
Lorsqu'on dit qu'une application a le focus, c'est que le clavier ou la souris de l'utilisateur s'y trouve. Tous les clics sur la souris ou appuis sur les touches du clavier que vous ferez seront envoyés à la fenêtre qui a le focus et non aux autres.
Une seule fenêtre peut avoir le focus à un instant donné (vous ne pouvez pas avoir deux fenêtres au premier plan en même temps !).

Étant donné le nombre de raisons qui peuvent avoir provoqué cet événement, il faut impérativement regarder dans des variables pour en savoir plus.

    • event.active.gain : indique si l'événement est un gain (1) ou une perte (0). Par exemple, si la fenêtre est passée en arrière-plan c'est une perte (0), si elle est remise au premier plan c'est un gain (1).

    • event.active.state : c'est une combinaison de flags indiquant le type d'événement qui s'est produit. Voici la liste des flags possibles :<liste>

    • SDL_APPMOUSEFOCUS: le curseur de la souris vient de rentrer ou de sortir de la fenêtre.

Il faut regarder la valeur de event.active.gain pour savoir si elle est rentrée (gain = 1) ou sortie (gain = 0) de la fenêtre ;

    • SDL_APPINPUTFOCUS : l'application vient de recevoir le focus du clavier ou de le perdre. Cela signifie en fait que votre fenêtre vient d'être mise au premier plan ou en arrière-plan.

Encore une fois, il faut regarder la valeur de event.active.gain pour savoir si la fenêtre a été mise au premier plan (gain = 1) ou en arrière-plan (gain = 0) ;

  • SDL_APPACTIVE : l'applicaton a été icônifiée, c'est-à-dire réduite dans la barre des tâches (gain = 0), ou remise dans son état normal (gain = 1).

Vous suivez toujours ? Il faut bien comparer les valeurs des deux sous-variables gain et state pour savoir exactement ce qui s'est produit.

Tester la valeur d'une combinaison de flags

event.active.state est une combinaison de flags. Cela signifie que dans un événement, il peut se produire deux choses à la fois (par exemple, si on réduit la fenêtre dans la barre des tâches, on perd aussi le focus du clavier et de la souris).
Il va donc falloir faire un test un peu plus compliqué qu'un simple…

if (event.active.state == SDL_APPACTIVE)

Pourquoi est-ce plus compliqué ?

Parce que c'est une combinaison de bits. Je ne vais pas vous faire un cours sur les opérations logiques bit à bit ici, ça serait un peu trop pour ce cours et vous n'avez pas nécessairement besoin d'en connaître davantage.
Je vais vous proposer un code prêt à l'emploi qu'il faut utiliser pour tester si un flag est présent dans une variable sans rentrer dans les détails.

Pour tester par exemple s'il y a eu un changement de focus de la souris, on doit écrire :

if ((event.active.state & SDL_APPMOUSEFOCUS) == SDL_APPMOUSEFOCUS)

Il n'y a pas d'erreur. Attention, c'est précis : il faut un seul & et deux =, et il faut bien utiliser les parenthèses comme je l'ai fait.

Cela fonctionne de la même manière pour les autres événements. Par exemple :

if ((event.active.state & SDL_APPACTIVE) == SDL_APPACTIVE)
Tester l'état et le gain à la fois

Dans la pratique, vous voudrez sûrement tester l'état et le gain à la fois. Vous pourrez ainsi savoir exactement ce qui s'est passé.

Supposons que vous ayez un jeu qui fait faire beaucoup de calculs à l'ordinateur. Vous voulez que le jeu se mette en pause automatiquement lorsque la fenêtre est réduite, et qu'il se relance lorsque la fenêtre est restaurée. Cela évite que le jeu continue pendant que le joueur n'est plus actif et cela évite aussi au processeur de faire trop de calculs par la même occasion.

Le code ci-dessous met en pause le jeu en activant un booléen pause à 1. Il remet en marche le jeu en désactivant le booléen à 0.

if ((event.active.state & SDL_APPACTIVE) == SDL_APPACTIVE)
{
    if (event.active.gain == 0) /* La fenêtre a été réduite */
        pause = 1;
    else if (event.active.gain == 1) /* La fenêtre a été restaurée */
        pause = 0;
}

Bien entendu, ce code n'est pas complet. Ce sera à vous de tester l'état de la variable pause pour savoir s'il faut ou non effectuer les calculs de votre jeu.

Je vous laisse faire d'autres tests pour les autres cas (par exemple, vérifier si le curseur de la souris est à l'intérieur ou à l'extérieur de la fenêtre). Vous pouvez — pour vous entraîner — faire bouger Zozor vers la droite lorsque la souris rentre dans la fenêtre, et le faire bouger vers la gauche lorsqu'elle en sort.

En résumé

  • Les événements sont des signaux que vous envoie la SDL pour vous informer d'une action de la part de l'utilisateur : appui sur une touche, mouvement ou clic de la souris, fermeture de la fenêtre, etc.

  • Les événements sont récupérés dans une variable de type SDL_Event avec la fonction SDL_WaitEvent (fonction bloquante mais facile à gérer) ou avec la fonction SDL_PollEvent (fonction non bloquante mais plus complexe à manipuler).

  • Il faut analyser la sous-variable event.type pour connaître le type d'événement qui s'est produit. On le fait en général dans un switch.

  • Une fois le type d'événement déterminé, il est le plus souvent nécessaire d'analyser l'événement dans le détail. Par exemple, lorsqu'une touche du clavier a été enfoncée (SDL_KEYDOWN) il faut analyser event.key.keysym.sym pour connaître la touche en question.

  • Le double buffering est une technique qui consiste à charger l'image suivante en tâche de fond et à l'afficher seulement une fois qu'elle est prête. Cela permet d'éviter des scintillements désagréables à l'écran.


Apprenez à programmer en C

TP : Mario Sokoban

La bibliothèque SDL fournit, comme vous l'avez vu, un grand nombre de fonctions prêtes à l'emploi. Il est facile de s'y perdre les premiers temps, surtout si on ne pratique pas.

Ce premier TP de la section SDL est justement là pour vous faire découvrir un cas pratique et surtout vous inviter à manipuler. Cette fois, vous l'aurez compris je crois, notre programme ne sera pas une console mais bel et bien une fenêtre !

Quel va être le sujet de ce TP ? Il va s'agir d'un jeu de Sokoban !
Peut-être que ce nom ne vous dit rien, mais le jeu est pourtant un grand classique des casse-têtes. Il consiste à pousser des caisses pour les amener à des points précis dans un labyrinthe.

Cahier des charges du Sokoban

À propos du Sokoban

« Sokoban » est un terme japonais qui signifie « Magasinier ».
Il s'agit d'un casse-tête inventé dans les années 80 par Hiroyuki Imabayashi. Le jeu a remporté un concours de programmation à cette époque.

Le but du jeu

Il est simple à comprendre : vous dirigez un personnage dans un labyrinthe. Il doit pousser des caisses pour les amener à des endroits précis. Le joueur ne peut pas déplacer deux caisses à la fois.

Si le principe se comprend vite et bien, cela ne veut pas dire pour autant que le jeu est toujours facile. Il est en effet possible de réaliser des casse-têtes vraiment… prise de tête !

La fig. suivante vous donne un aperçu du jeu que nous allons réaliser.

Le jeu Mario Sokoban que nous allons réaliser
Pourquoi avoir choisi ce jeu ?

Parce que c'est un jeu populaire, qu'il fait un bon sujet de programmation et qu'on peut le réaliser avec les connaissances que l'on a acquises.
Alors bien sûr, ça demande de l'organisation. La difficulté n'est pas vraiment dans le code lui-même mais dans l'organisation. Il va en effet falloir découper notre programme en plusieurs fichiers .c intelligemment et essayer de créer les bonnes fonctions.

C'est aussi pour cette raison que j'ai décidé cette fois de ne pas construire le TP comme les précédents : je ne vais pas vous donner des indices suivis d'une correction à la fin. Au contraire, je vais vous montrer comment je réalise tout le projet de A à Z.

Et si je veux m'entraîner tout seul ?

Pas de problème ! Allez-y lancez-vous, c'est même très bien !
Il vous faudra certainement un peu de temps : personnellement ça m'a pris une bonne petite journée, et encore c'est parce que j'ai un peu l'habitude de programmer et que j'évite certains pièges courants (cela ne m'a pas empêché de me prendre la tête à plusieurs reprises quand même ;) ).

Sachez qu'un tel jeu peut être réalisé de nombreuses façons différentes. Je vais vous montrer ma façon de faire : ce n'est pas la meilleure, mais ce n'est pas la plus mauvaise non plus.
Le TP se terminera par une série de suggestions d'améliorations et je vous proposerai de télécharger le code source complet bien entendu.

Encore une fois : je vous conseille d'essayer de vous y lancer par vous-mêmes. Passez-y deux ou trois jours et faites de votre mieux. Il est important que vous pratiquiez.

Le cahier des charges

Le cahier des charges est un document dans lequel on écrit tout ce que le programme doit savoir faire.
En l'occurence, que veut-on que notre jeu soit capable de faire ? C'est le moment de se décider !

Voici ce que je propose :

  • le joueur doit pouvoir se déplacer dans un labyrinthe et pousser des caisses ;

  • il ne peut pas pousser deux caisses à la fois ;

  • une partie est considérée comme gagnée lorsque toutes les caisses sont sur des objectifs ;

  • les niveaux de jeu seront enregistrés dans un fichier (par exemple niveaux.lvl) ;

  • un éditeur sera intégré au programme pour que n'importe qui puisse créer ses propres niveaux (ce n'est pas indispensable mais ça ajoutera du piment !).

Voilà qui nous donnera bien assez de travail.

À noter qu'il y a des choses que notre programme ne saura pas faire, ça aussi il faut le dire.

  • Notre programme ne pourra gérer qu'un seul niveau à la fois. Si vous voulez coder une « aventure » avec une suite de niveaux, vous n'aurez qu'à le faire vous-mêmes à la fin de ce TP.

  • Il n'y aura pas de gestion du temps écoulé (on ne sait pas encore faire ça) ni du score.

En fait, avec tout ce qu'on veut déjà faire (notamment l'éditeur de niveaux), il y en a pour un petit moment.

Je vous indiquerai à la fin du TP une liste d'idées pour améliorer le programme. Et ce ne seront pas des paroles en l'air, car ce sont des idées que j'aurai moi-même implémentées dans une version plus complète du programme que je vous proposerai de télécharger.
En revanche, je ne vous donnerai pas les codes source de la version complète pour vous forcer à travailler (je vais pas tout vous servir sur un plateau d'argent non plus ! ;) ).

Récupérer les sprites du jeu

Dans la plupart des jeux 2D, que ce soient des jeux de plate-forme ou de casse-tête comme ici, on appelle les images qui composent le jeu des sprites.
Dans notre cas, j'ai décidé qu'on créerait un Sokoban mettant en scène Mario (d'où le nom « Mario Sokoban »). Comme Mario est un personnage populaire dans le monde de la 2D, on n'aura pas trop de mal à trouver des sprites de Mario. Il faudra aussi trouver des sprites pour les murs de briques, les caisses, les objectifs, etc.

Si vous faites une recherche sur Google pour « sprites », vous trouverez de nombreuses réponses. En effet, il y a beaucoup de sites qui proposent de télécharger des sprites de jeux 2D auxquels vous avez sûrement joué par le passé.

Voici les sprites que nous allons utiliser :

Sprite | Description
 | Un mur
 | Une caisse
 | Une caisse placée sur un objectif.
 | Un objectif (où l'on doit mettre une caisse).
 | Le joueur (Mario) orienté vers le bas
 | Mario vers la droite
 | Mario vers la gauche
 | Mario vers le haut

Le plus simple pour vous sera de télécharger un pack que j'ai préparé contenant toutes ces images.

Télécharger toutes les images

Notez que j'aurais très bien pu n'utiliser qu'un sprite pour le joueur. J'aurais pu faire en sorte que Mario soit toujours orienté vers le bas, mais le fait de pouvoir le diriger dans les quatre directions ajoute un peu plus de réalisme. Ça ne fera qu'un petit défi de plus à relever !

J'ai aussi créé une petite image qui pourra servir de menu d'accueil au lancement du programme (fig. suivante), vous la trouverez dans le pack que vous venez normalement de télécharger.

L'écran d'accueil du programme

Vous noterez que les images sont dans différents formats. Il y a des GIF, des PNG et des JPEG. Nous allons donc avoir besoin de la bibliothèque SDL_Image.
Pensez à configurer votre projet pour qu'il gère la SDL et SDL_Image. Si vous avez oublié comment faire, revoyez les chapitres précédents. Si vous ne configurez pas votre projet correctement, on vous dira que les fonctions que vous utilisez (comme IMG_Load) n'existent pas !

Le main et les constantes

Chaque fois qu'on commence un projet assez important, il est nécessaire de bien s'organiser dès le départ.
En général, je commence par me créer un fichier de constantes constantes.h ainsi qu'un fichier main.c qui contiendra la fonction main (et uniquement celle-là). Ce n'est pas une règle : c'est juste ma façon de fonctionner. Chacun a sa propre manière de faire.

Les différents fichiers du projet

Je propose de créer dès à présent tous les fichiers du projet (même s'ils restent vides au départ).
Voici donc les fichiers que je crée :

  • constantes.h : les définitions de constantes globales à tout le programme ;

  • main.c : le fichier qui contient la fonction main (fonction principale du programme) ;

  • jeu.c : fonctions gérant une partie de Sokoban ;

  • jeu.h : prototypes des fonctions de jeu.c ;

  • editeur.c : fonctions gérant l'éditeur de niveaux ;

  • editeur.h : prototypes des fonctions de editeur.c ;

  • fichiers.c : fonctions gérant la lecture et l'écriture de fichiers de niveaux (comme niveaux.lvl par exemple) ;

  • fichiers.h : prototypes des fonctions de fichiers.c.

On va commencer par créer le fichier des constantes.

Les constantes : constantes.h

Voici le contenu de mon fichier de constantes :

/*
constantes.h
------------

Par mateo21, pour Le Site du Zéro (www.siteduzero.com)

Rôle : définit des constantes pour tout le programme (taille de la fenêtre...)
*/

#ifndef DEF_CONSTANTES
#define DEF_CONSTANTES

    #define TAILLE_BLOC         34 // Taille d'un bloc (carré) en pixels
    #define NB_BLOCS_LARGEUR    12
    #define NB_BLOCS_HAUTEUR    12
    #define LARGEUR_FENETRE     TAILLE_BLOC * NB_BLOCS_LARGEUR
    #define HAUTEUR_FENETRE     TAILLE_BLOC * NB_BLOCS_HAUTEUR

    enum {HAUT, BAS, GAUCHE, DROITE};
    enum {VIDE, MUR, CAISSE, OBJECTIF, MARIO, CAISSE_OK};

#endif

Vous noterez plusieurs points intéressants dans ce petit fichier.

  • Le fichier commence par un commentaire d'en-tête. Je recommande de mettre ce type de commentaire au début de chacun de vos fichiers (que ce soient des .h ou des .c). Généralement, un commentaire d'en-tête contient :

    • le nom du fichier ;

    • l'auteur ;

    • le rôle du fichier (ce à quoi servent les fonctions qu'il contient) ;

    • je ne l'ai pas fait là, mais généralement, on met aussi la date de création et la date de dernière modification. Ça permet de s'y retrouver, surtout dans les gros projets à plusieurs.

  • Le fichier est protégé contre les inclusions infinies. Il utilise la technique que l'on a apprise à la fin du chapitre sur le préprocesseur. Ici cette protection ne sert pas vraiment, mais j'ai pris l'habitude de faire ça pour chacun de mes fichiers .h sans exception.

  • Enfin, le cœur du fichier. Vous avez une série de define. J'indique la taille d'un petit bloc en pixels (tous mes sprites sont des carrés de 34 px). J'indique que ma fenêtre comportera 12 x 12 blocs de largeur. Je calcule comme ça les dimensions de la fenêtre par une simple multiplication des constantes. Ce que je fais là n'est pas obligatoire, mais a un énorme avantage : si plus tard je veux changer la taille du jeu, je n'aurai qu'à éditer ce fichier (et à recompiler) et tout mon code source s'adaptera en conséquence.

  • Enfin, j'ai défini d'autres constantes via des énumérations anonymes. C'est légèrement différent de ce qu'on a appris dans le chapitre sur les types de variables personnalisés. Ici, je ne crée pas un type personnalisé, je définis juste des constantes. Cela ressemble aux define à une différence près : c'est l'ordinateur qui attribue automatiquement un nombre à chacune des valeurs (en commençant par 0). Ainsi, on a HAUT = 0, BAS = 1, GAUCHE = 2, etc. Cela permettra de rendre notre code source beaucoup plus clair par la suite, vous verrez !

En résumé, j'utilise :

  • des define lorsque je veux attribuer une valeur précise à une constante (par exemple « 34 pixels ») ;

  • des énumérations lorsque la valeur attribuée à une constante ne m'importe pas. Ici, je me moque bien de savoir que HAUT vaut 0 (ça pourrait aussi bien valoir 150, ça ne changerait rien) ; tout ce qui compte pour moi, c'est que cette constante ait une valeur différente de BAS, GAUCHE et DROITE.

Inclure les définitions de constantes

Le principe sera d'inclure ce fichier de constantes dans chacun de mes fichiers .c.
Ainsi, partout dans mon code je pourrai utiliser les constantes que je viens de définir.

Il faudra donc taper la ligne suivante au début de chacun des fichiers .c :

#include "constantes.h"

Le main : main.c

La fonction main principale est extrêmement simple. Elle a pour rôle d'afficher l'écran d'accueil du jeu et de rediriger vers la bonne section.

/*
main.c
------

Par mateo21, pour Le Site du Zéro (www.siteduzero.com)

Rôle : menu du jeu. Permet de choisir entre l'éditeur et le jeu lui-même.
*/

#include <stdlib.h>
#include <stdio.h>
#include <SDL/SDL.h>
#include <SDL/SDL_image.h>

#include "constantes.h"
#include "jeu.h"
#include "editeur.h"

int main(int argc, char *argv[])
{
    SDL_Surface *ecran = NULL, *menu = NULL;
    SDL_Rect positionMenu;
    SDL_Event event;

    int continuer = 1;

    SDL_Init(SDL_INIT_VIDEO);

    SDL_WM_SetIcon(IMG_Load("caisse.jpg"), NULL); // L'icône doit être chargée avant SDL_SetVideoMode
    ecran = SDL_SetVideoMode(LARGEUR_FENETRE, HAUTEUR_FENETRE, 32, SDL_HWSURFACE | SDL_DOUBLEBUF);
    SDL_WM_SetCaption("Mario Sokoban", NULL);

    menu = IMG_Load("menu.jpg");
    positionMenu.x = 0;
    positionMenu.y = 0;

    while (continuer)
    {
        SDL_WaitEvent(&event);
        switch(event.type)
        {
            case SDL_QUIT:
                continuer = 0;
                break;
            case SDL_KEYDOWN:
                switch(event.key.keysym.sym)
                {
                    case SDLK_ESCAPE: // Veut arrêter le jeu
                        continuer = 0;
                        break;
                    case SDLK_KP1: // Demande à jouer
                        jouer(ecran);
                        break;
                    case SDLK_KP2: // Demande l'éditeur de niveaux
                        editeur(ecran);
                        break;
                }
                break;
        }

        // Effacement de l'écran
        SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 0, 0, 0));
        SDL_BlitSurface(menu, NULL, ecran, &positionMenu);
        SDL_Flip(ecran);
    }

    SDL_FreeSurface(menu);
    SDL_Quit();

    return EXIT_SUCCESS;
}

La fonction main se charge d'effectuer les initialisations de la SDL, de donner un titre à la fenêtre ainsi qu'une icône. À la fin de la fonction, SDL_Quit() est appelée pour arrêter la SDL proprement.

La fonction affiche un menu. Le menu est chargé en utilisant la fonction IMG_Load de SDL_Image :

menu = IMG_Load("menu.jpg");

Vous remarquerez que, pour donner les dimensions de la fenêtre, j'utilise les constantes LARGEUR_FENETRE et HAUTEUR_FENETRE qu'on a définies dans constantes.h.

La boucle des événements

La boucle infinie gère les événements suivants :

  • arrêt du programme (SDL_QUIT) : si on demande à fermer le programme (clic sur la croix en haut à droite de la fenêtre), alors on passe le booléen continuer à 0, et la boucle s'arrêtera. Bref, classique ;

  • appui sur la touche Echap : arrêt du programme (comme SDL_QUIT) ;

  • appui sur la touche 1 du pavé numérique : lancement du jeu (appel de la fonction jouer) ;

  • appui sur la touche 2 du pavé numérique : lancement de l'éditeur (appel de la fonction editeur).

Comme vous le voyez, c'est vraiment très simple. Si on appuie sur 1, le jeu est lancé. Une fois que le jeu est terminé, la fonction jouer s'arrête et on retourne dans le main dans lequel on refait un tour de boucle. Le main boucle à l'infini tant qu'on ne demande pas à arrêter le jeu.

Grâce à cette petite organisation très simple, on peut donc gérer le menu dans le main et laisser des fonctions spéciales (comme jouer, ou editeur) gérer les différentes parties du programme.

Le jeu

Attaquons maintenant le gros du sujet : la fonction jouer !
Cette fonction est la plus importante du programme, aussi soyez attentifs car c'est vraiment là qu'il faut comprendre. Vous verrez après que créer l'éditeur de niveaux n'est pas si compliqué que ça en a l'air.

Les paramètres envoyés à la fonction

La fonction jouera besoin d'un paramètre : la surface ecran. En effet, la fenêtre a été ouverte dans le main, et pour que la fonction jouer puisse y dessiner, il faut qu'elle récupère le pointeur sur ecran !

Si vous regardez le main à nouveau, vous voyez qu'on appelle jouer en lui envoyant ecran :

jouer(ecran);

Le prototype de la fonction, que vous pouvez mettre dans jeu.h, est donc le suivant :

void jouer(SDL_Surface* ecran);

La fonction ne renvoie aucune valeur (d'où le void), mais on pourrait en renvoyer une, si on voulait. On pourrait par exemple renvoyer un booléen pour dire si oui ou non on a gagné.

Les déclarations de variables

Cette fonction va avoir besoin de nombreuses variables.
Je n'ai pas pensé à toutes les variables dont j'ai eu besoin du premier coup. Il y en a donc certaines que j'ai ajoutées par la suite.

Variables de types définis par la SDL

Voici pour commencer toutes les variables de types définis par la SDL dont j'ai besoin :

SDL_Surface *mario[4] = {NULL}; // 4 surfaces pour 4 directions de mario
SDL_Surface *mur = NULL, *caisse = NULL, *caisseOK = NULL, *objectif = NULL, *marioActuel = NULL;
SDL_Rect position, positionJoueur;
SDL_Event event;

J'ai créé un tableau de SDL_Surface appelé mario. C'est un tableau de quatre cases qui stockera Mario dans chacune des directions (un vers le bas, un autre vers la gauche, vers le haut et vers la droite).

Il y a ensuite plusieurs surfaces correspondant à chacun des sprites que je vous ai fait télécharger plus haut : mur, caisse, caisseOK (une caisse sur un objectif) et objectif.

À quoi sert marioActuel ?

C'est un pointeur vers une surface. Il pointe sur la surface correspondant au Mario orienté dans la direction actuelle. C'est donc marioActuel que l'on blittera à l'écran. Si vous regardez tout en bas de la fonction jouer, vous verrez justement :

SDL_BlitSurface(marioActuel, NULL, ecran, &position);

On ne blitte donc pas un élément du tableau mario, mais le pointeur marioActuel.
Ainsi, en blittant marioActuel, on blitte soit le Mario vers le bas, soit celui vers le haut, etc. Le pointeur marioActuel pointe vers une des cases du tableau mario.

Quoi d'autre à part ça ?
Une variable position de type SDL_Rect dont on se servira pour définir la position des éléments à blitter (on s'en servira pour tous les sprites, inutile de créer un SDL_Rect pour chaque surface !). positionJoueur est en revanche un peu différente : elle indique à quelle case sur la carte se trouve actuellement le joueur. Enfin, la variable event traitera les événements.

Variables plus « classiques »

J'ai aussi besoin de me créer des variables un peu plus classiques de type int (entier).

int continuer = 1, objectifsRestants = 0, i = 0, j = 0;
int carte[NB_BLOCS_LARGEUR][NB_BLOCS_HAUTEUR] = {0};

continuer et objectifsRestants sont des booléens.
i et j sont des petites variables qui vont me permettre de parcourir le tableau carte.

C'est là que les choses deviennent vraiment intéressantes. J'ai en effet créé un tableau à deux dimensions. Je ne vous ai pas parlé de ce type de tableaux auparavant, mais c'est justement le moment idéal pour vous apprendre ce que c'est. Ce n'est pas bien compliqué, vous allez voir.

Regardez la définition de plus près :

int carte[NB_BLOCS_LARGEUR][NB_BLOCS_HAUTEUR] = {0};

En fait, il s'agit d'un tableau d'int (entiers) qui a la particularité d'avoir deux paires de crochets [ ].
Si vous vous souvenez bien de constantes.h, NB_BLOCS_LARGEUR et NB_BLOCS_HAUTEUR sont des constantes qui valent toutes les deux 12.

Ce tableau sera donc à la compilation créé comme ceci :

int carte[12][12] = {0};

Mais qu'est-ce que ça veut dire ?

Ça veut dire que pour chaque « case » de carte, il y a 12 sous-cases.
Il y aura donc les variables suivantes :

carte[0][0]
carte[0][1]
carte[0][2]
carte[0][3]
carte[0][4]
carte[0][5]
carte[0][6]
carte[0][7]
carte[0][8]
carte[0][9]
carte[0][10]
carte[0][11]
carte[1][0]
carte[1][1]
carte[1][2]
carte[1][3]
carte[1][4]
carte[1][5]
carte[1][6]
carte[1][7]
carte[1][8]
carte[1][9]
carte[1][10]
...
carte[11][2]
carte[11][3]
carte[11][4]
carte[11][5]
carte[11][6]
carte[11][7]
carte[11][8]
carte[11][9]
carte[11][10]
carte[11][11]

C'est donc un tableau de 12 * 12 = 144 cases !
Chacune des ces cases représente une case de la carte.

La fig. suivante vous donne une idée de la façon dont la carte est représentée.

Découpage de la carte

Ainsi, la case en haut à gauche est stockée dans carte[0][0].
La case en haut à droite est stockée dans carte[0][11].
La case en bas à droite (la toute dernière) est stockée dans carte[11][11].

Selon la valeur de la case (qui est un nombre entier), on sait si la case contient un mur, une caisse, un objectif, etc.). C'est justement là que va servir notre énumération de tout à l'heure !

enum {VIDE, MUR, CAISSE, OBJECTIF, MARIO, CAISSE_OK};

Si la case vaut VIDE (0) on sait que cette partie de l'écran devra rester blanche. Si elle vaut MUR (1), on sait qu'il faudra blitter une image de mur, etc.

Initialisations

Chargement des surfaces

Maintenant qu'on a passé en revue toutes les variables de la fonction jouer, on peut commencer à faire quelques initialisations :

// Chargement des sprites (décors, personnage...)
mur = IMG_Load("mur.jpg");
caisse = IMG_Load("caisse.jpg");
caisseOK = IMG_Load("caisse_ok.jpg");
objectif = IMG_Load("objectif.png");
mario[BAS] = IMG_Load("mario_bas.gif");
mario[GAUCHE] = IMG_Load("mario_gauche.gif");
mario[HAUT] = IMG_Load("mario_haut.gif");
mario[DROITE] = IMG_Load("mario_droite.gif");

Rien de sorcier là-dedans : on charge tout grâce à IMG_Load.
S'il y a une petite particularité, c'est le chargement de mario. On charge en effet Mario dans chacune des directions dans le tableau mario en utilisant les constantes HAUT, BAS, GAUCHE, DROITE. Le fait d'utiliser les constantes rend ici — comme vous le voyez — le code plus clair. On aurait très bien pu écrire mario[0], mais c'est quand même plus lisible d'avoir mario[HAUT] par exemple !

Orientation initiale du Mario (marioActuel)

On initialise ensuite marioActuel pour qu'il ait une direction au départ :

marioActuel = mario[BAS]; // Mario sera dirigé vers le bas au départ

J'ai trouvé plus logique de commencer la partie avec un Mario qui regarde vers le bas (c'est-à-dire vers nous). Si vous voulez, vous pouvez changer cette ligne et mettre :

marioActuel = mario[DROITE];

Vous verrez que Mario sera alors orienté vers la droite au début du jeu.

Chargement de la carte

Maintenant, il va falloir remplir notre tableau à deux dimensions carte. Pour l'instant, ce tableau ne contient que des 0.
Il faut lire le niveau qui est stocké dans le fichier niveaux.lvl.

// Chargement du niveau
if (!chargerNiveau(carte))
    exit(EXIT_FAILURE); // On arrête le jeu si on n'a pas pu charger le niveau

J'ai choisi de faire gérer le chargement (et l'enregistrement) de niveaux par des fonctions situées dans fichiers.c.
Ici, on appelle donc la fonction chargerNiveau. On l'étudiera plus en détails plus loin (elle n'est pas très compliquée, de toute manière). Tout ce qui nous intéresse ici c'est de savoir que notre niveau a été chargé dans le tableau carte.

Si le niveau n'a pas pu être chargé (parce que niveaux.lvl n'existe pas), la fonction renverra « faux ». Sinon, elle renverra « vrai ».

On teste donc le résultat du chargement dans une condition. Si le résultat est négatif (d'où le point d'exclamation qui sert à exprimer la négation), on arrête tout : on appelle exit.
Sinon, c'est que tout va bien et on peut continuer.

Nous possédons maintenant un tableau carte qui décrit le contenu de chaque case : MUR, VIDE, CAISSE…

Recherche de la position de départ de Mario

Il faut maintenant initialiser la variable positionJoueur.
Cette variable, de type SDL_Rect, est un peu particulière. On ne s'en sert pas pour stocker des coordonnées en pixels. On s'en sert pour stocker des coordonnées en « cases » sur la carte. Ainsi, si on a :

positionJoueur.x == 11 positionJoueur.y == 11

… c'est que le joueur se trouve dans la toute dernière case en bas à droite de la carte.
Reportez-vous au schéma de la carte de la fig. suivante pour bien voir à quoi ça correspond si vous avez (déjà) oublié.

On doit parcourir notre tableau carte à deux dimensions à l'aide d'une double boucle. On utilise la petite variable i pour parcourir le tableau verticalement et la variable j pour le parcourir horizontalement :

// Recherche de la position de Mario au départ
for (i = 0 ; i < NB_BLOCS_LARGEUR ; i++)
{
    for (j = 0 ; j < NB_BLOCS_HAUTEUR ; j++)
    {
        if (carte[i][j] == MARIO) // Si Mario se trouve à cette position
        {
            positionJoueur.x = i;
            positionJoueur.y = j;
            carte[i][j] = VIDE;
        }
    }
}

À chaque case, on teste si elle contient MARIO (c'est-à-dire le départ du joueur sur la carte). Si c'est le cas, on stocke les coordonnées actuelles (situées dans i et j) dans la variable positionJoueur.
On efface aussi la case en la mettant à VIDE pour qu'elle soit considérée comme une case vide par la suite.

Activation de la répétition des touches

Dernière chose, très simple : on active la répétition des touches pour qu'on puisse se déplacer sur la carte en laissant une touche enfoncée.

// Activation de la répétition des touches
SDL_EnableKeyRepeat(100, 100);

La boucle principale

Pfiou ! Nos initialisations sont faites, on peut maintenant s'occuper de la boucle principale.

C'est une boucle classique qui fonctionne sur le même schéma que celles qu'on a vues jusqu'ici. Elle est juste un peu plus grosse et un peu plus complète (faut c'qui faut comme on dit !).

Regardons de plus près le switch qui teste l'événement :

switch(event.type)
{
    case SDL_QUIT:
        continuer = 0;
        break;
    case SDL_KEYDOWN:
        switch(event.key.keysym.sym)
        {
            case SDLK_ESCAPE:
                continuer = 0;
                break;
            case SDLK_UP:
                marioActuel = mario[HAUT];
                deplacerJoueur(carte, &positionJoueur, HAUT);
                break;
            case SDLK_DOWN:
                marioActuel = mario[BAS];
                deplacerJoueur(carte, &positionJoueur, BAS);
                break;
            case SDLK_RIGHT:
                marioActuel = mario[DROITE];
                deplacerJoueur(carte, &positionJoueur, DROITE);
                break;
            case SDLK_LEFT:
                marioActuel = mario[GAUCHE];
                deplacerJoueur(carte, &positionJoueur, GAUCHE);
                break;
        }
        break;
}

Si on appuie sur la touche Echap, le jeu s'arrêtera et on retournera au menu principal.

Comme vous le voyez, il n'y a pas 36 événements différents à gérer : on teste juste si le joueur appuie sur les touches « haut », « bas », « gauche » ou « droite » de son clavier.
Selon la touche enfoncée, on change la direction de Mario. C'est là qu'intervient marioActuel ! Si on appuie vers le haut, alors :

marioActuel = mario[HAUT];

Si on appuie vers le bas, alors :

marioActuel = mario[BAS];

marioActuel pointe donc sur la surface représentant Mario dans la position actuelle. C'est ainsi qu'en blittant marioActuel tout à l'heure, on sera certain de blitter Mario dans la bonne direction.

Maintenant, chose très importante : on appelle une fonction deplacerJoueur. Cette fonction va déplacer le joueur sur la carte s'il a le droit de le faire.

  • Par exemple, on ne peut pas faire monter Mario d'un cran vers le haut s'il se trouve déjà tout en haut de la carte.

  • On ne peut pas non plus le faire monter s'il y a un mur au-dessus de lui.

  • On ne peut pas le faire monter s'il y a deux caisses au-dessus de lui.

  • Par contre, on peut le faire monter s'il y a juste une caisse au-dessus de lui.

  • Mais attention, on ne peut pas le faire monter s'il y a une caisse au-dessus de lui et que la caisse se trouve au bord de la carte !

Oh la la, c'est quoi ce bazar ?

C'est ce qu'on appelle la gestion des collisions. Si ça peut vous rassurer, ici c'est une gestion des collisions extrêmement simple, vu que le joueur se déplace par « cases » et dans seulement quatre directions possibles à la fois. Dans un jeu 2D où on peut se déplacer dans toutes les directions pixel par pixel, la gestion des collisions est bien plus complexe.

Mais il y a pire : la 3D. La gestion des collisions dans un jeu 3D est vraiment la bête noire des programmeurs. Heureusement, il existe des bibliothèques de gestion des collisions en 3D qui font le gros du travail à notre place.

Revenons à la fonction deplacerJoueur et concentrons-nous. On lui envoie trois paramètres :

  • la carte : pour qu'elle puisse la lire mais aussi la modifier, si on déplace une caisse par exemple ;

  • la position du joueur : là aussi, la fonction devra lire et éventuellement modifier la position du joueur ;

  • la direction dans laquelle on demande à aller : on utilise là encore les constantes HAUT, BAS, GAUCHE, DROITE pour plus de lisibilité.

Nous étudierons la fonction deplacerJoueur plus loin. J'aurais très bien pu mettre tous les tests dans le switch, mais celui-ci serait devenu énorme et illisible. C'est là que découper son programme en fonctions prend tout son intérêt.

Blittons, blittons, la queue du cochon

Notre switch est terminé : à ce stade du programme, la carte et le joueur ont probablement changé. Quoi qu'il en soit, c'est l'heure du blit !

On commence par effacer l'écran en lui donnant une couleur de fond blanche :

// Effacement de l'écran
SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 255, 255, 255));

Et maintenant, on parcourt tout notre tableau à deux dimensions carte pour savoir quel élément blitter à quel endroit sur l'écran.
On effectue une double boucle comme on l'a vu plus tôt pour parcourir toutes les 144 cases du tableau :

// Placement des objets à l'écran
objectifsRestants = 0;

for (i = 0 ; i < NB_BLOCS_LARGEUR ; i++)
{
    for (j = 0 ; j < NB_BLOCS_HAUTEUR ; j++)
    {
        position.x = i * TAILLE_BLOC;
        position.y = j * TAILLE_BLOC;

        switch(carte[i][j])
        {
            case MUR:
                SDL_BlitSurface(mur, NULL, ecran, &position);
                break;
            case CAISSE:
                SDL_BlitSurface(caisse, NULL, ecran, &position);
                break;
            case CAISSE_OK:
                SDL_BlitSurface(caisseOK, NULL, ecran, &position);
                break;
            case OBJECTIF:
                SDL_BlitSurface(objectif, NULL, ecran, &position);
                objectifsRestants = 1;
                break;
        }
    }
}

Pour chacune des cases, on prépare la variable position (de type SDL_Rect) pour placer l'élément actuel à la bonne position sur l'écran.
Le calcul est très simple :

position.x = i * TAILLE_BLOC;
position.y = j * TAILLE_BLOC;

Il suffit de multiplier i par TAILLE_BLOC pour avoir position.x.
Ainsi, si on se trouve à la troisième case, c'est que i vaut 2 (n'oubliez pas que i commence à 0 !). On fait donc le calcul 2 * 34 = 68. On blittera donc l'image 68 pixels vers la droite sur ecran.
On fait la même chose pour les ordonnées y.

Ensuite, on fait un switch sur la case de la carte qu'on est en train d'analyser.
Là encore, avoir défini des constantes est vraiment pratique et rend les choses plus lisibles.
On teste donc si la case vaut MUR, dans ce cas on blitte un mur. De même pour les caisses et les objectifs.

Test de victoire

Vous remarquerez qu'avant la double boucle, on initialise le booléen objectifsRestants à 0.
Ce booléen sera mis à 1 dès qu'on aura détecté un objectif sur la carte. S'il ne reste plus d'objectifs, c'est que toutes les caisses sont sur des objectifs (il n'y a plus que des CAISSE_OK).

Il suffit de tester si le booléen vaut « faux », c'est-à-dire s'il ne reste plus d'objectifs.
Dans ce cas, on met la variable continuer à 0 pour arrêter la partie :

// Si on n'a trouvé aucun objectif sur la carte, c'est qu'on a gagné
if (!objectifsRestants)
    continuer = 0;
Le joueur

Il nous reste à blitter le joueur :

// On place le joueur à la bonne position
position.x = positionJoueur.x * TAILLE_BLOC;
position.y = positionJoueur.y * TAILLE_BLOC;
SDL_BlitSurface(marioActuel, NULL, ecran, &position);

On calcule sa position (en pixels cette fois) en faisant une simple multiplication entre positionJoueur et TAILLE_BLOC. On blitte ensuite le joueur à la position indiquée.

Flip !

On a tout fait, il ne nous reste plus qu'à afficher l'écran au joueur :

SDL_Flip(ecran);

Fin de la fonction : déchargements

Après la boucle principale, on doit faire quelques FreeSurface pour libérer la mémoire des sprites qu'on a chargés.
On désactive aussi la répétition des touches en envoyant les valeurs 0 à la fonction SDL_EnableKeyRepeat :

// Désactivation de la répétition des touches (remise à 0)
SDL_EnableKeyRepeat(0, 0);

// Libération des surfaces chargées
SDL_FreeSurface(mur);
SDL_FreeSurface(caisse);
SDL_FreeSurface(caisseOK);
SDL_FreeSurface(objectif);
for (i = 0 ; i < 4 ; i++)
    SDL_FreeSurface(mario[i]);

La fonction deplacerJoueur

La fonction deplacerJoueur se trouve elle aussi dans jeu.c.
C'est une fonction… assez délicate à écrire. C'est peut-être la principale difficulté que l'on rencontre lorsqu'on code un jeu de Sokoban.

Rappel : la fonction deplacerJoueur vérifie si on a le droit de déplacer le joueur dans la direction demandée. Elle met à jour la position du joueur (positionJoueur) et aussi la carte si une caisse a été déplacée.

Voici le prototype de la fonction :

void deplacerJoueur(int carte[][NB_BLOCS_HAUTEUR], SDL_Rect *pos, int direction);

Ce prototype est un peu particulier. Vous voyez que j'envoie le tableau carte et que je précise la taille de la deuxième dimension (NB_BLOCS_HAUTEUR).
Pourquoi cela ?

La réponse est un peu compliquée pour que je la développe au milieu de ce cours. Pour faire simple, le C ne devine pas qu'il s'agit d'un tableau à deux dimensions et il faut au moins donner la taille de la seconde dimension pour que ça fonctionne.
Donc, lorsque vous envoyez un tableau à deux dimensions à une fonction, vous devez indiquer la taille de la seconde dimension dans le prototype. C'est comme ça, c'est obligatoire.

Autre chose : vous noterez que positionJoueur s'appelle en fait pos dans cette fonction. J'ai choisi de raccourcir le nom parce que c'est plus court à écrire, et vu qu'on va avoir besoin de l'écrire de nombreuses fois, autant ne pas se fatiguer.

Commençons par tester la direction dans laquelle on veut aller via un grand switch :

switch(direction)
{
    case HAUT:
    /* etc. */
Et c'est parti pour des tests de folie !

Il faut maintenant écrire tous les tests de tous les cas possibles, en essayant de ne pas en oublier un seul.

Voici comment je procède : je teste toutes les possibilités de collision cas par cas, et dès que je détecte une collision (qui fait que le joueur ne peut pas bouger), je fais un break; pour sortir du switch, et donc empêcher le déplacement.

Voici par exemple toutes les possibilités de collision qui existent pour un joueur qui veut se déplacer vers le haut :

  • le joueur est déjà tout en haut de la carte ;

  • il y a un mur au-dessus du joueur ;

  • il y a deux caisses au-dessus du joueur (et il ne peut pas déplacer deux caisses à la fois, rappelez-vous) ;

  • il y a une caisse puis le bord de la carte.

Si tous ces tests sont ok, alors je me permets de déplacer le joueur.

Je vais vous montrer les tests pour un déplacement vers le haut. Pour les autres sens, il suffira d'adapter un petit peu le code.

if (pos->y - 1 < 0) // Si le joueur dépasse l'écran, on arrête
    break;

On commence par vérifier si le joueur est déjà tout en haut de l'écran. En effet, si on essayait d'appeler carte[5][-1] par exemple, ce serait le plantage du programme assuré !
On commence donc par vérifier qu'on ne va pas « déborder » de l'écran.

Ensuite :

if (carte[pos->x][pos->y - 1] == MUR) // S'il y a un mur, on arrête
    break;

Là encore c'est simple. On vérifie s'il n'y a pas un mur au-dessus du joueur. Si tel est le cas, on arrête (break).

Ensuite (attention les yeux) :

// Si on veut pousser une caisse, il faut vérifier qu'il n'y a pas de mur derrière (ou une autre caisse, ou la limite du monde)
if ((carte[pos->x][pos->y - 1] == CAISSE || carte[pos->x][pos->y - 1] == CAISSE_OK) &&
    (pos->y - 2 < 0 || carte[pos->x][pos->y - 2] == MUR ||
    carte[pos->x][pos->y - 2] == CAISSE || carte[pos->x][pos->y - 2] == CAISSE_OK))
    break;

Ce gros test peut se traduire comme ceci : « SI au-dessus du joueur il y a une caisse (ou une caisse_ok, c'est-à-dire une caisse bien placée)
ET SI au-dessus de cette caisse il y a soit le vide (on déborde du niveau car on est tout en haut), soit une autre caisse, soit une caisse_ok :
ALORS on ne peut pas se déplacer : break. »

Si on arrive à passer ce test, on a le droit de déplacer le joueur. Ouf !
On appelle d'abord une fonction qui va déplacer une caisse si nécessaire :

// Si on arrive là, c'est qu'on peut déplacer le joueur !
// On vérifie d'abord s'il y a une caisse à déplacer
deplacerCaisse(&carte[pos->x][pos->y - 1], &carte[pos->x][pos->y - 2]);
Le déplacement de caisse : deplacerCaisse

J'ai choisi de gérer le déplacement de caisse dans une autre fonction car c'est le même code pour les quatre directions. On doit juste s'être assuré avant qu'on a le droit de se déplacer (ce qu'on vient de faire).
On envoie à la fonction deux paramètres : le contenu de la case dans laquelle on veut aller et le contenu de la case d'après.

void deplacerCaisse(int *premiereCase, int *secondeCase)
{
    if (*premiereCase == CAISSE || *premiereCase == CAISSE_OK)
    {
        if (*secondeCase == OBJECTIF)
            *secondeCase = CAISSE_OK;
        else
            *secondeCase = CAISSE;

        if (*premiereCase == CAISSE_OK)
            *premiereCase = OBJECTIF;
        else
            *premiereCase = VIDE;
    }
}

Cette fonction met à jour la carte car elle prend en paramètres des pointeurs sur les cases concernées.
Je vous laisse la lire, c'est assez simple à comprendre. Il ne faut pas oublier que si on déplace une CAISSE_OK, il faut remplacer la case où elle se trouvait par un OBJECTIF. Sinon, si c'est une simple CAISSE, alors on remplace la case en question par du VIDE.

Déplacer le joueur

On retourne dans la fonction deplacerJoueur.
Cette fois c'est la bonne, on peut déplacer le joueur.

Comment fait-on ? C'est très très simple :

pos->y--; // On peut enfin faire monter le joueur (oufff !)

Il suffit de diminuer l'ordonnée y (car le joueur veut monter).

Résumé

En guise de résumé, voici tous les tests pour le cas HAUT :

switch(direction)
{
    case HAUT:
        if (pos->y - 1 < 0) // Si le joueur dépasse l'écran, on arrête
            break;
        if (carte[pos->x][pos->y - 1] == MUR) // S'il y a un mur, on arrête
            break;
        // Si on veut pousser une caisse, il faut vérifier qu'il n'y a pas de mur derrière (ou une autre caisse, ou la limite du monde)
        if ((carte[pos->x][pos->y - 1] == CAISSE || carte[pos->x][pos->y - 1] == CAISSE_OK) &&
            (pos->y - 2 < 0 || carte[pos->x][pos->y - 2] == MUR ||
            carte[pos->x][pos->y - 2] == CAISSE || carte[pos->x][pos->y - 2] == CAISSE_OK))
            break;

        // Si on arrive là, c'est qu'on peut déplacer le joueur !
        // On vérifie d'abord s'il y a une caisse à déplacer
        deplacerCaisse(&carte[pos->x][pos->y - 1], &carte[pos->x][pos->y - 2]);

        pos->y--; // On peut enfin faire monter le joueur (oufff !)
        break;

Je vous laisse le soin de faire du copier-coller pour les autres cas (attention, il faudra adapter le code, ce n'est pas exactement pareil à chaque fois !).

Et voilà, on vient de finir de coder le jeu !
Enfin presque : il nous reste à voir la fonction de chargement (et de sauvegarde) de niveaux.
On verra ensuite comment créer l'éditeur. Rassurez-vous, ça ira bien plus vite !

Chargement et enregistrement de niveaux

Le fichier fichiers.c contient deux fonctions :

  • chargerNiveau ;

  • sauvegarderNiveau.

Commençons par le chargement de niveau.

chargerNiveau

Cette fonction prend un paramètre : la carte. Là encore, il faut préciser la taille de la seconde dimension car il s'agit d'un tableau à deux dimensions.
La fonction renvoie un booléen : « vrai » si le chargement a réussi, « faux » si c'est un échec.

Le prototype est donc :

int chargerNiveau(int niveau[][NB_BLOCS_HAUTEUR]);

Voyons le début de la fonction :

FILE* fichier = NULL;
char ligneFichier[NB_BLOCS_LARGEUR * NB_BLOCS_HAUTEUR + 1] = {0};
int i = 0, j = 0;

fichier = fopen("niveaux.lvl", "r");
if (fichier == NULL)
    return 0;

On crée un tableau de char pour stocker temporairement le résultat du chargement du niveau.
On ouvre le fichier en lecture seule ("r"). On arrête la fonction en renvoyant 0 (« faux ») si l'ouverture a échoué. Classique.

Le fichier niveaux.lvl contient une ligne qui est une suite de nombres. Chaque nombre représente une case du niveau. Par exemple :

11111001111111111400000111110001100103310101101100000200121110 [...]

On va donc lire cette ligne avec un fgets :

fgets(ligneFichier, NB_BLOCS_LARGEUR * NB_BLOCS_HAUTEUR + 1, fichier);

On va analyser le contenu de ligneFichier. On sait que les 12 premiers caractères représentent la première ligne, les 12 suivants la seconde ligne, etc.

for (i = 0 ; i < NB_BLOCS_LARGEUR ; i++)
{
    for (j = 0 ; j < NB_BLOCS_HAUTEUR ; j++)
    {
        switch (ligneFichier[(i * NB_BLOCS_LARGEUR) + j])
        {
            case '0':
                niveau[j][i] = 0;
                break;
            case '1':
                niveau[j][i] = 1;
                break;
            case '2':
                niveau[j][i] = 2;
                break;
            case '3':
                niveau[j][i] = 3;
                break;
            case '4':
                niveau[j][i] = 4;
                break;
        }
    }
}

Par un simple petit calcul, on prend le caractère qui nous intéresse dans ligneFichier et on analyse sa valeur.

Ce sont des « lettres » qui sont stockées dans le fichier. Je veux dire par là que '0' est stocké comme le caractère ASCII '0', et sa valeur n'est pas 0 !
Pour analyser le fichier, il faut tester avec case '0' et non avec case 0 ! Attention à ne pas mélanger les chiffres et les lettres !

Le switch fait la conversion '0' => 0, '1' => 1, etc. Il place tout dans le tableau carte. La carte s'appelle niveau dans cette fonction d'ailleurs, mais ça ne change rien.

Une fois que c'est fait, on peut fermer le fichier et renvoyer 1 pour dire que tout s'est bien passé :

fclose(fichier);
return 1;

Finalement, le chargement du niveau dans le fichier n'était pas bien compliqué. Le seul piège à éviter c'était de bien penser à convertir la valeur ASCII '0' en un nombre 0 (et de même pour 1, 2, 3, 4…).

sauvegarderNiveau

Cette fonction est là encore simple :

int sauvegarderNiveau(int niveau[][NB_BLOCS_HAUTEUR])
{
    FILE* fichier = NULL;
    int i = 0, j = 0;

    fichier = fopen("niveaux.lvl", "w");
    if (fichier == NULL)
        return 0;

    for (i = 0 ; i < NB_BLOCS_LARGEUR ; i++)
    {
        for (j = 0 ; j < NB_BLOCS_HAUTEUR ; j++)
        {
            fprintf(fichier, "%d", niveau[j][i]);
        }
    }

    fclose(fichier);
    return 1;
}

J'utilise fprintf pour « traduire » les nombres du tableau niveau en caractères ASCII. C'était là encore la seule difficulté (mais à l'envers) : il ne faut pas écrire 0 mais '0'.

L'éditeur de niveaux

L'éditeur de niveaux est plus facile à créer qu'on ne pourrait l'imaginer.
En plus c'est une fonctionnalité qui va considérablement allonger la durée de vie de notre jeu, alors pourquoi s'en priver ?

Voilà comment l'éditeur va fonctionner.

  • On utilise la souris pour placer les blocs qu'on veut sur l'écran.

  • Un clic droit efface le bloc sur lequel se trouve la souris.

  • Un clic gauche place un objet. Cet objet est mémorisé : par défaut, on pose des murs avec le clic gauche. On peut changer l'objet en cours en appuyant sur les touches du pavé numérique :

    • 1 : mur,

    • 2 : caisse,

    • 3 : objectif,

    • 4 : départ du joueur Mario.

  • En appuyant sur S, le niveau sera sauvegardé.

  • On peut revenir au menu principal en appuyant sur Echap.

Édition d'un niveau avec l'éditeur

Initialisations

Globalement, la fonction ressemble à celle du jeu. J'ai d'ailleurs commencé à la créer en faisant un simple copier-coller de la fonction de jeu, puis en enlevant ce qui ne servait plus et en ajoutant de nouvelles fonctionnalités.

Le début y ressemble déjà pas mal :

void editeur(SDL_Surface* ecran)
{
    SDL_Surface *mur = NULL, *caisse = NULL, *objectif = NULL, *mario = NULL;
    SDL_Rect position;
    SDL_Event event;

    int continuer = 1, clicGaucheEnCours = 0, clicDroitEnCours = 0;
    int objetActuel = MUR, i = 0, j = 0;
    int carte[NB_BLOCS_LARGEUR][NB_BLOCS_HAUTEUR] = {0};

    // Chargement des objets et du niveau
    mur = IMG_Load("mur.jpg");
    caisse = IMG_Load("caisse.jpg");
    objectif = IMG_Load("objectif.png");
    mario = IMG_Load("mario_bas.gif");

    if (!chargerNiveau(carte))
        exit(EXIT_FAILURE);

Là, vous avez les définitions de variables et les initialisations.
Vous remarquerez que je ne charge qu'un Mario (celui dirigé vers le bas). En effet, on ne va pas diriger Mario au clavier là, on a juste besoin d'un sprite représentant la position de départ de Mario.

La variable objetActuel retient l'objet actuellement sélectionné par l'utilisateur. Par défaut, c'est un MUR. Le clic gauche créera donc un mur au départ, mais cela pourra être changé par l'utilisateur en appuyant sur 1, 2, 3 ou 4.

Les booléens clicGaucheEnCourset clicDroitEnCours, comme leurs noms l'indiquent, permettent de mémoriser si un clic est en cours (si le bouton de la souris est enfoncé). Je vous expliquerai le principe un peu plus loin. Cela nous permettra de poser des objets à l'écran en laissant le bouton de la souris enfoncé. Sinon on est obligé de cliquer frénétiquement avec la souris pour placer plusieurs fois le même objet à différents endroits, ce qui est un peu fatigant.

Enfin, la carte actuellement sauvegardée dans niveaux.lvl est chargée. Ce sera notre point de départ.

La gestion des événements

Cette fois, on va devoir gérer un nombre important d'événements différents. Allons-y, un par un.

SDL_QUIT
case SDL_QUIT:
    continuer = 0;
    break;

Si on clique sur la croix, la boucle s'arrête et on revient au menu principal.
Notez que ce n'est pas ce qu'il y a de plus ergonomique pour l'utilisateur : celui-ci s'attend plutôt à ce que le programme s'arrête quand on clique sur la croix, or ce n'est pas ce qu'il se passe ici car on ne fait que revenir au menu. Il faudrait peut-être trouver un moyen d'arrêter le programme en renvoyant une valeur spéciale à la fonction main par exemple. Je vous laisse réfléchir à une solution.

SDL_MOUSEBUTTONDOWN
case SDL_MOUSEBUTTONDOWN:
    if (event.button.button == SDL_BUTTON_LEFT)
    {
        // On met l'objet actuellement choisi (mur, caisse...) à l'endroit du clic
        carte[event.button.x / TAILLE_BLOC][event.button.y / TAILLE_BLOC] = objetActuel;
        clicGaucheEnCours = 1; // On retient qu'un bouton est enfoncé
    }
    else if (event.button.button == SDL_BUTTON_RIGHT) // Clic droit pour effacer
    {
        carte[event.button.x / TAILLE_BLOC][event.button.y /TAILLE_BLOC] = VIDE;
        clicDroitEnCours = 1;
    }
    break;

On commence par tester le bouton qui est enfoncé (on vérifie si c'est le clic gauche ou le clic droit) :

  • si c'est un clic gauche, on place l'objetActuel sur la carte à la position de la souris ;

  • si c'est un clic droit, on efface ce qu'il y a à cet endroit sur la carte (on met VIDE comme je vous avais dit).

Comment sait-on sur quelle « case » de la carte on se trouve ?

On le retrouve grâce à un petit calcul. Il suffit de prendre les coordonnées de la souris (event.button.x par exemple) et de diviser cette valeur par la taille d'un bloc (TAILLE_BLOC).
C'est une division de nombres entiers. Comme en C une division de nombres entiers donne un nombre entier, on est sûr d'avoir une valeur qui corresponde à une des cases de la carte.

Par exemple, si je suis au 75e pixel sur la carte (sur l'axe des abscisses x), je divise ce nombre par TAILLE_BLOC qui vaut ici 34.

75 / 34 = 2

N'oubliez pas que le reste est ignoré. On ne garde que la partie entière de la division en C car il s'agit d'une divison de nombres entiers.
On sait donc qu'on se trouve sur la case n° 2 (c'est-à-dire la troisième case, car un tableau commence à 0, souvenez-vous).

Autre exemple : si je suis au 10e pixel (c'est-à-dire très proche du bord), ça va donner le calcul suivant :

10 / 34 = 0

On est donc à la case n° 0 !

C'est comme ça qu'un simple petit calcul nous permet de savoir sur quelle case de la carte on se situe.

carte[event.button.x / TAILLE_BLOC][event.button.y / TAILLE_BLOC] = objetActuel;

Autre chose très importante : on met un booléen clicGaucheEnCours (ou clicDroit selon le cas) à 1. Cela nous permettra de savoir lors d'un événement MOUSEMOTION si un bouton de la souris est enfoncé pendant le déplacement.

SDL_MOUSEBUTTONUP
case SDL_MOUSEBUTTONUP: // On désactive le booléen qui disait qu'un bouton était enfoncé
    if (event.button.button == SDL_BUTTON_LEFT)
        clicGaucheEnCours = 0;
    else if (event.button.button == SDL_BUTTON_RIGHT)
        clicDroitEnCours = 0;
    break;

L'événement MOUSEBUTTONUP sert simplement à remettre le booléen à 0. On sait que le clic est terminé et donc qu'il n'y a plus de « clic en cours ».

SDL_MOUSEMOTION
case SDL_MOUSEMOTION:
    if (clicGaucheEnCours) // Si on déplace la souris et que le bouton gauche de la souris est enfoncé
    {
        carte[event.motion.x / TAILLE_BLOC][event.motion.y / TAILLE_BLOC] = objetActuel;
    }
    else if (clicDroitEnCours) // Pareil pour le bouton droit de la souris
    {
        carte[event.motion.x / TAILLE_BLOC][event.motion.y / TAILLE_BLOC] = VIDE;
    }
    break;

C'est là que nos booléens prennent toute leur importance. On vérifie quand on bouge la souris si un clic est en cours. Si tel est le cas, on place sur la carte un objet (ou du vide si c'est un clic droit).
Cela nous permet donc de placer d'affilée plusieurs objets du même type sans avoir à cliquer plusieurs fois. On a juste à déplacer la souris en maintenant son bouton enfoncé !

En clair : à chaque fois qu'on bouge la souris (ne serait-ce que d'un pixel), on vérifie si un des booléens est activé. Si tel est le cas, alors on pose un objet sur la carte. Sinon, on ne fait rien.

Résumé : je résume la technique, car vous vous en servirez certainement dans d'autres programmes.
Cette technique permet de savoir si un bouton de la souris est enfoncé lorsqu'on la déplace. On peut s'en servir pour coder un glisser-déplacer.

  1. Lors d'un MOUSEBUTTONDOWN: on met un booléen clicEnCours à 1.

  2. Lors d'un MOUSEMOTION: on teste si le booléen clicEnCours vaut « vrai ». S'il vaut « vrai », on sait qu'on est en train de faire une sorte de glisser-déplacer avec la souris.

  3. Lors d'un MOUSEBUTTONUP: on remet le booléen clicEnCours à 0, car le clic est terminé (relâchement du bouton de la souris).

SDL_KEYDOWN

Les touches du clavier permettent de charger et de sauvegarder le niveau ainsi que de changer l'objet actuellement sélectionné pour le clic gauche de la souris.

case SDL_KEYDOWN:
    switch(event.key.keysym.sym)
    {
        case SDLK_ESCAPE:
            continuer = 0;
            break;
        case SDLK_s:
            sauvegarderNiveau(carte);
            break;
        case SDLK_c:
            chargerNiveau(carte);
            break;
        case SDLK_KP1:
            objetActuel = MUR;
            break;
        case SDLK_KP2:
            objetActuel = CAISSE;
            break;
        case SDLK_KP3:
            objetActuel = OBJECTIF;
            break;
        case SDLK_KP4:
            objetActuel = MARIO;
            break;
    }
    break;

Ce code est très simple. On change l'objet actuel si on appuie sur une des touches numériques, on enregistre le niveau si on appuie sur S, ou on charge le dernier niveau enregistré si on appuie sur C.

Blit time !

Voilà : on a passé en revue tous les événements.
Maintenant, on n'a plus qu'à blitter chacun des éléments de la carte à l'aide d'une double boucle. C'est à peu de choses près le même code que celui de la fonction de jeu. Je vous le redonne, mais pas la peine de vous le réexpliquer ici.

// Effacement de l'écran
SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 255, 255, 255));

// Placement des objets à l'écran
for (i = 0 ; i < NB_BLOCS_LARGEUR ; i++)
{
    for (j = 0 ; j < NB_BLOCS_HAUTEUR ; j++)
    {
        position.x = i * TAILLE_BLOC;
        position.y = j * TAILLE_BLOC;

        switch(carte[i][j])
        {
            case MUR:
                SDL_BlitSurface(mur, NULL, ecran, &position);
                break;
            case CAISSE:
                SDL_BlitSurface(caisse, NULL, ecran, &position);
                break;
            case OBJECTIF:
                SDL_BlitSurface(objectif, NULL, ecran, &position);
                break;
            case MARIO:
                SDL_BlitSurface(mario, NULL, ecran, &position);
                break;
        }
    }
}

// Mise à jour de l'écran
SDL_Flip(ecran);

Il ne faut pas oublier après la boucle principale de faire les SDL_FreeSurface qui s'imposent :

SDL_FreeSurface(mur);
SDL_FreeSurface(caisse);
SDL_FreeSurface(objectif);
SDL_FreeSurface(mario);

Avec ça, le ménage est fait. :-)

Résumé et améliorations

Bien, on a fait le tour. L'heure est au résumé.

Alors résumons !

Et quel meilleur résumé pourrait-on imaginer que le code source complet du programme avec les commentaires ?

Pour éviter de vous proposer des dizaines de pages de code qui répètent tout ce qu'on vient de voir, je vous propose plutôt de télécharger le code source complet du programme ainsi que l'exécutable (compilé pour Windows).

Télécharger le programme + les sources (436 Ko)

Ce fichier .zip contient :

  • l'exécutable pour Windows (si vous êtes sous un autre OS, il suffira de recompiler) ;

  • les DLL de la SDL et de SDL_Image ;

  • toutes les images dont a besoin le programme (je vous les ai fait télécharger plus tôt dans le pack « sprites ») ;

  • les sources complètes du programme ;

  • le fichier .cbp de projet Code::Blocks. Si vous voulez ouvrir le projet sous un autre IDE, créez un nouveau projet de type SDL (configurez-le correctement pour la SDL) et ajoutez-y manuellement tous les fichiers .c et .h. Ce n'est pas bien compliqué, vous verrez.

Vous noterez que le projet contient, en plus des .c et des .h, un fichier ressources.rc.
C'est un fichier qui peut être ajouté au projet (uniquement sous Windows) et qui permet d'intégrer des fichiers dans l'exécutable. Ici, je me sers du fichier de ressources pour intégrer une icône dans l'exécutable. Cela aura pour effet de donner une icône à l'exécutable, visible dans l'explorateur Windows (fig. suivante).

Intégration d'une icône à l'exécutable

Avouez que c'est quand même plus sympa que d'avoir l'icône par défaut de Windows pour les exécutables !
Vous trouverez plus d'informations sur cette technique sur le cours Créer une icône pour son programme.

Améliorez !

Ce programme n'est pas parfait, loin de là !
Vous voulez des idées pour l'améliorer ?

  • Il manque un mode d'emploi. Affichez un écran d'explications juste avant le lancement d'une partie et avant le lancement de l'éditeur. Indiquez en particulier les touches à utiliser.

  • Dans l'éditeur de niveaux, on ne sait pas quel est l'objet actuellement sélectionné. Ce qui serait bien, c'est que l'objet actuellement sélectionné suive le curseur de la souris. Comme ça, l'utilisateur verrait ce qu'il s'apprête à mettre sur la carte. C'est facile à faire : on a déjà fait un Zozor qui suit le curseur de la souris dans le chapitre précédent !

  • Dans l'éditeur de niveaux, on apprécierait de pouvoir choisir une CAISSE_OK (une caisse bien placée sur un objectif dès le départ). En effet, je me suis rendu compte par la suite qu'il y a de nombreux niveaux qui commencent avec des caisses bien placées dès le départ (ça ne veut pas dire que le niveau est plus facile, loin de là).

  • Dans l'éditeur toujours, il faudrait empêcher que l'on puisse placer plus d'un départ de joueur sur une même carte !

  • Lorsqu'on réussit un niveau, on retourne immédiatement au menu. C'est un peu brut. Que diriez-vous d'afficher un message « Bravo » au centre de l'écran quand on gagne ?

  • Enfin, il serait bien que le programme puisse gérer plus d'un niveau à la fois. Il faudrait que l'on puisse créer une véritable petite aventure d'une vingtaine de niveaux par exemple. C'est un petit peu plus compliqué à coder mais faisable. Il faudra adapter le jeu et l'éditeur de niveaux en conséquence. Je vous suggère de mettre un niveau par ligne dans niveaux.lvl.

Comme promis, pour vous prouver que c'est faisable… je l'ai fait !
Je ne vous donne pas le code source, en revanche (je crois que je vous en ai déjà assez donné jusqu'ici !), mais je vous donne le programme complet compilé pour Windows et Linux.

Le programme comporte une aventure de 20 niveaux (de très très facile à… super difficile).
Pour réaliser certains de ces niveaux, je me suis basé sur le site d'un passionné de Sokoban (sokoban.online.fr).

Voici le Mario Sokoban amélioré pour Windows et Linux :

Téléchargez l'installation du Mario Sokoban amélioré (665 Ko)
Ou bien : Téléchargez la version compilée pour linux au format .tar.gz (64 Ko)

Le programme d'installation a été créé à l'aide d'Inno Setup. Pour plus d'informations, voir le cours Créer une installation .


Apprenez à programmer en C

Maîtrisez le temps !

Ce chapitre est d'une importance capitale : il va vous apprendre à contrôler le temps en SDL. Il est rare que l'on crée un programme SDL sans faire appel aux fonctions de gestion du temps, bien que le TP Mario Sokoban constitue un contre-exemple. Il n'en reste pas moins que pour la majorité des jeux, la gestion du temps est fondamentale.

Par exemple, comment vous y prendriez-vous pour réaliser un Tetris ou un Snake (jeu du serpent) ? Il faut bien que les blocs bougent toutes les X secondes, ce que vous ne savez pas faire. Du moins, pas avant d'avoir lu ce chapitre. ;-)

Le Delay et les Ticks

Dans un premier temps, nous allons apprendre à utiliser deux fonctions très simples :

  • SDL_Delay : permet de mettre en pause le programme un certain nombre de millisecondes ;

  • SDL_GetTicks : retourne le nombre de millisecondes écoulées depuis le lancement du programme.

Ces deux fonctions sont très simples comme nous allons le voir, mais bien les utiliser n'est pas si évident que ça en a l'air…

SDL_Delay

Cette fonction effectue une pause sur le programme durant un certain temps. Pendant que le programme est en pause, on dit qu'il dort (« sleep » en anglais) : il n'utilise pas le processeur.

SDL_Delay peut donc être utilisée pour réduire l'utilisation du processeur (notez que j'abrègerai CPU), désormais. C'est une abréviation courante qui signifie Central Processing Unit, soit « Unité centrale de calcul ».}.
Grâce à SDL_Delay, vous pourrez rendre votre programme moins gourmand en ressources processeur. Il fera donc moins « ramer » votre PC si SDL_Delay est utilisée intelligemment.

Tout dépend du programme que vous créez : parfois, on aimerait bien que notre programme utilise le moins de CPU possible pour que l'utilisateur puisse faire autre chose en même temps, comme c'est le cas pour un lecteur MP3 qui tourne en fond pendant que vous naviguez sur Internet.
Mais… d'autres fois, on se moque complètement que notre programme utilise tout le temps 100 % de CPU. C'est le cas de la quasi-totalité des jeux.

Revenons à la fonction qui nous intéresse. Son prototype est d'une simplicité désolante :

void SDL_Delay(Uint32 ms);

En clair, vous envoyez à la fonction le nombre de millisecondes pendant lesquelles votre programme doit « dormir ». C'est une simple mise en pause.

Par exemple, si vous voulez que votre programme se mette en pause 1 seconde, vous devrez écrire :

SDL_Delay(1000);

N'oubliez pas que ce sont des millisecondes :

  • 1000 millisecondes = 1 seconde ;

  • 500 millisecondes = 1/2 seconde ;

  • 250 millisecondes = 1/4 seconde.

Vous ne pouvez rien faire dans votre programme pendant qu'il est en pause ! Un programme qui « dort » ne peut rien faire puisqu'il n'est pas actif pour l'ordinateur.

Le problème de la granularité du temps

Non, rassurez-vous, je ne vais pas vous faire un traité de physique quantique au beau milieu d'un chapitre SDL ! Toutefois, j'estime qu'il y a quelque chose que vous devez savoir : SDL_Delay n'est pas une fonction « parfaite ». Et ce n'est pas de sa faute, c'est la faute de votre OS (Windows, Linux, Mac OS X…).

Pourquoi l'OS intervient-il là-dedans ? Tout simplement parce que c'est lui qui contrôle les programmes qui tournent ! Votre programme va donc dire à l'OS : « Je dors, réveille-moi dans 1 seconde ». Mais l'OS ne va pas forcément le réveiller exactement au bout d'une seconde.

En effet, il aura peut-être un peu de retard (un retard de 10 ms en moyenne environ, ça dépend des PC). Pourquoi ? Parce que votre CPU ne peut travailler que sur un programme à la fois. Le rôle de l'OS est de dire au CPU ce sur quoi il doit travailler : « Alors, pendant 40 ms tu vas travailler sur firefox.exe, puis pendant 110 ms sur explorer.exe ; ensuite, pendant 80 ms tu vas travailler sur programme_sdl.exe, puis retravailler sur firefox.exe pendant 65 ms », etc. L'OS est le véritable chef d'orchestre de l'ordinateur !

Maintenant, imaginez qu'au bout d'une seconde un autre programme soit encore en train de travailler : il faudra qu'il ait fini pour que votre programme puisse « reprendre la main » comme on dit, c'est-à-dire être traité à nouveau par le CPU.

Qu'est-ce qu'il faut retenir ? Que votre CPU ne peut pas gérer plus d'un programme à la fois. Pour donner l'impression que l'on peut faire tourner plusieurs programmes en même temps sur un ordinateur, l'OS « découpe » le temps et autorise les programmes à travailler tour à tour.
C'est toutefois de moins en moins vrai. Les CPU double cœur ont en effet la capacité de travailler sur deux programmes à la fois, maintenant.

Or, cette gestion des programmes est très complexe et on ne peut donc pas avoir la garantie que notre programme sera réveillé au bout d'une seconde exactement.

Toutefois, cela dépend des PC comme je vous l'ai dit plus haut. Chez moi, j'ai pu constater que la fonction SDL_Delay était assez précise.

À cause de ce problème de granularité du temps, vous ne
pouvez donc pas mettre en pause votre programme pendant un temps trop court. Par exemple, si vous faites SDL_Delay(1);, vous pouvez être certains que votre programme ne sera pas mis en pause 1 ms mais un peu plus (peut-être 9-10 ms).

SDL_Delay est donc bien pratique, mais ne lui faites pas trop confiance. Elle ne mettra pas en pause votre programme pendant le temps exact que vous indiquez.
Ce n'est pas parce que la fonction est mal codée, c'est parce que le fonctionnement d'un ordinateur est très complexe et ne permet pas d'être très précis à ce niveau.

SDL_GetTicks

Cette fonction renvoie le nombre de millisecondes écoulées depuis le lancement du programme. C'est un indicateur de temps indispensable. Cela vous permet de vous repérer dans le temps, vous allez voir !

Voici le prototype :

Uint32 SDL_GetTicks(void);

La fonction n'attend aucun paramètre, elle renvoie juste le nombre de millisecondes écoulées.
Ce nombre augmente au fur et à mesure que le temps passe, inlassablement. Pour info, la doc' de la SDL indique que le nombre atteint son maximum et est réinitialisé au bout de 49 jours ! A priori votre programme SDL devrait tourner moins longtemps que ça, donc pas de souci de ce côté-là.

Utiliser SDL_GetTicks pour gérer le temps

Si SDL_Delay est assez facile à comprendre et à utiliser, ce n'est pas le cas de la fonction SDL_GetTicks. Il est temps d'apprendre à bien s'en servir…
Voici un exemple ! Nous allons reprendre notre bon vieux programme avec la fenêtre affichant Zozor à l'écran (fig. suivante).

Zozor au centre de l'écran

Cette fois, au lieu de le diriger au clavier ou à la souris, nous allons faire en sorte qu'il bouge tout seul sur l'écran !
Pour faire simple, on va le faire bouger horizontalement sur la fenêtre.

On reprend pour commencer exactement le même code source que celui qu'on avait utilisé dans le chapitre sur les événements. Vous devriez pouvoir créer un programme aussi simple sans avoir besoin de mon aide, ici. Si néanmoins vous en avez besoin, vous pouvez récupérer le code source de base sur Internet.

int main(int argc, char *argv[])
{
    SDL_Surface *ecran = NULL, *zozor = NULL;
    SDL_Rect positionZozor;
    SDL_Event event;
    int continuer = 1;

    SDL_Init(SDL_INIT_VIDEO);

    ecran = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE | SDL_DOUBLEBUF);
    SDL_WM_SetCaption("Gestion du temps en SDL", NULL);

    zozor = SDL_LoadBMP("zozor.bmp");
    SDL_SetColorKey(zozor, SDL_SRCCOLORKEY, SDL_MapRGB(zozor->format, 0, 0, 255));

    positionZozor.x = ecran->w / 2 - zozor->w / 2;
    positionZozor.y = ecran->h / 2 - zozor->h / 2;

    SDL_EnableKeyRepeat(10, 10);

    while (continuer)
    {
        SDL_WaitEvent(&event);
        switch(event.type)
        {
            case SDL_QUIT:
                continuer = 0;
                break;
        }

        SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 255, 255, 255));
        SDL_BlitSurface(zozor, NULL, ecran, &positionZozor);
        SDL_Flip(ecran);
    }

    SDL_FreeSurface(zozor);
    SDL_Quit();

    return EXIT_SUCCESS;
}

Intéressons-nous à Zozor. Nous voulons le faire bouger. Pour cela, le mieux est d'utiliser SDL_GetTicks. On va avoir besoin de deux variables : tempsPrecedent et tempsActuel. Elles vont stocker le temps retourné par SDL_GetTicks à des moments différents.
Il nous suffira de faire la différence entre tempsActuel et tempsPrecedent pour voir le temps qui s'est écoulé. Si le temps écoulé est par exemple supérieur à 30 ms, alors on change les coordonnées de Zozor.

Commencez donc par créer ces deux variables dont on va avoir besoin :

int tempsPrecedent = 0, tempsActuel = 0;

Maintenant, dans notre boucle infinie, nous allons ajouter le code suivant :

tempsActuel = SDL_GetTicks();
if (tempsActuel - tempsPrecedent > 30) /* Si 30 ms se sont écoulées */
{
    positionZozor.x++; /* On bouge Zozor */
    tempsPrecedent = tempsActuel; /* Le temps "actuel" devient le temps "precedent" pour nos futurs calculs */
}

Comprenez bien ce qui se passe.

  1. On prend le temps actuel grâce à SDL_GetTicks.

  2. On compare au temps précédemment enregistré. S'il y a un écart de 30 ms au moins, alors…

  3. … on bouge Zozor, car on veut qu'il se déplace toutes les 30 ms. Ici, on le décale juste vers la droite toutes les 30 ms.
    Il faut vérifier si le temps est supérieur à 30 ms, et non égal à 30 ms ! En effet, il faut vérifier si au moins 30 ms se sont écoulées. Rien ne vous garantit que l'instruction sera exécutée pile poil toutes les 30 ms.

  4. Puis, et c'est vraiment ce qu'il ne faut pas oublier, on place le temps « actuel » dans le temps « précédent ». En effet, imaginez le prochain tour de boucle : le temps « actuel » aura changé, et on pourra le comparer au temps précédent. À nouveau, on pourra vérifier si 30 ms se seront écoulées et bouger Zozor.

Et que se passe-t-il si la boucle met moins de temps que 30 ms ?

Lisez mon code : il ne se passe rien !
On ne rentre pas dans le if, on ne fait donc rien. On attend le prochain tour de boucle où on vérifiera à nouveau si 30 ms se seront écoulées depuis la dernière fois qu'on a fait bouger Zozor.

Ce code est court, mais il faut le comprendre ! Relisez mes explications autant de fois que nécessaire, parce que c'était probablement le passage le plus important du chapitre.

Un changement dans la gestion des événements

Notre code est parfait à un détail près : la fonction SDL_WaitEvent.
Elle était très pratique jusqu'ici, puisqu'on n'avait pas à gérer le temps. Cette fonction mettait en pause le programme (un peu à la manière de SDL_Delay) tant qu'il n'y avait pas d'événement.

Or ici, on n'a pas besoin d'attendre un événement pour faire bouger Zozor ! Il doit bouger tout seul.
Vous n'allez quand même pas bouger la souris juste pour générer des événements et donc faire sortir le programme de la fonction SDL_WaitEvent !

La solution ? SDL_PollEvent.
Je vous avais déjà présenté cette fonction : contrairement à SDL_WaitEvent, elle renvoie une valeur, qu'il y ait eu un événement ou non. On dit que la fonction n'est pas bloquante : elle ne met pas en pause le programme, la boucle infinie va donc tourner tout le temps.

Code complet

Voici le code final que vous pouvez tester :

int main(int argc, char *argv[])
{
    SDL_Surface *ecran = NULL, *zozor = NULL;
    SDL_Rect positionZozor;
    SDL_Event event;
    int continuer = 1;
    int tempsPrecedent = 0, tempsActuel = 0;

    SDL_Init(SDL_INIT_VIDEO);

    ecran = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE | SDL_DOUBLEBUF);
    SDL_WM_SetCaption("Gestion du temps en SDL", NULL);

    zozor = SDL_LoadBMP("zozor.bmp");
    SDL_SetColorKey(zozor, SDL_SRCCOLORKEY, SDL_MapRGB(zozor->format, 0, 0, 255));

    positionZozor.x = ecran->w / 2 - zozor->w / 2;
    positionZozor.y = ecran->h / 2 - zozor->h / 2;

    SDL_EnableKeyRepeat(10, 10);

    while (continuer)
    {
        SDL_PollEvent(&event); /* On utilise PollEvent et non WaitEvent pour ne pas bloquer le programme */
        switch(event.type)
        {
            case SDL_QUIT:
                continuer = 0;
                break;
        }

        tempsActuel = SDL_GetTicks();
        if (tempsActuel - tempsPrecedent > 30) /* Si 30 ms se sont écoulées depuis le dernier tour de boucle */
        {
            positionZozor.x++; /* On bouge Zozor */
            tempsPrecedent = tempsActuel; /* Le temps "actuel" devient le temps "precedent" pour nos futurs calculs */
        }

        SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 255, 255, 255));
        SDL_BlitSurface(zozor, NULL, ecran, &positionZozor);
        SDL_Flip(ecran);
    }

    SDL_FreeSurface(zozor);
    SDL_Quit();

    return EXIT_SUCCESS;
}

Vous devriez voir Zozor bouger tout seul sur l'écran. Il se décale vers la droite.
Essayez par exemple de changer le temps de 30 ms en 15 ms : Zozor devrait se déplacer deux fois plus vite ! En effet, il se déplacera une fois toutes les 15 ms au lieu d'une fois toutes les 30 ms auparavant.

Consommer moins de CPU

Actuellement, notre programme tourne en boucle indéfiniment à la vitesse de la lumière (enfin, presque). Il consomme donc 100 % du CPU.
Pour voir cela, il vous suffit par exemple de faire CTRL + ALT + SUPPR (onglet Processus) sous Windows (fig. suivante).

Consommation du CPU par le programme

Comme vous pouvez le voir, notre CPU est utilisé à 100 % par notre programme testsdl.exe.
Je vous l'ai dit plus tôt : si vous codez un jeu (surtout un jeu plein écran), ce n'est pas grave si vous utilisez 100 % du CPU. Mais si c'est un jeu dans une fenêtre par exemple, il vaut mieux qu'il utilise le moins de CPU possible pour que l'utilisateur puisse faire autre chose sans que son PC ne « rame ».

La solution ? On va reprendre exactement le même code que ci-dessus, mais on va lui ajouter en plus un SDL_Delay pour patienter le temps qu'il faut afin que ça fasse 30 ms.

On va juste ajouter un SDL_Delay dans un else :

tempsActuel = SDL_GetTicks();
if (tempsActuel - tempsPrecedent > 30)
{
    positionZozor.x++;
    tempsPrecedent = tempsActuel;
}
else /* Si ça fait moins de 30 ms depuis le dernier tour de boucle, on endort le programme le temps qu'il faut */
{
    SDL_Delay(30 - (tempsActuel - tempsPrecedent));
}

Comment cela fonctionne-t-il, cette fois ? C'est simple, il y a deux possibilités (d'après le if) :

  • soit ça fait plus de 30 ms qu'on n'a pas bougé Zozor, dans ce cas on le bouge ;

  • soit ça fait moins de 30 ms, dans ce cas on fait dormir le programme avec SDL_Delay le temps qu'il faut pour atteindre les 30 ms environ. D'où mon petit calcul 30 - (tempsActuel - tempsPrecedent). Si la différence entre le temps actuel et le temps précédent est par exemple de 20 ms, alors on endormira le programme (30 - 20) = 10 ms afin d'atteindre les 30 ms.

Rappelez-vous que SDL_Delay mettra peut-être quelques millisecondes de plus que prévu…

Avec ce code, notre programme va « dormir » la plupart du temps et donc consommer très peu de CPU (fig. suivante).

Consommation de CPU réduite

En moyenne, le programme utilise maintenant entre 0 et 1 % de CPU… Parfois il utilise légèrement plus, mais il retombe rapidement à 0 % de CPU.

Contrôler le nombre d'images par seconde

Vous vous demandez certainement comment on peut limiter (fixer) le nombre d'images par seconde (couramment abrégé FPS pour « Frames per second ») affichées par l'ordinateur.

Eh bien c'est exactement ce qu'on est en train de faire ! Ici, on affiche une nouvelle image toutes les 30 ms en moyenne.
Sachant qu'une seconde vaut 1000 ms, pour trouver le nombre de FPS (images par seconde), il suffit de faire une division : 1000 / 30 = 33 images par seconde environ.

Pour l'œil humain, une animation est fluide si elle contient au moins 25 images / seconde. Avec 33 images / seconde, notre animation sera donc tout à fait fluide, elle n'apparaîtra pas « saccadée ».

Si vous voulez plus d'images par seconde, il faut réduire la limite de temps entre deux images. Passez de 30 à 20 ms, et ça vous fera du 1000 / 20 = 50 FPS.

Exercices

La manipulation du temps n'est pas évidente, il serait bien de vous entraîner un peu, qu'en dites-vous ? Voici quelques exercices.

    • Pour le moment, Zozor se décale vers la droite puis disparaît de l'écran. Ce serait mieux s'il repartait dans l'autre sens une fois arrivé tout à droite, non ? Cela donnerait l'impression qu'il rebondit.

Je vous conseille de créer un booléen versLaDroite qui vaut « vrai » si Zozor se déplace vers la droite (et « faux » s'il va vers la gauche). Si le booléen vaut vrai, vous décalez donc Zozor vers la droite, sinon vous le décalez vers la gauche. Surtout, n'oubliez pas de changer la valeur du booléen lorsque Zozor atteint le bord droit ou le bord gauche. Eh oui, il faut bien qu'il reparte dans l'autre sens !

  • Plutôt que de faire rebondir Zozor de droite à gauche, faites le rebondir en diagonale sur l'écran ! Il vous suffira de modifier positionZozor.x et positionZozor.y simultanément. Vous pouvez essayer de voir ce que ça fait si on augmente x et si on diminue y en même temps, ou bien si on augmente les deux en même temps, etc.

  • Faites en sorte qu'un appui sur la touche P empêche Zozor de se déplacer, et qu'un nouvel appui sur la touche P relance le déplacement de Zozor. C'est un bête booléen à activer et désactiver.

Les timers

L'utilisation des timers est un peu complexe. Elle fait intervenir une notion qu'on n'a pas vue jusqu'ici : les pointeurs de fonctions. Il n'est pas indispensable d'utiliser les timers : si vous les trouvez trop délicats à utiliser, vous pouvez passer votre chemin sans problème.

Les timers constituent une autre façon de réaliser ce qu'on vient de faire avec la fonction SDL_GetTicks.
C'est une technique un peu particulière. Certains la trouveront pratique, d'autres non. Cela dépend donc des goûts du programmeur.

Qu'est-ce qu'un timer ?
C'est un système qui permet de demander à la SDL d'appeler une fonction toutes les X millisecondes. Vous pourriez ainsi créer une fonction bougerEnnemi() que la SDL appellerait automatiquement toutes les 50 ms afin que l'ennemi se déplace à intervalles réguliers.

Comme je viens de vous le dire, cela est aussi faisable avec SDL_GetTicks en utilisant la technique qu'on a vue plus haut.
Quel avantage, alors ? Eh bien disons que les timers nous obligent à mieux structurer notre programme en fonctions.

Initialiser le système de timers

Pour pouvoir utiliser les timers, vous devez d'abord initialiser la SDL avec un flag spécial : SDL_INIT_TIMER.
Vous devriez donc appeler votre fonction SDL_Init comme ceci :

SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER);

La SDL est maintenant prête à utiliser les timers !

Ajouter un timer

Pour ajouter un timer, on fait appel à la fonction SDL_AddTimer dont voici le prototype.

SDL_TimerID SDL_AddTimer(Uint32 interval, SDL_NewTimerCallback callback, void *param);

Il existe en fait deux fonctions permettant d'ajouter un timer en SDL : SDL_AddTimer et SDL_SetTimer.
Elles sont quasiment identiques. Cependant, SDL_SetTimer est une fonction ancienne qui existe toujours pour des raisons de compatibilité. Aujourd'hui, si on veut bien faire les choses, on nous recommande donc d'utiliser SDL_AddTimer.

On envoie trois paramètres à la fonction :

  • l'intervalle de temps (en ms) entre chaque appel de la fonction ;

  • le nom de la fonction à appeler. On appelle cela un callback : le programme se charge de rappeler cette fonction de callback régulièrement ;

  • les paramètres à envoyer à votre fonction de callback.

Comment ? Un nom de fonction peut servir de paramètre ? Je croyais qu'on ne pouvait envoyer que des variables !

En fait, les fonctions sont aussi stockées en mémoire au chargement du programme. Elles ont donc elles aussi une adresse. Du coup, on peut créer des… pointeurs de fonctions ! Il suffit d'écrire le nom de la fonction à appeler pour indiquer l'adresse de la fonction. Ainsi, la SDL saura à quelle adresse en mémoire elle doit se rendre pour appeler votre fonction de callback.
Si vous souhaitez en savoir plus sur les pointeurs de fonctions, je vous invite à lire le tutoriel rédigé par mleg qui traite de ce sujet.

SDL_AddTimer renvoie un numéro de timer (un « ID »). Vous devez stocker ce résultat dans une variable de type SDL_TimerID. Cela vous permettra par la suite de désactiver le timer : il vous suffira d'indiquer l'ID du timer à arrêter.

La SDL vous permet d'activer plusieurs timers en même temps. Cela explique l'intérêt de stocker un ID de timer pour pouvoir les différencier.

On va donc créer un ID de timer :

SDL_TimerID timer; /* Variable pour stocker le numéro du timer */

… puis on va créer notre timer :

timer = SDL_AddTimer(30, bougerZozor, &positionZozor); /* Démarrage du timer */

Ici, je crée un timer qui a les propriétés suivantes :

  • il sera appelé toutes les 30 ms ;

  • il appellera la fonction de callback bougerZozor ;

  • il lui enverra comme paramètre un pointeur sur la position de Zozor pour qu'il puisse la modifier.

Vous l'aurez compris : le rôle de la fonction bougerZozor sera de changer la position de Zozor toutes les 30 ms.

Création de la fonction de callback

Attention : il faut être particulièrement vigilant ici. Votre fonction de callback doit obligatoirement avoir le prototype suivant :

Uint32 nomDeLaFonction(Uint32 intervalle, void *parametre);

Pour créer le callback bougerZozor, je devrai donc écrire la fonction comme ceci :

Uint32 bougerZozor(Uint32 intervalle, void *parametre);

Voici maintenant le contenu de ma fonction bougerZozor, qui est plus délicate qu'il n'y paraît :

/* Fonction de callback (sera appelée toutes les 30 ms) */
Uint32 bougerZozor(Uint32 intervalle, void *parametre)
{
    SDL_Rect* positionZozor = parametre; /* Conversion de void* en SDL_Rect* */
    positionZozor->x++;

    return intervalle;
}

La fonction bougerZozor sera donc automatiquement appelée toutes les 30 ms par la SDL.
La SDL lui enverra toujours deux paramètres (ni plus, ni moins) :

  • l'intervalle de temps qui sépare deux appels de la fonction (ici, ça sera 30 ms) ;

  • le paramètre « personnalisé » que vous avez demandé à envoyer à la fonction. Remarquez, et c'est très important, que ce paramètre est un pointeur sur void. Cela signifie que c'est un pointeur qui peut pointer sur n'importe quoi : un int, une structure personnalisée, ou comme ici un SDL_Rect (positionZozor).
    Notez qu'il n'est pas possible d'envoyer plus d'un paramètre personnalisé à la fonction de callback. Heureusement, vous pouvez toujours créer un type personnalisé (ou un tableau) qui sera un assemblage des variables que vous voulez transmettre.

Le problème, c'est que ce paramètre est un pointeur de type inconnu (void) pour la fonction. Il va donc falloir dire à l'ordinateur que ce paramètre est un SDL_Rect* (un pointeur sur SDL_Rect).

Pour faire ça, je crée un pointeur sur SDL_Rect dans ma fonction qui prend comme valeur… le pointeur parametre.

Quel intérêt d'avoir créé un DEUXIÈME pointeur qui contient la même adresse ?

L'intérêt, c'est que positionZozor est de type SDL_Rect* contrairement à la variable parametre qui était de type void*.

Vous pourrez donc accéder à positionZozor->x et positionZozor->y.
Si vous aviez fait parametre->x ou parametre->y, le compilateur aurait tout rejeté en bloc parce que le type void ne contient pas de sous-variable x et y.

Après, la ligne suivante est simple : on modifie la valeur de positionZozor->x pour décaler Zozor vers la droite.

Dernière chose, très importante : vous devez retourner la variable intervalle. Cela indiquera à la SDL qu'on veut continuer à faire en sorte que la fonction soit appelée toutes les 30 ms.
Si vous voulez changer l'intervalle d'appel, il suffit de renvoyer une autre valeur (mais bien souvent, on ne change pas cet intervalle).

Arrêter le timer

Pour arrêter le timer, c'est très simple :

SDL_RemoveTimer(timer); /* Arrêt du timer */

Il suffit d'appeler SDL_RemoveTimer en indiquant l'ID du timer à arrêter.
Ici, j'arrête le timer juste après la boucle infinie, au même endroit que les SDL_FreeSurface.

En résumé

  • La fonction SDL_Delay permet de mettre en pause le programme un certain nombre de millisecondes. Cela permet de réduire l'utilisation du CPU qui n'est alors plus monopolisé par votre programme.

  • On peut connaître le nombre de millisecondes écoulées depuis le lancement du programme avec SDL_GetTicks. Avec quelques petits calculs, on peut s'en servir pour effectuer une gestion des événements non bloquante avec SDL_PollEvent.

  • Les timers constituent un système qui permet de rappeler une de vos fonctions (dite de callback) à intervalles réguliers. Le même résultat peut être obtenu avec SDL_GetTicks mais les timers aident à rendre le programme plus lisible et mieux structuré.


Apprenez à programmer en C

Écrire du texte avec SDL_ttf

Je suis persuadé que la plupart d'entre vous se sont déjà posé cette question : « Mais bon sang, il n'y a donc aucune fonction pour écrire du texte dans une fenêtre SDL ? ». Il est temps de vous apporter la réponse : c'est non.

Cependant, il y a quand même moyen d'y arriver. Il suffit d'utiliser… la ruse ! On peut par exemple blitter des images de lettres une à une à l'écran. Ça fonctionne, mais ce n'est pas ce qu'il y a de plus pratique.

Heureusement, il y a plus simple : on peut utiliser la bibliothèque SDL_ttf. C'est une bibliothèque qui vient s'ajouter par-dessus la SDL, tout comme SDL_image. Son rôle est de créer une SDL_Surface à partir du texte que vous lui envoyez.

Installer SDL_ttf

Il faut savoir que, comme SDL_image, SDL_ttf est une bibliothèque qui nécessite que la SDL soit installée. Bon : si à ce stade du cours vous n'avez toujours pas installé la SDL, c'est grave, donc je vais supposer que c'est déjà fait !

Tout comme SDL_image, SDL_ttf est une des bibliothèques liées à la SDL les plus populaires (c'est-à-dire qu'elle est très téléchargée). Comme vous allez pouvoir le constater, cette bibliothèque est effectivement bien faite. Une fois que vous aurez appris à l'utiliser, vous ne pourrez plus vous en passer !

Comment fonctionne SDL_ttf ?

SDL_ttf n'utilise pas des images bitmap pour générer du texte dans des surfaces. C'est une méthode en effet assez lourde à mettre en place et on n'aurait pu utiliser qu'une seule police.
En fait, SDL_ttf fait appel à une autre bibliothèque : FreeType. C'est une bibliothèque capable de lire les fichiers de police (.ttf) et d'en sortir l'image. SDL_ttf récupère donc cette image et la convertit pour la SDL en créant une SDL_Surface.

SDL_ttf a donc besoin de la bibliothèque FreeType pour fonctionner, sinon elle ne sera pas capable de lire les fichiers .ttf.

Si vous êtes sous Windows et que vous prenez, comme je le fais, la version compilée de la bibliothèque, vous n'aurez pas besoin de télécharger quoi que ce soit de plus car FreeType sera incluse dans la DLL SDL_ttf.dll. Vous n'avez donc rien à faire.

Si vous êtes sous Linux ou Mac OS et que vous devez recompiler la bibliothèque, il vous faudra en revanche FreeType pour compiler. Rendez-vous sur la page de téléchargement de FreeType pour récupérer les fichiers pour développeurs.

Installer SDL_ttf

Rendez-vous sur la page de téléchargement de SDL_ttf.

Là, choisissez le fichier qu'il vous faut dans la section Binary

Sous Windows, vous remarquerez qu'il n'y a que deux fichiers .zip ayant le suffixe win32 et VC6.
Le premier (win32) contient la DLL que vous aurez besoin de livrer avec votre exécutable. Vous aurez aussi besoin de mettre cette DLL dans le dossier de votre projet pour pouvoir tester votre programme, évidemment.

Le second (VC6) contient les .h et .lib dont vous allez avoir besoin pour programmer. On pourrait penser d'après le nom que ça n'est fait que pour Visual C++, mais en fait, exceptionnellement, le fichier .lib livré ici marche aussi avec mingw32, il fonctionnera donc sous Code::Blocks.

Le fichier ZIP contient comme d'habitude un dossier include et un dossier lib. Placez le contenu du dossier include dans mingw32/include/SDL et le contenu du dossier lib dans mingw32/lib.

Vous devez copier le fichier SDL_ttf.h dans le dossier mingw32/include/SDL et non pas dans mingw32/include tout court. Attention aux erreurs !

Configurer un projet pour SDL_ttf

Il nous reste une dernière petite chose à faire : configurer notre projet pour qu'il utilise bien SDL_ttf. Il va falloir modifier les options du linker pour qu'il compile bien votre programme en utilisant la bibliothèque SDL_ttf.

Vous avez déjà appris à faire cette opération pour la SDL et pour SDL_image, je vais donc aller plus vite.
Comme je travaille sous Code::Blocks, je vais vous donner la procédure avec cet IDE. Ce n'est pas bien différent avec les autres IDE :

  • rendez-vous dans le menu Project / Build Options ;

  • dans l'onglet Linker, cliquez sur le petit bouton Add ;

  • indiquez où se trouve le fichier SDL_ttf.lib (chez moi, c'est dans C:\Program Files\CodeBlocks\mingw32\lib) ;

  • on vous demande Keep this as a relative path? Peu importe ce que vous répondez, ça marchera dans les deux cas. Je vous conseille quand même de répondre par la négative, car sinon votre projet ne fonctionnera plus si vous le déplacez dans un autre dossier ;

  • validez en cliquant sur OK.

On n'a pas besoin de linker avec la bibliothèque FreeType aussi ?

Non, car comme je vous l'ai dit FreeType est incluse dans la DLL de SDL_ttf. Vous n'avez pas à vous préoccuper de FreeType, c'est SDL_ttf qui gère ça, maintenant.

La documentation

Maintenant que vous commencez à devenir des programmeurs aguerris, vous devriez vous demander immédiatement : « Mais où est la doc' ? ». Si vous ne vous êtes pas encore posé cette question, c'est que vous n'êtes pas encore des programmeurs aguerris. ;)

Il y a certes des cours qui détaillent le fonctionnement des bibliothèques, comme ce livre. Toutefois…

  • Je ne vais pas faire un chapitre pour toutes les bibliothèques qui existent (même en y passant ma vie, je n'aurais pas le temps). Il va donc falloir tôt ou tard lire une doc', et mieux vaut commencer à apprendre à le faire maintenant !

  • D'autre part, une bibliothèque est en général assez complexe et contient beaucoup de fonctions. Je ne peux pas présenter toutes ces fonctions dans un chapitre, ce serait bien trop long !

En clair : les documentations ont l'avantage d'être complètes et on ne peut parfois pas y couper. Je vous conseille donc de mettre dès à présent dans vos favoris l'adresse de la doc' de SDL_ttf.

La doc' est disponible en plusieurs formats : HTML en ligne, HTML zippé, PDF, etc. Prenez la version qui vous arrange le plus.

Vous verrez que SDL_ttf est une bibliothèque très simple : il y a peu de fonctions (environ 40-50, oui, c'est peu !). Cela devrait être signe (pour les programmeurs aguerris que vous êtes ;-) ) que cette bibliothèque est simple et que vous saurez la manier assez vite.

Allez, il est temps d'apprendre à utiliser SDL_ttf, maintenant !

Chargement de SDL_ttf

L'include

Avant toute chose, il faut ajouter l'include suivant en haut de votre fichier .c :

#include <SDL/SDL_ttf.h>

Si vous avez des erreurs de compilation à ce stade, vérifiez si vous avez bien placé le fichier SDL_ttf.h dans le dossier mingw32/include/SDL et non dans mingw32/include tout court.

Démarrage de SDL_ttf

Tout comme la SDL, SDL_ttf a besoin d'être démarrée et arrêtée.
Il y a donc des fonctions très similaires à celles de la SDL :

  • TTF_Init : démarre SDL_ttf ;

  • TTF_Quit : arrête SDL_ttf.

Il n'est pas nécessaire que la SDL soit démarrée avant SDL_ttf.

Pour démarrer SDL_ttf (on dit aussi « initialiser »), on doit donc appeler la fonction TTF_Init().
Aucun paramètre n'est nécessaire. La fonction renvoie -1 s'il y a eu une erreur.

Vous pouvez donc démarrer SDL_ttf très simplement comme ceci :

TTF_Init();

Si vous voulez vérifier s'il y a une erreur et être ainsi plus rigoureux, utilisez plutôt ce code :

if(TTF_Init() == -1)
{
    fprintf(stderr, "Erreur d'initialisation de TTF_Init : %s\n", TTF_GetError());
    exit(EXIT_FAILURE);
}

S'il y a eu une erreur au démarrage de SDL_ttf, un fichier stderr.txt sera créé (sous Windows, du moins) contenant un message explicatif de l'erreur.
Pour ceux qui se poseraient la question : la fonction TTF_GetError() renvoie le dernier message d'erreur de SDL_ttf. C'est pour cela qu'on l'utilise dans le fprintf.

Arrêt de SDL_ttf

Pour arrêter SDL_ttf, on appelle TTF_Quit(). Là encore, pas de paramètre, pas de prise de tête. Vous pouvez appeler TTF_Quit avant ou après SDL_Quit, peu importe.

TTF_Quit();

Chargement d'une police

Bon tout ça c'est bien beau mais ce n'est pas assez compliqué, on ne s'amuse pas. Passons aux choses sérieuses, si vous le voulez bien : maintenant que SDL_ttf est chargée, nous devons charger une police. Une fois que cela sera fait, nous pourrons enfin voir comment écrire du texte !

Là encore il y a deux fonctions :

  • TTF_OpenFont : ouvre un fichier de police (.ttf) ;

  • TTF_CloseFont : ferme une police ouverte.

TTF_OpenFont doit stocker son résultat dans une variable de type TTF_Font. Vous devez créer un pointeur de TTF_Font, comme ceci :

TTF_Font *police = NULL;

Le pointeur police contiendra donc les informations sur la police une fois qu'on l'aura ouverte.

La fonction TTF_OpenFont prend deux paramètres :

    • le nom du fichier de police (au format .ttf) à ouvrir. L'idéal c'est de mettre le fichier de police dans le répertoire de votre projet. Exemple de fichier : arial.ttf (pour la police Arial) ;

    • la taille de la police à utiliser. Vous pouvez par exemple utiliser une taille de 22.

Ce sont les mêmes tailles que celles que vous utilisez dans un logiciel de traitement de texte tel que Word.

Il nous reste à trouver ces fameuses polices .ttf. Vous en avez déjà un certain nombre sur votre ordinateur, mais vous pouvez en télécharger sur Internet comme on va le voir.

Sur votre ordinateur

}Vous en avez déjà sur votre ordinateur !
Si vous êtes sous Windows, vous en trouverez beaucoup dans le dossier C:\Windows\Fonts.
Vous n'avez qu'à copier le fichier de police qui vous plaît dans le dossier de votre projet.

Si le nom contient des caractères « bizarres » comme des espaces, des accents ou même des majuscules, je vous conseille de le renommer. Pour être sûr de n'avoir aucun problème, n'utilisez que des minuscules et évitez les espaces.

  • Exemple de nom incorrect : TIMES NEW ROMAN.TTF ;

  • Exemple de nom correct : times.ttf.

Sur Internet

Autre possibilité : récupérer une police sur Internet. Vous trouverez plusieurs sites proposant des polices gratuites et originales à télécharger.

Je vous recommande personnellement dafont.com, qui est bien classé, très bien fourni et varié.

Les fig. suivante, suivante et suivante vous donnent un aperçu de polices que vous pourrez y trouver très facilement.

Police Alpha Wood
Charger la police

Je vous propose d'utiliser la police Angelina pour la suite des exemples.

On ouvrira la police comme ceci :

police = TTF_OpenFont("angelina.ttf", 65);

La police utilisée sera angelina.ttf. J'ai bien pris soin de mettre le fichier dans le dossier de mon projet et de le renommer pour qu'il soit tout en minuscules.
La police sera de taille 65. Ça paraît gros mais visiblement, c'est une police qu'il faut écrire en gros pour qu'on puisse la voir.

Ce qui est très important, c'est que TTF_OpenFont stocke le résultat dans la variable police. Vous allez réutiliser cette variable tout à l'heure en écrivant du texte. Elle permettra d'indiquer la police que vous voulez utiliser pour écrire votre texte.

Vous n'avez pas besoin d'ouvrir la police à chaque fois que vous écrivez du texte : ouvrez-la une fois au début du programme et fermez-la à la fin.

Fermer la police

Il faut penser à fermer chaque police ouverte avant l'appel à TTF_Quit().
Dans mon cas, ça donnera donc le code suivant :

TTF_CloseFont(police); /* Doit être avant TTF_Quit() */
TTF_Quit();

Et voilà le travail !

Les différentes méthodes d'écriture

Maintenant que SDL_ttf est chargée et qu'on a une variable police chargée elle aussi, plus rien ni personne ne nous empêchera d'écrire du texte dans notre fenêtre SDL !

Bien : écrire du texte c'est bien, mais avec quelle fonction ? D'après la doc', pas moins de 12 fonctions sont disponibles !

En fait, il y a trois façons différentes pour SDL_ttf de dessiner du texte.

  • Solid (fig. suivante) : c'est la technique la plus rapide. Le texte sera rapidement écrit dans une SDL_Surface. La surface sera transparente mais n'utilisera qu'un niveau de transparence (on a appris ça il y a quelques chapitres). C'est pratique, mais le texte ne sera pas très joli, pas très « arrondi », surtout s'il est écrit gros. Utilisez cette technique lorsque vous devez souvent changer le texte, par exemple pour afficher le temps qui s'écoule ou le nombre de FPS d'un jeu.

  • Shaded (fig. suivante) : cette fois, le texte sera joli. Les lettres seront antialiasées (cela signifie que leurs contours seront adoucis, ce qui est plus agréable à l'œil) et le texte apparaîtra plus lisse. Il y a un défaut, en revanche : le fond doit être d'une couleur unie. Il est impossible de rendre le fond de la SDL_Surface transparente en Shaded.

  • Blended (fig. suivante) : c'est la technique la plus puissante, mais elle est lente. En fait, elle met autant de temps que Shaded à créer la SDL_Surface. La seule différence avec Shaded, c'est que vous pouvez blitter le texte sur une image et la transparence sera respectée (contrairement à Shaded qui imposait un fond uni). Attention : le calcul du blit sera plus lent que pour Shaded.

Solid : mode d'écriture très rapide mais peu esthétique (texte non lissé) Shaded : mode d'écriture lent mais plus joli car antialisé — fond obligatoirement uni Blended : mode d'écriture lent (et blit lent) mais bien plus beau car antialisé et fonctionne sur un fond non uni

En résumé :

  • si vous avez un texte qui change souvent, comme un compte à rebours, utilisez Solid ;

  • si votre texte ne change pas très souvent et que vous voulez blitter votre texte sur un fond uni, utilisez Shaded ;

  • si votre texte ne change pas très souvent mais que vous voulez blitter sur un fond non uni (comme une image), utilisez Blended.

Voilà, vous devriez déjà être un peu plus familiers avec ces trois types d'écriture de SDL_ttf.

Je vous avais dit qu'il y avait 12 fonctions en tout.
En effet, pour chacun de ces trois types d'écriture, il y a quatre fonctions. Chaque fonction écrit le texte à l'aide d'un charset différent, c'est-à-dire d'une palette de caractères différente. Ces quatre fonctions sont :

  • Latin1 ;

  • UTF8 ;

  • Unicode ;

  • Unicode Glyph.

L'idéal est d'utiliser l'Unicode car c'est un charset gérant la quasi-totalité des caractères existant sur Terre. Toutefois, utiliser l'Unicode n'est pas toujours forcément simple (un caractère prend plus que la taille d'un char en mémoire), nous ne verrons donc pas comment l'utiliser ici.

A priori, si votre programme est écrit en français le mode Latin1 suffit amplement, vous pouvez vous contenter de celui-là.

Les trois fonctions utilisant le charset Latin1 sont :

  • TTF_RenderText_Solid ;

  • TTF_RenderText_Shaded ;

  • TTF_RenderText_Blended.

Exemple d'écriture de texte en Blended

Pour spécifier une couleur à SDL_ttf, on ne va pas utiliser le même type qu'avec la SDL (un Uint32 créé à l'aide de la fonction SDL_MapRGB).
Au contraire, nous allons utiliser une structure toute prête de la SDL : SDL_Color. Cette structure comporte trois sous-variables : la quantité de rouge, de vert et de bleu.

Si vous voulez créer une variable couleurNoire, vous devrez donc écrire :

SDL_Color couleurNoire = {0, 0, 0};

Attention à ne pas confondre avec les couleurs qu'utilise habituellement la SDL !
La SDL utilise des Uint32 créés à l'aide de SDL_MapRGB.
SDL_ttf utilise des SDL_Color.

On va écrire un texte en noir dans une SDL_Surface texte :

texte = TTF_RenderText_Blended(police, "Salut les Zér0s !", couleurNoire);

Vous voyez dans l'ordre les paramètres à envoyer : la police (de type TTF_Font), le texte à écrire, et enfin la couleur (de type SDL_Color).
Le résultat est stocké dans une SDL_Surface. SDL_ttf calcule automatiquement la taille nécessaire à donner à la surface en fonction de la taille du texte et du nombre de caractères que vous avez voulu écrire.

Comme toute SDL_Surface, notre pointeur texte contient les sous-variables w et h indiquant respectivement sa largeur et sa hauteur. C'est donc un bon moyen de connaître les dimensions du texte une fois que celui-ci a été écrit dans la SDL_Surface. Vous n'aurez qu'à écrire :

texte->w /* Donne la largeur */
texte->h /* Donne la hauteur */

Code complet d'écriture de texte

Vous savez désormais tout ce qu'il faut connaître sur SDL_ttf. Voyons pour résumer un code complet d'écriture de texte en mode Blended :

#include <stdlib.h>
#include <stdio.h>
#include <SDL/SDL.h>
#include <SDL/SDL_image.h>
#include <SDL/SDL_ttf.h>

int main(int argc, char *argv[])
{
    SDL_Surface *ecran = NULL, *texte = NULL, *fond = NULL;
    SDL_Rect position;
    SDL_Event event;
    TTF_Font *police = NULL;
    SDL_Color couleurNoire = {0, 0, 0};
    int continuer = 1;

    SDL_Init(SDL_INIT_VIDEO);
    TTF_Init();

    ecran = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE | SDL_DOUBLEBUF);
    SDL_WM_SetCaption("Gestion du texte avec SDL_ttf", NULL);

    fond = IMG_Load("moraira.jpg");

    /* Chargement de la police */
    police = TTF_OpenFont("angelina.ttf", 65);
    /* Écriture du texte dans la SDL_Surface texte en mode Blended (optimal) */
    texte = TTF_RenderText_Blended(police, "Salut les Zér0s !", couleurNoire);

    while (continuer)
    {
        SDL_WaitEvent(&event);
        switch(event.type)
        {
            case SDL_QUIT:
                continuer = 0;
                break;
        }

        SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 255, 255, 255));

        position.x = 0;
        position.y = 0;
        SDL_BlitSurface(fond, NULL, ecran, &position); /* Blit du fond */

        position.x = 60;
        position.y = 370;
        SDL_BlitSurface(texte, NULL, ecran, &position); /* Blit du texte */
        SDL_Flip(ecran);
    }

    TTF_CloseFont(police);
    TTF_Quit();

    SDL_FreeSurface(texte);
    SDL_Quit();

    return EXIT_SUCCESS;
}

Le résultat vous est présenté sur la fig. suivante.

Un texte écrit à l'aide de SDL_ttf

Si vous voulez changer de mode d'écriture pour tester, il n'y a qu'une ligne à modifier : celle créant la surface (avec l'appel à la fonction TTF_RenderText_Blended).

La fonction TTF_RenderText_Shaded prend un quatrième paramètre, contrairement aux deux autres. Ce dernier paramètre est la couleur de fond à utiliser. Vous devrez donc créer une autre variable de type SDL_Color pour indiquer une couleur de fond (par exemple le blanc).

Attributs d'écriture du texte

Il est aussi possible de spécifier des attributs d'écriture, comme gras, italique et souligné.

Il faut d'abord que la police soit chargée. Vous devriez donc avoir une variable police valide. Vous pouvez alors faire appel à la fonction TTF_SetFontStyle qui va modifier la police pour qu'elle soit en gras, italique ou souligné selon vos désirs.

La fonction prend deux paramètres :

  • la police à modifier ;

  • une combinaison de flags pour indiquer le style à donner : gras, italique ou souligné.

Pour les flags, vous devez utiliser ces constantes :

  • TTF_STYLE_NORMAL : normal ;

  • TTF_STYLE_BOLD : gras ;

  • TTF_STYLE_ITALIC : italique ;

  • TTF_STYLE_UNDERLINE : souligné.

Comme c'est une liste de flags, vous pouvez les combiner à l'aide du symbole | comme on a appris à le faire.

Testons :

/* Chargement de la police */
police = TTF_OpenFont("angelina.ttf", 65);
/* Le texte sera écrit en italique et souligné */
TTF_SetFontStyle(police, TTF_STYLE_ITALIC | TTF_STYLE_UNDERLINE);
/* Écriture du texte en italique et souligné */
texte = TTF_RenderText_Blended(police, "Salut les Zér0s !", couleurNoire);

Résultat, le texte est écrit en italique et souligné (fig. suivante).

Écriture en italique et souligné

Pour restaurer une police à son état normal, il suffit de refaire appel à la fonction TTF_SetFontStyle en utilisant cette fois le flag TTF_STYLE_NORMAL.

Exercice : le compteur

Cet exercice va cumuler ce que vous avez appris dans ce chapitre et dans le chapitre sur la gestion du temps. Votre mission, si vous l'acceptez, consistera à créer un compteur qui s'incrémentera tous les dixièmes de seconde.

Ce compteur va donc progressivement afficher : 0,
100,
200,
300,
400,
etc. Au bout d'une seconde le nombre 1000 devrait donc s'afficher.

Astuce pour écrire dans une chaîne

Pour réaliser cet exercice, vous aurez besoin de savoir comment écrire dans une chaîne de caractères en mémoire.
En effet, vous devez donner un char* à TTF_RenderText mais vous, ce que vous aurez, c'est un nombre (un int par exemple). Comment convertir un nombre en chaîne de caractères ?

On peut utiliser pour cela la fonction sprintf.
Elle marche de la même manière que fprintf, sauf qu'au lieu d'écrire dans un fichier elle écrit dans une chaîne (le « s » de sprintf signifie « string », c'est-à-dire « chaîne » en anglais).
Le premier paramètre que vous lui donnerez sera donc un pointeur sur un tableau de char.

Veillez à réserver suffisamment d'espace pour le tableau de char si vous ne voulez pas déborder en mémoire !

Exemple :

sprintf(temps, "Temps : %d", compteur);

Ici, temps est un tableau de char (20 caractères), et compteur est un int qui contient le temps.
Après cette instruction, la chaîne temps contiendra par exemple "Temps : 500".

À vous de jouer !

Correction

Voici une correction possible de l'exercice :

int main(int argc, char *argv[])
{
    SDL_Surface *ecran = NULL, *texte = NULL;
    SDL_Rect position;
    SDL_Event event;
    TTF_Font *police = NULL;
    SDL_Color couleurNoire = {0, 0, 0}, couleurBlanche = {255, 255, 255};
    int continuer = 1;
    int tempsActuel = 0, tempsPrecedent = 0, compteur = 0;
    char temps[20] = ""; /* Tableau de char suffisamment grand */

    SDL_Init(SDL_INIT_VIDEO);
    TTF_Init();

    ecran = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE | SDL_DOUBLEBUF);
    SDL_WM_SetCaption("Gestion du texte avec SDL_ttf", NULL);

    /* Chargement de la police */
    police = TTF_OpenFont("angelina.ttf", 65);

    /* Initialisation du temps et du texte */
    tempsActuel = SDL_GetTicks();
    sprintf(temps, "Temps : %d", compteur);
    texte = TTF_RenderText_Shaded(police, temps, couleurNoire, couleurBlanche);

    while (continuer)
    {
        SDL_PollEvent(&event);
        switch(event.type)
        {
            case SDL_QUIT:
                continuer = 0;
                break;
        }

        SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 255, 255, 255));

        tempsActuel = SDL_GetTicks();
        if (tempsActuel - tempsPrecedent >= 100) /* Si 100 ms au moins se sont écoulées */
        {
            compteur += 100; /* On rajoute 100 ms au compteur */
            sprintf(temps, "Temps : %d", compteur); /* On écrit dans la chaîne "temps" le nouveau temps */
            SDL_FreeSurface(texte); /* On supprime la surface précédente */
            texte = TTF_RenderText_Shaded(police, temps, couleurNoire, couleurBlanche); /* On écrit la chaîne temps dans la SDL_Surface */
            tempsPrecedent = tempsActuel; /* On met à jour le tempsPrecedent */
        }

        position.x = 180;
        position.y = 210;
        SDL_BlitSurface(texte, NULL, ecran, &position); /* Blit du texte */
        SDL_Flip(ecran);

    }

    TTF_CloseFont(police);
    TTF_Quit();

    SDL_FreeSurface(texte);
    SDL_Quit();

    return EXIT_SUCCESS;
}

La fig. suivante vous présente le résultat au bout de 13,9 secondes très exactement.

Le compteur au bout de 13,9 secondes

N'hésitez pas à télécharger ce projet si vous souhaitez l'étudier en détail et l'améliorer. Il n'est pas encore parfait : on pourrait par exemple utiliser SDL_Delay pour éviter d'utiliser 100 % du CPU.

Télécharger le projet (437 Ko)

Pour aller plus loin

Si vous voulez améliorer ce petit bout de programme, vous pouvez essayer d'en faire un jeu où il faut cliquer le plus de fois possible dans la fenêtre avec la souris dans un temps imparti. Un compteur s'incrémentera à chaque clic de la souris.

Un compte à rebours doit s'afficher. Lorsqu'il atteint 0, on récapitule le nombre de clics effectués et on demande si on veut faire une nouvelle partie.

Vous pouvez aussi gérer les meilleurs scores en les enregistrant dans un fichier. Cela vous fera travailler à nouveau la gestion des fichiers en C.

Bon courage !

En résumé

  • On ne peut pas écrire de texte en SDL, à moins d'utiliser une extension comme la bibliothèque SDL_ttf.

  • Cette bibliothèque permet de charger un fichier de police au format .ttf à l'aide de la fonction TTF_OpenFont.

  • Il y a trois modes d'écriture du texte, du plus simple au plus sophistiqué : Solid, Shaded et Blended.

  • On écrit dans une SDL_Surface via des fonctions comme TTF_RenderText_Blended.


Apprenez à programmer en C

Jouer du son avec FMOD

Depuis que nous avons découvert la SDL, nous avons appris à placer des images dans la fenêtre, à faire interagir l'utilisateur avec le clavier et la souris, à écrire du texte, mais il manque clairement un élément : le son !

Ce chapitre va combler ce manque. Parce que les possibilités offertes par la SDL en matière d'audio sont très limitées, nous allons découvrir ici une bibliothèque spécialisée dans le son : FMOD.

Installer FMOD

Pourquoi FMOD ?

Vous le savez maintenant : la SDL n'est pas seulement une bibliothèque graphique. Elle permet aussi de gérer le son via un module appelé SDL_audio. Alors que vient faire dans ce chapitre une bibliothèque externe qui n'a rien à voir comme FMOD ?

C'est en fait un choix que j'ai fait après de nombreux tests. J'aurais pu vous expliquer comment gérer le son en SDL mais j'ai préféré ne pas le faire.
Je m'explique.

Pourquoi j'ai évité SDL_audio

La gestion du son en SDL est « bas niveau ». Trop à mon goût. Il faut effectuer plusieurs manipulations très précises pour jouer du son. C'est donc complexe et je ne trouve pas ça amusant. Il y a bien d'autres bibliothèques qui proposent de jouer du son simplement.

Petit rappel : une bibliothèque « bas niveau » est une bibliothèque proche de l'ordinateur. On doit donc connaître un peu le fonctionnement interne de l'ordinateur pour s'en servir et il faut généralement plus de temps pour arriver à faire la même chose qu'avec une bibliothèque « haut niveau ».
N'oubliez pas que tout est relatif : il n'y a pas les bibliothèques bas niveau d'un côté et les bibliothèques haut niveau de l'autre. Certaines sont juste plus ou moins haut niveau que d'autres. Par exemple FMOD est plus haut niveau que le module SDL_audio de la SDL.

Autre détail important, la SDL ne permet de jouer que des sons au format WAV. Le format WAV est un format de son non compressé. Une musique de 3 minutes dans ce format prend plusieurs dizaines de Mo, contrairement à un format compressé comme MP3 ou Ogg qui occupe beaucoup moins d'espace (2 à 3 Mo).

En fait, si on y réfléchit bien, c'était un peu pareil avec les images. La SDL ne gère que les BMP (images non compressées) à la base. On a dû installer une bibliothèque supplémentaire (SDL_image) pour pouvoir lire d'autres images comme les JPEG, PNG, GIF, etc.

Eh bien figurez-vous qu'il y a une bibliothèque équivalente pour le son : SDL_mixer. Elle est capable de lire un grand nombre de formats audio, parmi lesquels les MP3, les Ogg, les Midi… Et pourtant, là encore j'ai évité de vous parler de cette bibliothèque. Pourquoi ?

Pourquoi j'ai évité SDL_mixer

SDL_mixer est une bibliothèque qu'on ajoute en plus de la SDL, à la manière de SDL_image. Elle est simple à utiliser et lit beaucoup de formats audio différents. Toutefois, après mes tests, il s'est avéré que la bibliothèque comportait des bugs gênants en plus d'être relativement limitée en fonctionnalités.

C'est donc pour cela que je me suis ensuite penché sur FMOD, une bibliothèque qui n'a certes rien à voir avec la SDL, mais qui a l'avantage d'être puissante et réputée.

Télécharger FMOD

Si je vous raconte tout ça, c'est pour vous expliquer que le choix de FMOD n'est pas anodin. C'est tout simplement parce que c'est la meilleure bibliothèque gratuite que j'ai pu trouver.
Elle est aussi simple à utiliser que SDL_mixer, avec un avantage non négligeable : elle n'est pas buggée.

FMOD permet en outre de réaliser plusieurs effets intéressants que SDL_mixer ne propose pas, comme des effets sonores 3D.

FMOD est une bibliothèque gratuite mais pas sous license LGPL, contrairement à la SDL. Cela signifie que vous pouvez l'utiliser gratuitement tant que vous ne réalisez pas de programme payant avec. Si vous voulez faire payer votre programme, il faudra payer une redevance à l'auteur (je vous laisse consulter les prix sur le site de FMOD).
De nombreux jeux commerciaux utilisent FMOD ; parmi les plus connus : Starcraft II, World of Warcraft : Cataclysm, Crysis 2, etc.

Il existe plusieurs versions de FMOD, en particulier celle destinée à une utilisation sous des OS dits habituels (Linux, Windows, Mac…), elle s'appelle FMOD Ex Programmers API.

Téléchargez donc la version de FMOD Ex correspondant à votre OS. Prenez la version dite « stable ».
Vérifiez en particulier si vous avez un OS 32 bits ou 64 bits (sous Windows, faites un clic droit sur « Ordinateur », puis « Propriétés » pour le savoir).

Télécharger FMOD

Installer FMOD

L'installation fonctionne sur le même principe que les autres bibliothèques, comme la SDL.

Le fichier que vous avez téléchargé est normalement un exécutable (sous Windows), ou une archive (.dmg sous Mac, .tar.gz sous Linux). Je vais détailler ici la procédure sous Windows, mais cela fonctionne sur le même principe sous Mac OS X et Linux.

  1. Installez FMOD Ex sur votre disque. Les fichiers dont nous avons besoin seront placés dans un répertoire similaire à celui-ci : C:\Program Files\FMOD SoundSystem\FMOD Programmers API Win32\api.

  2. Dans ce dossier, vous trouverez la DLL de FMOD Ex (fmodex.dll) à placer dans le répertoire de votre projet. L'autre DLL, fmodexL.dll, sert à effectuer du débogage. Nous n'en ferons pas ici. Retenez surtout que c'est le fichier fmodex.dll que vous devrez livrer avec votre programme.

  3. Dans le dossier api/inc, vous trouverez les .h. Placez-les à côté des autres .h dans le dossier de votre IDE. Par exemple Code Blocks/mingw32/include/fmodex (j'ai créé un dossier spécial pour FMOD comme pour SDL).

  4. Dans le dossier api/lib, récupérez le fichier qui correspond à votre compilateur. Un fichier texte doit vous indiquer quel fichier vous devez prendre :

      • Si vous utilisez Code Blocks, donc le compilateur mingw, copiez libfmodex.a dans le dossier lib de votre IDE.

    Dans le cas de Code Blocks, c'est le dossier CodeBlocks/mingw32/lib ;

    • Si vous utilisez Visual C++, récupérez le fichier fmodex_vc.lib.

  5. Enfin, et c'est peut-être le plus important, il y a un dossier documentation dans le répertoire de FMOD Ex. Normalement, des raccourcis ont été créés dans le menu Démarrer vers cette documentation. Gardez un œil dessus, car nous ne pourrons pas découvrir toutes les fonctionnalités de FMOD Ex dans ce cours. Vous en aurez très certainement besoin dans peu de temps.

Il reste à configurer notre projet. Là encore, c'est comme les autres fois : vous ouvrez votre projet avec votre IDE favori et vous ajoutez le fichier .a (ou .lib) à la liste des fichiers que le linker doit récupérer.
Sous Code Blocks (j'ai l'impression de me répéter), menu Project > Build Options, onglet Linker, cliquez sur Add et indiquez où se trouve le fichier .a. Si on vous demande « Keep as a relative path ? », je vous conseille de répondre non, mais de toute manière cela devrait fonctionner dans les deux cas.

FMOD Ex est installé, voyons rapidement de quoi il est constitué.

Initialiser et libérer un objet système

La librairie FMOD Ex est disponible pour les deux langages C et C++.
Sa particularité c'est que les développeurs de cette librairie ont gardé une certaine cohérence de syntaxe dans les deux langages. Le premier avantage est que si vous apprenez à manipuler FMOD Ex en C, vous savez le faire en C++ à 95%.

Inclure le header

Avant toute chose, vous avez dû le faire instinctivement maintenant mais ça ne coûte rien de le préciser, il faut inclure le fichier .h de FMOD.

#include <fmodex/fmod.h>

J'ai placé ce fichier dans un sous-dossier fmodex. Adaptez cette ligne en fonction de la position du fichier si c'est différent chez vous.
Si vous êtes sous Linux, l'installation se fait automatiquement dans le dossier fmodex.

Créer et initialiser un objet système

Un objet système est une variable qui nous servira tout au long du programme à définir des paramètres de la librairie.
Rappelez-vous qu'avec SDL par exemple, il fallait initialiser la lib explicitement avec une fonction. Ici, le mode d'emploi est un petit peu différent : au lieu d'initialiser toute la librairie, on travaille avec un objet dont le rôle est de définir le comportement de celle-ci.

Pour créer un objet système, il suffit de déclarer un pointeur sur le type FMOD_SYSTEM. Par exemple :

FMOD_SYSTEM *system;

Pour allouer dans la mémoire cet objet système, on utilise la fonction FMOD_System_Create dont voici le prototype :

FMOD_RESULT FMOD_System_Create(
  FMOD_SYSTEM **  system
);

Remarquons que cette fonction prend en paramètre un pointeur sur un pointeur de FMOD_SYSTEM.
Les plus agiles d'entre vous auront remarqué que lors de la déclaration du pointeur sur FMOD_SYSTEM, il n'a pas été alloué avec malloc() ou une autre fonction. C'est justement la raison pour laquelle la fonction FMOD_System_Create prend un tel paramètre pour, entre autres, allouer le pointeur system.

Concrètement, après avoir déclaré notre objet système, il suffit de faire :

FMOD_SYSTEM *system;
FMOD_System_Create(&system);

Voilà, maintenant on dispose d'un objet système alloué, il ne reste plus qu'à l'initialiser. Pour ce faire, on utilise la fonction FMOD_System_Init dont le prototype est :

FMOD_RESULT FMOD_System_Init(
  FMOD_SYSTEM *  system,
  int  maxchannels,
  FMOD_INITFLAGS  flags,
  void *  extradriverdata
);

  • Le paramètre system est le paramètre qui nous intéresse le plus, car c'est le pointeur qu'on veut initialiser.

  • Le paramètre maxchannels est le nombre maximum de canaux que devra gérer FMOD. En d'autres termes, c'est le nombre maximal de sons qui pourront être joués en même temps. Tout dépend de la puissance de votre carte son ; on conseille généralement une valeur de 32 (ce sera suffisant pour la plupart des petits jeux). Pour info, FMOD peut théoriquement gérer jusqu'à 1024 canaux différents, mais à ce niveau ça risque de beaucoup faire travailler votre ordinateur !

  • Le paramètre flag ne nous intéressera pas énormément dans ce cours ; on se contentera de lui donner la valeur FMOD_INIT_NORMAL.

  • Le paramètre extradriverdata ne nous intéressera pas non plus, et on lui donnera comme valeur NULL.

Par exemple, pour déclarer, allouer et initialiser un objet système, on fera comme suit :

FMOD_SYSTEM *system;
FMOD_System_Create(&system);
FMOD_System_Init(system, 2, FMOD_INIT_NORMAL, NULL);

Maintenant, nous disposons d'un objet système prêt à l'emploi.

Fermer et libérer un objet système

On ferme puis on libère un objet système avec deux fonctions :

FMOD_System_Close(system);
FMOD_System_Release(system);

Ai-je vraiment besoin de commenter ce code ?

Les sons courts

Nous commencerons par étudier les sons courts.
Un « son court », comme je l'appelle, est un son qui dure généralement quelques secondes (parfois moins d'une seconde) et qui est généralement destiné à être joué régulièrement.

Quelques exemples de sons courts :

  • un bruit de balle ;

  • un bruit de pas ;

  • un tic-tac (pour faire stresser le joueur avant la fin d'un compte à rebours) ;

  • des applaudissements ;

  • etc.

Bref, ça correspond à tous les sons qui ne sont pas des musiques.
Généralement, ces sons sont tellement courts qu'on ne prend pas la peine de les compresser. On les trouve donc le plus souvent sous le format WAV non compressé.

Trouver des sons courts

Avant de commencer, il serait bien de connaître quelques sites qui proposent des banques de sons. En effet, tout le monde ne veut pas forcément enregistrer les sons chez soi.

Ça tombe bien, le net regorge de sons courts, généralement au format WAV.
Où les trouver ? Ça peut paraître bête, on n'y pense pas forcément (et pourtant on devrait), mais Google est notre ami. Au hasard, je tape « Free Sounds », ce qui signifie « sons gratuits » en anglais, et boum… des centaines de millions de résultats !

Rien qu'avec les sites de la première page, vous devriez trouver votre bonheur.
Personnellement, j'ai retenu FindSounds.com, un moteur de recherche pour sons. Je ne sais pas si c'est le meilleur, mais en tout cas il est bien complet.

Si vous ne savez pas quels mots-clés taper pour votre recherche, rendez-vous sur la page des exemples de recherche. Certes, il faut connaître quelques mots d'anglais (mais de toute façon, si vous voulez programmer, comment voulez-vous faire sans être au moins capables de lire l'anglais ?).

En recherchant « gun », on trouve des tonnes de sons de tir de fusil ; en tapant « door » on trouve des bruits de porte (figure suivante), etc.

FindSounds.com propose des sons de porte

Les étapes à suivre pour jouer un son

La première étape consiste à charger en mémoire le son que vous voulez jouer.
Il est conseillé de charger tous les sons qui seront fréquemment utilisés dans le jeu dès le début du programme. Vous les libérerez à la fin. En effet, une fois que le son est chargé en mémoire, sa lecture est très rapide.

Le pointeur

Première étape : créer un pointeur de type FMOD_SOUND qui représentera notre son.

FMOD_SOUND *tir = NULL;
Charger le son

Deuxième étape : charger le son avec la fonction FMOD_System_CreateSound. Elle prend… 5 paramètres :

    • Un objet système dont on a parlé précédemment.

Bien sûr cet objet doit être prêt à l'emploi (déclaré, alloué et initialisé).

    • Le nom du fichier son à charger. Il peut être de format WAV, MP3, OGG, etc. Toutefois, il vaut mieux charger des sons courts (quelques secondes maximum) plutôt que des sons longs. En effet, la fonction chargera et décodera tout le son en mémoire, ce qui peut prendre de la place si le son est une musique !

    • Le troisième paramètre est un paramètre flag.

Il nous intéresse particulièrement ici, car c'est grâce à lui qu'on pourra dire à FMOD que le son qu'on veut jouer est un son court. Pour ceci, on utilisera la valeur FMOD_CREATESAMPLE.

    • Le quatrième paramètre ne nous intéresse pas, et on mettra NULL comme valeur.

    • Le dernier paramètre est du type FMOD_SOUND ** sound, et c'est ce pointeur-là qu'on utilisera par la suite pour jouer notre son.

En quelque sorte, on peut considérer que ce pointeur pointera à l'avenir vers notre son.

Voici un exemple de chargement :

FMOD_System_CreateSound(system, "pan.wav", FMOD_CREATESAMPLE, 0, &tir);

Ici, je charge le son pan.wav. Le pointeur tir fera référence à ce son par la suite.

Si vous voulez faire les tests en même temps que moi, je vous propose de télécharger le son pan.wav, que j'utiliserai également par la suite.

Si tout se passe bien, la fonction renvoie la valeur FMOD_OK. Sinon, c'est qu'il y a eu un problème lors de l'ouverture du fichier audio (fichier corrompu ou inexistant par exemple).

Jouer le son

Vous voulez jouer le son ? Pas de problème avec la fonction FMOD_System_PlaySound !
Il suffit de lui donner un objet système prêt à l'emploi, un numéro de canal sur lequel jouer ainsi que le pointeur sur le son, et d'autres paramètres qui ne nous intéressent pas et qu'on mettra à NULL ou à 0. Pour le numéro de canal, ne vous prenez pas la tête et envoyez FMOD_CHANNEL_FREE pour laisser FMOD gérer ça.

FMOD_System_PlaySound(system, FMOD_CHANNEL_FREE, tir, 0, NULL);
Libérer le son de la mémoire

Lorsque vous n'avez plus besoin du son, vous devez le libérer de la mémoire.
Il n'y a rien de plus simple, il suffit d'indiquer le pointeur à libérer avec la fonction FMOD_Sound_Release

FMOD_Sound_Release(tir);

Exemple : un jeu de tir

Le mieux maintenant est de résumer tout ce qu'on a vu dans un cas concret de programme écrit en SDL.
Il n'y avait rien de compliqué et, normalement, vous ne devriez avoir aucune difficulté à réaliser cet exercice.

Le sujet

Votre mission est simple : créer un jeu de tir.
Bon, on ne va pas réaliser un jeu complet ici, mais juste la gestion du viseur. Je vous ai justement fait un petit viseur sous Paint (figure suivante).

Bref, voilà les objectifs :

  • Fond de fenêtre : noir.

  • Pointeur de la souris : invisible.

  • L'image du viseur est blittée à la position de la souris lorsqu'on la déplace. Attention : il faut que le CENTRE de l'image soit placé au niveau du pointeur de la souris.

  • Quand on clique, le son pan.wav doit être joué.

Ça peut être le début d'un jeu de tir.
Trop facile ? Ok, alors à vous de jouer !

La correction

Voici le code complet :

#include <stdlib.h>
#include <stdio.h>
#include <SDL/SDL.h>
#include <SDL/SDL_image.h>
#include <fmodex/fmod.h>

int main(int argc, char *argv[])
{
    SDL_Surface *ecran = NULL, *viseur = NULL;
    SDL_Event event;
    SDL_Rect position;
    int continuer = 1;

    FMOD_SYSTEM *system;
    FMOD_SOUND *tir;

    FMOD_RESULT resultat;

    /* Création et initialisation d'un objet système */
    FMOD_System_Create(&system);
    FMOD_System_Init(system, 1, FMOD_INIT_NORMAL, NULL);

    /* Chargement du son et vérification du chargement */
    resultat = FMOD_System_CreateSound(system, "pan.wav", FMOD_CREATESAMPLE, 0, &tir);
    if (resultat != FMOD_OK)
    {
        fprintf(stderr, "Impossible de lire pan.wav\n");
        exit(EXIT_FAILURE);
    }

    /* Initialisation de la SDL */
    SDL_Init(SDL_INIT_VIDEO);

    SDL_ShowCursor(SDL_DISABLE);
    ecran = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE | SDL_DOUBLEBUF);
    SDL_WM_SetCaption("Gestion du son avec FMOD", NULL);

    viseur = IMG_Load("viseur.png");

    while (continuer)
    {
        SDL_WaitEvent(&event);

        switch(event.type)
        {
        case SDL_QUIT:
            continuer = 0;
            break;
        case SDL_MOUSEBUTTONDOWN:
            /* Lorqu'on clique, on joue le son */
            FMOD_System_PlaySound(system, FMOD_CHANNEL_FREE, tir, 0, NULL);
            break;
        case SDL_MOUSEMOTION:
            /* Lorsqu'on déplace la souris, on place le centre du viseur à la position de la souris
               ... D'où notamment le "viseur->w / 2" pour réussir à faire cela */
            position.x = event.motion.x - (viseur->w / 2);
            position.y = event.motion.y - (viseur->h / 2);
            break;
        }

        SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 0, 0, 0));
        SDL_BlitSurface(viseur, NULL, ecran, &position);
        SDL_Flip(ecran);
    }

    /* On ferme la SDL */
    SDL_FreeSurface(viseur);
    SDL_Quit();

    /* On libère le son et on ferme et libère l'objet système */
    FMOD_Sound_Release(tir);
    FMOD_System_Close(system);
    FMOD_System_Release(system);

    return EXIT_SUCCESS;
}

La figure suivante vous donne un aperçu du mini-jeu, mais le mieux est encore de voir le résultat en vidéo avec le son sur le web !

Aperçu du mini-jeu du viseur

Voir la vidéo de la gestion du son avec FMOD : le viseur (111 Ko)

Ici, j'ai chargé FMOD avant la SDL et je l'ai libéré après la SDL. Il n'y a pas de règles au niveau de l'ordre (j'aurais tout aussi bien pu faire l'inverse). J'ai choisi de charger la SDL et d'ouvrir la fenêtre après le chargement de FMOD pour que le jeu soit prêt à être utilisé dès que la fenêtre s'ouvre (sinon il aurait peut-être fallu attendre quelques millisecondes le temps que FMOD se charge).
Ceci étant, libre à vous de choisir l'ordre que vous voulez ; c'est un détail.

Le code est, je pense, suffisamment commenté. Il n'y a pas de piège particulier, pas de nouveauté fracassante.

On notera la « petite » difficulté qui consistait à blitter le centre du viseur au niveau du pointeur de la souris. Le calcul de la position de l'image est fait en fonction.

Pour ceux qui n'auraient pas encore compris la différence, voici de quoi il retourne. Pour l'occasion, j'ai réactivé l'affichage du pointeur de la souris pour qu'on voie comment est placé le viseur par rapport au pointeur.

Code incorrect
(viseur mal placé)

position.x = event.motion.x;
position.y = event.motion.y;

Code correct
(viseur bien placé)

position.x = event.motion.x - (viseur->w / 2);
position.y = event.motion.y - (viseur->h / 2);
Idées d'amélioration

Ce code est la base d'un jeu de shoot. Vous avez le viseur, le bruit de tir, il ne vous reste plus qu'à faire apparaître ou défiler des ennemis et à marquer le score du joueur. Comme d'habitude, c'est à vous de jouer. Vous vouliez faire un jeu ? Qu'à cela ne tienne, vous avez le niveau maintenant, et même un code de base pour démarrer un jeu de tir ! Qu'est-ce que vous attendez, franchement ?

Bien sûr, les forums du Site du Zéro sont toujours là pour vous aider si vous êtes bloqués à un moment de la création de votre jeu. Il est normal de rencontrer des difficultés, quel que soit le niveau que l'on ait.

Les musiques (MP3, OGG, WMA…)

En théorie, le flag FMOD_CREATESAMPLE permet de charger n'importe quel type de son, y compris les formats compressés MP3, OGG, WMA. Le problème concerne les sons « longs », c'est-à-dire les musiques.

En effet, une musique dure en moyenne 3 à 4 minutes. Or, avec ce flag, la fonction FMOD_System_CreateSound charge tout le fichier en mémoire (et c'est la version décompressée qui est mise en mémoire, donc ça prend beaucoup de place !).

Si vous avez un son long (on va parler de « musique » dorénavant), il est préférable de le charger en streaming, c'est-à-dire d'en charger de petits bouts au fur et à mesure de la lecture ; c'est d'ailleurs ce que font tous les lecteurs audio.

Trouver des musiques

Là, on rentre en terrain miné, épineux, explosif (comme vous préférez).
En effet, la plupart des musiques et chansons que l'on connaît sont soumises au droit d'auteur. Même si vous ne faites qu'un petit programme, il faut verser une redevance à la SACEM (en France du moins).

Donc, mis à part les MP3 soumis à droit d'auteur, que nous reste-t-il ?
Heureusement, il y a des chansons libres de droit ! Les auteurs vous autorisent à diffuser librement leurs chansons, il n'y a donc aucun problème pour que vous les utilisiez dans vos programmes.

Si votre programme est payant, il faudra en parler à l'artiste, à moins que celui-ci n'autorise explicitement une utilisation commerciale de son œuvre. Une chanson libre de droit peut être téléchargée, copiée et écoutée librement, mais ça ne veut pas dire qu'on vous autorise à vous faire de l'argent sur le dos des artistes !

Bon, la question maintenant est : « où trouver des musiques libres de droit ? ». On pourrait faire une recherche de « Free Music » sur Google, mais là pour le coup il n'est pas notre ami. En effet, allez savoir pourquoi, on a beau taper le mot « Free », on tombe quand même sur des sites qui nous proposent d'acheter des musiques !

Il existe heureusement des sites qui sont dédiés à la musique libre de droit. Là, je vous recommande Jamendo qui est un très bon site, mais ce n'est pas le seul qui existe dans le domaine.

Les chansons sont classées par style. Vous avez beaucoup de choix. On y trouve du bon, du moins bon, du très très bon, du très très nul… En fait, tout dépend de vos goûts et de votre réceptivité aux différents styles de musique. De préférence, prenez une chanson qui peut servir de musique de fond et qui correspond bien à l'univers de votre jeu.

Pour information, cette chanson provient de l'album « Lies and Speeches » du groupe français « Hype ». Pour en savoir plus sur « Hype », vous pouvez visiter leur page MySpace.

Je suis parfaitement conscient que les goûts et les couleurs ne se discutent pas. N'ayez donc pas peur de prendre une autre musique si celle-ci ne vous plaisait pas.

J'ai donc téléchargé l'album et je vais utiliser la chanson « Home » au format MP3.
Vous pouvez la télécharger directement si vous voulez faire des tests en même temps que moi. C'est un des avantages de la musique libre : on peut la copier / distribuer librement, donc ne nous gênons pas.

Les étapes à suivre pour jouer une musique

La seule différence est le flag donné à la fonction FMOD_System_CreateSound.
Au lieu de lui donner le flag FMOD_CREATESAMPLE, on lui donnera les flags suivants : FMOD_SOFTWARE, FMOD_2D et FMOD_CREATESTREAM.
Ne vous attardez pas trop sur la signification de ces flags ; celui qui nous intéresse le plus est FMOD_CREATESTREAM, car c'est lui qui dira à FMOD de charger la musique bout par bout.

Pour utiliser tous ces flags en même temps, on utilisera l'opérateur logique | de cette façon :

FMOD_System_CreateSound(system, "ma_musique.mp3", FMOD_SOFTWARE | FMOD_2D | FMOD_CREATESTREAM, 0, &sound);

Et voilà le travail !
Mais ce n'est pas tout. Dans le cas d'une musique, il peut être bien de savoir modifier le volume, gérer les répétitions de la chanson, la mettre en pause ou même l'arrêter. C'est ce genre de choses que nous allons voir maintenant.
Mais avant ça, nous aurons besoin de travailler sur les canaux directement.

Récupérer un canal ou un groupe de canaux

Dans des versions précédentes de la librairie FMOD, le simple numéro d'identification d'un canal suffisait pour pouvoir modifier le volume ou bien mettre en pause une chanson.
Depuis FMOD Ex, il y a eu un petit changement : à partir du numéro de canal, on utilise une fonction qui fournit un pointeur vers ce canal. L'idée est restée la même, seule l'implémentation a changé.
Un canal est défini comme étant du type FMOD_CHANNEL, et la fonction qui permet de récupérer un canal à partir d'un numéro id est FMOD_System_GetChannel.
Par exemple, si j'ai un objet système system et que je veux récupérer le canal n°9, il faut faire :

FMOD_CHANNEL *channel;
FMOD_System_GetChannel(system, 9, &channel);

Rien de plus simple !

  • Le premier paramètre est l'objet système.

  • Le deuxième est le numéro id du canal.

  • Le troisième est l'adresse du pointeur où l'on veut stocker l'information voulue.

Une fois qu'on aura notre pointeur de canal, on pourra facilement manipuler la musique (modifier le volume, mettre en pause…).

Notez qu'on peut aussi récupérer tout un groupe de canaux en un seul pointeur ; ça évite de refaire la même manipulation pour chaque canal distinct.
Le type d'un groupe de canaux est FMOD_CHANNELGROUP, et une des fonctions qui nous intéresse le plus est FMOD_System_GetMasterChannelGroup, car elle permet d'obtenir un pointeur vers la totalité des canaux utilisés par un objet système.
Le mode de fonctionnement de cette fonction est identique à la précédente.

Modifier le volume

Pour modifier le volume, on peut le faire soit pour un canal précis, soit pour tous les canaux.
Par exemple, pour le faire pour tous les canaux, il faut d'abord récupérer un pointeur vers le groupe de canaux, puis utiliser la fonction FMOD_ChannelGroup_SetVolume dont le prototype est :

FMOD_RESULT FMOD_ChannelGroup_SetVolume(
  FMOD_CHANNELGROUP *  channelgroup,
  float  volume
);

Le paramètre channelgroup est celui qu'on vient de récupérer.
Le paramètre volume est du type float, tel que 0.0 correspond au silence, et 1.0 correspond à une lecture pleine puissance (c'est cette valeur qui est par défaut).

Répétition de la chanson

On a souvent besoin de répéter la musique de fond. C'est justement ce que propose la fonction FMOD_Sound_SetLoopCount. Elle prend 2 paramètres :

  • Le pointeur vers la chanson.

  • Le nombre de fois qu'elle doit être répétée. Si vous mettez 1, la chanson sera donc lue deux fois. Si vous mettez un nombre négatif (comme -1), la chanson sera répétée à l'infini.

Avec ce code source, notre musique sera donc répétée à l'infini :

FMOD_Sound_SetLoopCount(musique, -1);

Pour que la répétition fonctionne, il faut envoyer FMOD_LOOP_NORMAL en troisième paramètre de la fonction FMOD_System_CreateSound.

Mettre en pause la chanson

Il y a ici 2 fonctions à connaître :

  • FMOD_Channel_GetPaused(canal, &etat) : indique si la chanson jouée sur le canal indiqué est en pause ou pas. Elle met vrai dans etat si la chanson est en pause, faux si elle est en train d'être jouée.

  • FMOD_Channel_SetPaused(canal, etat) : met en pause ou réactive la lecture de la chanson sur le canal indiqué. Envoyez 1 (vrai) pour mettre en pause, 0 (faux) pour réactiver la lecture.

Ce bout de code de fenêtre SDL met en pause la chanson si on appuie sur la touche P du clavier, et la réactive si on appuie à nouveau sur P.

case SDL_KEYDOWN:
    if (event.key.keysym.sym == SDLK_p) // Si on appuie sur P
    {
        FMOD_BOOL etat;
        FMOD_Channel_GetPaused(canal, &etat);

        if (etat == 1) // Si la chanson est en pause
            FMOD_Channel_SetPaused(canal, 0); // On enlève la pause
        else // Sinon, elle est en cours de lecture
            FMOD_Channel_SetPaused(canal, 1); // On met en pause
    }
    break;

Si on veut appliquer le même traitement à tous les canaux réunis, on utilisera les fonctions FMOD_ChannelGroup_GetPaused et FMOD_ChannelGroup_SetPaused, à la seule différence qu'il faut faire passer comme paramètre un FMOD_CHANNELGROUP au lieu d'un FMOD_CHANNEL.

Stopper la lecture

Il suffit d'appeler FMOD_Channel_Stop pour stopper une musique sur un canal, ou bien FMOD_ChannelGroup_Stop pour un ensemble de canaux. On leur envoie respectivement le pointeur vers le canal ou bien le pointeur vers le groupe de canaux.

Et bien d'autres choses

On peut faire beaucoup d'autres choses, mais je ne vais pas vous les énumérer toutes ici, autant lire la doc ! Je vous invite donc à y jeter un œil si vous cherchez des fonctions supplémentaires.

Libérer la mémoire

Pour décharger la musique de la mémoire, appelez FMOD_Sound_Release et donnez-lui le pointeur.

FMOD_Sound_Release(musique);

Code complet de lecture du MP3

Le code ci-dessous vous montre un programme jouant la musique « Home » qu'on a récupérée sur Jamendo.
La musique est jouée dès le début du programme. On peut la mettre en pause en appuyant sur P.

#include <stdlib.h>
#include <stdio.h>
#include <SDL/SDL.h>
#include <SDL/SDL_image.h>
#include <fmodex/fmod.h>

int main(int argc, char *argv[])
{
    SDL_Surface *ecran = NULL, *pochette = NULL;
    SDL_Event event;
    SDL_Rect position;
    int continuer = 1;

    FMOD_SYSTEM *system;
    FMOD_SOUND *musique;
    FMOD_RESULT resultat;

    FMOD_System_Create(&system);
    FMOD_System_Init(system, 1, FMOD_INIT_NORMAL, NULL);

    /* On ouvre la musique */
    resultat = FMOD_System_CreateSound(system, "hype_home.mp3", FMOD_SOFTWARE | FMOD_2D | FMOD_CREATESTREAM, 0, &musique);

    /* On vérifie si elle a bien été ouverte (IMPORTANT) */
    if (resultat != FMOD_OK)
    {
        fprintf(stderr, "Impossible de lire le fichier mp3\n");
        exit(EXIT_FAILURE);
    }

    /* On active la répétition de la musique à l'infini */
    FMOD_Sound_SetLoopCount(musique, -1);

    /* On joue la musique */
    FMOD_System_PlaySound(system, FMOD_CHANNEL_FREE, musique, 0, NULL);

    SDL_Init(SDL_INIT_VIDEO);

    ecran = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE | SDL_DOUBLEBUF);
    SDL_WM_SetCaption("Gestion du son avec FMOD", NULL);
    pochette = IMG_Load("hype_liesandspeeches.jpg");
    position.x = 0;
    position.y = 0;

    while (continuer)
    {
        SDL_WaitEvent(&event);
        switch(event.type)
        {
        case SDL_QUIT:
            continuer = 0;
            break;
        case SDL_KEYDOWN:
            if (event.key.keysym.sym == SDLK_p) //Si on appuie sur P
            {
                FMOD_CHANNELGROUP *canal;
                FMOD_BOOL etat;
                FMOD_System_GetMasterChannelGroup(system, &canal);
                FMOD_ChannelGroup_GetPaused(canal, &etat);

                if (etat) // Si la chanson est en pause
                    FMOD_ChannelGroup_SetPaused(canal, 0); // On enlève la pause
                else // Sinon, elle est en cours de lecture
                    FMOD_ChannelGroup_SetPaused(canal, 1); // On active la pause
            }
            break;
        }

        SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 0, 0, 0));
        SDL_BlitSurface(pochette, NULL, ecran, &position);
        SDL_Flip(ecran);
    }

    FMOD_Sound_Release(musique);
    FMOD_System_Close(system);
    FMOD_System_Release(system);

    SDL_FreeSurface(pochette);
    SDL_Quit();

    return EXIT_SUCCESS;
}

Histoire d'avoir autre chose qu'une fenêtre noire, j'ai mis la pochette de l'album en image de fond.

Pour apprécier pleinement le résultat, je vous invite à regarder la vidéo du programme en cours d'exécution.

Voir la vidéo d'une musique jouée avec FMOD (730 Ko)

En résumé

  • La SDL possède des fonctionnalités audio limitées et il est plutôt conseillé de se pencher sur une bibliothèque dédiée au son, comme FMOD.

  • On distingue 2 types de sons avec FMOD : des sons courts (un bruit de pas par exemple) et des sons longs (une musique par exemple).

  • Chacun de ces types se lit avec la même fonction mais avec des flags différents en option.

  • FMOD permet de jouer simultanément plusieurs sons différents à l'aide de plusieurs canaux.


Apprenez à programmer en C

TP : visualisation spectrale du son

Ce chapitre de travaux pratiques va vous proposer de manipuler la SDL et FMOD simultanément. Cette fois, nous n'allons pas travailler sur un jeu. Certes, la SDL est tout particulièrement adaptée à cela, mais on peut l'utiliser dans d'autres domaines. Ce chapitre va justement vous prouver qu'elle peut servir à autre chose.

Nous allons réaliser ici une visualisation du spectre sonore en SDL. Cela consiste à afficher la composition du son que l'on joue, par exemple une musique. On retrouve cela dans de nombreux lecteurs audio. C'est amusant et ce n'est pas si compliqué que ça en a l'air !

Ce chapitre va nous permettre de travailler autour de notions que nous avons découvertes récemment :

  • la gestion du temps ;

  • la bibliothèque FMOD.

Nous découvrirons en outre comment modifier une surface pixel par pixel.

La figure suivante vous donne un aperçu du programme que nous allons créer dans ce chapitre.

Visualisation du spectre sonore

C'est le genre de visualisation qu'on peut retrouver dans des lecteurs audio tels que Winamp, Windows Media Player ou encore AmaroK.
Et pour ne rien gâcher, comme je vous l'ai dit ce n'est pas bien difficile à faire. D'ailleurs, contrairement au TP Mario Sokoban, cette fois c'est vous qui allez travailler. Ça vous fera un très bon exercice.

Les consignes

Les consignes sont simples. Suivez-les pas à pas dans l'ordre, et vous n'aurez pas d'ennuis.

1/ Lire un MP3

Pour commencer, vous devez créer un programme qui lit un fichier MP3. Vous n'avez qu'à reprendre la chanson « Home » du groupe Hype que nous avons utilisée dans le chapitre sur FMOD pour illustrer le fonctionnement de la lecture d'une musique.

Si vous avez bien suivi le chapitre sur FMOD, il ne vous faudra pas plus de quelques minutes pour arriver à le faire. Je vous conseille au passage de placer le MP3 dans le dossier de votre projet.

2/ Récupérer les données spectrales du son

Pour comprendre comment la visualisation spectrale fonctionne, il est indispensable que je vous explique un peu comment cela fonctionne à l'intérieur (pas dans le détail non plus, sinon ça va se transformer en cours de maths).

Un son peut être découpé en fréquences. Certaines fréquences sont basses, d'autres moyennes, et d'autres hautes.
Ce que nous allons faire dans notre visualisation, c'est afficher la quantité de chacune de ces fréquences sous forme de barres. Plus la barre est grande, plus la fréquence est utilisée (figure suivante).

La visualisation des fréquences d'un son

Sur la gauche de la fenêtre, nous faisons donc apparaître les basses fréquences, et sur la droite les hautes fréquences.

Mais comment récupérer les quantités de chaque fréquence ?

FMOD nous mâche le travail. On peut faire appel à la fonction FMOD_Channel_GetSpectrum. Son prototype est le suivant :

FMOD_RESULT FMOD_Channel_GetSpectrum(
  FMOD_CHANNEL *  channel,
  float *  spectrumarray,
  int  numvalues,
  int  channeloffset,
  FMOD_DSP_FFT_WINDOW  windowtype
);

Et voici ses paramètres :

  • Le canal sur lequel la musique est jouée. Donc a priori il faut récupérer un pointeur vers ce canal.

  • Un tableau de float. Il faut que ce tableau soit déjà alloué, statiquement ou dynamiquement, pour permettre à FMOD de le remplir correctement.

  • La taille du tableau. Cette taille doit obligatoirement être une puissance de 2, par exemple 512.

  • Ce paramètre sert à définir à quelle sortie on s'intéresse. Par exemple si vous êtes en stéréo, 0 veut dire gauche, et 1 veut dire droite.

  • Ce paramètre est un peu plus complexe, et ne nous intéresse pas vraiment dans ce cours. On se contentera de lui donner la valeur FMOD_DSP_FFT_WINDOW_RECT.

Rappel : le type floatest un type décimal, au même titre que double. La différence entre les deux vient du fait que double est plus précis que float, mais dans notre cas, le type float est suffisant. C'est celui utilisé par FMOD ici, donc c'est celui que nous devrons utiliser nous aussi.

En clair, on déclare notre tableau de float :

float spectre[512];

Ensuite, lorsque la musique est en train d'être jouée, on demande à FMOD de remplir le tableau du spectre en faisant par exemple :

FMOD_Channel_GetSpectrum(canal, spectre, 512, 0,  FMOD_DSP_FFT_WINDOW_RECT);

On peut ensuite parcourir ce tableau pour obtenir les valeurs de chacune des fréquences :

spectre[0] // Fréquence la plus basse (à gauche)
spectre[1]
spectre[2]
...
spectre[509]
spectre[510]
spectre[511] // Fréquence la plus haute (à droite)

Chaque fréquence est un nombre décimal compris entre 0 (rien) et 1 (maximum). Votre travail va consister à afficher une barre plus ou moins grande en fonction de la valeur que contient chaque case du tableau.

Par exemple, si la valeur est 0.5, vous devrez tracer une barre dont la hauteur correspondra à la moitié de la fenêtre.
Si la valeur est 1, elle devra faire toute la hauteur de la fenêtre.

Généralement, les valeurs sont assez faibles (plutôt proches de 0 que de 1). Je recommande de multiplier par 20 toutes les valeurs pour mieux voir le spectre.
Attention : si vous faites ça, vérifiez que vous ne dépassez pas 1 (arrondissez à 1 s'il le faut). Si vous vous retrouvez avec des valeurs supérieures à 1, vous risquez d'avoir des problèmes pour tracer les barres verticales par la suite !

Mais les barres doivent bouger au fur et à mesure du temps non ? Comme le son change tout le temps, il faut mettre à jour le graphique. Comment faire ?

Bonne question. En effet, le tableau de 512 float que vous renvoie FMOD change toutes les 25 ms (pour être à jour par rapport au son actuel). Il va donc falloir dans votre code que vous relisiez le tableau de 512 floats (en refaisant appel à FMOD_Channel_GetSpectrum toutes les 25 ms), puis que vous mettiez à jour votre graphique en barres.

Relisez le chapitre sur la gestion du temps en SDL pour vous rappeler comment faire. Vous avez le choix entre une solution à base de GetTicks ou à base de callbacks. Faites ce qui vous paraît le plus facile.

4/ Réaliser le dégradé

Dans un premier temps, vous pouvez réaliser des barres de couleur unie. Vous pourrez donc créer des surfaces. Il devra y avoir 512 surfaces : une pour chaque barre. Chaque surface fera donc 1 pixel de large et la hauteur des surfaces variera en fonction de l'intensité de chaque fréquence.

Toutefois, je vous propose ensuite d'effectuer une amélioration : la barre doit tendre vers le rouge lorsque le son devient de plus en plus intense. En clair, la barre doit être verte en bas et rouge en haut.

Mais… une surface ne peut avoir qu'une seule couleur si on utilise SDL_FillRect(). On ne peut pas faire de dégradé !

En effet. On pourrait certes créer des surfaces de 1 pixel de large et 1 pixel de haut pour chaque couleur du dégradé, mais ça ferait vraiment beaucoup de surfaces à gérer et ce ne serait pas très optimisé !

Comment fait-on pour dessiner pixel par pixel ?
Je ne vous l'ai pas appris auparavant, car cette technique ne méritait pas un chapitre entier. Vous allez voir en effet que ce n'est pas bien compliqué.

En fait, la SDL ne propose aucune fonction pour dessiner pixel par pixel. Mais on a le droit de l'écrire nous-mêmes.

Pour ce faire, il faut suivre ces étapes méthodiquement dans l'ordre :

    1. Faites appel à la fonction SDL_LockSurfacepour annoncer à la SDL que vous allez modifier la surface manuellement. Cela « bloque » la surface pour la SDL et vous êtes le seul à y avoir accès tant que la surface est bloquée.

Ici, je vous conseille de ne travailler qu'avec une seule surface : l'écran. Si vous voulez dessiner un pixel à un endroit précis de l'écran, vous devrez donc bloquer la surface ecran :

SDL_LockSurface(ecran);

    1. Vous pouvez ensuite modifier le contenu de chaque pixel de la surface. Comme la SDL ne propose aucune fonction pour faire ça, il va falloir l'écrire nous-même dans notre programme.

Cette fonction, je vous la donne. Je la tire de la documentation de la SDL. Elle est un peu compliquée car elle travaille sur la surface directement et gère toutes les profondeurs de couleurs (bits par pixel) possibles. Pas besoin de la retenir ou de la comprendre, copiez-la simplement dans votre programme pour pouvoir l'utiliser :

void setPixel(SDL_Surface *surface, int x, int y, Uint32 pixel)
{
    int bpp = surface->format->BytesPerPixel;

    Uint8 *p = (Uint8 *)surface->pixels + y * surface->pitch + x * bpp;

    switch(bpp) {
    case 1:
        *p = pixel;
        break;

    case 2:
        *(Uint16 *)p = pixel;
        break;

    case 3:
        if(SDL_BYTEORDER == SDL_BIG_ENDIAN) {
            p[0] = (pixel >> 16) & 0xff;
            p[1] = (pixel >> 8) & 0xff;
            p[2] = pixel & 0xff;
        } else {
            p[0] = pixel & 0xff;
            p[1] = (pixel >> 8) & 0xff;
            p[2] = (pixel >> 16) & 0xff;
        }
        break;

    case 4:
        *(Uint32 *)p = pixel;
        break;
    }
}

Elle est simple à utiliser. Envoyez les paramètres suivants :

  • le pointeur vers la surface à modifier (cette surface doit préalablement avoir été bloquée avec SDL_LockSurface) ;

  • la position en abscisse du pixel à modifier dans la surface (x) ;

  • la position en ordonnée du pixel à modifier dans la surface (y) ;

  • la nouvelle couleur à donner à ce pixel. Cette couleur doit être au format Uint32 ; vous pouvez donc la générer à l'aide de la fonction SDL_MapRGB() que vous connaissez bien maintenant.

  • Enfin, lorsque vous avez fini de travailler sur la surface, il ne faut pas oublier de la débloquer en appelant SDL_UnlockSurface.

SDL_UnlockSurface(ecran);
Code résumé d'exemple

Si on résume, vous allez voir que c'est tout simple.
Ce code dessine un pixel rouge au milieu de la surface ecran (donc au milieu de la fenêtre).

SDL_LockSurface(ecran); /* On bloque la surface */
setPixel(ecran, ecran->w / 2, ecran->h / 2, SDL_MapRGB(ecran->format, 255, 0, 0)); /* On dessine un pixel rouge au milieu de l'écran */
SDL_UnlockSurface(ecran); /* On débloque la surface*/

Avec cette base vous devriez pouvoir réaliser des dégradés du vert au rouge (un indice : il faut utiliser des boucles :p ).

La solution

Alors, comment avez-vous trouvé le sujet ? Il n'est pas bien difficile à appréhender, il faut juste faire quelques calculs, surtout pour la réalisation du dégradé. C'est du niveau de tout le monde, il faut juste réfléchir un petit peu.

Certains mettent plus de temps que d'autres pour trouver la solution. Si vous avez du mal, ce n'est pas bien grave. Ce qui compte, c'est de finir par y arriver. Quel que soit le projet dans lequel vous vous lancerez, vous aurez forcément des moments où il ne suffit pas de savoir programmer ; il faut aussi être logique et bien réfléchir.

Je vous donne le code complet ci-dessous. Il est suffisamment commenté.

#include <stdlib.h>
#include <stdio.h>
#include <SDL/SDL.h>
#include <fmodex/fmod.h>

#define LARGEUR_FENETRE         512 /* DOIT rester à 512 impérativement car il y a 512 barres (correspondant aux 512 floats) */
#define HAUTEUR_FENETRE         400 /* Vous pouvez la faire varier celle-là par contre */
#define RATIO                   (HAUTEUR_FENETRE / 255.0)
#define DELAI_RAFRAICHISSEMENT  25 /* Temps en ms entre chaque mise à jour du graphe. 25 ms est la valeur minimale. */
#define TAILLE_SPECTRE          512

void setPixel(SDL_Surface *surface, int x, int y, Uint32 pixel);

int main(int argc, char *argv[])
{
    SDL_Surface *ecran = NULL;
    SDL_Event event;
    int continuer = 1, hauteurBarre = 0, tempsActuel = 0, tempsPrecedent = 0, i = 0, j = 0;
    float spectre[TAILLE_SPECTRE];

    /* Initialisation de FMOD
       ----------------------

       On charge FMOD, la musique on lance la lecture de la musique.

    */

    FMOD_SYSTEM *system;
    FMOD_SOUND *musique;
    FMOD_CHANNEL *canal;
    FMOD_RESULT resultat;

    FMOD_System_Create(&system);
    FMOD_System_Init(system, 1, FMOD_INIT_NORMAL, NULL);

    /* On ouvre la musique */
    resultat = FMOD_System_CreateSound(system, "hype_home.mp3", FMOD_SOFTWARE | FMOD_2D | FMOD_CREATESTREAM, 0, &musique);

    /* On vérifie si elle a bien été ouverte (IMPORTANT) */
    if (resultat != FMOD_OK)
    {
        fprintf(stderr, "Impossible de lire le fichier mp3\n");
        exit(EXIT_FAILURE);
    }

    /* On joue la musique */
    FMOD_System_PlaySound(system, FMOD_CHANNEL_FREE, musique, 0, NULL);

    /* On récupère le pointeur du canal */
    FMOD_System_GetChannel(system, 0, &canal);

    /* Initialisation de la SDL
       ------------------------

       On charge la SDL, on ouvre la fenêtre et on écrit dans sa barre de titre
       On récupère au passage un pointeur vers la surface ecran
       Qui sera la seule surface utilisée dans ce programme */

    SDL_Init(SDL_INIT_VIDEO);
    ecran = SDL_SetVideoMode(LARGEUR_FENETRE, HAUTEUR_FENETRE, 32, SDL_SWSURFACE | SDL_DOUBLEBUF);
    SDL_WM_SetCaption("Visualisation spectrale du son", NULL);

    /* Boucle principale */

    while (continuer)
    {
        SDL_PollEvent(&event); // On doit utiliser PollEvent car il ne faut pas attendre d'évènement de l'utilisateur pour mettre à jour la fenêtre
        switch(event.type)
        {
        case SDL_QUIT:
            continuer = 0;
            break;
        }

        /* On efface l'écran à chaque fois avant de dessiner le graphe (fond noir) */
        SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 0, 0, 0));

        /* Gestion du temps
           -----------------
           On compare le temps actuel par rapport au temps précédent (dernier passage dans la boucle)
           Si ça fait moins de 25 ms (DELAI_RAFRAICHISSEMENT)
           Alors on attend le temps qu'il faut pour qu'au moins 25 ms se soient écoulées.
           On met ensuite à jour tempsPrecedent avec le nouveau temps */

        tempsActuel = SDL_GetTicks();
        if (tempsActuel - tempsPrecedent < DELAI_RAFRAICHISSEMENT)
        {
            SDL_Delay(DELAI_RAFRAICHISSEMENT - (tempsActuel - tempsPrecedent));
        }
        tempsPrecedent = SDL_GetTicks();

        /* Dessin du spectre sonore
           ------------------------

           C'est la partie la plus intéressante. Il faut réfléchir un peu à la façon de dessiner pour y arriver, mais c'est tout à fait faisable (la preuve).

           On remplit le tableau de 512 floats via FMOD_Channel_GetSpectrum()
           On travaille ensuite pixel par pixel sur la surface ecran pour dessiner les barres.
           On fait une première boucle pour parcourir la fenêtre en largeur.
           La seconde boucle parcourt la fenêtre en hauteur pour dessiner chaque barre.
        */

        /* On remplit le tableau de 512 floats. J'ai choisi de m'intéresser à la sortie gauche */
        FMOD_Channel_GetSpectrum(canal, spectre, TAILLE_SPECTRE, 0,  FMOD_DSP_FFT_WINDOW_RECT);

        SDL_LockSurface(ecran); /* On bloque la surface ecran car on va directement modifier ses pixels */

        /* BOUCLE 1 : on parcourt la fenêtre en largeur (pour chaque barre verticale) */
        for (i = 0 ; i < LARGEUR_FENETRE ; i++)
        {
            /* On calcule la hauteur de la barre verticale qu'on va dessiner.
               spectre[i] nous renvoie un nombre entre 0 et 1 qu'on multiplie par 20 pour zoomer afin de voir un peu mieux (comme je vous avais dit).
               On multiplie ensuite par HAUTEUR_FENETRE pour que la barre soit agrandie par rapport à la taille de la fenêtre. */
            hauteurBarre = spectre[i] * 20 * HAUTEUR_FENETRE;

            /* On vérifie que la barre ne dépasse pas la hauteur de la fenêtre
               Si tel est le cas on coupe la barre au niveau de la hauteur de la fenêtre. */
            if (hauteurBarre > HAUTEUR_FENETRE)
                hauteurBarre = HAUTEUR_FENETRE;

            /* BOUCLE 2 : on parcourt en hauteur la barre verticale pour la dessiner */
            for (j = HAUTEUR_FENETRE - hauteurBarre ; j < HAUTEUR_FENETRE ; j++)
            {
                /* On dessine chaque pixel de la barre à la bonne couleur.
                   On fait simplement varier le rouge et le vert, chacun dans un sens différent.

                   j ne varie pas entre 0 et 255 mais entre 0 et HAUTEUR_FENETRE.
                   Si on veut l'adapter proportionnellement à la hauteur de la fenêtre, il suffit de faire le calcul j / RATIO, où RATIO vaut (HAUTEUR_FENETRE / 255.0).
                   J'ai dû réfléchir 2-3 minutes pour trouver le bon calcul à faire, mais c'est du niveau de tout le monde. Il suffit de réfléchir un tout petit peu */
                setPixel(ecran, i, j, SDL_MapRGB(ecran->format, 255 - (j / RATIO), j / RATIO, 0));
            }
        }

        SDL_UnlockSurface(ecran); /* On a fini de travailler sur l'écran, on débloque la surface */

        SDL_Flip(ecran);
    }

    /* Le programme se termine.
       On libère la musique de la mémoire
       et on ferme FMOD et SDL */

    FMOD_Sound_Release(musique);
    FMOD_System_Close(system);
    FMOD_System_Release(system);

    SDL_Quit();

    return EXIT_SUCCESS;
}

/* La fonction setPixel permet de dessiner pixel par pixel dans une surface */
void setPixel(SDL_Surface *surface, int x, int y, Uint32 pixel)
{
    int bpp = surface->format->BytesPerPixel;

    Uint8 *p = (Uint8 *)surface->pixels + y * surface->pitch + x * bpp;

    switch(bpp) {
    case 1:
        *p = pixel;
        break;

    case 2:
        *(Uint16 *)p = pixel;
        break;

    case 3:
        if(SDL_BYTEORDER == SDL_BIG_ENDIAN) {
            p[0] = (pixel >> 16) & 0xff;
            p[1] = (pixel >> 8) & 0xff;
            p[2] = pixel & 0xff;
        } else {
            p[0] = pixel & 0xff;
            p[1] = (pixel >> 8) & 0xff;
            p[2] = (pixel >> 16) & 0xff;
        }
        break;

    case 4:
        *(Uint32 *)p = pixel;
        break;
    }
}

Vous devriez obtenir un résultat correspondant à la figure suivante.

Visualisation spectrale d'un son

Bien entendu, il vaut mieux une animation pour apprécier ce résultat. C'est donc ce que je vous propose de visualiser.

Voir l'animation "Visualisation spectrale du son" (4,3 Mo)

Notez que la compression a réduit la qualité du son et le nombre d'images par seconde.
Le mieux est encore de télécharger le programme complet (avec son code source) pour tester chez soi. Vous pourrez ainsi apprécier le programme dans les meilleures conditions. :)

Télécharger le projet Code::Blocks complet + l'exécutable Windows (335 Ko)

Il faut impérativement que le fichier Hype_Home.mp3 soit placé dans le dossier du programme pour qu'il fonctionne (sinon il s'arrêtera tout de suite).

Idées d'amélioration

Il est toujours possible d'améliorer un programme. Ici, j'ai par exemple des tonnes d'idées d'extensions qui pourraient aboutir à la création d'un véritable petit lecteur MP3.

  • Il serait bien qu'on puisse choisir le MP3 qu'on veut lire. Il faudrait par exemple lister tous les .mp3 présents dans le dossier du programme. Nous n'avons pas vu comment faire ça, mais vous pouvez le découvrir par vous-mêmes. Un indice : utilisez la librairie dirent (il faudra inclure dirent.h). À vous de chercher des informations sur le web pour savoir comment l'utiliser.

  • Si votre programme était capable de lire et gérer les playlists, ça serait encore mieux. Il existe plusieurs formats de playlist, le plus connu est le format M3U.

  • Vous pourriez afficher le nom du MP3 en cours de lecture dans la fenêtre (il faudra utiliser SDL_ttf).

  • Vous pourriez afficher un indicateur pour qu'on sache où en est la lecture du morceau, comme cela se fait sur la plupart des lecteurs MP3.

  • Vous pourriez aussi proposer de modifier le volume de lecture.

  • etc.

Bref, il y a beaucoup à faire. Vous avez la possibilité de créer de beaux lecteurs, il ne tient plus qu'à vous de les coder !


Site WEB

SDL

Une page de Wikiversité.

la SDL "Simple DirectMedia Layer" est une librairie libre multiplateforme qui permet de faire des programmes avec une interface graphique, qui prend en compte les images, les sons et les entrées du clavier, de la souris, des manettes, et bien d'autres...

Installation de la libraire

Code de base

#include <stdlib.h>
#include <stdio.h>

//include de la sdl en fonction du dossier dans lequel vous l'avez installée

#include <SDL/SDL.h>

int main(int argc, char *argv[])
{
//permet de créer une variable de type sdl qui gère les surfaces

SDL_Surface *ecran = NULL;

//variable de type sdl qui gère les événements du clavier

SDL_Event event;

// simple int qui servira à savoir s'il faut fermer le programme ou non

int continuer = 1;

//initialisation de la sdl

SDL_Init(SDL_INIT_VIDEO);

//fonction de la sdl qui permet de nommer le programme au dessus de la fenêtre

SDL_WM_SetCaption("Nom du programme",NULL);

/*permet de paramétrer la surface qui servira de fenêtre qui prend quatre valeur
'définition axe x',
'définition axe y',
'en quel nombre de bits le programme doit tourner',
'le type de mémoire utilisé, ici c'est la mémoire de la RAM mais j'aurais pu mettre la mémoire de la carte graphique qui est plus rapide' */

ecran = SDL_SetVideoMode(640,480,32,SDL_SWSURFACE);

// le while qui fera tourner en boucle le cœur du programme

while(continuer)
{
/* fonction qui permet de prendre en compte les événements du clavier,
 mais en waitevent qui signifie que le programme ne fais rien tant qu'il ne reçoit aucun
événement*/

 SDL_WaitEvent(&event);

 //switch qui reçoit les entrées du clavier que l'on peut ensuite conditionner avec des case

 switch(event.type)
 {
     // case qui teste si l'on clique sur l'icone quitter de la fenêtre
     case SDL_QUIT:
          //cela signifie que la boucle while sera fini
          continuer = 0;
          break;
 }
//permet de colorer la surface ecran en blanc

SDL_FillRect(ecran,NULL,SDL_MapRGB(ecran->format,255,255,255));
//flip permet d'actualiser la surface ecran qui je vous le rappelle sert de fenêtre
SDL_Flip(ecran);
}
//permet de désallouer tous les élément de la sdl qui on été chargés en initiant la sdl
SDL_Quit();
//retourne exit_success qui veut dire que le programme s'est bien terminé
return EXIT_SUCCESS;
}

Liens utile

site officiel de SDL

  • (en) http://www.libsdl.org/

SiteDuZero

Le site contient d'excellents tutoriels qui permettent d'apprendre à utiliser le langage C et SDL.

  • (fr) http://www.siteduzero.com/

Récupérée de « http://fr.wikiversity.org/w/index.php?title=SDL&oldid=460435 »

Catégories :

  • Leçons du jour de niveau 15
  • Programmation informatique

Catégories cachées :

  • Inter-projet avec Wikpédia
  • Leçons du jour sans discussion
  • Leçons du jour créées en 2010
  • Leçons du jour de la faculté informatique


Site WEB

Using SDL_net

This tutorial introduces the use of networking in your application, using the SDL_net library. Knowledge of BSD Sockets is recommended before starting with this tutorial. Using TCP If you need a reliable - but slow - connection, TCP fit your needs: this protocol is connection oriented - that is: you connect to a host, and transfer data using a socket. Data will arrive at the destination in order and will not be corrupted. In games, TCP is used mostly when you don't need a fast connection, since TCP can really slow down communications, but when you need to be sure that data arrives in order, and is not corrupted (in this case, UDP isn't the best choice).
In this simple client-server example, we do the following:

Server side
1.Initialize SDL_net library
2.Set a port to listen on
3.Open a socket bound to that port
4.Accept a connection
5.Receive data
6.Do something with the data
7.Clean and quit

Client side
1.Initialize SDL_net library
2.Resolve the server address
3.Open a socket
4.Read data from the user
5.Send the data
6.Clean and exit

If you are experienced with sockets, you'll find that SDL_net is easier to manage.

After accepting the connection, we get some info about the host. This is optional, written here just for the sake of this tutorial.

Note also that this server accepts and manages only one connection at time, but of course you can use these techniques to manage many connections together (i.e. using threads).
Code Here's the server code, with a compilation script included in the header:

And here is the code for the client:

#if 0
#!/bin/sh
gcc -Wall `sdl-config --cflags` tcpc.c -o tcpc `sdl-config --libs` -lSDL_net

exit
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "SDL_net.h"

int main(int argc, char **argv)
{
        IPaddress ip;          /* Server address */
        TCPsocket sd;          /* Socket descriptor */
        int quit, len;
        char buffer[512];

        /* Simple parameter checking */
        if (argc < 3)
        {
               fprintf(stderr, "Usage: %s host port\n", argv[0]);
               exit(EXIT_FAILURE);
        }

        if (SDLNet_Init() < 0)
        {
               fprintf(stderr, "SDLNet_Init: %s\n", SDLNet_GetError());
               exit(EXIT_FAILURE);
        }

        /* Resolve the host we are connecting to */
        if (SDLNet_ResolveHost(&ip, argv[1], atoi(argv[2])) < 0)
        {
               fprintf(stderr, "SDLNet_ResolveHost: %s\n", SDLNet_GetError());
               exit(EXIT_FAILURE);
        }

        /* Open a connection with the IP provided (listen on the host's port) */
        if (!(sd = SDLNet_TCP_Open(&ip)))
        {
               fprintf(stderr, "SDLNet_TCP_Open: %s\n", SDLNet_GetError());
               exit(EXIT_FAILURE);
        }

        /* Send messages */
        quit = 0;
        while (!quit)
        {
               printf("Write something:\n>");
               scanf("%s", buffer);

               len = strlen(buffer) + 1;
               if (SDLNet_TCP_Send(sd, (void *)buffer, len) < len)
               {
                       fprintf(stderr, "SDLNet_TCP_Send: %s\n", SDLNet_GetError());
                       exit(EXIT_FAILURE);
               }

               if(strcmp(buffer, "exit") == 0)
                       quit = 1;
               if(strcmp(buffer, "quit") == 0)
                       quit = 1;
        }

        SDLNet_TCP_Close(sd);
        SDLNet_Quit();

        return EXIT_SUCCESS;
}

Using UDP

In most games the protocol used is UDP, because it is faster than TCP. The speed of course doesn't come for free: UDP does not provide a reliable way to send packets since UDP packets are not guaranteed to reach their destination, and the packets may also arrive out of order. UDP is a connectionless protocol, this means that you don't need to establish a connection between hosts, but you simply send a message to a specified port or wait for a message on a port.

Anyway, SDL_net provides a half-way method for using connections with UDP: you can specify a channel and bind a remote host (and the relative port) to the UDP socket. In this way you don't need to supply the address of the source packet everytime you send something.

In this example of using UDP in connectionless mode, we do the following:

Server side
  1. Initialize SDL_net library
  2. Open a socket on a specified port
  3. Allocate memory for packets
  4. Wait for a packet from the client
  5. Do something with the packet
  6. Free the memory and quit
Client side
  1. Initialize SDL_net library
  2. Open a socket on a random usable port
  3. Resolve the server address
  4. Allocate memory for packets
  5. Fill and send the packet
  6. Free the memory and quit

As you can see, the procedure is quite simple, considering that initialization and freeing memory is done usually only once.

Code

Now let's write the code for this application.

File udps.c the server, with compiling script:

#if 0
#!/bin/sh
gcc -Wall `sdl-config --cflags` udps.c -o udps `sdl-config --libs` -lSDL_net

exit
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "SDL_net.h"

int main(int argc, char **argv)
{
        UDPsocket sd;       /* Socket descriptor */
        UDPpacket *p;       /* Pointer to packet memory */
        int quit;

        /* Initialize SDL_net */
        if (SDLNet_Init() < 0)
        {
               fprintf(stderr, "SDLNet_Init: %s\n", SDLNet_GetError());
               exit(EXIT_FAILURE);
        }

        /* Open a socket */
        if (!(sd = SDLNet_UDP_Open(2000)))
        {
               fprintf(stderr, "SDLNet_UDP_Open: %s\n", SDLNet_GetError());
               exit(EXIT_FAILURE);
        }

        /* Make space for the packet */
        if (!(p = SDLNet_AllocPacket(512)))
        {
               fprintf(stderr, "SDLNet_AllocPacket: %s\n", SDLNet_GetError());
               exit(EXIT_FAILURE);
        }

        /* Main loop */
        quit = 0;
        while (!quit)
        {
               /* Wait a packet. UDP_Recv returns != 0 if a packet is coming */
               if (SDLNet_UDP_Recv(sd, p))
               {
                       printf("UDP Packet incoming\n");
                       printf("\tChan:    %d\n", p->channel);
                       printf("\tData:    %s\n", (char *)p->data);
                       printf("\tLen:     %d\n", p->len);
                       printf("\tMaxlen:  %d\n", p->maxlen);
                       printf("\tStatus:  %d\n", p->status);
                       printf("\tAddress: %x %x\n", p->address.host, p->address.port);

                       /* Quit if packet contains "quit" */
                       if (strcmp((char *)p->data, "quit") == 0)
                               quit = 1;
               }
        }

        /* Clean and exit */
        SDLNet_FreePacket(p);
        SDLNet_Quit();
        return EXIT_SUCCESS;
}

File udpc.c the client, with autocompiling script:

#if 0
#!/bin/sh

gcc -Wall `sdl-config --cflags` udpc.c -o udpc `sdl-config --libs` -lSDL_net

exit
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "SDL_net.h"

int main(int argc, char **argv)
{
        UDPsocket sd;
        IPaddress srvadd;
        UDPpacket *p;
        int quit;

        /* Check for parameters */
        if (argc < 3)
        {
               fprintf(stderr, "Usage: %s host port\n", argv[0]);
               exit(EXIT_FAILURE);
        }

        /* Initialize SDL_net */
        if (SDLNet_Init() < 0)
        {
               fprintf(stderr, "SDLNet_Init: %s\n", SDLNet_GetError());
               exit(EXIT_FAILURE);
        }

        /* Open a socket on random port */
        if (!(sd = SDLNet_UDP_Open(0)))
        {
               fprintf(stderr, "SDLNet_UDP_Open: %s\n", SDLNet_GetError());
               exit(EXIT_FAILURE);
        }

        /* Resolve server name  */
        if (SDLNet_ResolveHost(&srvadd, argv[1], atoi(argv[2])) == -1)
        {
               fprintf(stderr, "SDLNet_ResolveHost(%s %d): %s\n", argv[1], atoi(argv[2]), SDLNet_GetError());
               exit(EXIT_FAILURE);
        }

        /* Allocate memory for the packet */
        if (!(p = SDLNet_AllocPacket(512)))
        {
               fprintf(stderr, "SDLNet_AllocPacket: %s\n", SDLNet_GetError());
               exit(EXIT_FAILURE);
        }

        /* Main loop */
        quit = 0;
        while (!quit)
        {
               printf("Fill the buffer\n>");
               scanf("%s", (char *)p->data);

               p->address.host = srvadd.host; /* Set the destination host */
               p->address.port = srvadd.port; /* And destination port */

               p->len = strlen((char *)p->data) + 1;
               SDLNet_UDP_Send(sd, -1, p); /* This sets the p->channel */

               /* Quit if packet contains "quit" */
               if (!strcmp((char *)p->data, "quit"))
                       quit = 1;
        }

        SDLNet_FreePacket(p);
        SDLNet_Quit();

        return EXIT_SUCCESS;
}

In these examples no channel is used, so no binds are used. Anyway you can bind the connection in the client using

 SDLNet_UDP_Bind(udpSocketDescriptor, channelNumber, &serverIPIddress)
Compiling script note

As you can see, these sources (tested on Gentoo GNU/Linux 2005.1) have a compiling script in the header. This method is easy and fast, but portability is relative. So i think i shoud explain how this works: The row:

#if 0

is used to prevent the script from being read by the compiler. In this way the script isn't processed, and won't generate errors. Of course, we need to add:

 exit
 #endif

to exit the script and the comment. The line:

 #!/bin/sh

is a directive to the shell, which specifies that the script should be executed with the Shell interpreter, whose binary file is under /bin/. Next, the compiling row:

 gcc -Wall `sdl-config --cflags` tcps.c -o tcps `sdl-config --libs` -lSDL_net

gcc is of course the GNU C Compiler, used to compile and link the source code. Note the use of sdl-config: the correct way to compile SDL is using this tool. This should be in any system where SDL is installed, and this tool provides help locating the correct directories where header files (sdl-config --cflags) and libraries (sdl-config --libs) are stored. To be more precise, some systems (like FreeBSD) don't have the sdl-config executable, but another one which works similarly (actually, sdl11-config if I'm not wrong). Since every OS can have different tools to locate the headers/libs directory, a standard way has been defined: If the enviroment variable SDL_CONFIG is set, then use its value as the path to the tool, otherwise use the sdl-config tool as default.

So a partially correct compiling script should be:

 #!/bin/sh
 if [ -z "$SDL_CONFIG" ]; then
        SDL_CONFIG="sdl-config" # You should check if sdl-config exists
 fi
 gcc `$SDL_CONFIG --cflags` blah blah `$SDL_CONFIG --libs`

To be really correct, you should also check if the tool sdl-config is present on the system, using the "which" command.

Retrieved from ‘http://content.gpwiki.org/index.php?title=SDL:Tutorial:Using_SDL_net&oldid=30390’

Categories:

  • C and SDL NET
  • All C articles
  • All SDL NET articles


Site WEB

Simple DirectMedia Layer

Simple DirectMedia Layer (SDL) est une bibliothèque très utilisée dans le monde de la création d'applications multimédias en deux dimensions comme les jeux vidéo, les démos graphiques, les émulateurs, etc. Sa simplicité, sa flexibilité, sa portabilité et surtout sa licence zlib, très permissive..., contribuent à son grand succès. Elle est de plus considérée [Par qui ?] comme un outil suffisamment simple, et est souvent conseillée aux programmeurs débutants[1] pour commencer dans le monde de la programmation multimédia.

Cette bibliothèque se retrouve sous de nombreux systèmes d'exploitation, comme Linux, Windows, Windows CE, BeOS, Mac OS, Mac OS X, FreeBSD, NetBSD, OpenBSD, BSD/OS, Solaris, IRIX, et QNX. Elle fonctionne aussi sous quelques systèmes embarqués, par exemple des consoles de jeu portable.

Écrite en C, elle peut cependant être utilisée par de nombreux langages de programmation, comme l'Ada, C/C++, C#, Ch, D, Eiffel, Erlang, Euphoria, Guile, Genie, Haskell, OCaml, Java, Lisp, Lua, ML, Objective C, Pascal, Perl, PHP, Pike, Pliant, Python (grâce à Pygame), Ruby, Smalltalk et Vala.

Sommaire

  • 1 Fonctionnalités
  • 2 Historique
  • 3 Galerie
  • 4 Références
  • 5 Voir aussi
    • 5.1 Article connexe
    • 5.2 Liens externes

Fonctionnalités[modifier | modifier le code]

En outre, la SDL gère :

  • l'affichage vidéo ;
  • les événements ;
  • l'audio numérique ;
  • la gestion des périphériques communs comme le clavier et la souris mais aussi le joystick ;
  • la lecture de CD-Audio ;
  • le multithreading ;
  • l'utilisation de timers précis.

Il existe des modules pour la SDL (pour les plus connus) :

  • SDL_ttf : gestion des polices TrueType Font
  • SDL_image : gestion d'un large type de formats d'images: BMP, PNM, XPM, LBM, PCX, GIF, JPEG, PNG, TGA
  • bibliothèque de primitives pour le dessin 2D
  • SDL_net : gestion réseau.
  • SDL_gfx : rotation et zoom sur les surfaces.

De plus, la SDL peut également servir de support à OpenGL

Historique[modifier | modifier le code]

Sam Lantinga créa la bibliothèque en 1998. L'idée lui vint tandis qu'il faisait un port d'application de Windows vers Macintosh. Peu après, il utilisa SDL pour porter Doom sur BeOS. Sam travailla ensuite pour Loki Software et d'autres bibliothèques libres se joignant à SDL, tel SMPEG et OpenAL.

La version 2.0.0 a été publiée le 12 août 2013 [2]. Parmi les changements majeurs, on notera la possibilité d'utiliser l'accélération matérielle pour le rendu et un changement de licence : licence Zlib à la place de la LGPL. Un guide de migration de SDL 1.2 vers 2.0.0 est disponible.

Galerie[modifier | modifier le code]

Freeciv

Unknown Horizons

0 A.D.

Hedgewars

Frets on Fire

OpenTTD[3]

Battle for Wesnoth[4]

Oolite[5]

Secret Maryo Chronicles

Trine

Références[modifier | modifier le code]

  1. ↑ http://fr.openclassrooms.com/informatique/cours/apprenez-a-programmer-en-c/pourquoi-avoir-choisi-la-sdl
  2. ↑ http://lists.libsdl.org/pipermail/sdl-libsdl.org/2013-August/089854.html
  3. ↑ « OpenTTD development » (consulté le 2010-03-19)
  4. ↑ « CompilingWesnoth » ,‎ 2010-02-27 (consulté le 2013-10-02)
  5. ↑ « Oolite », Oolite.org (consulté le 2010-03-19)

Voir aussi[modifier | modifier le code]

Sur les autres projets Wikimedia :

· SDL, sur Wikiversity

Article connexe[modifier | modifier le code]

  • SFML
  • ClanLib
  • Allegro

Liens externes[modifier | modifier le code]

  • (en) Site officiel
  • (en) Catégorie SDL de l’annuaire DMOZ

· Portail de l’informatique Portail de l’informatique

· Portail des logiciels libres Portail des logiciels libres

· Portail de la programmation informatique Portail de la programmation informatique

Ce document provient de « http://fr.wikipedia.org/w/index.php?title=Simple_DirectMedia_Layer&oldid=109795399 ».

Catégories :

  • Logiciel libre sous licence LGPL
  • Logiciel libre sous licence Zlib
  • Bibliothèque logicielle
  • Interface de programmation
  • Logiciel graphique
  • Logiciel pour Unix
  • Logiciel pour Mac OS
  • Logiciel pour Windows

Catégories cachées :

  • Article au ton dithyrambique
  • Page utilisant l'infobox Logiciel avec une description mais pas d'image
  • Portail:Informatique/Articles liés
  • Portail:Logiciels libres/Articles liés
  • Portail:Logiciel/Articles liés
  • Portail:Programmation informatique/Articles liés


Simple DirectMedia Layer: Table Des Matières

[Suivante]

Table Des Matières:

  1. Introduction de l'auteur

  2. Simple DirectMedia Layer
    1. Qu'est ce que c'est?
      • SDL est une API multimédia multi-plateforme gratuite
      • Utilisée pour les jeux
      • Utilisée pour comme SDK de jeux
      • Utilisée pour des émulateurs
      • Utilisée pour des démos
      • Utilisée pour des applications multimédias
    2. Qu'est ce qu'elle sait faire?
      • Video
      • Evènements
      • Audio
      • CD-ROM audio
      • Threads
      • Timers
      • Endian independence
    3. Sur quelle plateforme cela fonctionne?
      • Linux
      • Win32
      • BeOS
      • Portages non-officiels, Portages en développement

  3. Utiliser l'API Simple DirectMedia Layer
    1. Initialiser l'API
    2. Video
      • Choisir le mode d'affichage vidéo(de façon facile)
      • Tracer un pixel à l'écran
      • Charger et afficher une image
    3. Evènements
      • Attendre un évènement
      • Interroger un évènement
      • Gérer la file d'attente des évènements
    4. Son
      • Ouvrir le périphérique audio
      • Charger et jouer un son
    5. CD-ROM audio
      • Gérer le périphérique CD-ROM
      • Utiliser un CD-ROM
    6. Threads
      • Créer un simple Thread
      • Accès synchronisé à une ressource
    7. Timers
      • Récupérer le temps actuel en millisecondes
      • Attendre un nombre spécifié de millisecondes
    8. Endian independence
      • Determine the endianness of the current system
      • Swap data on systems of differing endianness

[Suivante]


SDL: Qu'est ce que c'est?

Qu'est ce que c'est?

SDL est une API multimédia multi-plateforme gratuite
- http://www.libsdl.org/
Utilisée pour: jeux, SDK des jeux, démos, émulateurs, MPEG players, et autre applications.


SDL: Qu'est ce qu'elle sait faire?

Qu'est ce que SDL sait faire?

Video
  • Passage en n'importe quel mode vidéo(8bpp ou plus) avec support logiciel si le mode vidéo n'est pas supporté par le matériel.
  • Ecrire directement dans un framebuffer linéaire
  • Créer des surfaces avec des couleurs ou un alpha blending choisi
  • Les surfaces allouées sont automatiquement converties au format désiré en optimisant l'allocation. Les allocations optimisées pour les MMX sont valables sur les architectures x86.
  • L'accélération matérielle est utilisée si elle est supportée par le matériel.
 Astuce:
Vous pouvez décidez du titre de votre fenêtre et de celui de l'icône, en utilisant respectivement : SDL_WM_SetCaption() et SDL_WM_SetIcon().
Evènements
  • Evènements gérés :
    • Changement de visibilité de la fenêtre
    • Entrées clavier
    • Entrées souris
    • Requête utilisateur de fin d'exécution
  • Chaque évènement peut être activé ou désactivé par SDL_EventState().
  • Les évènements sont passés au travers d'un filtre spécifié par l'utilisateur avant d'être placé dans la file des évènements.
  • Gestion sure de la file d'évènements.
Astuce:
Utilisez SDL_PeepEvents() pour rechercher un évènement particulier dans la file.
Audio
  • Gestion 8 ou 16 bits de l'audio, mono ou stéréo, avec des conversions si le format n'est pas supporté par le matériel.
  • L'audio a son propre thread, executé via un 'callback' de l'utilisateur.
  • Crée pour des logiciels audio. Les exemples archivés contiennent une bibliothèque complète audio/musique.
Astuce:
Utilisez SDL_LockAudio et SDL_UnlockAudio pour synchroniner les accès aux données partagés par les callbacks audio et le reste du programme.
CD-ROM audio
  • API complète de gestion du lecteur de CD-ROMs
 Astuce:
Si vous passez un handle de CD-ROM NULL aux fonctions de l'API, elles agiront sur le dernier CD-ROM ouvert.
Threads
  • API simple de création de Threads
  • Synchronisation simple grâce aux sémaphores binaires.
Astuce:
N'utilisez pas la library C standard d'E/S et de mémoire si vous pouvez évitez dans la gestion des threads - car elle bloque les ressources utilisées par d'autres thtreads.
Timers
  • Récupérez le temps écoulé en millisecondes
  • Attendre un nombre déterminé de millisecondes
  • Définir un simple timer de 10ms de résolution
Astuce:
Vous pouvez remplacer la fonction Win32 GetTickCount() par SDL_GetTicks()
Endian independence
  • Déterminer si votre système est "little" ou "big" endian
  • Routines pour swapper rapidement des données
  • Lire et écrire des données pour un type endian donné
 Astuce:
Quand vous lisez des fichiers de données, vous pourriez avoir besoin d'un "byteswap" graphique de 16 bits.


SDL: What Platforms Does It Run On?

Sur quelles plateformes fonctionne SDL ?

Linux
  • SDL utilise X11 pour l'affichage vidéo, tirant avantage de l'extension DGA d'XFree86, et de la nouvelle accélération MTRR pour l'affichage en plein écran.
  • SDL utilise l'API OSS pour l'audio.
  • Les Threads son implémentés en utilisant soit l'appel au système clone() et SysV IPC, soit à la glibc-2.1 pthreads.
 Astuce:
You can get at the hidden portions of the SDL driver interface via the SDL_GetWMInfo() function. This allows you to do things like remove window decorations and programmatically iconify your window.
Win32
  • Deux versions, une utilisable sur tous les systèmes basés sur l'API Win32, et une autre plus performante, basée sur l'API DirectX.
  • La première version utilise les GDI pour l'affichage vidéo. L'autre version utilise DirectDraw pour l'affichage vidéo, prenant avantage de l'accélération matérielle si elle existe.
  • La première version utilise l'API waveOut pour le son. La seconde emploi DirectSound pour ce faire.
 Astuce:
Vous devez appeler périodiquement la fonction d'évènement de SDL depuis votre thread principal, pour garder votre application sensible et ré-actualiser la file des messages de Windows.
BeOS
  • BWindow est utilisée pour l'affichage vidéo.
  • L'API BSoundPlayer est utilisée pour la gestion du son.
 Astuce:
Linux et BeOS supportent le drapeau SDL_INIT_EVENTTHREAD qui, quand il est passé à SDL_Init(), demande à la boucle d'évènements d'être asynchrone et de s'éxécuter dans un autre thread. Ceci est utile,par exemple, pour les curseurs colorés qui répondent même quand l'application est occupée.
Portages non-officiels et portages en développements.
  • Solaris, IRIX, FreeBSD
  • MacOS


Using SDL: Intialization

Utiliser l'API Simple DirectMedia Layer

Initialiser la librairie

Utilisez SDL_Init() pour charger dynamiquement puis intialiser la lbrairie. Cette fonction prend un lot de drapeaux (flags), correspondant à ce que vous désirez activer :

SDL_INIT_AUDIO
SDL_INIT_VIDEO
SDL_INIT_CDROM
SDL_INIT_TIMER

Utiliser SDL_Quit() pour fermer la librairie quend vous avez terminé avec elle.

 Astuce:
SDL charge dynamiquement la librairie depuis l'emplacement standard de la librairie. Utilisez SDL_SetLibraryPath() afin de choisir un autre chemin de la librairie dynamique distribuée avec votre application.
Exemple:
#include <stdlib.h>
#include "SDL.h"

main(int argc, char *argv[])
{
    if ( SDL_Init(SDL_INIT_AUDIO|SDL_INIT_VIDEO) < 0 ) {
        fprintf(stderr, "Impossible d'initialiser SDL: %s\n", SDL_GetError());
        exit(1);
    }
    atexit(SDL_Quit);

    ...
}


Using SDL: Video

Utiliser l'API Simple DirectMedia Layer

Video

  • Choisir et employer un mode vidéo (de manière facile)

    Vous choisissez votre résolution et votre profondeur de bits et c'est tout!

 Astuce #1:
Vous pouvez trouver le mode vidéo le plus performant supporté par le matériel grâce à la fonction SDL_GetVideoInfo().

Astuce #2:
Vous pouvez récupérer une liste des résolutions supportées pour une profondeur demandée en utilisant la fonction SDL_ListModes().

Exemple:
{ SDL_Surface *screen;

    screen = SDL_SetVideoMode(640, 480, 16, SDL_SWSURFACE);
    if ( screen == NULL ) {
        fprintf(stderr, "Impossible de passer en 640x480 en 16 bpp: %s\n", SDL_GetError());
        exit(1);
    }
}
  • Tracer un pixel à l'écran

    Afficher directement à l'écran est réalisé en écrivant directement dans le framebuffer, et en appelant la fonction de mise à jour.

 Astuce:
Si vous allez faire beaucoup de traçage, il est bon de bloquer la mémoire vidéo (si nécessaire) avant d'afficher, tracez en gardant une liste des endroits à mettre à jour, et débloquez l'écran avant de mettre à jour.
Exemple:

Drawing a pixel on a screen of arbitrary format

#include <SDL_endian.h> /* Used for the endian-dependent 24 bpp mode */

void DrawPixel(SDL_Surface *screen, int x, int y,
Uint8 R, Uint8 G,
Uint8 B)
{
    Uint32 color = SDL_MapRGB(screen->format, R, G, B);

    if ( SDL_MUSTLOCK(screen) ) {
        if ( SDL_LockSurface(screen) < 0 ) {
            return;
        }
    }
    switch (screen->format->BytesPerPixel) {
        case 1: { /*On gère le mode 8bpp */
            Uint8 *bufp;

            bufp = (Uint8 *)screen->pixels + y*screen->pitch + x;
            *bufp = color;
        }
        break;

        case 2: { /* Certainement 15 ou 16 bpp */
            Uint16 *bufp;

            bufp = (Uint16 *)screen->pixels + y*screen->pitch/2 + x;
            *bufp = color;
        }
        break;

        case 3: { /* 24 bpp lent et généralement pas utilisé */
            Uint8 *bufp;

            bufp = (Uint8 *)screen->pixels + y*screen->pitch + x * 3;
            if(SDL_BYTEORDER == SDL_LIL_ENDIAN) {
                bufp[0] = color;
                bufp[1] = color >> 8;
                bufp[2] = color >> 16;
            } else {
                bufp[2] = color;
                bufp[1] = color >> 8;
                bufp[0] = color >> 16;
            }
        }
        break;

        case 4: { /* Probablement 32 bpp alors */
            Uint32 *bufp;

            bufp = (Uint32 *)screen->pixels + y*screen->pitch/4 + x;
            *bufp = color;
        }
        break;
    }
    if ( SDL_MUSTLOCK(screen) ) {
        SDL_UnlockSurface(screen);
    }
    SDL_UpdateRect(screen, x, y, 1, 1);
}
  • Charger et afficher une image

    SDL fournit une routine simple de chargement d'image : SDL_LoadBMP(). Une librairie pour le chargement de différents types d'images est téléchargeable dans les archives des démos SDL.

    Vous pouvez afficher des images en utilisant SDL_BlitSurface() pour les copier directement dans le framebuffer. SDL_BlitSurface() sélectionne automatiquement la portion à copier, qui sera passée à SDL_UpdateRects() afin de mettre à jour la portion qui a changé.

 Astuce #1:
Si vous chargez une image qui sera souvent affichée, vous pouvez améliorer la vitesse de copie en la convertissant au format de l'écran. Utilisez alors la fonction SDL_DisplayFormat().

Astuce #2:
Beaucoup de sprites ont un arrière plan transparent. Vous pouvez activer la copie transparente (colorkey blit) avec la fonction SDL_SetColorKey().
Exemple:
void ShowBMP(char *file, SDL_Surface *screen, int x, int y)
{
    SDL_Surface *image;
    SDL_Rect dest;

    /* Charger une image BMP dans une surface*/
    image = SDL_LoadBMP(file);
    if ( image == NULL ) {
        fprintf(stderr, "Impossible de charger %s: %s\n", file, SDL_GetError());
        return;
    }

    /* Copie à l'écran.
    La surface ne doit pas être bloquée maintenant
     */
    dest.x = x;
    dest.y = y;
    dest.w = image->w;
    dest.h = image->h;
    SDL_BlitSurface(image, NULL, screen, &dest);

    /*Mise à jour de la portion qui a changé */
    SDL_UpdateRects(screen, 1, &dest);
}


Using SDL: Events

Utiliser l'API Simple DirectMedia Layer

Evènements

  • Attendre un évènement

Pour attendre un évènement utiliser la fonction SDL_WaitEvent().

 Astuce:
SDL possède un support universel des claviers, transformant le scancode de l'évènement et plçant son équivalent dans event.key.keysym.unicode. Ce traitement réclame d'activer l'UNICODE en appelant SDL_EnableUNICODE().
Exemple:
{
    SDL_Event event;

    SDL_WaitEvent(&event);

    switch (event.type) {
        case SDL_KEYDOWN:
            printf("La touche %s a été préssée!\n",
                   SDL_GetKeyName(event.key.keysym.sym));
            break;
        case SDL_QUIT:
            exit(0);
    }
}
  • Questionner la file d'évènements.

Utiliser pour cela la fonction SDL_PollEvent().

 Astuce:
Vous pouvez "jeter un coup d'oeil" aux évènements de la file d'évènements sans les enlever en passant SDL_PEEKEVENT comme paramètre de SDL_PeepEvents().
Exemple:
{
    SDL_Event event;

    while ( SDL_PollEvent(&event) ) {
        switch (event.type) {
            case SDL_MOUSEMOTION:
                printf("La souris a bougé de %d,%d à (%d,%d)\n",
                       event.motion.xrel, event.motion.yrel,
                       event.motion.x, event.motion.y);
                break;
            case SDL_MOUSEBUTTONDOWN:
                printf("Le bouton %d de la souris a été préssé en (%d,%d)\n"),
                       event.button.button, event.button.x, event.button.y);
                break;
            case SDL_QUIT:
                exit(0);
        }
    }
}
  • S'interroger sur l'état d'un évènement

Afin de manipuler les évènements directement, chaque type d'évènement à une fonction qui vous permet de contrôler l'état d'une application. Si vous utilisez exclusivement ce système, vous pouvez ignorer tous les évènements grâce à SDL_EventState(), et appeler périodiquement SDL_PumpEvents() pour mettre à jour l'état de votre application.

 Astuce:
Vous pouvez cacher ou montrer le curseur de votre souris en utilisant SDL_ShowCursor().
Exemple:
{
    SDL_EventState(SDL_MOUSEMOTION, SDL_IGNORE);
}

void CheckMouseHover(void)
{
    int mouse_x, mouse_y;

    SDL_PumpEvents();

    SDL_GetMouseState(&mouse_x, &mouse_y);
    if ( (mouse_x < 32) && (mouse_y < 32) ) {
        printf("La souris est en haut à gauche!\n");
    }
}


Using SDL: Sound

Utiliser l'API Simple DirectMedia Layer

Son

  • Ouvrir le contexte de de périphérique audio

Vous devez avoir écrit une fonction de rappel qui mixte vos données audios et les placent dans le flux audio. Après ça, choisissez le format et le taux audio, et ouvrez le contexte de périphérique audio.

Le son ne sera pas joué tant que vous n'aurez pas appelé SDL_PauseAudio(0), vous permettant d'exécuter d'autres initialisations audio avant que votre fonction de rappel ne soit exécutée.Vous dévez refermer le contexte après son utilisation par SDL_CloseAudio().

 Astuce:
Si votre application peut manipuler différents formats audio, passer un second pointeur SDL_AudioSpec à SDL_OpenAudio() afin de récupérer le format audio actuel du matériel. Si vous laissez ce second pointeur à NULL, les données audio seront converties au format audio du matériel à l'exécution.
Exemple:
#include "SDL.h"
#include "SDL_audio.h"
{
    extern void mixaudio(void *unused, Uint8 *stream, int len);
    SDL_AudioSpec fmt;

    /* Un son sereo de 16 bits à 22kHz */
    fmt.freq = 22050;
    fmt.format = AUDIO_S16;
    fmt.channels = 2;
    fmt.samples = 512;        /*Une bonne valeur pour les jeux */
    fmt.callback = mixaudio;
    fmt.userdata = NULL;

    /* Ouvre le contexte audio et joue le son */
    if ( SDL_OpenAudio(&fmt, NULL) < 0 ) {
        fprintf(stderr, "Impossible d'accéder à l'audio: %s\n", SDL_GetError());
        exit(1);
    }
    SDL_PauseAudio(0);

    ...

    SDL_CloseAudio();
}
  • Charger et jouer un son

SDL vous fournie une unique routine de chargement, SDL_LoadWAV(). Après avoir charger votre son, vous devez le convertir au bon format audio en utilisant SDL_ConvertAudio().

 Astuce:
SDL est une API de bas niveau dans la gestion de l'audio. Un exemple complet d'une telle implémentation existe sous licence LGPL, et peut être trouvée dans les archives des démos SDL.
Example:
#define NUM_SOUNDS 2
struct sample {
    Uint8 *data;
    Uint32 dpos;
    Uint32 dlen;
} sounds[NUM_SOUNDS];

void mixaudio(void *unused, Uint8 *stream, int len)
{
    int i;
    Uint32 amount;

    for ( i=0; i<NUM_SOUNDS; ++i ) {
        amount = (sounds[i].dlen-sounds[i].dpos);
        if ( amount > len ) {
            amount = len;
        }
        SDL_MixAudio(stream, &sounds[i].data[sounds[i].dpos], amount, SDL_MIX_MAXVOLUME);
        sounds[i].dpos += amount;
    }
}

void PlaySound(char *file)
{
    int index;
    SDL_AudioSpec wave;
    Uint8 *data;
    Uint32 dlen;
    SDL_AudioCVT cvt;

    /* Look for an empty (or finished) sound slot */
    for ( index=0; index<NUM_SOUNDS; ++index ) {
        if ( sounds[index].dpos == sounds[index].dlen ) {
            break;
        }
    }
    if ( index == NUM_SOUNDS )
        return;

    /* Load the sound file and convert it to 16-bit stereo at 22kHz */
    if ( SDL_LoadWAV(file, &wave, &data, &dlen) == NULL ) {
        fprintf(stderr, "Couldn't load %s: %s\n", file, SDL_GetError());
        return;
    }
    SDL_BuildAudioCVT(&cvt, wave.format, wave.channels, wave.freq,
                            AUDIO_S16,   2,             22050);
    cvt.buf = malloc(dlen*cvt.len_mult);
    memcpy(cvt.buf, data, dlen);
    cvt.len = dlen;
    SDL_ConvertAudio(&cvt);
    SDL_FreeWAV(data);

    /* Put the sound data in the slot (it starts playing immediately) */
    if ( sounds[index].data ) {
        free(sounds[index].data);
    }
    SDL_LockAudio();
    sounds[index].data = cvt.buf;
    sounds[index].dlen = cvt.len_cvt;
    sounds[index].dpos = 0;
    SDL_UnlockAudio();
}


Using SDL: the CD-ROM

Utiliser l'API Simple DirectMedia Layer

CD-ROM audio

  • Accéder à un lecteur de CD-ROM

Vous pouvez combien de lecteurs de CD-ROM existent sur les système grâce à la fonction SDL_CDNumDrives(), et accéder à l'un d'entre eux avec SDL_CDOpen().

Le lecteur 0 est le lecteur par défaut sur le système. Le lecteur de CD-ROM peut être ouvert même si il ne contient aucun disque.

Vous devriez utiliser la fonction SDL_CDStatus() afin de déterminer l'état du lecteur de CD-ROM. Après avoir utilisé le lecteur vous devez le fermer avec SDL_CDClose().

 Astuce:
Vous pouvez récupérer le nom du lecteur de CD-ROM au sein du système, en utilisant le fonction SDL_CDName().
Exemple:
{
    SDL_CD *cdrom;

    if ( SDL_CDNumDrives() > 0 ) {
        cdrom = SDL_CDOpen(0);
        if ( cdrom == NULL ) {
            fprintf(stderr, "Impossible d'ouvrir le lecteur de CD-ROM par défaut %s\n" SDL_GetError());
            return;
        }

        ...

        SDL_CDClose(cdrom);
    }
}
  • Utiliser un lecteur de CD-ROM

Le temps spécifié par les lecteurs de CD-ROM est soit au format MSF (min/sec/frame) ou directement en "frames". Un frame est une unité standard de temps sur les CDs, il correspond à 1/75 d'une seconde. SDL utilise les frames au lieu du format MSF pour afficher la durée et la plage d'un CD, mais vous pouvez les convertir grâce aux macros : FRAMES_TO_MSF() et MSF_TO_FRAMES().

SDL ne remet pas à jour l'information sur les pistes dans la structure SDL_CD tant que vous n'appelez pas SDL_CDStatus(), donc vous devriez toujours l'appeler pour vérifier que le lecteur contient bien un CD, et déterminer ainsi quelles sont les pistes du CD avant de le jouer. Notez que l'index des pistes démarre à 0 pour la première piste.

SDL possède deux fonctions pour jouer un CD. Vous pouvez soit utiliser SDL_CDPlayTracks() pour jouer une piste précise, soit utiliser SDL_CDPlay() et jouer l'ensemble du CD.

SDL ne notifie pas automatiquement l'insertion d'un CD ou qu'un CD est fini. Vous devez donc vérifier l'état du lecteur périodiquement avec SDL_CDStatus(). Puisque cet appel lit la table des matières du CD, il ne devrait pas être appelé continuellement en une boucle hermétique.

 Astuce:
Vous pouvez déterminer quelles pistes sont des pistes audios et quelles pistes sont des pistes de données, en regardant cdrom->tracks[track].type, et en le comparant aux SDL_AUDIO_TRACK et SDL_DATA_TRACK.
Exemple:
void JouerPiste(SDL_CD *cdrom, int piste)
{
    if ( CD_INDRIVE(SDL_CDStatus(cdrom)) ) {
        SDL_CDPlayTracks(cdrom, piste, 0, piste+1, 0);
    }
    while ( SDL_CDStatus(cdrom) == CD_PLAYING ) {
        SDL_Delay(1000);
    }
}


Using SDL: Threads

Utiliser l'API Simple DirectMedia Layer

Threads

  • Créer un simple thread

Créer un thread est fait en passant une fonction à SDL_CreatThread(). Si la fonction réussie, alors votre fonction s'exécute parrallèlement au reste de votre application dans son propre contexte (pile,registres,...) et est capable d'accéder à la mémoire et aux handles de fichier utilisé par le reste de votre application.

 Astuce:
Le second paramètre de SDL_CreatThread() est passé comme un paramètre de votre fonction threadée. Vous pouvez ainsi passer des valeurs à la pile, ou simplement un pointeur sur données qui sera utilsé par le thread.
Exemple:
#include "SDL_thread.h"

int global_data = 0;

int thread_func(void *unused)
{
    int last_value = 0;

    while ( global_data != -1 ) {
        if ( global_data != last_value ) {
            printf("Data value changed to %d\n", global_data);
            last_value = global_data;
        }
        SDL_Delay(100);
    }
    printf("Thread quitting\n");
    return(0);
}

{
    SDL_Thread *thread;
    int i;

    thread = SDL_CreateThread(thread_func, NULL);
    if ( thread == NULL ) {
        fprintf(stderr, "Unable to create thread: %s\n", SDL_GetError());
        return;
    }

    for ( i=0; i<5; ++i ) {
        printf("Changing value to %d\n", i);
        global_data = i;
        SDL_Delay(1000);
    }

    printf("Signaling thread to quit\n");
    global_data = -1;
    SDL_WaitThread(thread, NULL);
}
  • Synchroniser l'accès aux ressources

Vous pouvez empêcher que plusieurs threads accèdent à une ressource en créant un mutex et en entourant l'accès à cette ressource avec des verrous : SDL_mutexP() et SDL_mutexV().

 Astuce:
Toutes données accessible par plusieurs threads devraient être protégée par un mutex.
Exemple:
#include "SDL_thread.h"
#include "SDL_mutex.h"

int potty = 0;
int gotta_go;

int thread_func(void *data)
{
    SDL_mutex *lock = (SDL_mutex *)data;
    int times_went;

    times_went = 0;
    while ( gotta_go ) {
        SDL_mutexP(lock);    /* Lock  the potty */
        ++potty;
        printf("Thread %d using the potty\n", SDL_ThreadID());
        if ( potty > 1 ) {
            printf("Uh oh, somebody else is using the potty!\n");
        }
        --potty;
        SDL_mutexV(lock);
        ++times_went;
    }
    printf("Yep\n");
    return(times_went);
}

{
    const int progeny = 5;
    SDL_Thread *kids[progeny];
    SDL_mutex  *lock;
    int i, lots;

    /* Create the synchronization lock */
    lock = SDL_CreateMutex();

    gotta_go = 1;
    for ( i=0; i<progeny; ++i ) {
        kids[i] = SDL_CreateThread(thread_func, lock);
    }

    SDL_Delay(5*1000);
    SDL_mutexP(lock);
    printf("Everybody done?\n");
    gotta_go = 0;
    SDL_mutexV(lock);

    for ( i=0; i<progeny; ++i ) {
        SDL_WaitThread(kids[i], &lots);
        printf("Thread %d used the potty %d times\n", i+1, lots);
    }
    SDL_DestroyMutex(lock);
}


Using SDL: Timers

Utiliser l'API Simple DirectMedia Layer

Timers

  • Récupérer le temps actuel en millisecondes

SDL_GetTicks() cous renseigne sur le nombre de millisecondes écoulés depuis un point arbitraire du passé.

Astuce:
En général, quand vous implémenter un jeu, il vaut mieux bouger des objets par rapport à un temps plutôt qu'à un framerate. Le jeu est ainsi plus logique sur des systèmes rapide ou lent.
Exemple:
#define TICK_INTERVAL    30

Uint32 TimeLeft(void)
{
    static Uint32 next_time = 0;
    Uint32 now;

    now = SDL_GetTicks();
    if ( next_time <= now ) {
        next_time = now+TICK_INTERVAL;
        return(0);
    }
    return(next_time-now);
}
  • Attendre une durée en millsecondes

SDL_Delay() vous permet d'attendre un nombre de millisecondes.

Du fait que les systèmes supportant SDL sont multitâches, il n'existe aucun moyen d'assurer que votre application va attendre le temps requis. Cette fonction sert plus à attendre un moment plutôt que marquer un évènement à un temps précis.

 Astuce:
Most operating systems have a scheduler timeslice of about 10 ms. You can use SDL_Delay(1) as a way of giving up CPU for the current timeslice, allowing other threads to run. This is important if you have a thread in a tight loop but want other threads (like audio) to keep running.
Exemple:
{
    while ( game_running ) {
        UpdateGameState();
        SDL_Delay(TimeLeft());
    }
}


Using SDL: Endian Independence

Utiliser l'API Simple DirectMedia Layer

Endian independence

  • Determiner le système d'ordre des bits courant.

Le préprocesseur du C défini SDL_BYTEORDER qui est soit SDL_LIL_ENDIAN soit SDL_BIG_ENDIAN, dépendant de l'ordre de codage des bits du système.

Un système "petit indien" qui désire écrire des données les présente comme ceci :
[lo-bytes] [hi-bytes]
Un système "grand indien" qui désire écrire des données les présente comme ceci :
[hi-bytes] [lo-bytes]

Astuce:
Les systèmes x86 sont "petit indien" et les sysèmes ppc sont "grand indien".
Exemple:
#include "SDL_endian.h"

#if SDL_BYTEORDER == SDL_LIL_ENDIAN
#define SWAP16(X)    (X)
#define SWAP32(X)    (X)
#else
#define SWAP16(X)    SDL_Swap16(X)
#define SWAP32(X)    SDL_Swap32(X)
#endif
  • Permuter les données de différents systemes d'ordre de bits.

SDL fournie un lot de macros performantes dans SDL_endian.h, SDL_Swap16() et SDL_Swap32(), qui change l'ordre des bits pour vous. On trouve aussi des macros qui permutent les données d'un ordre de bits particulier vers le système "indien" local.

 Astuce:
Si vous désirez simplement connaître le système de rangement des bits actuel, mais ne désirez pas toutes les fonctions de swap, incluez simplement SDL_byteorder.h au lieu de SDL_endian.h
Exemple:
#include "SDL_endian.h"

void ReadScanline16(FILE *file, Uint16 *scanline, int length)
{
    fread(scanline, length, sizeof(Uint16), file);
    if ( SDL_BYTEORDER == SDL_BIG_ENDIAN ) {
        int i;
        for ( i=length-1; i >= 0; --i )
            scanline[i] = SDL_SwapLE16(scanline[i]);
    }
}


SDL: Introduction de l'auteur

Introduction de l'auteur:

Sam Lantinga, Chef Programmeur chez Loki Entertainment Software. Loki Entertainment est une société devouée au portage de grands jeux sur Linux. Le site officiel de Loki http://www.lokigames.com/

Production:

  • Auteur de SDL Simple DirectMedia Layer
  • Portage de Maelstrom de Macintosh sur Linux
  • Travail sur le portage de l'émulateur Executor de Macintosh vers Win32
  • Portage de divers utilitaires pour DOOM du DOS (DEU, DHE, etc.)
  • Autres projets: http://www.devolution.com/~slouken/

Contact:

  • slouken@libsdl.org


Table of Contents
I. SDL Guide
Preface
About SDL
About SDLdoc
Credits
1. The Basics
Introduction
Initializing SDL
2. Graphics and Video
Introduction to SDL Video
Using OpenGL With SDL
3. Input handling
Handling Joysticks
Handling the Keyboard
4. Examples
Introduction
Event Examples
Audio Examples
CDROM Examples
Time Examples
II. SDL Reference
5. General
SDL_Init — Initializes SDL
SDL_InitSubSystem — Initialize subsystems
SDL_QuitSubSystem — Shut down a subsystem
SDL_Quit — Shut down SDL
SDL_WasInit — Check which subsystems are initialized
SDL_GetError — Get SDL error string
6. Video
SDL_GetVideoSurface — returns a pointer to the current display surface
SDL_GetVideoInfo — returns a pointer to information about the video hardware
SDL_VideoDriverName — Obtain the name of the video driver
SDL_ListModes — Returns a pointer to an array of available screen dimensions for the given format and video flags
SDL_VideoModeOK — Check to see if a particular video mode is supported.
SDL_SetVideoMode — Set up a video mode with the specified width, height and bits-per-pixel.
SDL_UpdateRect — Makes sure the given area is updated on the given screen.
SDL_UpdateRects — Makes sure the given list of rectangles is updated on the given screen.
SDL_Flip — Swaps screen buffers
SDL_SetColors — Sets a portion of the colormap for the given 8-bit surface.
SDL_SetPalette — Sets the colors in the palette of an 8-bit surface.
SDL_SetGamma — Sets the color gamma function for the display
SDL_GetGammaRamp — Gets the color gamma lookup tables for the display
SDL_SetGammaRamp — Sets the color gamma lookup tables for the display
SDL_MapRGB — Map a RGB color value to a pixel format.
SDL_MapRGBA — Map a RGBA color value to a pixel format.
SDL_GetRGB — Get RGB values from a pixel in the specified pixel format.
SDL_GetRGBA — Get RGBA values from a pixel in the specified pixel format.
SDL_CreateRGBSurface — Create an empty SDL_Surface
SDL_CreateRGBSurfaceFrom — Create an SDL_Surface from pixel data
SDL_FreeSurface — Frees (deletes) a SDL_Surface
SDL_LockSurface — Lock a surface for directly access.
SDL_UnlockSurface — Unlocks a previously locked surface.
SDL_LoadBMP — Load a Windows BMP file into an SDL_Surface.
SDL_SaveBMP — Save an SDL_Surface as a Windows BMP file.
SDL_SetColorKey — Sets the color key (transparent pixel) in a blittable surface and RLE acceleration.
SDL_SetAlpha — Adjust the alpha properties of a surface
SDL_SetClipRect — Sets the clipping rectangle for a surface.
SDL_GetClipRect — Gets the clipping rectangle for a surface.
SDL_ConvertSurface — Converts a surface to the same format as another surface.
SDL_BlitSurface — This performs a fast blit from the source surface to the destination surface.
SDL_FillRect — This function performs a fast fill of the given rectangle with some color
SDL_DisplayFormat — Convert a surface to the display format
SDL_DisplayFormatAlpha — Convert a surface to the display format
SDL_WarpMouse — Set the position of the mouse cursor.
SDL_CreateCursor — Creates a new mouse cursor.
SDL_FreeCursor — Frees a cursor created with SDL_CreateCursor.
SDL_SetCursor — Set the currently active mouse cursor.
SDL_GetCursor — Get the currently active mouse cursor.
SDL_ShowCursor — Toggle whether or not the cursor is shown on the screen.
SDL_GL_LoadLibrary — Specify an OpenGL library
SDL_GL_GetProcAddress — Get the address of a GL function
SDL_GL_GetAttribute — Get the value of a special SDL/OpenGL attribute
SDL_GL_SetAttribute — Set a special SDL/OpenGL attribute
SDL_GL_SwapBuffers — Swap OpenGL framebuffers/Update Display
SDL_CreateYUVOverlay — Create a YUV video overlay
SDL_LockYUVOverlay — Lock an overlay
SDL_UnlockYUVOverlay — Unlock an overlay
SDL_DisplayYUVOverlay — Blit the overlay to the display
SDL_FreeYUVOverlay — Free a YUV video overlay
SDL_GLattr — SDL GL Attributes
SDL_Rect — Defines a rectangular area
SDL_Color — Format independent color description
SDL_Palette — Color palette for 8-bit pixel formats
SDL_PixelFormat — Stores surface format information
SDL_Surface — Graphical Surface Structure
SDL_VideoInfo — Video Target information
SDL_Overlay — YUV video overlay
7. Window Management
SDL_WM_SetCaption — Sets the window tile and icon name.
SDL_WM_GetCaption — Gets the window title and icon name.
SDL_WM_SetIcon — Sets the icon for the display window.
SDL_WM_IconifyWindow — Iconify/Minimise the window
SDL_WM_ToggleFullScreen — Toggles fullscreen mode
SDL_WM_GrabInput — Grabs mouse and keyboard input.
8. Events
Introduction
SDL Event Structures.
Event Functions.
9. Joystick
SDL_NumJoysticks — Count available joysticks.
SDL_JoystickName — Get joystick name.
SDL_JoystickOpen — Opens a joystick for use.
SDL_JoystickOpened — Determine if a joystick has been opened
SDL_JoystickIndex — Get the index of an SDL_Joystick.
SDL_JoystickNumAxes — Get the number of joystick axes
SDL_JoystickNumBalls — Get the number of joystick trackballs
SDL_JoystickNumHats — Get the number of joystick hats
SDL_JoystickNumButtons — Get the number of joysitck buttons
SDL_JoystickUpdate — Updates the state of all joysticks
SDL_JoystickGetAxis — Get the current state of an axis
SDL_JoystickGetHat — Get the current state of a joystick hat
SDL_JoystickGetButton — Get the current state of a given button on a given joystick
SDL_JoystickGetBall — Get relative trackball motion
SDL_JoystickClose — Closes a previously opened joystick
10. Audio
SDL_AudioSpec — Audio Specification Structure
SDL_OpenAudio — Opens the audio device with the desired parameters.
SDL_PauseAudio — Pauses and unpauses the audio callback processing
SDL_GetAudioStatus — Get the current audio state
SDL_LoadWAV — Load a WAVE file
SDL_FreeWAV — Frees previously opened WAV data
SDL_AudioCVT — Audio Conversion Structure
SDL_BuildAudioCVT — Initializes a SDL_AudioCVT structure for conversion
SDL_ConvertAudio — Convert audio data to a desired audio format.
SDL_MixAudio — Mix audio data
SDL_LockAudio — Lock out the callback function
SDL_UnlockAudio — Unlock the callback function
SDL_CloseAudio — Shuts down audio processing and closes the audio device.
11. CD-ROM
SDL_CDNumDrives — Returns the number of CD-ROM drives on the system.
SDL_CDName — Returns a human-readable, system-dependent identifier for the CD-ROM.
SDL_CDOpen — Opens a CD-ROM drive for access.
SDL_CDStatus — Returns the current status of the given drive.
SDL_CDPlay — Play a CD
SDL_CDPlayTracks — Play the given CD track(s)
SDL_CDPause — Pauses a CDROM
SDL_CDResume — Resumes a CDROM
SDL_CDStop — Stops a CDROM
SDL_CDEject — Ejects a CDROM
SDL_CDClose — Closes a SDL_CD handle
SDL_CD — CDROM Drive Information
SDL_CDtrack — CD Track Information Structure
12. Multi-threaded Programming
SDL_CreateThread — Creates a new thread of execution that shares its parent's properties.
SDL_ThreadID — Get the 32-bit thread identifier for the current thread.
SDL_GetThreadID — Get the SDL thread ID of a SDL_Thread
SDL_WaitThread — Wait for a thread to finish.
SDL_KillThread — Gracelessly terminates the thread.
SDL_CreateMutex — Create a mutex
SDL_DestroyMutex — Destroy a mutex
SDL_mutexP — Lock a mutex
SDL_mutexV — Unlock a mutex
SDL_CreateSemaphore — Creates a new semaphore and assigns an initial value to it.
SDL_DestroySemaphore — Destroys a semaphore that was created by SDL_CreateSemaphore.
SDL_SemWait — Lock a semaphore and suspend the thread if the semaphore value is zero.
SDL_SemTryWait — Attempt to lock a semaphore but don't suspend the thread.
SDL_SemWaitTimeout — Lock a semaphore, but only wait up to a specified maximum time.
SDL_SemPost — Unlock a semaphore.
SDL_SemValue — Return the current value of a semaphore.
SDL_CreateCond — Create a condition variable
SDL_DestroyCond — Destroy a condition variable
SDL_CondSignal — Restart a thread wait on a condition variable
SDL_CondBroadcast — Restart all threads waiting on a condition variable
SDL_CondWait — Wait on a condition variable
SDL_CondWaitTimeout — Wait on a condition variable, with timeout
13. Time
SDL_GetTicks — Get the number of milliseconds since the SDL library initialization.
SDL_Delay — Wait a specified number of milliseconds before returning.
SDL_AddTimer — Add a timer which will call a callback after the specified number of milliseconds has elapsed.
SDL_RemoveTimer — Remove a timer which was added with SDL_AddTimer.
SDL_SetTimer — Set a callback to run after the specified number of milliseconds has elapsed.
List of Tables
8-1. SDL Keysym definitions
8-2. SDL modifier definitions
List of Examples
1-1. Initializing SDL
2-1. Initializing the Video Display
2-2. Initializing the Best Video Mode
2-3. Loading and Displaying a BMP File
2-4. getpixel()
2-5. putpixel()
2-6. Using putpixel()
2-7. Initializing SDL with OpenGL
2-8. SDL and OpenGL
3-1. Initializing SDL with Joystick Support
3-2. Querying the Number of Available Joysticks
3-3. Opening a Joystick
3-4. Joystick Axis Events
3-5. More Joystick Axis Events
3-6. Joystick Button Events
3-7. Joystick Ball Events
3-8. Joystick Hat Events
3-9. Querying Joystick Characteristics
3-10. Reading Keyboard Events
3-11. Interpreting Key Event Information
3-12. Proper Game Movement
Next
SDL Guide


SDL Guide

SDL Library Documentation
Prev Next

I. SDL Guide

Table of Contents
Preface
1. The Basics
2. Graphics and Video
3. Input handling
4. Examples
Prev | Home | Next
SDL Library Documentation |  | Preface


Preface

Table of Contents
About SDL
About SDLdoc
Credits

About SDL

The SDL library is designed to make it easy to write games that run on Linux, *BSD, MacOS, Win32 and BeOS using the various native high-performance media interfaces, (for video, audio, etc) and presenting a single source-code level API to your application. SDL is a fairly low level API, but using it, completely portable applications can be written with a great deal of flexibility.

Prev | Home | Next
SDL Guide | Up | About SDLdoc


About SDLdoc

SDLdoc (The SDL Documentation Project) was formed to completely rewrite the SDL documentation and to keep it continually up to date. The team consists completely of volunteers ranging from people working with SDL in their spare time to people who use SDL in their everyday working lives.

The latest version of this documentation can always be found here: http://sdldoc.csn.ul.ie Downloadable PS, man pages and html tarballs are available at http://sdldoc.csn.ul.ie/pub/

Prev | Home | Next
Preface | Up | Credits


Credits

Sam Lantinga, slouken@libsdl.org
Martin Donlon, akawaka@skynet.ie
Mattias Engdegård
Julian Peterson
Ken Jordan
Maxim Sobolev
Wesley Poole
Michael Vance
Andreas Umbach
Andreas Hofmeister

Prev | Home | Next
About SDLdoc | Up | The Basics


The Basics

SDL Library Documentation
Prev Next

Chapter 1. The Basics

Table of Contents
Introduction
Initializing SDL

Introduction

The SDL Guide section is pretty incomplete. If you feel you have anything to add mail akawaka@skynet.ie or visit http://akawaka.csn.ul.ie/tne/.

Prev | Home | Next
Credits | Up | Initializing SDL


Initializing SDL

SDL is composed of eight subsystems - Audio, CDROM, Event Handling, File I/O, Joystick Handling, Threading, Timers and Video. Before you can use any of these subsystems they must be initialized by calling SDL_Init (or SDL_InitSubSystem). SDL_Init must be called before any other SDL function. It automatically initializes the Event Handling, File I/O and Threading subsystems and it takes a parameter specifying which other subsystems to initialize. So, to initialize the default subsystems and the Video subsystems you would call:

    SDL_Init ( SDL_INIT_VIDEO );
To initialize the default subsystems, the Video subsystem and the Timers subsystem you would call:
    SDL_Init ( SDL_INIT_VIDEO | SDL_INIT_TIMER );

SDL_Init is complemented by SDL_Quit (and SDL_QuitSubSystem). SDL_Quit shuts down all subsystems, including the default ones. It should always be called before a SDL application exits.

With SDL_Init and SDL_Quit firmly embedded in your programmers toolkit you can write your first and most basic SDL application. However, we must be prepare to handle errors. Many SDL functions return a value and indicates whether the function has succeeded or failed, SDL_Init, for instance, returns -1 if it could not initialize a subsystem. SDL provides a useful facility that allows you to determine exactly what the problem was, every time an error occurs within SDL an error message is stored which can be retrieved using SDL_GetError. Use this often, you can never know too much about an error.

Example 1-1. Initializing SDL

#include "SDL.h"   /* All SDL App's need this */
#include <stdio.h>

int main() {

    printf("Initializing SDL.\n");

    /* Initialize defaults, Video and Audio */
    if((SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO)==-1)) {
        printf("Could not initialize SDL: %s.\n", SDL_GetError());
        exit(-1);
    }

    printf("SDL initialized.\n");

    printf("Quiting SDL.\n");

    /* Shutdown all subsystems */
    SDL_Quit();

    printf("Quiting....\n");

    exit(0);
}
Prev | Home | Next
The Basics | Up | Graphics and Video


Graphics and Video

SDL Library Documentation
Prev Next

Chapter 2. Graphics and Video

Table of Contents
Introduction to SDL Video
Using OpenGL With SDL

Introduction to SDL Video

Video is probably the most common thing that SDL is used for, and so it has the most complete subsystem. Here are a few examples to demonstrate the basics.

Initializing the Video Display

This is what almost all SDL programs have to do in one way or another.

Example 2-1. Initializing the Video Display

    SDL_Surface *screen;

    /* Initialize the SDL library */
    if( SDL_Init(SDL_INIT_VIDEO) < 0 ) {
        fprintf(stderr,
                "Couldn't initialize SDL: %s\n", SDL_GetError());
        exit(1);
    }

    /* Clean up on exit */
    atexit(SDL_Quit);

    /*
     * Initialize the display in a 640x480 8-bit palettized mode,
     * requesting a software surface
     */
    screen = SDL_SetVideoMode(640, 480, 8, SDL_SWSURFACE);
    if ( screen == NULL ) {
        fprintf(stderr, "Couldn't set 640x480x8 video mode: %s\n",
                        SDL_GetError());
        exit(1);
    }

Initializing the Best Video Mode

If you have a preference for a certain pixel depth but will accept any other, use SDL_SetVideoMode with SDL_ANYFORMAT as below. You can also use SDL_VideoModeOK() to find the native video mode that is closest to the mode you request.

Example 2-2. Initializing the Best Video Mode

    /* Have a preference for 8-bit, but accept any depth */
    screen = SDL_SetVideoMode(640, 480, 8, SDL_SWSURFACE|SDL_ANYFORMAT);
    if ( screen == NULL ) {
        fprintf(stderr, "Couldn't set 640x480x8 video mode: %s\n",
                        SDL_GetError());
        exit(1);
    }
    printf("Set 640x480 at %d bits-per-pixel mode\n",
           screen->format->BitsPerPixel);

Loading and Displaying a BMP File

The following function loads and displays a BMP file given as argument, once SDL is initialised and a video mode has been set.

Example 2-3. Loading and Displaying a BMP File

void display_bmp(char *file_name)
{
    SDL_Surface *image;

    /* Load the BMP file into a surface */
    image = SDL_LoadBMP(file_name);
    if (image == NULL) {
        fprintf(stderr, "Couldn't load %s: %s\n", file_name, SDL_GetError());
        return;
    }

    /*
     * Palettized screen modes will have a default palette (a standard
     * 8*8*4 colour cube), but if the image is palettized as well we can
     * use that palette for a nicer colour matching
     */
    if (image->format->palette && screen->format->palette) {
    SDL_SetColors(screen, image->format->palette->colors, 0,
                  image->format->palette->ncolors);
    }

    /* Blit onto the screen surface */
    if(SDL_BlitSurface(image, NULL, screen, NULL) < 0)
        fprintf(stderr, "BlitSurface error: %s\n", SDL_GetError());

    SDL_UpdateRect(screen, 0, 0, image->w, image->h);

    /* Free the allocated BMP surface */
    SDL_FreeSurface(image);
}

Drawing Directly to the Display

The following two functions can be used to get and set single pixels of a surface. They are carefully written to work with any depth currently supported by SDL. Remember to lock the surface before calling them, and to unlock it before calling any other SDL functions.

To convert between pixel values and their red, green, blue components, use SDL_GetRGB() and SDL_MapRGB().

Example 2-4. getpixel()

/*
 * Return the pixel value at (x, y)
 * NOTE: The surface must be locked before calling this!
 */
Uint32 getpixel(SDL_Surface *surface, int x, int y)
{
    int bpp = surface->format->BytesPerPixel;
    /* Here p is the address to the pixel we want to retrieve */
    Uint8 *p = (Uint8 *)surface->pixels + y * surface->pitch + x * bpp;

    switch(bpp) {
    case 1:
        return *p;

    case 2:
        return *(Uint16 *)p;

    case 3:
        if(SDL_BYTEORDER == SDL_BIG_ENDIAN)
            return p[0] << 16 | p[1] << 8 | p[2];
        else
            return p[0] | p[1] << 8 | p[2] << 16;

    case 4:
        return *(Uint32 *)p;

    default:
        return 0;       /* shouldn't happen, but avoids warnings */
    }
}

Example 2-5. putpixel()

/*
 * Set the pixel at (x, y) to the given value
 * NOTE: The surface must be locked before calling this!
 */
void putpixel(SDL_Surface *surface, int x, int y, Uint32 pixel)
{
    int bpp = surface->format->BytesPerPixel;
    /* Here p is the address to the pixel we want to set */
    Uint8 *p = (Uint8 *)surface->pixels + y * surface->pitch + x * bpp;

    switch(bpp) {
    case 1:
        *p = pixel;
        break;

    case 2:
        *(Uint16 *)p = pixel;
        break;

    case 3:
        if(SDL_BYTEORDER == SDL_BIG_ENDIAN) {
            p[0] = (pixel >> 16) & 0xff;
            p[1] = (pixel >> 8) & 0xff;
            p[2] = pixel & 0xff;
        } else {
            p[0] = pixel & 0xff;
            p[1] = (pixel >> 8) & 0xff;
            p[2] = (pixel >> 16) & 0xff;
        }
        break;

    case 4:
        *(Uint32 *)p = pixel;
        break;
    }
}

The following code uses the putpixel() function above to set a yellow pixel in the middle of the screen.

Example 2-6. Using putpixel()

    /* Code to set a yellow pixel at the center of the screen */

    int x, y;
    Uint32 yellow;

    /* Map the color yellow to this display (R=0xff, G=0xFF, B=0x00)
       Note:  If the display is palettized, you must set the palette first.
    */
    yellow = SDL_MapRGB(screen->format, 0xff, 0xff, 0x00);

    x = screen->w / 2;
    y = screen->h / 2;

    /* Lock the screen for direct access to the pixels */
    if ( SDL_MUSTLOCK(screen) ) {
        if ( SDL_LockSurface(screen) < 0 ) {
            fprintf(stderr, "Can't lock screen: %s\n", SDL_GetError());
            return;
        }
    }

    putpixel(screen, x, y, yellow);

    if ( SDL_MUSTLOCK(screen) ) {
        SDL_UnlockSurface(screen);
    }
    /* Update just the part of the display that we've changed */
    SDL_UpdateRect(screen, x, y, 1, 1);

    return;
Prev | Home | Next
Initializing SDL | Up | Using OpenGL With SDL


Using OpenGL With SDL

SDL has the ability to create and use OpenGL contexts on several platforms(Linux/X11, Win32, BeOS, MacOS Classic/Toolbox, MacOS X, FreeBSD/X11 and Solaris/X11). This allows you to use SDL's audio, event handling, threads and times in your OpenGL applications (a function often performed by GLUT).

Initialisation

Initialising SDL to use OpenGL is not very different to initialising SDL normally. There are three differences; you must pass SDL_OPENGL to SDL_SetVideoMode, you must specify several GL attributes (depth buffer size, framebuffer sizes) using SDL_GL_SetAttribute and finally, if you wish to use double buffering you must specify it as a GL attribute, not by passing the SDL_DOUBLEBUF flag to SDL_SetVideoMode.

Example 2-7. Initializing SDL with OpenGL

    /* Information about the current video settings. */
    const SDL_VideoInfo* info = NULL;
    /* Dimensions of our window. */
    int width = 0;
    int height = 0;
    /* Color depth in bits of our window. */
    int bpp = 0;
    /* Flags we will pass into SDL_SetVideoMode. */
    int flags = 0;

    /* First, initialize SDL's video subsystem. */
    if( SDL_Init( SDL_INIT_VIDEO ) < 0 ) {
        /* Failed, exit. */
        fprintf( stderr, "Video initialization failed: %s\n",
             SDL_GetError( ) );
        quit_tutorial( 1 );
    }

    /* Let's get some video information. */
    info = SDL_GetVideoInfo( );

    if( !info ) {
        /* This should probably never happen. */
        fprintf( stderr, "Video query failed: %s\n",
             SDL_GetError( ) );
        quit_tutorial( 1 );
    }

    /*
     * Set our width/height to 640/480 (you would
     * of course let the user decide this in a normal
     * app). We get the bpp we will request from
     * the display. On X11, VidMode can't change
     * resolution, so this is probably being overly
     * safe. Under Win32, ChangeDisplaySettings
     * can change the bpp.
     */
    width = 640;
    height = 480;
    bpp = info->vfmt->BitsPerPixel;

    /*
     * Now, we want to setup our requested
     * window attributes for our OpenGL window.
     * We want *at least* 5 bits of red, green
     * and blue. We also want at least a 16-bit
     * depth buffer.
     *
     * The last thing we do is request a double
     * buffered window. '1' turns on double
     * buffering, '0' turns it off.
     *
     * Note that we do not use SDL_DOUBLEBUF in
     * the flags to SDL_SetVideoMode. That does
     * not affect the GL attribute state, only
     * the standard 2D blitting setup.
     */
    SDL_GL_SetAttribute( SDL_GL_RED_SIZE, 5 );
    SDL_GL_SetAttribute( SDL_GL_GREEN_SIZE, 5 );
    SDL_GL_SetAttribute( SDL_GL_BLUE_SIZE, 5 );
    SDL_GL_SetAttribute( SDL_GL_DEPTH_SIZE, 16 );
    SDL_GL_SetAttribute( SDL_GL_DOUBLEBUFFER, 1 );

    /*
     * We want to request that SDL provide us
     * with an OpenGL window, in a fullscreen
     * video mode.
     *
     * EXERCISE:
     * Make starting windowed an option, and
     * handle the resize events properly with
     * glViewport.
     */
    flags = SDL_OPENGL | SDL_FULLSCREEN;

    /*
     * Set the video mode
     */
    if( SDL_SetVideoMode( width, height, bpp, flags ) == 0 ) {
        /*
         * This could happen for a variety of reasons,
         * including DISPLAY not being set, the specified
         * resolution not being available, etc.
         */
        fprintf( stderr, "Video mode set failed: %s\n",
             SDL_GetError( ) );
        quit_tutorial( 1 );
    }

Drawing

Apart from initialisation, using OpenGL within SDL is the same as using OpenGL with any other API, e.g. GLUT. You still use all the same function calls and data types. However if you are using a double-buffered display, then you must use SDL_GL_SwapBuffers() to swap the buffers and update the display. To request double-buffering with OpenGL, use SDL_GL_SetAttribute with SDL_GL_DOUBLEBUFFER, and use SDL_GL_GetAttribute to see if you actually got it.

A full example code listing is now presented below.

Example 2-8. SDL and OpenGL

/*
 * SDL OpenGL Tutorial.
 * (c) Michael Vance, 2000
 * briareos@lokigames.com
 *
 * Distributed under terms of the LGPL.
 */

#include <SDL/SDL.h>
#include <GL/gl.h>
#include <GL/glu.h>

#include <stdio.h>
#include <stdlib.h>

static GLboolean should_rotate = GL_TRUE;

static void quit_tutorial( int code )
{
    /*
     * Quit SDL so we can release the fullscreen
     * mode and restore the previous video settings,
     * etc.
     */
    SDL_Quit( );

    /* Exit program. */
    exit( code );
}

static void handle_key_down( SDL_keysym* keysym )
{

    /*
     * We're only interested if 'Esc' has
     * been presssed.
     *
     * EXERCISE:
     * Handle the arrow keys and have that change the
     * viewing position/angle.
     */
    switch( keysym->sym ) {
    case SDLK_ESCAPE:
        quit_tutorial( 0 );
        break;
    case SDLK_SPACE:
        should_rotate = !should_rotate;
        break;
    default:
        break;
    }

}

static void process_events( void )
{
    /* Our SDL event placeholder. */
    SDL_Event event;

    /* Grab all the events off the queue. */
    while( SDL_PollEvent( &event ) ) {

        switch( event.type ) {
        case SDL_KEYDOWN:
            /* Handle key presses. */
            handle_key_down( &event.key.keysym );
            break;
        case SDL_QUIT:
            /* Handle quit requests (like Ctrl-c). */
            quit_tutorial( 0 );
            break;
        }

    }

}

static void draw_screen( void )
{
    /* Our angle of rotation. */
    static float angle = 0.0f;

    /*
     * EXERCISE:
     * Replace this awful mess with vertex
     * arrays and a call to glDrawElements.
     *
     * EXERCISE:
     * After completing the above, change
     * it to use compiled vertex arrays.
     *
     * EXERCISE:
     * Verify my windings are correct here ;).
     */
    static GLfloat v0[] = { -1.0f, -1.0f,  1.0f };
    static GLfloat v1[] = {  1.0f, -1.0f,  1.0f };
    static GLfloat v2[] = {  1.0f,  1.0f,  1.0f };
    static GLfloat v3[] = { -1.0f,  1.0f,  1.0f };
    static GLfloat v4[] = { -1.0f, -1.0f, -1.0f };
    static GLfloat v5[] = {  1.0f, -1.0f, -1.0f };
    static GLfloat v6[] = {  1.0f,  1.0f, -1.0f };
    static GLfloat v7[] = { -1.0f,  1.0f, -1.0f };
    static GLubyte red[]    = { 255,   0,   0, 255 };
    static GLubyte green[]  = {   0, 255,   0, 255 };
    static GLubyte blue[]   = {   0,   0, 255, 255 };
    static GLubyte white[]  = { 255, 255, 255, 255 };
    static GLubyte yellow[] = {   0, 255, 255, 255 };
    static GLubyte black[]  = {   0,   0,   0, 255 };
    static GLubyte orange[] = { 255, 255,   0, 255 };
    static GLubyte purple[] = { 255,   0, 255,   0 };

    /* Clear the color and depth buffers. */
    glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

    /* We don't want to modify the projection matrix. */
    glMatrixMode( GL_MODELVIEW );
    glLoadIdentity( );

    /* Move down the z-axis. */
    glTranslatef( 0.0, 0.0, -5.0 );

    /* Rotate. */
    glRotatef( angle, 0.0, 1.0, 0.0 );

    if( should_rotate ) {

        if( ++angle > 360.0f ) {
            angle = 0.0f;
        }

    }

    /* Send our triangle data to the pipeline. */
    glBegin( GL_TRIANGLES );

    glColor4ubv( red );
    glVertex3fv( v0 );
    glColor4ubv( green );
    glVertex3fv( v1 );
    glColor4ubv( blue );
    glVertex3fv( v2 );

    glColor4ubv( red );
    glVertex3fv( v0 );
    glColor4ubv( blue );
    glVertex3fv( v2 );
    glColor4ubv( white );
    glVertex3fv( v3 );

    glColor4ubv( green );
    glVertex3fv( v1 );
    glColor4ubv( black );
    glVertex3fv( v5 );
    glColor4ubv( orange );
    glVertex3fv( v6 );

    glColor4ubv( green );
    glVertex3fv( v1 );
    glColor4ubv( orange );
    glVertex3fv( v6 );
    glColor4ubv( blue );
    glVertex3fv( v2 );

    glColor4ubv( black );
    glVertex3fv( v5 );
    glColor4ubv( yellow );
    glVertex3fv( v4 );
    glColor4ubv( purple );
    glVertex3fv( v7 );

    glColor4ubv( black );
    glVertex3fv( v5 );
    glColor4ubv( purple );
    glVertex3fv( v7 );
    glColor4ubv( orange );
    glVertex3fv( v6 );

    glColor4ubv( yellow );
    glVertex3fv( v4 );
    glColor4ubv( red );
    glVertex3fv( v0 );
    glColor4ubv( white );
    glVertex3fv( v3 );

    glColor4ubv( yellow );
    glVertex3fv( v4 );
    glColor4ubv( white );
    glVertex3fv( v3 );
    glColor4ubv( purple );
    glVertex3fv( v7 );

    glColor4ubv( white );
    glVertex3fv( v3 );
    glColor4ubv( blue );
    glVertex3fv( v2 );
    glColor4ubv( orange );
    glVertex3fv( v6 );

    glColor4ubv( white );
    glVertex3fv( v3 );
    glColor4ubv( orange );
    glVertex3fv( v6 );
    glColor4ubv( purple );
    glVertex3fv( v7 );

    glColor4ubv( green );
    glVertex3fv( v1 );
    glColor4ubv( red );
    glVertex3fv( v0 );
    glColor4ubv( yellow );
    glVertex3fv( v4 );

    glColor4ubv( green );
    glVertex3fv( v1 );
    glColor4ubv( yellow );
    glVertex3fv( v4 );
    glColor4ubv( black );
    glVertex3fv( v5 );

    glEnd( );

    /*
     * EXERCISE:
     * Draw text telling the user that 'Spc'
     * pauses the rotation and 'Esc' quits.
     * Do it using vetors and textured quads.
     */

    /*
     * Swap the buffers. This this tells the driver to
     * render the next frame from the contents of the
     * back-buffer, and to set all rendering operations
     * to occur on what was the front-buffer.
     *
     * Double buffering prevents nasty visual tearing
     * from the application drawing on areas of the
     * screen that are being updated at the same time.
     */
    SDL_GL_SwapBuffers( );
}

static void setup_opengl( int width, int height )
{
    float ratio = (float) width / (float) height;

    /* Our shading model--Gouraud (smooth). */
    glShadeModel( GL_SMOOTH );

    /* Culling. */
    glCullFace( GL_BACK );
    glFrontFace( GL_CCW );
    glEnable( GL_CULL_FACE );

    /* Set the clear color. */
    glClearColor( 0, 0, 0, 0 );

    /* Setup our viewport. */
    glViewport( 0, 0, width, height );

    /*
     * Change to the projection matrix and set
     * our viewing volume.
     */
    glMatrixMode( GL_PROJECTION );
    glLoadIdentity( );
    /*
     * EXERCISE:
     * Replace this with a call to glFrustum.
     */
    gluPerspective( 60.0, ratio, 1.0, 1024.0 );
}

int main( int argc, char* argv[] )
{
    /* Information about the current video settings. */
    const SDL_VideoInfo* info = NULL;
    /* Dimensions of our window. */
    int width = 0;
    int height = 0;
    /* Color depth in bits of our window. */
    int bpp = 0;
    /* Flags we will pass into SDL_SetVideoMode. */
    int flags = 0;

    /* First, initialize SDL's video subsystem. */
    if( SDL_Init( SDL_INIT_VIDEO ) < 0 ) {
        /* Failed, exit. */
        fprintf( stderr, "Video initialization failed: %s\n",
             SDL_GetError( ) );
        quit_tutorial( 1 );
    }

    /* Let's get some video information. */
    info = SDL_GetVideoInfo( );

    if( !info ) {
        /* This should probably never happen. */
        fprintf( stderr, "Video query failed: %s\n",
             SDL_GetError( ) );
        quit_tutorial( 1 );
    }

    /*
     * Set our width/height to 640/480 (you would
     * of course let the user decide this in a normal
     * app). We get the bpp we will request from
     * the display. On X11, VidMode can't change
     * resolution, so this is probably being overly
     * safe. Under Win32, ChangeDisplaySettings
     * can change the bpp.
     */
    width = 640;
    height = 480;
    bpp = info->vfmt->BitsPerPixel;

    /*
     * Now, we want to setup our requested
     * window attributes for our OpenGL window.
     * We want *at least* 5 bits of red, green
     * and blue. We also want at least a 16-bit
     * depth buffer.
     *
     * The last thing we do is request a double
     * buffered window. '1' turns on double
     * buffering, '0' turns it off.
     *
     * Note that we do not use SDL_DOUBLEBUF in
     * the flags to SDL_SetVideoMode. That does
     * not affect the GL attribute state, only
     * the standard 2D blitting setup.
     */
    SDL_GL_SetAttribute( SDL_GL_RED_SIZE, 5 );
    SDL_GL_SetAttribute( SDL_GL_GREEN_SIZE, 5 );
    SDL_GL_SetAttribute( SDL_GL_BLUE_SIZE, 5 );
    SDL_GL_SetAttribute( SDL_GL_DEPTH_SIZE, 16 );
    SDL_GL_SetAttribute( SDL_GL_DOUBLEBUFFER, 1 );

    /*
     * We want to request that SDL provide us
     * with an OpenGL window, in a fullscreen
     * video mode.
     *
     * EXERCISE:
     * Make starting windowed an option, and
     * handle the resize events properly with
     * glViewport.
     */
    flags = SDL_OPENGL | SDL_FULLSCREEN;

    /*
     * Set the video mode
     */
    if( SDL_SetVideoMode( width, height, bpp, flags ) == 0 ) {
        /*
         * This could happen for a variety of reasons,
         * including DISPLAY not being set, the specified
         * resolution not being available, etc.
         */
        fprintf( stderr, "Video mode set failed: %s\n",
             SDL_GetError( ) );
        quit_tutorial( 1 );
    }

    /*
     * At this point, we should have a properly setup
     * double-buffered window for use with OpenGL.
     */
    setup_opengl( width, height );

    /*
     * Now we want to begin our normal app process--
     * an event loop with a lot of redrawing.
     */
    while( 1 ) {
        /* Process incoming events. */
        process_events( );
        /* Draw the screen. */
        draw_screen( );
    }

    /*
     * EXERCISE:
     * Record timings using SDL_GetTicks() and
     * and print out frames per second at program
     * end.
     */

    /* Never reached. */
    return 0;
}
Prev | Home | Next
Graphics and Video | Up | Input handling


Input handling

SDL Library Documentation
Prev Next

Chapter 3. Input handling

Table of Contents
Handling Joysticks
Handling the Keyboard

Handling Joysticks

Initialization

The first step in using a joystick in a SDL program is to initialize the Joystick subsystems of SDL. This done by passing the SDL_INIT_JOYSTICK flag to SDL_Init. The joystick flag will usually be used in conjunction with other flags (like the video flag) because the joystick is usually used to control something.

Example 3-1. Initializing SDL with Joystick Support

    if (SDL_Init( SDL_INIT_VIDEO | SDL_INIT_JOYSTICK ) < 0)
    {
        fprintf(stderr, "Couldn't initialize SDL: %s\n", SDL_GetError());
        exit(1);
    }

This will attempt to start SDL with both the video and the joystick subsystems activated.

Querying

If we have reached this point then we can safely assume that the SDL library has been initialized and that the Joystick subsystem is active. We can now call some video and/or sound functions to get things going before we need the joystick. Eventually we have to make sure that there is actually a joystick to work with. It's wise to always check even if you know a joystick will be present on the system because it can also help detect when the joystick is unplugged. The function used to check for joysticks is SDL_NumJoysticks.

This function simply returns the number of joysticks available on the system. If it is at least one then we are in good shape. The next step is to determine which joystick the user wants to use. If the number of joysticks available is only one then it is safe to assume that one joystick is the one the user wants to use. SDL has a function to get the name of the joysticks as assigned by the operations system and that function is SDL_JoystickName. The joystick is specified by an index where 0 is the first joystick and the last joystick is the number returned by SDL_NumJoysticks - 1. In the demonstration a list of all available joysticks is printed to stdout.

Example 3-2. Querying the Number of Available Joysticks

    printf("%i joysticks were found.\n\n", SDL_NumJoysticks() );
    printf("The names of the joysticks are:\n");

    for( i=0; i < SDL_NumJoysticks(); i++ )
    {
        printf("    %s\n", SDL_JoystickName(i));
    }

Opening a Joystick and Receiving Joystick Events

SDL's event driven architecture makes working with joysticks a snap. Joysticks can trigger 4 different types of events:

SDL_JoyAxisEvent | Occurs when an axis changes
SDL_JoyBallEvent | Occurs when a joystick trackball's position changes
SDL_JoyHatEvent | Occurs when a hat's position changes
SDL_JoyButtonEvent | Occurs when a button is pressed or released

Events are received from all joysticks opened. The first thing that needs to be done in order to receive joystick events is to call SDL_JoystickEventState with the SDL_ENABLE flag. Next you must open the joysticks that you want to receive envents from. This is done with the SDL_JoystickOpen function. For the example we are only interested in events from the first joystick on the system, regardless of what it may be. To receive events from it we would do this:

Example 3-3. Opening a Joystick

    SDL_Joystick *joystick;

    SDL_JoystickEventState(SDL_ENABLE);
    joystick = SDL_JoystickOpen(0);

If we wanted to receive events for other joysticks we would open them with calls to SDL_JoystickOpen just like we opened joystick 0, except we would store the SDL_Joystick structure they return in a different pointer. We only need the joystick pointer when we are querying the joysticks or when we are closing the joystick.

Up to this point all the code we have is used just to initialize the joysticks in order to read values at run time. All we need now is an event loop, which is something that all SDL programs should have anyway to receive the systems quit events. We must now add code to check the event loop for at least some of the above mentioned events. Let's assume our event loop looks like this:

    SDL_Event event;
    /* Other initializtion code goes here */

    /* Start main game loop here */

    while(SDL_PollEvent(&event))
    {
        switch(event.type)
        {
            case SDL_KEYDOWN:
            /* handle keyboard stuff here */
            break;

            case SDL_QUIT:
            /* Set whatever flags are necessary to */
            /* end the main game loop here */
            break;
        }
    }

    /* End loop here */
To handle Joystick events we merely add cases for them, first we'll add axis handling code. Axis checks can get kinda of tricky because alot of the joystick events received are junk. Joystick axis have a tendency to vary just a little between polling due to the way they are designed. To compensate for this you have to set a threshold for changes and ignore the events that have'nt exceeded the threshold. 10% is usually a good threshold value. This sounds a lot more complicated than it is. Here is the Axis event handler:

Example 3-4. Joystick Axis Events

    case SDL_JOYAXISMOTION:  /* Handle Joystick Motion */
    if ( ( event.jaxis.value < -3200 ) || (event.jaxis.value > 3200 ) )
    {
      /* code goes here */
    }
    break;

Another trick with axis events is that up-down and left-right movement are two different sets of axes. The most important axis is axis 0 (left-right) and axis 1 (up-down). To handle them seperatly in the code we do the following:

Example 3-5. More Joystick Axis Events

    case SDL_JOYAXISMOTION:  /* Handle Joystick Motion */
    if ( ( event.jaxis.value < -3200 ) || (event.jaxis.value > 3200 ) )
    {
        if( event.jaxis.axis == 0)
        {
            /* Left-right movement code goes here */
        }

        if( event.jaxis.axis == 1)
        {
            /* Up-Down movement code goes here */
        }
    }
    break;

Ideally the code here should use event.jaxis.value to scale something. For example lets assume you are using the joystick to control the movement of a spaceship. If the user is using an analog joystick and they push the stick a little bit they expect to move less than if they pushed it a lot. Designing your code for this situation is preferred because it makes the experience for users of analog controls better and remains the same for users of digital controls.

If your joystick has any additional axis then they may be used for other sticks or throttle controls and those axis return values too just with different event.jaxis.axis values.

Button handling is simple compared to the axis checking.

Example 3-6. Joystick Button Events

    case SDL_JOYBUTTONDOWN:  /* Handle Joystick Button Presses */
    if ( event.jbutton.button == 0 )
    {
        /* code goes here */
    }
    break;

Button checks are simpler than axis checks because a button can only be pressed or not pressed. The SDL_JOYBUTTONDOWN event is triggered when a button is pressed and the SDL_JOYBUTTONUP event is fired when a button is released. We do have to know what button was pressed though, that is done by reading the event.jbutton.button field.

Lastly when we are through using our joysticks we should close them with a call to SDL_JoystickClose. To close our opened joystick 0 we would do this at the end of our program:

    SDL_JoystickClose(joystick);

Advanced Joystick Functions

That takes care of the controls that you can count on being on every joystick under the sun, but there are a few extra things that SDL can support. Joyballs are next on our list, they are alot like axis with a few minor differences. Joyballs store relative changes unlike the the absolute postion stored in a axis event. Also one trackball event contains both the change in x and they change in y. Our case for it is as follows:

Example 3-7. Joystick Ball Events

    case SDL_JOYBALLMOTION:  /* Handle Joyball Motion */
    if( event.jball.ball == 0 )
    {
      /* ball handling */
    }
    break;

The above checks the first joyball on the joystick. The change in position will be stored in event.jball.xrel and event.jball.yrel.

Finally we have the hat event. Hats report only the direction they are pushed in. We check hat's position with the bitmasks:

SDL_HAT_CENTERED
SDL_HAT_UP
SDL_HAT_RIGHT
SDL_HAT_DOWN
SDL_HAT_LEFT

Also there are some predefined combinations of the above:

SDL_HAT_RIGHTUP
SDL_HAT_RIGHTDOWN
SDL_HAT_LEFTUP
SDL_HAT_LEFTDOWN

Our case for the hat may resemble the following:

Example 3-8. Joystick Hat Events

    case SDL_JOYHATMOTION:  /* Handle Hat Motion */
    if ( event.jhat.hat & SDL_HAT_UP )
    {
        /* Do up stuff here */
    }

    if ( event.jhat.hat & SDL_HAT_LEFT )
    {
        /* Do left stuff here */
    }

    if ( event.jhat.hat & SDL_HAT_RIGHTDOWN )
    {
        /* Do right and down together stuff here */
    }
    break;

In addition to the queries for number of joysticks on the system and their names there are additional functions to query the capabilities of attached joysticks:

SDL_JoystickNumAxes | Returns the number of joysitck axes
SDL_JoystickNumButtons | Returns the number of joysitck buttons
SDL_JoystickNumBalls | Returns the number of joysitck balls
SDL_JoystickNumHats | Returns the number of joysitck hats

To use these functions we just have to pass in the joystick structure we got when we opened the joystick. For Example:

Example 3-9. Querying Joystick Characteristics

    int number_of_buttons;
    SDL_Joystick *joystick;

    joystick = SDL_JoystickOpen(0);
    number_of_buttons = SDL_JoystickNumButtons(joystick);

This block of code would get the number of buttons on the first joystick in the system.

Prev | Home | Next
Using OpenGL With SDL | Up | Handling the Keyboard


Handling the Keyboard

Keyboard Related Structures

It should make it a lot easier to understand this tutorial is you are familiar with the data types involved in keyboard access, so I'll explain them first.

SDLKey

SDLKey is an enumerated type defined in SDL/include/SDL_keysym.h and detailed here. Each SDLKey symbol represents a key, SDLK_a corresponds to the 'a' key on a keyboard, SDLK_SPACE corresponds to the space bar, and so on.

SDLMod

SDLMod is an enumerated type, similar to SDLKey, however it enumerates keyboard modifiers (Control, Alt, Shift). The full list of modifier symbols is here. SDLMod values can be AND'd together to represent several modifiers.

SDL_keysym

typedef struct{
  Uint8 scancode;
  SDLKey sym;
  SDLMod mod;
  Uint16 unicode;
} SDL_keysym;

The SDL_keysym structure describes a key press or a key release. The scancode field is hardware specific and should be ignored unless you know what your doing. The sym field is the SDLKey value of the key being pressed or released. The mod field describes the state of the keyboard modifiers at the time the key press or release occurred. So a value of KMOD_NUM | KMOD_CAPS | KMOD_LSHIFT would mean that Numlock, Capslock and the left shift key were all press (or enabled in the case of the lock keys). Finally, the unicode field stores the 16-bit unicode value of the key.

Note: It should be noted and understood that this field is only valid when the SDL_keysym is describing a key press, not a key release. Unicode values only make sense on a key press because the unicode value describes an international character and only key presses produce characters. More information on Unicode can be found at www.unicode.org

Note: Unicode translation must be enabled using the SDL_EnableUNICODE function.

SDL_KeyboardEvent

typedef struct{
  Uint8 type;
  Uint8 state;
  SDL_keysym keysym;
} SDL_KeyboardEvent;

The SDL_KeyboardEvent describes a keyboard event (obviously). The key member of the SDL_Event union is a SDL_KeyboardEvent structure. The type field specifies whether the event is a key release (SDL_KEYUP) or a key press (SDL_KEYDOWN) event. The state is largely redundant, it reports the same information as the type field but uses different values (SDL_RELEASED and SDL_PRESSED). The keysym contains information of the key press or release that this event represents (see above).

Reading Keyboard Events

Reading keybaord events from the event queue is quite simple (the event queue and using it is described here). We read events using SDL_PollEvent in a while() loop and check for SDL_KEYUP and SDL_KEYDOWN events using a switch statement, like so:

Example 3-10. Reading Keyboard Events

  SDL_Event event;
  .
  .
  /* Poll for events. SDL_PollEvent() returns 0 when there are no  */
  /* more events on the event queue, our while loop will exit when */
  /* that occurs.                                                  */
  while( SDL_PollEvent( &event ) ){
    /* We are only worried about SDL_KEYDOWN and SDL_KEYUP events */
    switch( event.type ){
      case SDL_KEYDOWN:
        printf( "Key press detected\n" );
        break;

      case SDL_KEYUP:
        printf( "Key release detected\n" );
        break;

      default:
        break;
    }
  }
  .
  .

This is a very basic example. No information about the key press or release is interpreted. We will explore the other extreme out our first full example below - reporting all available information about a keyboard event.

A More Detailed Look

Before we can read events SDL must be initialised with SDL_Init and a video mode must be set using SDL_SetVideoMode. There are, however, two other functions we must use to obtain all the information required. We must enable unicode translation by calling SDL_EnableUNICODE(1) and we must convert SDLKey values into something printable, using SDL_GetKeyName

Note: It is useful to note that unicode values < 0x80 translate directly a characters ASCII value. THis is used in the example below

Example 3-11. Interpreting Key Event Information

    #include "SDL.h"

    /* Function Prototypes */
    void PrintKeyInfo( SDL_KeyboardEvent *key );
    void PrintModifiers( SDLMod mod );

    /* main */
    int main( int argc, char *argv[] ){

        SDL_Event event;
        int quit = 0;

        /* Initialise SDL */
        if( SDL_Init( SDL_INIT_VIDEO ) < 0){
            fprintf( stderr, "Could not initialise SDL: %s\n", SDL_GetError() );
            exit( -1 );
        }

        /* Set a video mode */
        if( !SDL_SetVideoMode( 320, 200, 0, 0 ) ){
            fprintf( stderr, "Could not set video mode: %s\n", SDL_GetError() );
            SDL_Quit();
            exit( -1 );
        }

        /* Enable Unicode translation */
        SDL_EnableUNICODE( 1 );

        /* Loop until an SDL_QUIT event is found */
        while( !quit ){

            /* Poll for events */
            while( SDL_PollEvent( &event ) ){

                switch( event.type ){
                    /* Keyboard event */
                    /* Pass the event data onto PrintKeyInfo() */
                    case SDL_KEYDOWN:
                    case SDL_KEYUP:
                        PrintKeyInfo( &event.key );
                        break;

                    /* SDL_QUIT event (window close) */
                    case SDL_QUIT:
                        quit = 1;
                        break;

                    default:
                        break;
                }

            }

        }

        /* Clean up */
        SDL_Quit();
        exit( 0 );
    }

    /* Print all information about a key event */
    void PrintKeyInfo( SDL_KeyboardEvent *key ){
        /* Is it a release or a press? */
        if( key->type == SDL_KEYUP )
            printf( "Release:- " );
        else
            printf( "Press:- " );

        /* Print the hardware scancode first */
        printf( "Scancode: 0x%02X", key->keysym.scancode );
        /* Print the name of the key */
        printf( ", Name: %s", SDL_GetKeyName( key->keysym.sym ) );
        /* We want to print the unicode info, but we need to make */
        /* sure its a press event first (remember, release events */
        /* don't have unicode info                                */
        if( key->type == SDL_KEYDOWN ){
            /* If the Unicode value is less than 0x80 then the    */
            /* unicode value can be used to get a printable       */
            /* representation of the key, using (char)unicode.    */
            printf(", Unicode: " );
            if( key->keysym.unicode < 0x80 && key->keysym.unicode > 0 ){
                printf( "%c (0x%04X)", (char)key->keysym.unicode,
                        key->keysym.unicode );
            }
            else{
                printf( "? (0x%04X)", key->keysym.unicode );
            }
        }
        printf( "\n" );
        /* Print modifier info */
        PrintModifiers( key->keysym.mod );
    }

    /* Print modifier info */
    void PrintModifiers( SDLMod mod ){
        printf( "Modifers: " );

        /* If there are none then say so and return */
        if( mod == KMOD_NONE ){
            printf( "None\n" );
            return;
        }

        /* Check for the presence of each SDLMod value */
        /* This looks messy, but there really isn't    */
        /* a clearer way.                              */
        if( mod & KMOD_NUM ) printf( "NUMLOCK " );
        if( mod & KMOD_CAPS ) printf( "CAPSLOCK " );
        if( mod & KMOD_LCTRL ) printf( "LCTRL " );
        if( mod & KMOD_RCTRL ) printf( "RCTRL " );
        if( mod & KMOD_RSHIFT ) printf( "RSHIFT " );
        if( mod & KMOD_LSHIFT ) printf( "LSHIFT " );
        if( mod & KMOD_RALT ) printf( "RALT " );
        if( mod & KMOD_LALT ) printf( "LALT " );
        if( mod & KMOD_CTRL ) printf( "CTRL " );
        if( mod & KMOD_SHIFT ) printf( "SHIFT " );
        if( mod & KMOD_ALT ) printf( "ALT " );
        printf( "\n" );
    }

Game-type Input

I have found that people using keyboard events for games and other interactive applications don't always understand one fundemental point.

Keyboard events only take place when a keys state changes from being unpressed to pressed, and vice versa.

Imagine you have an image of an alien that you wish to move around using the cursor keys - when you pressed the left arrow key you want him to slide over to the left, when you press the down key you want him to slide down the screen. Examine the following code, it highlights and error that many people have made.

    /* Alien screen coordinates */
    int alien_x=0, alien_y=0;
    .
    .
    /* Initialise SDL and video modes and all that */
    .
    /* Main game loop */
    /* Check for events */
    while( SDL_PollEvent( &event ) ){
        switch( event.type ){
            /* Look for a keypress */
            case SDL_KEYDOWN:
                /* Check the SDLKey values and move change the coords */
                switch( event.key.keysym.sym ){
                    case SDLK_LEFT:
                        alien_x -= 1;
                        break;
                    case SDLK_RIGHT:
                        alien_x += 1;
                        break;
                    case SDLK_UP:
                        alien_y -= 1;
                        break;
                    case SDLK_DOWN:
                        alien_y += 1;
                        break;
                    default:
                        break;
                }
            }
        }
    }
    .
    .
At first glance you may think this is a perfectly reasonable piece of code for the task, but it isn't. Like I said keyboard events only occur when a key changes state, so the user would have to press and release the left cursor key 100 times to move the alien 100 pixels to the left.

To get around this problem we must not use the events to change the position of the alien, we use the events to set flags which are then used in a seperate section of code to move the alien. Something like this:

Example 3-12. Proper Game Movement

    /* Alien screen coordinates */
    int alien_x=0, alien_y=0;
    int alien_xvel=0, alien_yvel=0;
    .
    .
    /* Initialise SDL and video modes and all that */
    .
    /* Main game loop */
    /* Check for events */
    while( SDL_PollEvent( &event ) ){
        switch( event.type ){
            /* Look for a keypress */
            case SDL_KEYDOWN:
                /* Check the SDLKey values and move change the coords */
                switch( event.key.keysym.sym ){
                    case SDLK_LEFT:
                        alien_xvel = -1;
                        break;
                    case SDLK_RIGHT:
                        alien_xvel =  1;
                        break;
                    case SDLK_UP:
                        alien_yvel = -1;
                        break;
                    case SDLK_DOWN:
                        alien_yvel =  1;
                        break;
                    default:
                        break;
                }
                break;
            /* We must also use the SDL_KEYUP events to zero the x */
            /* and y velocity variables. But we must also be       */
            /* careful not to zero the velocities when we shouldn't*/
            case SDL_KEYUP:
                switch( event.key.keysym.sym ){
                    case SDLK_LEFT:
                        /* We check to make sure the alien is moving */
                        /* to the left. If it is then we zero the    */
                        /* velocity. If the alien is moving to the   */
                        /* right then the right key is still press   */
                        /* so we don't tocuh the velocity            */
                        if( alien_xvel < 0 )
                            alien_xvel = 0;
                        break;
                    case SDLK_RIGHT:
                        if( alien_xvel > 0 )
                            alien_xvel = 0;
                        break;
                    case SDLK_UP:
                        if( alien_yvel < 0 )
                            alien_yvel = 0;
                        break;
                    case SDLK_DOWN:
                        if( alien_yvel > 0 )
                            alien_yvel = 0;
                        break;
                    default:
                        break;
                }
                break;

            default:
                break;
        }
    }
    .
    .
    /* Update the alien position */
    alien_x += alien_xvel;
    alien_y += alien_yvel;

As can be seen, we use two extra variables, alien_xvel and alien_yvel, which represent the motion of the ship, it is these variables that we update when we detect keypresses and releases.

Prev | Home | Next
Input handling | Up | Examples


Examples

SDL Library Documentation
Prev Next

Chapter 4. Examples

Table of Contents
Introduction
Event Examples
Audio Examples
CDROM Examples
Time Examples

Introduction

For the moment these examples are taken directly from the old SDL documentation. By the 1.2 release these examples should hopefully deal with most common SDL programming problems.

Prev | Home | Next
Handling the Keyboard | Up | Event Examples


Event Examples

Filtering and Handling Events

#include <stdio.h>
#include <stdlib.h>

#include "SDL.h"

/* This function may run in a separate event thread */
int FilterEvents(const SDL_Event *event) {
    static int boycott = 1;

    /* This quit event signals the closing of the window */
    if ( (event->type == SDL_QUIT) && boycott ) {
        printf("Quit event filtered out -- try again.\n");
        boycott = 0;
        return(0);
    }
    if ( event->type == SDL_MOUSEMOTION ) {
        printf("Mouse moved to (%d,%d)\n",
                event->motion.x, event->motion.y);
        return(0);    /* Drop it, we've handled it */
    }
    return(1);
}

int main(int argc, char *argv[])
{
    SDL_Event event;

    /* Initialize the SDL library (starts the event loop) */
    if ( SDL_Init(SDL_INIT_VIDEO) < 0 ) {
        fprintf(stderr,
                "Couldn't initialize SDL: %s\n", SDL_GetError());
        exit(1);
    }

    /* Clean up on exit, exit on window close and interrupt */
    atexit(SDL_Quit);

    /* Ignore key events */
    SDL_EventState(SDL_KEYDOWN, SDL_IGNORE);
    SDL_EventState(SDL_KEYUP, SDL_IGNORE);

    /* Filter quit and mouse motion events */
    SDL_SetEventFilter(FilterEvents);

    /* The mouse isn't much use unless we have a display for reference */
    if ( SDL_SetVideoMode(640, 480, 8, 0) == NULL ) {
        fprintf(stderr, "Couldn't set 640x480x8 video mode: %s\n",
                        SDL_GetError());
        exit(1);
    }

    /* Loop waiting for ESC+Mouse_Button */
    while ( SDL_WaitEvent(&event) >= 0 ) {
        switch (event.type) {
            case SDL_ACTIVEEVENT: {
                if ( event.active.state & SDL_APPACTIVE ) {
                    if ( event.active.gain ) {
                        printf("App activated\n");
                    } else {
                        printf("App iconified\n");
                    }
                }
            }
            break;

            case SDL_MOUSEBUTTONDOWN: {
                Uint8 *keys;

                keys = SDL_GetKeyState(NULL);
                if ( keys[SDLK_ESCAPE] == SDL_PRESSED ) {
                    printf("Bye bye...\n");
                    exit(0);
                }
                printf("Mouse button pressed\n");
            }
            break;

            case SDL_QUIT: {
                printf("Quit requested, quitting.\n");
                exit(0);
            }
            break;
        }
    }
    /* This should never happen */
    printf("SDL_WaitEvent error: %s\n", SDL_GetError());
    exit(1);
}

Prev | Home | Next
Examples | Up | Audio Examples


Audio Examples

Opening the audio device

    SDL_AudioSpec wanted;
    extern void fill_audio(void *udata, Uint8 *stream, int len);

    /* Set the audio format */
    wanted.freq = 22050;
    wanted.format = AUDIO_S16;
    wanted.channels = 2;    /* 1 = mono, 2 = stereo */
    wanted.samples = 1024;  /* Good low-latency value for callback */
    wanted.callback = fill_audio;
    wanted.userdata = NULL;

    /* Open the audio device, forcing the desired format */
    if ( SDL_OpenAudio(&wanted, NULL) < 0 ) {
        fprintf(stderr, "Couldn't open audio: %s\n", SDL_GetError());
        return(-1);
    }
    return(0);

Playing audio

    static Uint8 *audio_chunk;
    static Uint32 audio_len;
    static Uint8 *audio_pos;

    /* The audio function callback takes the following parameters:
       stream:  A pointer to the audio buffer to be filled
       len:     The length (in bytes) of the audio buffer
    */
    void fill_audio(void *udata, Uint8 *stream, int len)
    {
        /* Only play if we have data left */
        if ( audio_len == 0 )
            return;

        /* Mix as much data as possible */
        len = ( len > audio_len ? audio_len : len );
        SDL_MixAudio(stream, audio_pos, len, SDL_MIX_MAXVOLUME);
        audio_pos += len;
        audio_len -= len;
    }

    /* Load the audio data ... */

    ;;;;;

    audio_pos = audio_chunk;

    /* Let the callback function play the audio chunk */
    SDL_PauseAudio(0);

    /* Do some processing */

    ;;;;;

    /* Wait for sound to complete */
    while ( audio_len > 0 ) {
        SDL_Delay(100);         /* Sleep 1/10 second */
    }
    SDL_CloseAudio();

Prev | Home | Next
Event Examples | Up | CDROM Examples


CDROM Examples

Listing CD-ROM drives

    #include "SDL.h"

    /* Initialize SDL first */
    if ( SDL_Init(SDL_INIT_CDROM) < 0 ) {
        fprintf(stderr, "Couldn't initialize SDL: %s\n",SDL_GetError());
        exit(1);
    }
    atexit(SDL_Quit);

    /* Find out how many CD-ROM drives are connected to the system */
    printf("Drives available: %d\n", SDL_CDNumDrives());
    for ( i=0; i<SDL_CDNumDrives(); ++i ) {
        printf("Drive %d:  \"%s\"\n", i, SDL_CDName(i));
    }

Opening the default drive

    SDL_CD *cdrom;
    CDstatus status;
    char *status_str;

    cdrom = SDL_CDOpen(0);
    if ( cdrom == NULL ) {
        fprintf(stderr, "Couldn't open default CD-ROM drive: %s\n",
                        SDL_GetError());
        exit(2);
    }

    status = SDL_CDStatus(cdrom);
    switch (status) {
        case CD_TRAYEMPTY:
            status_str = "tray empty";
            break;
        case CD_STOPPED:
            status_str = "stopped";
            break;
        case CD_PLAYING:
            status_str = "playing";
            break;
        case CD_PAUSED:
            status_str = "paused";
            break;
        case CD_ERROR:
            status_str = "error state";
            break;
    }
    printf("Drive status: %s\n", status_str);
    if ( status >= CD_PLAYING ) {
        int m, s, f;
        FRAMES_TO_MSF(cdrom->cur_frame, &m, &s, &f);
        printf("Currently playing track %d, %d:%2.2d\n",
        cdrom->track[cdrom->cur_track].id, m, s);
    }

Listing the tracks on a CD

    SDL_CD *cdrom;          /* Assuming this has already been set.. */
    int i;
    int m, s, f;

    SDL_CDStatus(cdrom);
    printf("Drive tracks: %d\n", cdrom->numtracks);
    for ( i=0; i<cdrom->numtracks; ++i ) {
        FRAMES_TO_MSF(cdrom->track[i].length, &m, &s, &f);
        if ( f > 0 )
            ++s;
        printf("\tTrack (index %d) %d: %d:%2.2d\n", i,
        cdrom->track[i].id, m, s);
    }

Play an entire CD

    SDL_CD *cdrom;          /* Assuming this has already been set.. */

    // Play entire CD:
    if ( CD_INDRIVE(SDL_CDStatus(cdrom)) )
        SDL_CDPlayTracks(cdrom, 0, 0, 0, 0);

        // Play last track:
        if ( CD_INDRIVE(SDL_CDStatus(cdrom)) ) {
            SDL_CDPlayTracks(cdrom, cdrom->numtracks-1, 0, 0, 0);
        }

        // Play first and second track and 10 seconds of third track:
        if ( CD_INDRIVE(SDL_CDStatus(cdrom)) )
            SDL_CDPlayTracks(cdrom, 0, 0, 2, CD_FPS * 10);

Prev | Home | Next
Audio Examples | Up | Time Examples


Time Examples

Time based game loop

#define TICK_INTERVAL    30

static Uint32 next_time;

Uint32 time_left(void)
{
    Uint32 now;

    now = SDL_GetTicks();
    if(next_time <= now)
        return 0;
    else
        return next_time - now;
}

/* main game loop */

    next_time = SDL_GetTicks() + TICK_INTERVAL;
    while ( game_running ) {
        update_game_state();
        SDL_Delay(time_left());
        next_time += TICK_INTERVAL;
    }

Prev | Home | Next
CDROM Examples | Up | SDL Reference


Video Examples

Initializing the video display

    SDL_Surface *screen;

    /* Initialize the SDL library */
    if( SDL_Init(SDL_INIT_VIDEO) < 0 ) {
        fprintf(stderr,
                "Couldn't initialize SDL: %s\n", SDL_GetError());
        exit(1);
    }

    /* Clean up on exit */
    atexit(SDL_Quit);

    /* Initialize the display in a 640x480 8-bit palettized mode */
    screen = SDL_SetVideoMode(640, 480, 8, SDL_SWSURFACE);
    if ( screen == NULL ) {
        fprintf(stderr, "Couldn't set 640x480x8 video mode: %s\n",
                        SDL_GetError());
        exit(1);
    }

Initializing the best video mode

    /* Have a preference for 8-bit, but accept any depth */
    screen = SDL_SetVideoMode(640, 480, 8, SDL_SWSURFACE|SDL_ANYFORMAT);
    if ( screen == NULL ) {
        fprintf(stderr, "Couldn't set 640x480x8 video mode: %s\n",
                        SDL_GetError());
        exit(1);
    }
    printf("Set 640x480 at %d bits-per-pixel mode\n",
           screen->format->BitsPerPixel);

Loading and displaying a BMP file

    SDL_Surface *image;
    SDL_Rect dest;
    int ncolors, i;
    SDL_Color *colors;

    /* Load the BMP file into a surface */
    image = SDL_LoadBMP("sample.bmp");
    if ( image == NULL ) {
        fprintf(stderr, "Couldn't load sample.bmp: %s\n",
                        SDL_GetError());
        return;
    }

    /* Set the display colors -- SDL_SetColors() only does something on
       palettized displays, but it doesn't hurt anything on HiColor or
       TrueColor displays.
       If the display colors have already been set, this step can be
       skipped, and the library will automatically map the image to
       the current display colors.
    */
    if ( image->format->palette ) {
        ncolors = image->format->palette->ncolors;
        colors  = (SDL_Color *)malloc(ncolors*sizeof(SDL_Color));
        memcpy(colors, image->format->palette->colors, ncolors);
    }
    else {
        int r, g, b;

        /* Allocate 256 color palette */
        ncolors = 256;
        colors  = (SDL_Color *)malloc(ncolors*sizeof(SDL_Color));

        /* Set a 3,3,2 color cube */
        for ( r=0; r<8; ++r ) {
            for ( g=0; g<8; ++g ) {
                for ( b=0; b<4; ++b ) {
                    i = ((r<<5)|(g<<2)|b);
                    colors[i].r = r<<5;
                    colors[i].g = g<<5;
                    colors[i].b = b<<6;
                }
            }
        }
        /* Note: A better way of allocating the palette might be
           to calculate the frequency of colors in the image
           and create a palette based on that information.
        */
    }
    /* Set colormap, try for all the colors, but don't worry about it */
    SDL_SetColors(screen, colors, 0, ncolors);
    free(colors);

    /* Blit onto the screen surface */
    dest.x = 0;
    dest.y = 0;
    dest.w = image->w;
    dest.h = image->h;
    SDL_BlitSurface(image, NULL, screen, &dest);

    SDL_UpdateRects(screen, 1, &dest);

    /* Free the allocated BMP surface */
    SDL_FreeSurface(image);
    return;

Drawing directly to the display

    /* Code to set a yellow pixel at the center of the screen */

    Sint32   X, Y;
    Uint32   pixel;
    Uint8   *bits, bpp;

    /* Map the color yellow to this display (R=0xFF, G=0xFF, B=0x00)
       Note:  If the display is palettized, you must set the palette first.
    */
    pixel = SDL_MapRGB(screen->format, 0xFF, 0xFF, 0x00);

    /* Calculate the framebuffer offset of the center of the screen */
    if ( SDL_MUSTLOCK(screen) ) {
        if ( SDL_LockSurface(screen) < 0 )
            return;
    }
    bpp = screen->format->BytesPerPixel;
    X = screen->w/2;
    Y = screen->h/2;
    bits = ((Uint8 *)screen->pixels)+Y*screen->pitch+X*bpp;

    /* Set the pixel */
    switch(bpp) {
        case 1:
            *((Uint8 *)(bits)) = (Uint8)pixel;
            break;
        case 2:
            *((Uint16 *)(bits)) = (Uint16)pixel;
            break;
        case 3: { /* Format/endian independent */
            Uint8 r, g, b;

            r = (pixel>>screen->format->Rshift)&0xFF;
            g = (pixel>>screen->format->Gshift)&0xFF;
            b = (pixel>>screen->format->Bshift)&0xFF;
            *((bits)+screen->format->Rshift/8) = r;
            *((bits)+screen->format->Gshift/8) = g;
            *((bits)+screen->format->Bshift/8) = b;
            }
            break;
        case 4:
            *((Uint32 *)(bits)) = (Uint32)pixel;
            break;
    }

    /* Update the display */
    if ( SDL_MUSTLOCK(screen) ) {
        SDL_UnlockSurface(screen);
    }
    SDL_UpdateRect(screen, X, Y, 1, 1);

    return;

Fastest possible surface blit

There are three different ways you can draw an image to the screen:

1.Create a surface and use SDL_BlitSurface to blit it to the screen
2.Create the video surface in system memory and call SDL_UpdateRect
3.Create the video surface in video memory and call SDL_LockSurface

The best way to do this is to combine methods:
#include <stdio.h>
#include <stdlib.h>
#include "SDL.h"
#include "SDL_timer.h"

void ComplainAndExit(void)
{
    fprintf(stderr, "Problem: %s\n", SDL_GetError());
    exit(1);
}

int main(int argc, char *argv[])
{
    SDL_PixelFormat fmt;
    SDL_Surface *screen, *locked;
    SDL_Surface *imagebmp, *image;
    SDL_Rect dstrect;
    int i;
    Uint8 *buffer;

    /* Initialize SDL */
    if ( SDL_Init(SDL_INIT_VIDEO) < 0 ) {
        ComplainAndExit();
    }
    atexit(SDL_Quit);

    /* Load a BMP image into a surface */
    imagebmp = SDL_LoadBMP("image.bmp");
    if ( imagebmp == NULL ) {
        ComplainAndExit();
    }

    /* Set the video mode (640x480 at native depth) */
    screen = SDL_SetVideoMode(640, 480, 0, SDL_HWSURFACE|SDL_FULLSCREEN);
    if ( screen == NULL ) {
        ComplainAndExit();
    }

    /* Set the video colormap */
    if ( imagebmp->format->palette != NULL ) {
        SDL_SetColors(screen,
                      imagebmp->format->palette->colors, 0,
                      imagebmp->format->palette->ncolors);
    }

    /* Convert the image to the video format (maps colors) */
    image = SDL_DisplayFormat(imagebmp);
    SDL_FreeSurface(imagebmp);
    if ( image == NULL ) {
        ComplainAndExit();
    }

    /* Draw bands of color on the raw surface */
    if ( SDL_MUSTLOCK(screen) ) {
        if ( SDL_LockSurface(screen) < 0 )
            ComplainAndExit();
    }
    buffer=(Uint8 *)screen->pixels;
    for ( i=0; i<screen->h; ++i ) {
        memset(buffer,(i*255)/screen->h,
               screen->w*screen->format->BytesPerPixel);
               buffer += screen->pitch;
    }
    if ( SDL_MUSTLOCK(screen) ) {
        SDL_UnlockSurface(screen);
    }

    /* Blit the image to the center of the screen */
    dstrect.x = (screen->w-image->w)/2;
    dstrect.y = (screen->h-image->h)/2;
    dstrect.w = image->w;
    dstrect.h = image->h;
    if ( SDL_BlitSurface(image, NULL, screen, &dstrect) < 0 ) {
        SDL_FreeSurface(image);
        ComplainAndExit();
    }
    SDL_FreeSurface(image);

    /* Update the screen */
    SDL_UpdateRects(screen, 1, &dstrect);

    SDL_Delay(5000);        /* Wait 5 seconds */
    exit(0);
}

Prev | Home | Next
Examples | Up | Event Examples


SDL Reference

SDL Library Documentation
Prev Next

II. SDL Reference

Table of Contents
5. General
6. Video
7. Window Management
8. Events
9. Joystick
10. Audio
11. CD-ROM
12. Multi-threaded Programming
13. Time
Prev | Home | Next
Time Examples |  | General


General

SDL Library Documentation
Prev Next

Chapter 5. General

Table of Contents
SDL_Init — Initializes SDL
SDL_InitSubSystem — Initialize subsystems
SDL_QuitSubSystem — Shut down a subsystem
SDL_Quit — Shut down SDL
SDL_WasInit — Check which subsystems are initialized
SDL_GetError — Get SDL error string

Before SDL can be used in a program it must be initialized with SDL_Init. SDL_Init initializes all the subsystems that the user requests (video, audio, joystick, timers and/or cdrom). Once SDL is initialized with SDL_Init subsystems can be shut down and initialized as needed using SDL_InitSubSystem and SDL_QuitSubSystem.

SDL must also be shut down before the program exits to make sure it cleans up correctly. Calling SDL_Quit shuts down all subsystems and frees any resources allocated to SDL.

Prev | Home | Next
SDL Reference | Up | SDL_Init


SDL_Init

Name

SDL_Init -- Initializes SDL

Synopsis

#include "SDL.h"

int SDL_Init(Uint32 flags);

Description

Initializes SDL. This should be called before all other SDL functions. The flags parameter specifies what part(s) of SDL to initialize.

SDL_INIT_TIMER | Initializes the timer subsystem.
SDL_INIT_AUDIO | Initializes the audio subsystem.
SDL_INIT_VIDEO | Initializes the video subsystem.
SDL_INIT_CDROM | Initializes the cdrom subsystem.
SDL_INIT_JOYSTICK | Initializes the joystick subsystem.
SDL_INIT_EVERYTHING | Initialize all of the above.
SDL_INIT_NOPARACHUTE | Prevents SDL from catching fatal signals.
SDL_INIT_EVENTTHREAD |

Return Value

Returns -1 on an error or 0 on success.

See Also

SDL_Quit, SDL_InitSubSystem

Prev | Home | Next
General | Up | SDL_InitSubSystem


SDL_InitSubSystem

Name

SDL_InitSubSystem -- Initialize subsystems

Synopsis

#include "SDL.h"

int SDL_InitSubSystem(Uint32 flags);

Description

After SDL has been initialized with SDL_Init you may initialize uninitialized subsystems with SDL_InitSubSystem. The flags parameter is the same as that used in SDL_Init.

Examples

/* Seperating Joystick and Video initialization. */
SDL_Init(SDL_INIT_VIDEO);
.
.
SDL_SetVideoMode(640, 480, 16, SDL_DOUBLEBUF|SDL_FULLSCREEN);
.
/* Do Some Video stuff */
.
.
/* Initialize the joystick subsystem */
SDL_InitSubSystem(SDL_INIT_JOYSTICK);

/* Do some stuff with video and joystick */
.
.
.
/* Shut them both down */
SDL_Quit();

Return Value

Returns -1 on an error or 0 on success.

See Also

SDL_Init, SDL_Quit, SDL_QuitSubSystem

Prev | Home | Next
SDL_Init | Up | SDL_QuitSubSystem


SDL_QuitSubSystem

Name

SDL_QuitSubSystem -- Shut down a subsystem

Synopsis

#include "SDL.h"

void SDL_QuitSubSystem(Uint32 flags);

Description

SDL_QuitSubSystem allows you to shut down a subsystem that has been previously initialized by SDL_Init or SDL_InitSubSystem. The flags tells SDL_QuitSubSystem which subsystems to shut down, it uses the same values that are passed to SDL_Init.

See Also

SDL_Quit, SDL_Init, SDL_InitSubSystem

Prev | Home | Next
SDL_InitSubSystem | Up | SDL_Quit


SDL_Quit

Name

SDL_Quit -- Shut down SDL

Synopsis

#include "SDL.h"

void SDL_Quit(void);

Description

SDL_Quit shuts down all SDL subsystems and frees the resources allocated to them. This should always be called before you exit. For the sake of simplicity you can set SDL_Quit as your atexit call, like:

SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO);
atexit(SDL_Quit);
.
.

Note: While using atexit maybe be fine for small programs, more advanced users should shut down SDL in their own cleanup code. Plus, using atexit in a library is a sure way to crash dynamically loaded code

See Also

SDL_QuitSubsystem, SDL_Init

Prev | Home | Next
SDL_QuitSubSystem | Up | SDL_WasInit


SDL_WasInit

Name

SDL_WasInit -- Check which subsystems are initialized

Synopsis

#include "SDL.h"

Uint32 SDL_WasInit(Uint32 flags);

Description

SDL_WasInit allows you to see which SDL subsytems have been initialized. flags is a bitwise OR'd combination of the subsystems you wish to check (see SDL_Init for a list of subsystem flags).

Return Value

SDL_WasInit returns a bitwised OR'd combination of the initialized subsystems.

Examples

/* Here are several ways you can use SDL_WasInit() */

/* Get init data on all the subsystems */
Uint32 subsystem_init;

subsystem_init=SDL_WasInit(SDL_INIT_EVERYTHING);

if(subsystem_init&SDL_INIT_VIDEO)
  printf("Video is initialized.\n");
else
  printf("Video is not initialized.\n");

/* Just check for one specfic subsystem */

if(SDL_WasInit(SDL_INIT_VIDEO)!=0)
  printf("Video is initialized.\n");
else
  printf("Video is not initialized.\n");

/* Check for two subsystems */

Uint32 subsystem_mask=SDL_INIT_VIDEO|SDL_INIT_AUDIO;

if(SDL_WasInit(subsystem_mask)==subsystem_mask)
  printf("Video and Audio initialized.\n");
else
  printf("Video and Audio not initialized.\n");

See Also

SDL_Init, SDL_Subsystem

Prev | Home | Next
SDL_Quit | Up | SDL_GetError


SDL_GetError

Name

SDL_GetError -- Get SDL error string

Synopsis

#include "SDL/SDL.h"

char *SDL_GetError(void);

Description

SDL_GetError returns a NULL terminated string containing information about the last internal SDL error.

Return Value

SDL_GetError returns a string containing the last error.

Prev | Home | Next
SDL_WasInit | Up | Video


Video

SDL Library Documentation
Prev Next

Chapter 6. Video

Table of Contents
SDL_GetVideoSurface — returns a pointer to the current display surface
SDL_GetVideoInfo — returns a pointer to information about the video hardware
SDL_VideoDriverName — Obtain the name of the video driver
SDL_ListModes — Returns a pointer to an array of available screen dimensions for the given format and video flags
SDL_VideoModeOK — Check to see if a particular video mode is supported.
SDL_SetVideoMode — Set up a video mode with the specified width, height and bits-per-pixel.
SDL_UpdateRect — Makes sure the given area is updated on the given screen.
SDL_UpdateRects — Makes sure the given list of rectangles is updated on the given screen.
SDL_Flip — Swaps screen buffers
SDL_SetColors — Sets a portion of the colormap for the given 8-bit surface.
SDL_SetPalette — Sets the colors in the palette of an 8-bit surface.
SDL_SetGamma — Sets the color gamma function for the display
SDL_GetGammaRamp — Gets the color gamma lookup tables for the display
SDL_SetGammaRamp — Sets the color gamma lookup tables for the display
SDL_MapRGB — Map a RGB color value to a pixel format.
SDL_MapRGBA — Map a RGBA color value to a pixel format.
SDL_GetRGB — Get RGB values from a pixel in the specified pixel format.
SDL_GetRGBA — Get RGBA values from a pixel in the specified pixel format.
SDL_CreateRGBSurface — Create an empty SDL_Surface
SDL_CreateRGBSurfaceFrom — Create an SDL_Surface from pixel data
SDL_FreeSurface — Frees (deletes) a SDL_Surface
SDL_LockSurface — Lock a surface for directly access.
SDL_UnlockSurface — Unlocks a previously locked surface.
SDL_LoadBMP — Load a Windows BMP file into an SDL_Surface.
SDL_SaveBMP — Save an SDL_Surface as a Windows BMP file.
SDL_SetColorKey — Sets the color key (transparent pixel) in a blittable surface and RLE acceleration.
SDL_SetAlpha — Adjust the alpha properties of a surface
SDL_SetClipRect — Sets the clipping rectangle for a surface.
SDL_GetClipRect — Gets the clipping rectangle for a surface.
SDL_ConvertSurface — Converts a surface to the same format as another surface.
SDL_BlitSurface — This performs a fast blit from the source surface to the destination surface.
SDL_FillRect — This function performs a fast fill of the given rectangle with some color
SDL_DisplayFormat — Convert a surface to the display format
SDL_DisplayFormatAlpha — Convert a surface to the display format
SDL_WarpMouse — Set the position of the mouse cursor.
SDL_CreateCursor — Creates a new mouse cursor.
SDL_FreeCursor — Frees a cursor created with SDL_CreateCursor.
SDL_SetCursor — Set the currently active mouse cursor.
SDL_GetCursor — Get the currently active mouse cursor.
SDL_ShowCursor — Toggle whether or not the cursor is shown on the screen.
SDL_GL_LoadLibrary — Specify an OpenGL library
SDL_GL_GetProcAddress — Get the address of a GL function
SDL_GL_GetAttribute — Get the value of a special SDL/OpenGL attribute
SDL_GL_SetAttribute — Set a special SDL/OpenGL attribute
SDL_GL_SwapBuffers — Swap OpenGL framebuffers/Update Display
SDL_CreateYUVOverlay — Create a YUV video overlay
SDL_LockYUVOverlay — Lock an overlay
SDL_UnlockYUVOverlay — Unlock an overlay
SDL_DisplayYUVOverlay — Blit the overlay to the display
SDL_FreeYUVOverlay — Free a YUV video overlay
SDL_GLattr — SDL GL Attributes
SDL_Rect — Defines a rectangular area
SDL_Color — Format independent color description
SDL_Palette — Color palette for 8-bit pixel formats
SDL_PixelFormat — Stores surface format information
SDL_Surface — Graphical Surface Structure
SDL_VideoInfo — Video Target information
SDL_Overlay — YUV video overlay

SDL presents a very simple interface to the display framebuffer. The framebuffer is represented as an offscreen surface to which you can write directly. If you want the screen to show what you have written, call the update function which will guarantee that the desired portion of the screen is updated.

Before you call any of the SDL video functions, you must first call SDL_Init(SDL_INIT_VIDEO), which initializes the video and events in the SDL library. Check the return code, which should be 0, to see if there were any errors in starting up.

If you use both sound and video in your application, you need to call SDL_Init(SDL_INIT_AUDIO | SDL_INIT_VIDEO) before opening the sound device, otherwise under Win32 DirectX, you won't be able to set full-screen display modes.

After you have initialized the library, you can start up the video display in a number of ways. The easiest way is to pick a common screen resolution and depth and just initialize the video, checking for errors. You will probably get what you want, but SDL may be emulating your requested mode and converting the display on update. The best way is to query, for the best video mode closest to the desired one, and then convert your images to that pixel format.

SDL currently supports any bit depth >= 8 bits per pixel. 8 bpp formats are considered 8-bit palettized modes, while 12, 15, 16, 24, and 32 bits per pixel are considered "packed pixel" modes, meaning each pixel contains the RGB color components packed in the bits of the pixel.

After you have initialized your video mode, you can take the surface that was returned, and write to it like any other framebuffer, calling the update routine as you go.

When you have finished your video access and are ready to quit your application, you should call "SDL_Quit()" to shutdown the video and events.

Prev | Home | Next
SDL_GetError | Up | SDL_GetVideoSurface


SDL_GetVideoSurface

Name

SDL_GetVideoSurface -- returns a pointer to the current display surface

Synopsis

#include "SDL.h"

SDL_Surface *SDL_GetVideoSurface(void);

Description

This function returns a pointer to the current display surface. If SDL is doing format conversion on the display surface, this function returns the publicly visible surface, not the real video surface.

See Also

SDL_Surface

Prev | Home | Next
Video | Up | SDL_GetVideoInfo


SDL_GetVideoInfo

Name

SDL_GetVideoInfo -- returns a pointer to information about the video hardware

Synopsis

#include "SDL.h"

SDL_VideoInfo *SDL_GetVideoInfo(void);

Description

This function returns a read-only pointer to information about the video hardware. If this is called before SDL_SetVideoMode, the vfmt member of the returned structure will contain the pixel format of the "best" video mode.

See Also

SDL_SetVideoMode, SDL_VideoInfo

Prev | Home | Next
SDL_GetVideoSurface | Up | SDL_VideoDriverName


SDL_VideoDriverName

Name

SDL_VideoDriverName -- Obtain the name of the video driver

Synopsis

#include "SDL.h"

char *SDL_VideoDriverName(char *namebuf, int maxlen);

Description

The buffer pointed to by namebuf is filled up to a maximum of maxlen characters (include the NULL terminator) with the name of the initialised video driver. The driver name is a simple one word identifier like "x11" or "windib".

Return Value

Returns NULL if video has not been initialised with SDL_Init or a pointer to namebuf otherwise.

See Also

SDL_Init SDL_InitSubSystem

Prev | Home | Next
SDL_GetVideoInfo | Up | SDL_ListModes


SDL_ListModes

Name

SDL_ListModes -- Returns a pointer to an array of available screen dimensions for the given format and video flags

Synopsis

#include "SDL.h"

SDL_Rect **SDL_ListModes(SDL_PixelFormat *format, Uint32 flags);

Description

Return a pointer to an array of available screen dimensions for the given format and video flags, sorted largest to smallest. Returns NULL if there are no dimensions available for a particular format, or -1 if any dimension is okay for the given format.

If format is NULL, the mode list will be for the format returned by SDL_GetVideoInfo()->vfmt. The flag parameter is an OR'd combination of surface flags. The flags are the same as those used SDL_SetVideoMode and they play a strong role in deciding what modes are valid. For instance, if you pass SDL_HWSURFACE as a flag only modes that support hardware video surfaces will be returned.

Example

SDL_Rect **modes;
int i;
.
.
.

/* Get available fullscreen/hardware modes */
modes=SDL_ListModes(NULL, SDL_FULLSCREEN|SDL_HWSURFACE);

/* Check is there are any modes available */
if(modes == (SDL_Rect **)0){
  printf("No modes available!\n");
  exit(-1);
}

/* Check if or resolution is restricted */
if(modes == (SDL_Rect **)-1){
  printf("All resolutions available.\n");
}
else{
  /* Print valid modes */
  printf("Available Modes\n");
  for(i=0;modes[i];++i)
    printf("  %d x %d\n", modes[i]->w, modes[i]->h);
}
.
.

See Also

SDL_SetVideoMode, SDL_GetVideoInfo, SDL_Rect, SDL_PixelFormat

Prev | Home | Next
SDL_VideoDriverName | Up | SDL_VideoModeOK


SDL_VideoModeOK

Name

SDL_VideoModeOK -- Check to see if a particular video mode is supported.

Synopsis

#include "SDL.h"

int SDL_VideoModeOK(int width, int height, int bpp, Uint32 flags);

Description

SDL_VideoModeOK returns 0 if the requested mode is not supported under any bit depth, or returns the bits-per-pixel of the closest available mode with the given width, height and requested surface flags (see SDL_SetVideoMode).

The bits-per-pixel value returned is only a suggested mode. You can usually request and bpp you want when setting the video mode and SDL will emulate that color depth with a shadow video surface.

The arguments to SDL_VideoModeOK are the same ones you would pass to SDL_SetVideoMode

Example

SDL_Surface *screen;
Uint32 bpp;
.
.
.
printf("Checking mode 640x480@16bpp.\n");
bpp=SDL_VideoModeOK(640, 480, 16, SDL_HWSURFACE);

if(!bpp){
  printf("Mode not available.\n");
  exit(-1);
}

printf("SDL Recommends 640x480@%dbpp.\n", bpp);
screen=SDL_SetVideoMode(640, 480, bpp, SDL_HWSURFACE);
.
.

See Also

SDL_SetVideoMode, SDL_GetVideoInfo

Prev | Home | Next
SDL_ListModes | Up | SDL_SetVideoMode


SDL_SetVideoMode

Name

SDL_SetVideoMode -- Set up a video mode with the specified width, height and bits-per-pixel.

Synopsis

#include "SDL.h"

SDL_Surface *SDL_SetVideoMode(int width, int height, int bpp, Uint32 flags);

Description

Set up a video mode with the specified width, height and bits-per-pixel.

If bpp is 0, it is treated as the current display bits per pixel.

The flags parameter is the same as the flags field of the SDL_Surface structure. OR'd combinations of the following values are valid.

SDL_SWSURFACE | Create the video surface in system memory
SDL_HWSURFACE | Create the video surface in video memory
SDL_ASYNCBLIT | Enables the use of asynchronous updates of the display surface. This will usually slow down blitting on single CPU machines, but may provide a speed increase on SMP systems.
SDL_ANYFORMAT | Normally, if a video surface of the requested bits-per-pixel (bpp) is not available, SDL will emulate one with a shadow surface. Passing SDL_ANYFORMAT prevents this and causes SDL to use the video surface, regardless of its pixel depth.
SDL_HWPALETTE | Give SDL exclusive palette access. Without this flag you may not always get the the colors you request with SDL_SetColors or SDL_SetPalette.
SDL_DOUBLEBUF | Enable hardware double buffering; only valid with SDL_HWSURFACE. Calling SDL_Flip will flip the buffers and update the screen. All drawing will take place on the surface that is not displayed at the moment. If double buffering could not be enabled then SDL_Flip will just perform a SDL_UpdateRect on the entire screen.
SDL_FULLSCREEN | SDL will attempt to use a fullscreen mode. If a hardware resolution change is not possible (for whatever reason), the next higher resolution will be used and the display window centered on a black background.
SDL_OPENGL | Create an OpenGL rendering context. You should have previously set OpenGL video attributes with SDL_GL_SetAttribute.
SDL_OPENGLBLIT | Create an OpenGL rendering context, like above, but allow normal blitting operations. The screen (2D) surface may have an alpha channel, and SDL_UpdateRects must be used for updating changes to the screen surface.
SDL_RESIZABLE | Create a resizable window. When the window is resized by the user a SDL_VIDEORESIZE event is generated and SDL_SetVideoMode can be called again with the new size.
SDL_NOFRAME | If possible, SDL_NOFRAME causes SDL to create a window with no title bar or frame decoration. Fullscreen modes automatically have this flag set.

Note: Whatever flags SDL_SetVideoMode could satisfy are set in the flags member of the returned surface.

Note: The bpp parameter is the number of bits per pixel, so a bpp of 24 uses the packed representation of 3 bytes/pixel. For the more common 4 bytes/pixel mode, use a bpp of 32. Somewhat oddly, both 15 and 16 will request a 2 bytes/pixel mode, but different pixel formats.

Return Value

The framebuffer surface, or NULL if it fails. The surface returned is freed by SDL_Quit() and should nt be freed by the caller.

See Also

SDL_LockSurface, SDL_SetColors, SDL_Flip, SDL_Surface

Prev | Home | Next
SDL_VideoModeOK | Up | SDL_UpdateRect


SDL_UpdateRect

Name

SDL_UpdateRect -- Makes sure the given area is updated on the given screen.

Synopsis

#include "SDL.h"

void SDL_UpdateRect(SDL_Surface *screen, Sint32 x, Sint32 y, Sint32 w, Sint32 h);

Description

Makes sure the given area is updated on the given screen. The rectangle must be confined within the screen boundaries (no clipping is done).

If 'x', 'y', 'w' and 'h' are all 0, SDL_UpdateRect will update the entire screen.

This function should not be called while 'screen' is locked.

See Also

SDL_UpdateRects, SDL_Rect, SDL_Surface, SDL_LockSurface

Prev | Home | Next
SDL_SetVideoMode | Up | SDL_UpdateRects


SDL_UpdateRects

Name

SDL_UpdateRects -- Makes sure the given list of rectangles is updated on the given screen.

Synopsis

#include "SDL.h"

void SDL_UpdateRects(SDL_Surface *screen, int numrects, SDL_Rect *rects);

Description

Makes sure the given list of rectangles is updated on the given screen. The rectangles must all be confined within the screen boundaries (no clipping is done).

This function should not be called while screen is locked.

Note: It is adviced to call this function only once per frame, since each call has some processing overhead. This is no restriction since you can pass any number of rectangles each time.

The rectangles are not automatically merged or checked for overlap. In general, the programmer can use his knowledge about his particular rectangles to merge them in an efficient way, to avoid overdraw.

See Also

SDL_UpdateRect, SDL_Rect, SDL_Surface, SDL_LockSurface

Prev | Home | Next
SDL_UpdateRect | Up | SDL_Flip


SDL_Flip

Name

SDL_Flip -- Swaps screen buffers

Synopsis

#include "SDL.h"

int SDL_Flip(SDL_Surface *screen);

Description

On hardware that supports double-buffering, this function sets up a flip and returns. The hardware will wait for vertical retrace, and then swap video buffers before the next video surface blit or lock will return. On hardware that doesn't support double-buffering, this is equivalent to calling SDL_UpdateRect (screen, 0, 0, 0, 0)

The SDL_DOUBLEBUF flag must have been passed to SDL_SetVideoMode, when setting the video mode for this function to perform hardware flipping.

Return Value

This function returns 0 if successful, or -1 if there was an error.

See Also

SDL_SetVideoMode, SDL_UpdateRect, SDL_Surface

Prev | Home | Next
SDL_UpdateRects | Up | SDL_SetColors


SDL_SetColors

Name

SDL_SetColors -- Sets a portion of the colormap for the given 8-bit surface.

Synopsis

#include "SDL.h"

int SDL_SetColors(SDL_Surface *surface, SDL_Color *colors, int firstcolor, int ncolors);

Description

Sets a portion of the colormap for the given 8-bit surface.

When surface is the surface associated with the current display, the display colormap will be updated with the requested colors. If SDL_HWPALETTE was set in SDL_SetVideoMode flags, SDL_SetColors will always return 1, and the palette is guaranteed to be set the way you desire, even if the window colormap has to be warped or run under emulation.

The color components of a SDL_Color structure are 8-bits in size, giving you a total of 2563 =16777216 colors.

Palettized (8-bit) screen surfaces with the SDL_HWPALETTE flag have two palettes, a logical palette that is used for mapping blits to/from the surface and a physical palette (that determines how the hardware will map the colors to the display). SDL_SetColors modifies both palettes (if present), and is equivalent to calling SDL_SetPalette with the flags set to (SDL_LOGPAL | SDL_PHYSPAL).

Return Value

If surface is not a palettized surface, this function does nothing, returning 0. If all of the colors were set as passed to SDL_SetColors, it will return 1. If not all the color entries were set exactly as given, it will return 0, and you should look at the surface palette to determine the actual color palette.

Example

/* Create a display surface with a grayscale palette */
SDL_Surface *screen;
SDL_Color colors[256];
int i;
.
.
.
/* Fill colors with color information */
for(i=0;i<256;i++){
  colors[i].r=i;
  colors[i].g=i;
  colors[i].b=i;
}

/* Create display */
screen=SDL_SetVideoMode(640, 480, 8, SDL_HWPALETTE);
if(!screen){
  printf("Couldn't set video mode: %s\n", SDL_GetError());
  exit(-1);
}

/* Set palette */
SDL_SetColors(screen, colors, 0, 256);
.
.
.
.

See Also

SDL_Color SDL_Surface, SDL_SetPalette, SDL_SetVideoMode

Prev | Home | Next
SDL_Flip | Up | SDL_SetPalette


SDL_SetPalette

Name

SDL_SetPalette -- Sets the colors in the palette of an 8-bit surface.

Synopsis

#include "SDL.h"

int SDL_SetPalette(SDL_Surface *surface, int flags, SDL_Color *colors, int firstcolor, int ncolors);

Description

Sets a portion of the palette for the given 8-bit surface.

Palettized (8-bit) screen surfaces with the SDL_HWPALETTE flag have two palettes, a logical palette that is used for mapping blits to/from the surface and a physical palette (that determines how the hardware will map the colors to the display). SDL_BlitSurface always uses the logical palette when blitting surfaces (if it has to convert between surface pixel formats). Because of this, it is often useful to modify only one or the other palette to achieve various special color effects (e.g., screen fading, color flashes, screen dimming).

This function can modify either the logical or physical palette by specifing SDL_LOGPAL or SDL_PHYSPALthe in the flags parameter.

When surface is the surface associated with the current display, the display colormap will be updated with the requested colors. If SDL_HWPALETTE was set in SDL_SetVideoMode flags, SDL_SetPalette will always return 1, and the palette is guaranteed to be set the way you desire, even if the window colormap has to be warped or run under emulation.

The color components of a SDL_Color structure are 8-bits in size, giving you a total of 2563=16777216 colors.

Return Value

If surface is not a palettized surface, this function does nothing, returning 0. If all of the colors were set as passed to SDL_SetPalette, it will return 1. If not all the color entries were set exactly as given, it will return 0, and you should look at the surface palette to determine the actual color palette.

Example

        /* Create a display surface with a grayscale palette */
        SDL_Surface *screen;
        SDL_Color colors[256];
        int i;
        .
        .
        .
        /* Fill colors with color information */
        for(i=0;i<256;i++){
          colors[i].r=i;
          colors[i].g=i;
          colors[i].b=i;
        }

        /* Create display */
        screen=SDL_SetVideoMode(640, 480, 8, SDL_HWPALETTE);
        if(!screen){
          printf("Couldn't set video mode: %s\n", SDL_GetError());
          exit(-1);
        }

        /* Set palette */
        SDL_SetPalette(screen, SDL_LOGPAL|SDL_PHYSPAL, colors, 0, 256);
        .
        .
        .
        .

See Also

SDL_SetColors, SDL_SetVideoMode, SDL_Surface, SDL_Color

Prev | Home | Next
SDL_SetColors | Up | SDL_SetGamma


SDL_SetGamma

Name

SDL_SetGamma -- Sets the color gamma function for the display

Synopsis

#include "SDL.h"

int SDL_SetGamma(float redgamma, float greengamma, float bluegamma);

Description

Sets the "gamma function" for the display of each color component. Gamma controls the brightness/contrast of colors displayed on the screen. A gamma value of 1.0 is identity (i.e., no adjustment is made).

This function adjusts the gamma based on the "gamma function" parameter, you can directly specify lookup tables for gamma adjustment with SDL_SetGammaRamp.

Not all display hardware is able to change gamma.

Return Value

Returns -1 on error (or if gamma adjustment is not supported).

See Also

SDL_GetGammaRamp SDL_SetGammaRamp

Prev | Home | Next
SDL_SetPalette | Up | SDL_GetGammaRamp


SDL_GetGammaRamp

Name

SDL_GetGammaRamp -- Gets the color gamma lookup tables for the display

Synopsis

#include "SDL.h"

int SDL_GetGammaRamp(Uint16 *redtable, Uint16 *greentable, Uint16 *bluetable);

Description

Gets the gamma translation lookup tables currently used by the display. Each table is an array of 256 Uint16 values.

Not all display hardware is able to change gamma.

Return Value

Returns -1 on error.

See Also

SDL_SetGamma SDL_SetGammaRamp

Prev | Home | Next
SDL_SetGamma | Up | SDL_SetGammaRamp


SDL_SetGammaRamp

Name

SDL_SetGammaRamp -- Sets the color gamma lookup tables for the display

Synopsis

#include "SDL.h"

int SDL_SetGammaRamp(Uint16 *redtable, Uint16 *greentable, Uint16 *bluetable);

Description

Sets the gamma lookup tables for the display for each color component. Each table is an array of 256 Uint16 values, representing a mapping between the input and output for that channel. The input is the index into the array, and the output is the 16-bit gamma value at that index, scaled to the output color precision. You may pass NULL to any of the channels to leave them unchanged.

This function adjusts the gamma based on lookup tables, you can also have the gamma calculated based on a "gamma function" parameter with SDL_SetGamma.

Not all display hardware is able to change gamma.

Return Value

Returns -1 on error (or if gamma adjustment is not supported).

See Also

SDL_SetGamma SDL_GetGammaRamp

Prev | Home | Next
SDL_GetGammaRamp | Up | SDL_MapRGB


SDL_MapRGB

Name

SDL_MapRGB -- Map a RGB color value to a pixel format.

Synopsis

#include "SDL.h"

Uint32 SDL_MapRGB(SDL_PixelFormat *fmt, Uint8 r, Uint8 g, Uint8 b);

Description

Maps the RGB color value to the specified pixel format and returns the pixel value as a 32-bit int.

If the format has a palette (8-bit) the index of the closest matching color in the palette will be returned.

If the specified pixel format has an alpha component it will be returned as all 1 bits (fully opaque).

Return Value

A pixel value best approximating the given RGB color value for a given pixel format. If the pixel format bpp (color depth) is less than 32-bpp then the unused upper bits of the return value can safely be ignored (e.g., with a 16-bpp format the return value can be assigned to a Uint16, and similarly a Uint8 for an 8-bpp format).

See Also

SDL_GetRGB, SDL_GetRGBA, SDL_MapRGBA, SDL_PixelFormat

Prev | Home | Next
SDL_SetGammaRamp | Up | SDL_MapRGBA


SDL_MapRGBA

Name

SDL_MapRGBA -- Map a RGBA color value to a pixel format.

Synopsis

#include "SDL.h"

Uint32 SDL_MapRGBA(SDL_PixelFormat *fmt, Uint8 r, Uint8 g, Uint8 b, Uint8 a);

Description

Maps the RGBA color value to the specified pixel format and returns the pixel value as a 32-bit int.

If the format has a palette (8-bit) the index of the closest matching color in the palette will be returned.

If the specified pixel format has no alpha component the alpha value will be ignored (as it will be in formats with a palette).

Return Value

A pixel value best approximating the given RGBA color value for a given pixel format. If the pixel format bpp (color depth) is less than 32-bpp then the unused upper bits of the return value can safely be ignored (e.g., with a 16-bpp format the return value can be assigned to a Uint16, and similarly a Uint8 for an 8-bpp format).

See Also

SDL_GetRGB, SDL_GetRGBA, SDL_MapRGB, SDL_PixelFormat

Prev | Home | Next
SDL_MapRGB | Up | SDL_GetRGB


SDL_GetRGB

Name

SDL_GetRGB -- Get RGB values from a pixel in the specified pixel format.

Synopsis

#include "SDL.h"

void SDL_GetRGB(Uint32 pixel, SDL_PixelFormat *fmt, Uint8 *r, Uint8 *g, Uint8 *b);

Description

Get RGB component values from a pixel stored in the specified pixel format.

This function uses the entire 8-bit [0..255] range when converting color components from pixel formats with less than 8-bits per RGB component (e.g., a completely white pixel in 16-bit RGB565 format would return [0xff, 0xff, 0xff] not [0xf8, 0xfc, 0xf8]).

See Also

SDL_GetRGBA, SDL_MapRGB, SDL_MapRGBA, SDL_PixelFormat

Prev | Home | Next
SDL_MapRGBA | Up | SDL_GetRGBA


SDL_GetRGBA

Name

SDL_GetRGBA -- Get RGBA values from a pixel in the specified pixel format.

Synopsis

#include "SDL.h"

void SDL_GetRGBA(Uint32 pixel, SDL_PixelFormat *fmt, Uint8 *r, Uint8 *g, Uint8 *b, Uint8 *a);

Description

Get RGBA component values from a pixel stored in the specified pixel format.

This function uses the entire 8-bit [0..255] range when converting color components from pixel formats with less than 8-bits per RGB component (e.g., a completely white pixel in 16-bit RGB565 format would return [0xff, 0xff, 0xff] not [0xf8, 0xfc, 0xf8]).

If the surface has no alpha component, the alpha will be returned as 0xff (100% opaque).

See Also

SDL_GetRGB, SDL_MapRGB, SDL_MapRGBA, SDL_PixelFormat

Prev | Home | Next
SDL_GetRGB | Up | SDL_CreateRGBSurface


SDL_CreateRGBSurface

Name

SDL_CreateRGBSurface -- Create an empty SDL_Surface

Synopsis

#include "SDL.h"

SDL_Surface *SDL_CreateRGBSurface(Uint32 flags, int width, int height, int depth, Uint32 Rmask, Uint32 Gmask, Uint32 Bmask, Uint32 Amask);

Description

Allocate an empty surface (must be called after SDL_SetVideoMode)

If depth is 8 bits an empty palette is allocated for the surface, otherwise a 'packed-pixel' SDL_PixelFormat is created using the [RGBA]mask's provided (see SDL_PixelFormat). The flags specifies the type of surface that should be created, it is an OR'd combination of the following possible values.

SDL_SWSURFACE | SDL will create the surface in system memory. This improves the performance of pixel level access, however you may not be able to take advantage of some types of hardware blitting.
SDL_HWSURFACE | SDL will attempt to create the surface in video memory. This will allow SDL to take advantage of Video->Video blits (which are often accelerated).
SDL_SRCCOLORKEY | This flag turns on colourkeying for blits from this surface. If SDL_HWSURFACE is also specified and colourkeyed blits are hardware-accelerated, then SDL will attempt to place the surface in video memory. Use SDL_SetColorKey to set or clear this flag after surface creation.
SDL_SRCALPHA | This flag turns on alpha-blending for blits from this surface. If SDL_HWSURFACE is also specified and alpha-blending blits are hardware-accelerated, then the surface will be placed in video memory if possible. Use SDL_SetAlpha to set or clear this flag after surface creation.

Note: If an alpha-channel is specified (that is, if Amask is nonzero), then the SDL_SRCALPHA flag is automatically set. You may remove this flag by calling SDL_SetAlpha after surface creation.

Return Value

Returns the created surface, or NULL upon error.

Example

    /* Create a 32-bit surface with the bytes of each pixel in R,G,B,A order,
       as expected by OpenGL for textures */
    SDL_Surface *surface;
    Uint32 rmask, gmask, bmask, amask;

    /* SDL interprets each pixel as a 32-bit number, so our masks must depend
       on the endianness (byte order) of the machine */
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
    rmask = 0xff000000;
    gmask = 0x00ff0000;
    bmask = 0x0000ff00;
    amask = 0x000000ff;
#else
    rmask = 0x000000ff;
    gmask = 0x0000ff00;
    bmask = 0x00ff0000;
    amask = 0xff000000;
#endif

    surface = SDL_CreateRGBSurface(SDL_SWSURFACE, width, height, 32,
                                   rmask, gmask, bmask, amask);
    if(surface == NULL) {
        fprintf(stderr, "CreateRGBSurface failed: %s\n", SDL_GetError());
        exit(1);
    }

See Also

SDL_CreateRGBSurfaceFrom, SDL_FreeSurface, SDL_SetVideoMode, SDL_LockSurface, SDL_PixelFormat, SDL_Surface SDL_SetAlpha SDL_SetColorKey

Prev | Home | Next
SDL_GetRGBA | Up | SDL_CreateRGBSurfaceFrom


SDL_CreateRGBSurfaceFrom

Name

SDL_CreateRGBSurfaceFrom -- Create an SDL_Surface from pixel data

Synopsis

#include "SDL.h"

SDL_Surface *SDL_CreateRGBSurfaceFrom(void *pixels, int width, int height, int depth, int pitch, Uint32 Rmask, Uint32 Gmask, Uint32 Bmask, Uint32 Amask);

Description

Creates an SDL_Surface from the provided pixel data.

The data stored in pixels is assumed to be of the depth specified in the parameter list. The pixel data is not copied into the SDL_Surface structure so it should not be freed until the surface has been freed with a called to SDL_FreeSurface. pitch is the length of each scanline in bytes.

See SDL_CreateRGBSurface for a more detailed description of the other parameters.

Return Value

Returns the created surface, or NULL upon error.

See Also

SDL_CreateRGBSurface, SDL_FreeSurface

Prev | Home | Next
SDL_CreateRGBSurface | Up | SDL_FreeSurface


SDL_FreeSurface

Name

SDL_FreeSurface -- Frees (deletes) a SDL_Surface

Synopsis

#include "SDL.h"

void SDL_FreeSurface(SDL_Surface *surface);

Description

Frees the resources used by a previously created SDL_Surface. If the surface was created using SDL_CreateRGBSurfaceFrom then the pixel data is not freed.

See Also

SDL_CreateRGBSurface SDL_CreateRGBSurfaceFrom

Prev | Home | Next
SDL_CreateRGBSurfaceFrom | Up | SDL_LockSurface


SDL_LockSurface

Name

SDL_LockSurface -- Lock a surface for directly access.

Synopsis

#include "SDL.h"

int SDL_LockSurface(SDL_Surface *surface);

Description

SDL_LockSurface sets up a surface for directly accessing the pixels. Between calls to SDL_LockSurface and SDL_UnlockSurface, you can write to and read from surface->pixels, using the pixel format stored in surface->format. Once you are done accessing the surface, you should use SDL_UnlockSurface to release it.

Not all surfaces require locking. If SDL_MUSTLOCK(surface) evaluates to 0, then you can read and write to the surface at any time, and the pixel format of the surface will not change.

No operating system or library calls should be made between lock/unlock pairs, as critical system locks may be held during this time.

It should be noted, that since SDL 1.1.8 surface locks are recursive. This means that you can lock a surface multiple times, but each lock must have a match unlock.

    .
    .
    SDL_LockSurface( surface );
    .
    /* Surface is locked */
    /* Direct pixel access on surface here */
    .
    SDL_LockSurface( surface );
    .
    /* More direct pixel access on surface */
    .
    SDL_UnlockSurface( surface );
    /* Surface is still locked */
    /* Note: Is versions < 1.1.8, the surface would have been */
    /* no longer locked at this stage                         */
    .
    SDL_UnlockSurface( surface );
    /* Surface is now unlocked */
    .
    .

Return Value

SDL_LockSurface returns 0, or -1 if the surface couldn't be locked.

See Also

SDL_UnlockSurface

Prev | Home | Next
SDL_FreeSurface | Up | SDL_UnlockSurface


SDL_UnlockSurface

Name

SDL_UnlockSurface -- Unlocks a previously locked surface.

Synopsis

#include "SDL.h"

void SDL_UnlockSurface(SDL_Surface *surface);

Description

Surfaces that were previously locked using SDL_LockSurface must be unlocked with SDL_UnlockSurface. Surfaces should be unlocked as soon as possible.

It should be noted that since 1.1.8, surface locks are recursive. See SDL_LockSurface.

See Also

SDL_LockSurface

Prev | Home | Next
SDL_LockSurface | Up | SDL_LoadBMP


SDL_LoadBMP

Name

SDL_LoadBMP -- Load a Windows BMP file into an SDL_Surface.

Synopsis

#include "SDL.h"

SDL_Surface *SDL_LoadBMP(const char *file);

Description

Loads a surface from a named Windows BMP file.

Return Value

Returns the new surface, or NULL if there was an error.

See Also

SDL_SaveBMP

Prev | Home | Next
SDL_UnlockSurface | Up | SDL_SaveBMP


SDL_SaveBMP

Name

SDL_SaveBMP -- Save an SDL_Surface as a Windows BMP file.

Synopsis

#include "SDL.h"

int SDL_SaveBMP(SDL_Surface *surface, const char *file);

Description

Saves the SDL_Surface surface as a Windows BMP file named file.

Return Value

Returns 0 if successful or -1 if there was an error.

See Also

SDL_LoadBMP

Prev | Home | Next
SDL_LoadBMP | Up | SDL_SetColorKey


SDL_SetColorKey

Name

SDL_SetColorKey -- Sets the color key (transparent pixel) in a blittable surface and RLE acceleration.

Synopsis

#include "SDL.h"

int SDL_SetColorKey(SDL_Surface *surface, Uint32 flag, Uint32 key);

Description

Sets the color key (transparent pixel) in a blittable surface and enables or disables RLE blit acceleration.

RLE acceleration can substantially speed up blitting of images with large horizontal runs of transparent pixels (i.e., pixels that match the key value). The key must be of the same pixel format as the surface, SDL_MapRGB is often useful for obtaining an acceptable value.

If flag is SDL_SRCCOLORKEY then key is the transparent pixel value in the source image of a blit.

If flag is OR'd with SDL_RLEACCEL then the surface will be draw using RLE acceleration when drawn with SDL_BlitSurface. The surface will actually be encoded for RLE acceleration the first time SDL_BlitSurface or SDL_DisplayFormat is called on the surface.

If flag is 0, this function clears any current color key.

Return Value

This function returns 0, or -1 if there was an error.

See Also

SDL_BlitSurface, SDL_DisplayFormat, SDL_MapRGB, SDL_SetAlpha

Prev | Home | Next
SDL_SaveBMP | Up | SDL_SetAlpha


SDL_SetAlpha

Name

SDL_SetAlpha -- Adjust the alpha properties of a surface

Synopsis

#include "SDL.h"

int SDL_SetAlpha(SDL_Surface *surface, Uint32 flag, Uint8 alpha);

Description

Note: This function and the semantics of SDL alpha blending have changed since version 1.1.4. Up until version 1.1.5, an alpha value of 0 was considered opaque and a value of 255 was considered transparent. This has now been inverted: 0 (SDL_ALPHA_TRANSPARENT) is now considered transparent and 255 (SDL_ALPHA_OPAQUE) is now considered opaque.

SDL_SetAlpha is used for setting the per-surface alpha value and/or enabling and disabling alpha blending.

Thesurface parameter specifies which surface whose alpha attributes you wish to adjust. flags is used to specify whether alpha blending should be used (SDL_SRCALPHA) and whether the surface should use RLE acceleration for blitting (SDL_RLEACCEL). flags can be an OR'd combination of these two options, one of these options or 0. If SDL_SRCALPHA is not passed as a flag then all alpha information is ignored when blitting the surface. The alpha parameter is the per-surface alpha value; a surface need not have an alpha channel to use per-surface alpha and blitting can still be accelerated with SDL_RLEACCEL.

Note: The per-surface alpha value of 128 is considered a special case and is optimised, so it's much faster than other per-surface values.

Alpha effects surface blitting in the following ways:

RGBA->RGB with SDL_SRCALPHA | The source is alpha-blended with the destination, using the alpha channel. SDL_SRCCOLORKEY and the per-surface alpha are ignored.
RGBA->RGB without SDL_SRCALPHA | The RGB data is copied from the source. The source alpha channel and the per-surface alpha value are ignored.
RGB->RGBA with SDL_SRCALPHA | The source is alpha-blended with the destination using the per-surface alpha value. If SDL_SRCCOLORKEY is set, only the pixels not matching the colorkey value are copied. The alpha channel of the copied pixels is set to opaque.
RGB->RGBA without SDL_SRCALPHA | The RGB data is copied from the source and the alpha value of the copied pixels is set to opaque. If SDL_SRCCOLORKEY is set, only the pixels not matching the colorkey value are copied.
RGBA->RGBA with SDL_SRCALPHA | The source is alpha-blended with the destination using the source alpha channel. The alpha channel in the destination surface is left untouched. SDL_SRCCOLORKEY is ignored.
RGBA->RGBA without SDL_SRCALPHA | The RGBA data is copied to the destination surface. If SDL_SRCCOLORKEY is set, only the pixels not matching the colorkey value are copied.
RGB->RGB with SDL_SRCALPHA | The source is alpha-blended with the destination using the per-surface alpha value. If SDL_SRCCOLORKEY is set, only the pixels not matching the colorkey value are copied.
RGB->RGB without SDL_SRCALPHA | The RGB data is copied from the source. If SDL_SRCCOLORKEY is set, only the pixels not matching the colorkey value are copied.

Note: Note that RGBA->RGBA blits (with SDL_SRCALPHA set) keep the alpha of the destination surface. This means that you cannot compose two arbitrary RGBA surfaces this way and get the result you would expect from "overlaying" them; the destination alpha will work as a mask.

Also note that per-pixel and per-surface alpha cannot be combined; the per-pixel alpha is always used if available

Return Value

This function returns 0, or -1 if there was an error.

See Also

SDL_MapRGBA, SDL_GetRGBA, SDL_DisplayFormatAlpha, SDL_BlitSurface

Prev | Home | Next
SDL_SetColorKey | Up | SDL_SetClipRect


SDL_SetClipRect

Name

SDL_SetClipRect -- Sets the clipping rectangle for a surface.

Synopsis

#include "SDL.h"

void SDL_SetClipRect(SDL_Surface *surface, SDL_Rect *rect);

Description

Sets the clipping rectangle for a surface. When this surface is the destination of a blit, only the area within the clip rectangle will be drawn into.

The rectangle pointed to by rect will be clipped to the edges of the surface so that the clip rectangle for a surface can never fall outside the edges of the surface.

If rect is NULL the clipping rectangle will be set to the full size of the surface.

See Also

SDL_GetClipRect, SDL_BlitSurface, SDL_Surface

Prev | Home | Next
SDL_SetAlpha | Up | SDL_GetClipRect


SDL_GetClipRect

Name

SDL_GetClipRect -- Gets the clipping rectangle for a surface.

Synopsis

#include "SDL.h"

void SDL_GetClipRect(SDL_Surface *surface, SDL_Rect *rect);

Description

Gets the clipping rectangle for a surface. When this surface is the destination of a blit, only the area within the clip rectangle is drawn into.

The rectangle pointed to by rect will be filled with the clipping rectangle of the surface.

See Also

SDL_SetClipRect, SDL_BlitSurface, SDL_Surface

Prev | Home | Next
SDL_SetClipRect | Up | SDL_ConvertSurface


SDL_ConvertSurface

Name

SDL_ConvertSurface -- Converts a surface to the same format as another surface.

Synopsis

#include "SDL/SDL.h"

SDL_Surface *SDL_ConvertSurface(SDL_Surface *src, SDL_PixelFormat *fmt, Uint32 flags);

Description

Creates a new surface of the specified format, and then copies and maps the given surface to it. If this function fails, it returns NULL.

The flags parameter is passed to SDL_CreateRGBSurface and has those semantics.

This function is used internally by SDL_DisplayFormat.

This function can only be called after SDL_Init.

Return Value

Returns either a pointer to the new surface, or NULL on error.

See Also

SDL_CreateRGBSurface, SDL_DisplayFormat, SDL_PixelFormat, SDL_Surface

Prev | Home | Next
SDL_GetClipRect | Up | SDL_BlitSurface


SDL_BlitSurface

Name

SDL_BlitSurface -- This performs a fast blit from the source surface to the destination surface.

Synopsis

#include "SDL.h"

int SDL_BlitSurface(SDL_Surface *src, SDL_Rect *srcrect, SDL_Surface *dst, SDL_Rect *dstrect);

Description

This performs a fast blit from the source surface to the destination surface.

Only the position is used in the dstrect (the width and height are ignored).

If either srcrect or dstrect are NULL, the entire surface (src or dst) is copied.

The final blit rectangle is saved in dstrect after all clipping is performed (srcrect is not modified).

The blit function should not be called on a locked surface.

The results of blitting operations vary greatly depending on whether SDL_SRCAPLHA is set or not. See SDL_SetAlpha for an explaination of how this affects your results. Colorkeying and alpha attributes also interact with surface blitting, as the following pseudo-code should hopefully explain.

if (source surface has SDL_SRCALPHA set) {
    if (source surface has alpha channel (that is, format->Amask != 0))
        blit using per-pixel alpha, ignoring any colour key
    else {
        if (source surface has SDL_SRCCOLORKEY set)
            blit using the colour key AND the per-surface alpha value
        else
            blit using the per-surface alpha value
    }
} else {
    if (source surface has SDL_SRCCOLORKEY set)
        blit using the colour key
    else
        ordinary opaque rectangular blit
}

Return Value

If the blit is successful, it returns 0, otherwise it returns -1.

If either of the surfaces were in video memory, and the blit returns -2, the video memory was lost, so it should be reloaded with artwork and re-blitted:

        while ( SDL_BlitSurface(image, imgrect, screen, dstrect) == -2 ) {
                while ( SDL_LockSurface(image)) < 0 )
                        Sleep(10);
                -- Write image pixels to image->pixels --
                SDL_UnlockSurface(image);
        }
This happens under DirectX 5.0 when the system switches away from your fullscreen application. Locking the surface will also fail until you have access to the video memory again.

See Also

SDL_LockSurface, SDL_FillRect, SDL_Surface, SDL_Rect

Prev | Home | Next
SDL_ConvertSurface | Up | SDL_FillRect


SDL_FillRect

Name

SDL_FillRect -- This function performs a fast fill of the given rectangle with some color

Synopsis

#include "SDL.h"

int SDL_FillRect(SDL_Surface *dst, SDL_Rect *dstrect, Uint32 color);

Description

This function performs a fast fill of the given rectangle with color. If dstrect is NULL, the whole surface will be filled with color.

The color should be a pixel of the format used by the surface, and can be generated by the SDL_MapRGB or SDL_MapRGBA functions. If the color value contains an alpha value then the destination is simply "filled" with that alpha information, no blending takes place.

If there is a clip rectangle set on the destination (set via SDL_SetClipRect) then this function will clip based on the intersection of the clip rectangle and the dstrect rectangle and the dstrect rectangle will be modified to represent the area actually filled.

Return Value

This function returns 0 on success, or -1 on error.

See Also

SDL_MapRGB, SDL_MapRGBA, SDL_BlitSurface, SDL_Rect

Prev | Home | Next
SDL_BlitSurface | Up | SDL_DisplayFormat


SDL_DisplayFormat

Name

SDL_DisplayFormat -- Convert a surface to the display format

Synopsis

#include "SDL.h"

SDL_Surface *SDL_DisplayFormat(SDL_Surface *surface);

Description

This function takes a surface and copies it to a new surface of the pixel format and colors of the video framebuffer, suitable for fast blitting onto the display surface. It calls SDL_ConvertSurface

If you want to take advantage of hardware colorkey or alpha blit acceleration, you should set the colorkey and alpha value before calling this function.

If you want an alpha channel, see SDL_DisplayFormatAlpha.

Return Value

If the conversion fails or runs out of memory, it returns NULL

See Also

SDL_ConvertSurface, SDL_DisplayFormatAlpha SDL_SetAlpha, SDL_SetColorKey, SDL_Surface

Prev | Home | Next
SDL_FillRect | Up | SDL_DisplayFormatAlpha


SDL_DisplayFormatAlpha

Name

SDL_DisplayFormatAlpha -- Convert a surface to the display format

Synopsis

#include "SDL.h"

SDL_Surface *SDL_DisplayFormatAlpha(SDL_Surface *surface);

Description

This function takes a surface and copies it to a new surface of the pixel format and colors of the video framebuffer plus an alpha channel, suitable for fast blitting onto the display surface. It calls SDL_ConvertSurface

If you want to take advantage of hardware colorkey or alpha blit acceleration, you should set the colorkey and alpha value before calling this function.

This function can be used to convert a colourkey to an alpha channel, if the SDL_SRCCOLORKEY flag is set on the surface. The generated surface will then be transparent (alpha=0) where the pixels match the colourkey, and opaque (alpha=255) elsewhere.

Return Value

If the conversion fails or runs out of memory, it returns NULL

See Also

SDL_ConvertSurface, SDL_SetAlpha, SDL_SetColorKey, SDL_DisplayFormat, SDL_Surface

Prev | Home | Next
SDL_DisplayFormat | Up | SDL_WarpMouse


SDL_WarpMouse

Name

SDL_WarpMouse -- Set the position of the mouse cursor.

Synopsis

#include "SDL.h"

void SDL_WarpMouse(Uint16 x, Uint16 y);

Description

Set the position of the mouse cursor (generates a mouse motion event).

See Also

SDL_MouseMotionEvent

Prev | Home | Next
SDL_DisplayFormatAlpha | Up | SDL_CreateCursor


SDL_CreateCursor

Name

SDL_CreateCursor -- Creates a new mouse cursor.

Synopsis

#include "SDL.h"

SDL_Cursor *SDL_CreateCursor(Uint8 *data, Uint8 *mask, int w, int h, int hot_x, int hot_y);

Description

Create a cursor using the specified data and mask (in MSB format). The cursor width must be a multiple of 8 bits.

The cursor is created in black and white according to the following:

Data / Mask | Resulting pixel on screen
0 / 1 | White
1 / 1 | Black
0 / 0 | Transparent
1 / 0 | Inverted color if possible, black if not.

Cursors created with this function must be freed with SDL_FreeCursor.

Example

/* Stolen from the mailing list */
/* Creates a new mouse cursor from an XPM */

/* XPM */
static const char *arrow[] = {
  /* width height num_colors chars_per_pixel */
  "    32    32        3            1",
  /* colors */
  "X c #000000",
  ". c #ffffff",
  "  c None",
  /* pixels */
  "X                               ",
  "XX                              ",
  "X.X                             ",
  "X..X                            ",
  "X...X                           ",
  "X....X                          ",
  "X.....X                         ",
  "X......X                        ",
  "X.......X                       ",
  "X........X                      ",
  "X.....XXXXX                     ",
  "X..X..X                         ",
  "X.X X..X                        ",
  "XX  X..X                        ",
  "X    X..X                       ",
  "     X..X                       ",
  "      X..X                      ",
  "      X..X                      ",
  "       XX                       ",
  "                                ",
  "                                ",
  "                                ",
  "                                ",
  "                                ",
  "                                ",
  "                                ",
  "                                ",
  "                                ",
  "                                ",
  "                                ",
  "                                ",
  "                                ",
  "0,0"
};

static SDL_Cursor *init_system_cursor(const char *image[])
{
  int i, row, col;
  Uint8 data[4*32];
  Uint8 mask[4*32];
  int hot_x, hot_y;

  i = -1;
  for ( row=0; row<32; ++row ) {
    for ( col=0; col<32; ++col ) {
      if ( col % 8 ) {
        data[i] <<= 1;
        mask[i] <<= 1;
      } else {
        ++i;
        data[i] = mask[i] = 0;
      }
      switch (image[4+row][col]) {
        case 'X':
          data[i] |= 0x01;
          k[i] |= 0x01;
          break;
        case '.':
          mask[i] |= 0x01;
          break;
        case ' ':
          break;
      }
    }
  }
  sscanf(image[4+row], "%d,%d", &hot_x, &hot_y);
  return SDL_CreateCursor(data, mask, 32, 32, hot_x, hot_y);
}

See Also

SDL_FreeCursor, SDL_SetCursor, SDL_ShowCursor

Prev | Home | Next
SDL_WarpMouse | Up | SDL_FreeCursor


SDL_FreeCursor

Name

SDL_FreeCursor -- Frees a cursor created with SDL_CreateCursor.

Synopsis

#include "SDL.h"

void SDL_FreeCursor(SDL_Cursor *cursor);

Description

Frees a SDL_Cursor that was created using SDL_CreateCursor.

See Also

SDL_CreateCursor

Prev | Home | Next
SDL_CreateCursor | Up | SDL_SetCursor


SDL_SetCursor

Name

SDL_SetCursor -- Set the currently active mouse cursor.

Synopsis

#include "SDL.h"

void *SDL_SetCursor(SDL_Cursor *cursor);

Description

Sets the currently active cursor to the specified one. If the cursor is currently visible, the change will be immediately represented on the display.

See Also

SDL_GetCursor, SDL_CreateCursor, SDL_ShowCursor

Prev | Home | Next
SDL_FreeCursor | Up | SDL_GetCursor


SDL_GetCursor

Name

SDL_GetCursor -- Get the currently active mouse cursor.

Synopsis

#include "SDL.h"

SDL_Cursor *SDL_GetCursor(void);

Description

Returns the currently active mouse cursor.

See Also

SDL_SetCursor, SDL_CreateCursor, SDL_ShowCursor

Prev | Home | Next
SDL_SetCursor | Up | SDL_ShowCursor


SDL_ShowCursor

Name

SDL_ShowCursor -- Toggle whether or not the cursor is shown on the screen.

Synopsis

#include "SDL.h"

int SDL_ShowCursor(int toggle);

Description

Toggle whether or not the cursor is shown on the screen. Passing SDL_ENABLE displays the cursor and passing SDL_DISABLE hides it. The current state of the mouse cursor can be queried by passing SDL_QUERY, either SDL_DISABLE or SDL_ENABLE will be returned.

The cursor starts off displayed, but can be turned off.

Return Value

Returns the current state of the cursor.

See Also

SDL_CreateCursor, SDL_SetCursor

Prev | Home | Next
SDL_GetCursor | Up | SDL_GL_LoadLibrary


SDL_GL_LoadLibrary

Name

SDL_GL_LoadLibrary -- Specify an OpenGL library

Synopsis

#include "SDL.h"

int SDL_GL_LoadLibrary(const char *path);

Description

If you wish, you may load the OpenGL library at runtime, this must be done before SDL_SetVideoMode is called. The path of the GL library is passed to SDL_GL_LoadLibrary and it returns 0 on success, or -1 on an error. You must then use SDL_GL_GetProcAddress to retrieve function pointers to GL functions.

See Also

SDL_GL_GetProcAddress

Prev | Home | Next
SDL_ShowCursor | Up | SDL_GL_GetProcAddress


SDL_GL_GetProcAddress

Name

SDL_GL_GetProcAddress -- Get the address of a GL function

Synopsis

#include "SDL.h"

void *SDL_GL_GetProcAddress(const char* proc);

Description

Returns the address of the GL function proc, or NULL if the function is not found. If the GL library is loaded at runtime, with SDL_GL_LoadLibrary, then all GL functions must be retrieved this way. Usually this is used to retrieve function pointers to OpenGL extensions.

Example

typedef void (*GL_ActiveTextureARB_Func)(unsigned int);
GL_ActiveTextureARB_Func glActiveTextureARB_ptr = 0;
int has_multitexture=1;
.
.
.
/* Get function pointer */
glActiveTextureARB_ptr=(GL_ActiveTextureARB_Func) SDL_GL_GetProcAddress("glActiveTextureARB");

/* Check for a valid function ptr */
if(!glActiveTextureARB_ptr){
  fprintf(stderr, "Multitexture Extensions not present.\n");
  has_multitexture=0;
}
.
.
.
.
if(has_multitexture){
  glActiveTextureARB_ptr(GL_TEXTURE0_ARB);
  .
  .
}
else{
  .
  .
}

See Also

SDL_GL_LoadLibrary

Prev | Home | Next
SDL_GL_LoadLibrary | Up | SDL_GL_GetAttribute


SDL_GL_GetAttribute

Name

SDL_GL_GetAttribute -- Get the value of a special SDL/OpenGL attribute

Synopsis

#include "SDL.h"

int SDL_GL_GetAttribute(SDLGLattr attr, int *value);

Description

Places the value of the SDL/OpenGL attribute attr into value. This is useful after a call to SDL_SetVideoMode to check whether your attributes have been set as you expected.

Return Value

Returns 0 on success, or -1 on an error.

See Also

SDL_GL_SetAttribute, GL Attributes

Prev | Home | Next
SDL_GL_GetProcAddress | Up | SDL_GL_SetAttribute


SDL_GL_SetAttribute

Name

SDL_GL_SetAttribute -- Set a special SDL/OpenGL attribute

Synopsis

#include "SDL.h"

int SDL_GL_SetAttribute(SDL_GLattr attr, int value);

Description

Sets the OpenGL attribute attr to value. The attributes you set don't take effect until after a call to SDL_SetVideoMode. You should use SDL_GL_GetAttribute to check the values after a SDL_SetVideoMode call.

Return Value

Returns 0 on success, or -1 on error.

Example

SDL_GL_SetAttribute( SDL_GL_RED_SIZE, 5 );
SDL_GL_SetAttribute( SDL_GL_GREEN_SIZE, 5 );
SDL_GL_SetAttribute( SDL_GL_BLUE_SIZE, 5 );
SDL_GL_SetAttribute( SDL_GL_DEPTH_SIZE, 16 );
SDL_GL_SetAttribute( SDL_GL_DOUBLEBUFFER, 1 );
if ( (screen=SDL_SetVideoMode( 640, 480, 16, SDL_OPENGL )) == NULL ) {
  fprintf(stderr, "Couldn't set GL mode: %s\n", SDL_GetError());
  SDL_Quit();
  return;
}

Note: The SDL_DOUBLEBUF flag is not required to enable double buffering when setting an OpenGL video mode. Double buffering is enabled or disabled using the SDL_GL_DOUBLEBUFFER attribute.

See Also

SDL_GL_GetAttribute, GL Attributes

Prev | Home | Next
SDL_GL_GetAttribute | Up | SDL_GL_SwapBuffers


SDL_GL_SwapBuffers

Name

SDL_GL_SwapBuffers -- Swap OpenGL framebuffers/Update Display

Synopsis

#include "SDL.h"

void SDL_GL_SwapBuffers(void );

Description

Swap the OpenGL buffers, if double-buffering is supported.

See Also

SDL_SetVideoMode, SDL_GL_SetAttribute

Prev | Home | Next
SDL_GL_SetAttribute | Up | SDL_CreateYUVOverlay


SDL_CreateYUVOverlay

Name

SDL_CreateYUVOverlay -- Create a YUV video overlay

Synopsis

#include "SDL.h"

SDL_Overlay *SDL_CreateYUVOverlay(int width, int height, Uint32 format, SDL_Surface *display);

Description

SDL_CreateYUVOverlay creates a YUV overlay of the specified width, height and format (see SDL_Overlay for a list of available formats), for the provided display. A SDL_Overlay structure is returned.

The term 'overlay' is a misnomer since, unless the overlay is created in hardware, the contents for the display surface underneath the area where the overlay is shown will be overwritten when the overlay is displayed.

See Also

SDL_Overlay, SDL_DisplayYUVOverlay, SDL_FreeYUVOverlay

Prev | Home | Next
SDL_GL_SwapBuffers | Up | SDL_LockYUVOverlay


SDL_LockYUVOverlay

Name

SDL_LockYUVOverlay -- Lock an overlay

Synopsis

#include "SDL.h"

int SDL_LockYUVOverlay(SDL_Overlay *overlay);

Description

Much the same as SDL_LockSurface, SDL_LockYUVOverlay locks the overlay for direct access to pixel data.

Return Value

Returns 0 on success, or -1 on an error.

See Also

SDL_UnlockYUVOverlay, SDL_CreateYUVOverlay, SDL_Overlay

Prev | Home | Next
SDL_CreateYUVOverlay | Up | SDL_UnlockYUVOverlay


SDL_UnlockYUVOverlay

Name

SDL_UnlockYUVOverlay -- Unlock an overlay

Synopsis

#include "SDL.h"

void SDL_UnlockYUVOverlay(SDL_Overlay *overlay);

Description

The opposite to SDL_LockYUVOverlay. Unlocks a previously locked overlay. An overlay must be unlocked before it can be displayed.

See Also

SDL_UnlockYUVOverlay, SDL_CreateYUVOverlay, SDL_Overlay

Prev | Home | Next
SDL_LockYUVOverlay | Up | SDL_DisplayYUVOverlay


SDL_DisplayYUVOverlay

Name

SDL_DisplayYUVOverlay -- Blit the overlay to the display

Synopsis

#include "SDL.h"

int SDL_DisplayYUVOverlay(SDL_Overlay *overlay, SDL_Rect *dstrect);

Description

Blit the overlay to the surface specified when it was created. The SDL_Rect structure, dstrect, specifies the position and size of the destination. If the dstrect is a larger or smaller than the overlay then the overlay will be scaled, this is optimized for 2x scaling.

Return Values

Returns 0 on success

See Also

SDL_Overlay, SDL_CreateYUVOverlay

Prev | Home | Next
SDL_UnlockYUVOverlay | Up | SDL_FreeYUVOverlay


SDL_FreeYUVOverlay

Name

SDL_FreeYUVOverlay -- Free a YUV video overlay

Synopsis

#include "SDL.h"

void SDL_FreeYUVOverlay(SDL_Overlay *overlay);

Description

Frees and overlay created by SDL_CreateYUVOverlay.

See Also

SDL_Overlay, SDL_DisplayYUVOverlay, SDL_FreeYUVOverlay

Prev | Home | Next
SDL_DisplayYUVOverlay | Up | SDL_GLattr


SDL_GLattr

Name

SDL_GLattr -- SDL GL Attributes

Attributes

SDL_GL_RED_SIZE | Size of the framebuffer red component, in bits
SDL_GL_GREEN_SIZE | Size of the framebuffer green component, in bits
SDL_GL_BLUE_SIZE | Size of the framebuffer blue component, in bits
SDL_GL_ALPHA_SIZE | Size of the framebuffer alpha component, in bits
SDL_GL_DOUBLEBUFFER | 0 or 1, enable or disable double buffering
SDL_GL_BUFFER_SIZE | Size of the framebuffer, in bits
SDL_GL_DEPTH_SIZE | Size of the depth buffer, in bits
SDL_GL_STENCIL_SIZE | Size of the stencil buffer, in bits
SDL_GL_ACCUM_RED_SIZE | Size of the accumulation buffer red component, in bits
SDL_GL_ACCUM_GREEN_SIZE | Size of the accumulation buffer green component, in bits
SDL_GL_ACCUM_BLUE_SIZE | Size of the accumulation buffer blue component, in bits
SDL_GL_ACCUM_ALPHA_SIZE | Size of the accumulation buffer alpha component, in bits

Description

While you can set most OpenGL attributes normally, the attributes list above must be known before SDL sets the video mode. These attributes a set and read with SDL_GL_SetAttribute and SDL_GL_GetAttribute.

See Also

SDL_GL_SetAttribute, SDL_GL_GetAttribute

Prev | Home | Next
SDL_FreeYUVOverlay | Up | SDL_Rect


SDL_Rect

Name

SDL_Rect -- Defines a rectangular area

Structure Definition

typedef struct{
  Sint16 x, y;
  Uint16 w, h;
} SDL_Rect;

Structure Data

x, y | Position of the upper-left corner of the rectangle
w, h | The width and height of the rectangle

Description

A SDL_Rect defines a rectangular area of pixels. It is used by SDL_BlitSurface to define blitting regions and by several other video functions.

See Also

SDL_BlitSurface, SDL_UpdateRect

Prev | Home | Next
SDL_GLattr | Up | SDL_Color


SDL_Color

Name

SDL_Color -- Format independent color description

Structure Definition

typedef struct{
  Uint8 r;
  Uint8 g;
  Uint8 b;
  Uint8 unused;
} SDL_Color;

Structure Data

r | Red intensity
g | Green intensity
b | Blue intensity
unused | Unused

Description

SDL_Color describes a color in a format independent way. You can convert a SDL_Color to a pixel value for a certain pixel format using SDL_MapRGB.

See Also

SDL_PixelFormat, SDL_SetColors, SDL_Palette

Prev | Home | Next
SDL_Rect | Up | SDL_Palette


SDL_Palette

Name

SDL_Palette -- Color palette for 8-bit pixel formats

Structure Definition

typedef struct{
  int ncolors;
  SDL_Color *colors;
} SDL_Palette;

Structure Data

ncolors | Number of colors used in this palette
colors | Pointer to SDL_Color structures that make up the palette.

Description

Each pixel in an 8-bit surface is an index into the colors field of the SDL_Palette structure store in SDL_PixelFormat. A SDL_Palette should never need to be created manually. It is automatically created when SDL allocates a SDL_PixelFormat for a surface. The colors values of a SDL_Surfaces palette can be set with the SDL_SetColors.

See Also

SDL_Color, SDL_Surface, SDL_SetColors SDL_SetPalette

Prev | Home | Next
SDL_Color | Up | SDL_PixelFormat


SDL_PixelFormat

Name

SDL_PixelFormat -- Stores surface format information

Structure Definition

typedef struct{
  SDL_Palette *palette;
  Uint8  BitsPerPixel;
  Uint8  BytesPerPixel;
  Uint32 Rmask, Gmask, Bmask, Amask;
  Uint8  Rshift, Gshift, Bshift, Ashift;
  Uint8  Rloss, Gloss, Bloss, Aloss;
  Uint32 colorkey;
  Uint8  alpha;
} SDL_PixelFormat;

Structure Data

palette | Pointer to the palette, or NULL if the BitsPerPixel>8
BitsPerPixel | The number of bits used to represent each pixel in a surface. Usually 8, 16, 24 or 32.
BytesPerPixel | The number of bytes used to represent each pixel in a surface. Usually one to four.
[RGBA]mask | Binary mask used to retrieve individual color values
[RGBA]loss | Precision loss of each color component (2[RGBA]loss)
[RGBA]shift | Binary left shift of each color component in the pixel value
colorkey | Pixel value of transparent pixels
alpha | Overall surface alpha value

Description

A SDL_PixelFormat describes the format of the pixel data stored at the pixels field of a SDL_Surface. Every surface stores a SDL_PixelFormat in the format field.

If you wish to do pixel level modifications on a surface, then understanding how SDL stores its color information is essential.

8-bit pixel formats are the easiest to understand. Since its an 8-bit format, we have 8 BitsPerPixel and 1 BytesPerPixel. Since BytesPerPixel is 1, all pixels are represented by a Uint8 which contains an index into palette->colors. So, to determine the color of a pixel in a 8-bit surface: we read the color index from surface->pixels and we use that index to read the SDL_Color structure from surface->format->palette->colors. Like so:

SDL_Surface *surface;
SDL_PixelFormat *fmt;
SDL_Color *color;
Uint8 index;

.
.

/* Create surface */
.
.
fmt=surface->format;

/* Check the bitdepth of the surface */
if(fmt->BitsPerPixel!=8){
  fprintf(stderr, "Not an 8-bit surface.\n");
  return(-1);
}

/* Lock the surface */
SDL_LockSurface(surface);

/* Get the topleft pixel */
index=*(Uint8 *)surface->pixels;
color=fmt->palette->colors[index];

/* Unlock the surface */
SDL_UnlockSurface(surface);
printf("Pixel Color-> Red: %d, Green: %d, Blue: %d. Index: %d\n",
          color->r, color->g, color->b, index);
.
.

Pixel formats above 8-bit are an entirely different experience. They are considered to be "TrueColor" formats and the color information is stored in the pixels themselves, not in a palette. The mask, shift and loss fields tell us how the color information is encoded. The mask fields allow us to isolate each color component, the shift fields tell us the number of bits to the right of each component in the pixel value and the loss fields tell us the number of bits lost from each component when packing 8-bit color component in a pixel.

/* Extracting color components from a 32-bit color value */
SDL_PixelFormat *fmt;
SDL_Surface *surface;
Uint32 temp, pixel;
Uint8 red, green, blue, alpha;
.
.
.
fmt=surface->format;
SDL_LockSurface(surface);
pixel=*((Uint32*)surface->pixels);
SDL_UnlockSurface(surface);

/* Get Red component */
temp=pixel&fmt->Rmask; /* Isolate red component */
temp=temp>>fmt->Rshift;/* Shift it down to 8-bit */
temp=temp<<fmt->Rloss; /* Expand to a full 8-bit number */
red=(Uint8)temp;

/* Get Green component */
temp=pixel&fmt->Gmask; /* Isolate green component */
temp=temp>>fmt->Gshift;/* Shift it down to 8-bit */
temp=temp<<fmt->Gloss; /* Expand to a full 8-bit number */
green=(Uint8)temp;

/* Get Blue component */
temp=pixel&fmt->Bmask; /* Isolate blue component */
temp=temp>>fmt->Bshift;/* Shift it down to 8-bit */
temp=temp<<fmt->Bloss; /* Expand to a full 8-bit number */
blue=(Uint8)temp;

/* Get Alpha component */
temp=pixel&fmt->Amask; /* Isolate alpha component */
temp=temp>>fmt->Ashift;/* Shift it down to 8-bit */
temp=temp<<fmt->Aloss; /* Expand to a full 8-bit number */
alpha=(Uint8)temp;

printf("Pixel Color -> R: %d,  G: %d,  B: %d,  A: %d\n", red, green, blue, alpha);
.
.
.

See Also

SDL_Surface, SDL_MapRGB

Prev | Home | Next
SDL_Palette | Up | SDL_Surface


SDL_Surface

Name

SDL_Surface -- Graphical Surface Structure

Structure Definition

typedef struct SDL_Surface {
        Uint32 flags;                           /* Read-only */
        SDL_PixelFormat *format;                /* Read-only */
        int w, h;                               /* Read-only */
        Uint16 pitch;                           /* Read-only */
        void *pixels;                           /* Read-write */

        /* clipping information */
        SDL_Rect clip_rect;                     /* Read-only */

        /* Reference count -- used when freeing surface */
        int refcount;                           /* Read-mostly */

    /* This structure also contains private fields not shown here */
} SDL_Surface;

Structure Data

flags | Surface flags
format | Pixel format
w, h | Width and height of the surface
pitch | Length of a surface scanline in bytes
pixels | Pointer to the actual pixel data
clip_rect | surface clip rectangle

Description

SDL_Surface's represent areas of "graphical" memory, memory that can be drawn to. The video framebuffer is returned as a SDL_Surface by SDL_SetVideoMode and SDL_GetVideoSurface. Most of the fields should be pretty obvious. w and h are the width and height of the surface in pixels. pixels is a pointer to the actual pixel data, the surface should be locked before accessing this field. The clip_rect field is the clipping rectangle as set by SDL_SetClipRect.

The following are supported in the flags field.

SDL_SWSURFACE | Surface is stored in system memory
SDL_HWSURFACE | Surface is stored in video memory
SDL_ASYNCBLIT | Surface uses asynchronous blits if possible
SDL_ANYFORMAT | Allows any pixel-format (Display surface)
SDL_HWPALETTE | Surface has exclusive palette
SDL_DOUBLEBUF | Surface is double buffered (Display surface)
SDL_FULLSCREEN | Surface is full screen (Display Surface)
SDL_OPENGL | Surface has an OpenGL context (Display Surface)
SDL_OPENGLBLIT | Surface supports OpenGL blitting (Display Surface)
SDL_RESIZABLE | Surface is resizable (Display Surface)
SDL_HWACCEL | Surface blit uses hardware acceleration
SDL_SRCCOLORKEY | Surface use colorkey blitting
SDL_RLEACCEL | Colorkey blitting is accelerated with RLE
SDL_SRCALPHA | Surface blit uses alpha blending
SDL_PREALLOC | Surface uses preallocated memory

See Also

SDL_PixelFormat

Prev | Home | Next
SDL_PixelFormat | Up | SDL_VideoInfo


SDL_VideoInfo

Name

SDL_VideoInfo -- Video Target information

Structure Definition

typedef struct{
  Uint32 hw_available:1;
  Uint32 wm_available:1;
  Uint32 blit_hw:1;
  Uint32 blit_hw_CC:1;
  Uint32 blit_hw_A:1;
  Uint32 blit_sw:1;
  Uint32 blit_sw_CC:1;
  Uint32 blit_sw_A:1;
  Uint32 blit_fill;
  Uint32 video_mem;
  SDL_PixelFormat *vfmt;
} SDL_VideoInfo;

Structure Data

hw_available | Is it possible to create hardware surfaces?
wm_available | Is there a window manager available
blit_hw | Are hardware to hardware blits accelerated?
blit_hw_CC | Are hardware to hardware colorkey blits accelerated?
blit_hw_A | Are hardware to hardware alpha blits accelerated?
blit_sw | Are software to hardware blits accelerated?
blit_sw_CC | Are software to hardware colorkey blits accelerated?
blit_sw_A | Are software to hardware alpha blits accelerated?
blit_fill | Are color fills accelerated?
video_mem | Total amount of video memory in Kilobytes
vfmt | Pixel format of the video device

Description

This (read-only) structure is returned by SDL_GetVideoInfo. It contains information on either the 'best' available mode (if called before SDL_SetVideoMode) or the current video mode.

See Also

SDL_PixelFormat, SDL_GetVideoInfo

Prev | Home | Next
SDL_Surface | Up | SDL_Overlay


SDL_Overlay

Name

SDL_Overlay -- YUV video overlay

Structure Definition

typedef struct{
  Uint32 format;
  int w, h;
  int planes;
  Uint16 *pitches;
  Uint8 **pixels;
  Uint32 hw_overlay:1;
} SDL_Overlay;

Structure Data

format | Overlay format (see below)
w, h | Width and height of overlay
planes | Number of planes in the overlay. Usually either 1 or 3
pitches | An array of pitches, one for each plane. Pitch is the length of a row in bytes.
pixels | An array of pointers to teh data of each plane. The overlay should be locked before these pointers are used.
hw_overlay | This will be set to 1 if the overlay is hardware accelerated.

Description

A SDL_Overlay is similar to a SDL_Surface except it stores a YUV overlay. All the fields are read only, except for pixels which should be locked before use. The format field stores the format of the overlay which is one of the following:

#define SDL_YV12_OVERLAY  0x32315659  /* Planar mode: Y + V + U */
#define SDL_IYUV_OVERLAY  0x56555949  /* Planar mode: Y + U + V */
#define SDL_YUY2_OVERLAY  0x32595559  /* Packed mode: Y0+U0+Y1+V0 */
#define SDL_UYVY_OVERLAY  0x59565955  /* Packed mode: U0+Y0+V0+Y1 */
#define SDL_YVYU_OVERLAY  0x55595659  /* Packed mode: Y0+V0+Y1+U0 */
More information on YUV formats can be found at http://www.webartz.com/fourcc/indexyuv.htm.

See Also

SDL_CreateYUVOverlay, SDL_LockYUVOverlay, SDL_UnlockYUVOverlay, SDL_FreeYUVOverlay

Prev | Home | Next
SDL_VideoInfo | Up | Window Management


Window Management

SDL Library Documentation
Prev Next

Chapter 7. Window Management

Table of Contents
SDL_WM_SetCaption — Sets the window tile and icon name.
SDL_WM_GetCaption — Gets the window title and icon name.
SDL_WM_SetIcon — Sets the icon for the display window.
SDL_WM_IconifyWindow — Iconify/Minimise the window
SDL_WM_ToggleFullScreen — Toggles fullscreen mode
SDL_WM_GrabInput — Grabs mouse and keyboard input.

SDL provides a small set of window management functions which allow applications to change their title and toggle from windowed mode to fullscreen (if available)

Prev | Home | Next
SDL_Overlay | Up | SDL_WM_SetCaption


SDL_WM_GetCaption

Name

SDL_WM_GetCaption -- Gets the window title and icon name.

Synopsis

#include "SDL.h"

void SDL_WM_GetCaption(char **title, char **icon);

Description

Set pointers to the window title and icon name.

See Also

SDL_WM_SetCaption, SDL_WM_SetIcon

Prev | Home | Next
SDL_WM_SetCaption | Up | SDL_WM_SetIcon


SDL_WM_SetCaption

Name

SDL_WM_SetCaption -- Sets the window tile and icon name.

Synopsis

#include "SDL.h"

void SDL_WM_SetCaption(const char *title, const char *icon);

Description

Sets the title-bar and icon name of the display window.

See Also

SDL_WM_GetCaption, SDL_WM_SetIcon

Prev | Home | Next
Window Management | Up | SDL_WM_GetCaption


SDL_WM_SetIcon

Name

SDL_WM_SetIcon -- Sets the icon for the display window.

Synopsis

#include "SDL.h"

void SDL_WM_SetIcon(SDL_Surface *icon, Uint8 *mask);

Description

Sets the icon for the display window. Win32 icons must be 32x32.

This function must be called before the first call to SDL_SetVideoMode.

It takes an icon surface, and a mask in MSB format.

If mask is NULL, the entire icon surface will be used as the icon.

Example

SDL_WM_SetIcon(SDL_LoadBMP("icon.bmp"), NULL);

See Also

SDL_SetVideoMode, SDL_WM_SetCaption

Prev | Home | Next
SDL_WM_GetCaption | Up | SDL_WM_IconifyWindow


SDL_WM_IconifyWindow

Name

SDL_WM_IconifyWindow -- Iconify/Minimise the window

Synopsis

#include "SDL.h"

int SDL_WM_IconifyWindow(void);

Description

If the application is running in a window managed environment SDL attempts to iconify/minimise it. If SDL_WM_IconifyWindow is successful, the application will receive a SDL_APPACTIVE loss event.

Return Value

Returns non-zero on success or 0 if iconification is not support or was refused by the window manager.

Prev | Home | Next
SDL_WM_SetIcon | Up | SDL_WM_ToggleFullScreen


SDL_WM_ToggleFullScreen

Name

SDL_WM_ToggleFullScreen -- Toggles fullscreen mode

Synopsis

#include "SDL.h"

int SDL_WM_ToggleFullScreen(SDL_Surface *surface);

Description

Toggles the application between windowed and fullscreen mode, if supported. (X11 is the only target currently supported, BeOS support is experimental).

Return Value

Returns 0 on failure or 1 on success.

Prev | Home | Next
SDL_WM_IconifyWindow | Up | SDL_WM_GrabInput


SDL_WM_GrabInput

Name

SDL_WM_GrabInput -- Grabs mouse and keyboard input.

Synopsis

#include "SDL.h"

SDL_GrabMode SDL_WM_GrabInput(SDL_GrabMode mode);

Description

Grabbing means that the mouse is confined to the application window, and nearly all keyboard input is passed directly to the application, and not interpreted by a window manager, if any.

When mode is SDL_GRAB_QUERY the grab mode is not changed, but the current grab mode is returned.

typedef enum {
  SDL_GRAB_QUERY,
  SDL_GRAB_OFF,
  SDL_GRAB_ON
} SDL_GrabMode;

Return Value

The current/new SDL_GrabMode.

Prev | Home | Next
SDL_WM_ToggleFullScreen | Up | Events


Events

SDL Library Documentation
Prev Next

Chapter 8. Events

Table of Contents
Introduction
SDL Event Structures.
Event Functions.

Introduction

Event handling allows your application to receive input from the user. Event handling is initalised (along with video) with a call to:

SDL_Init(SDL_INIT_VIDEO);
Internally, SDL stores all the events waiting to be handled in an event queue. Using functions like SDL_PollEvent and SDL_PeepEvents you can observe and handle waiting input events.

The key to event handling in SDL is the SDL_Event union. The event queue itself is composed of a series of SDL_Event unions, one for each waiting event. SDL_Event unions are read from the queue with the SDL_PollEvent function and it is then up to the application to process the information stored with them.

Prev | Home | Next
SDL_WM_GrabInput | Up | SDL Event Structures.


SDL Event Structures.

Table of Contents
SDL_Event — General event structure
SDL_ActiveEvent — Application visibility event structure
SDL_KeyboardEvent — Keyboard event structure
SDL_MouseMotionEvent — Mouse motion event structure
SDL_MouseButtonEvent — Mouse button event structure
SDL_JoyAxisEvent — Joystick axis motion event structure
SDL_JoyButtonEvent — Joystick button event structure
SDL_JoyHatEvent — Joystick hat position change event structure
SDL_JoyBallEvent — Joystick trackball motion event structure
SDL_ResizeEvent — Window resize event structure
SDL_ExposeEvent — Quit requested event
SDL_SysWMEvent — Platform-dependent window manager event.
SDL_UserEvent — A user-defined event type
SDL_QuitEvent — Quit requested event
SDL_keysym — Keysym structure
SDLKey — Keysym definitions.
Prev | Home | Next
Events | Up | SDL_Event


SDL_Event

Name

SDL_Event -- General event structure

Structure Definition

typedef union{
  Uint8 type;
  SDL_ActiveEvent active;
  SDL_KeyboardEvent key;
  SDL_MouseMotionEvent motion;
  SDL_MouseButtonEvent button;
  SDL_JoyAxisEvent jaxis;
  SDL_JoyBallEvent jball;
  SDL_JoyHatEvent jhat;
  SDL_JoyButtonEvent jbutton;
  SDL_ResizeEvent resize;
  SDL_ExposeEvent expose;
  SDL_QuitEvent quit;
  SDL_UserEvent user;
  SDL_SywWMEvent syswm;
} SDL_Event;

Structure Data

type | The type of event
active | Activation event
key | Keyboard event
motion | Mouse motion event
button | Mouse button event
jaxis | Joystick axis motion event
jball | Joystick trackball motion event
jhat | Joystick hat motion event
jbutton | Joystick button event
resize | Application window resize event
expose | Application window expose event
quit | Application quit request event
user | User defined event
syswm | Undefined window manager event

Description

The SDL_Event union is the core to all event handling is SDL, its probably the most important structure after SDL_Surface. SDL_Event is a union of all event structures used in SDL, using it is a simple matter of knowing which union member relates to which event type.

Event type | Event Structure
SDL_ACTIVEEVENT | SDL_ActiveEvent
SDL_KEYDOWN/UP | SDL_KeyboardEvent
SDL_MOUSEMOTION | SDL_MouseMotionEvent
SDL_MOUSEBUTTONDOWN/UP | SDL_MouseButtonEvent
SDL_JOYAXISMOTION | SDL_JoyAxisEvent
SDL_JOYBALLMOTION | SDL_JoyBallEvent
SDL_JOYHATMOTION | SDL_JoyHatEvent
SDL_JOYBUTTONDOWN/UP | SDL_JoyButtonEvent
SDL_QUIT | SDL_QuitEvent
SDL_SYSWMEVENT | SDL_SysWMEvent
SDL_VIDEORESIZE | SDL_ResizeEvent
SDL_VIDEOEXPOSE | SDL_ExposeEvent
SDL_USEREVENT | SDL_UserEvent

Use

The SDL_Event structure has two uses

  • Reading events on the event queue

  • Placing events on the event queue

Reading events from the event queue is done with either SDL_PollEvent or SDL_PeepEvents. We'll use SDL_PollEvent and step through an example.

First off, we create an empty SDL_Event structure.

SDL_Event test_event;
SDL_PollEvent removes the next event from the event queue, if there are no events on the queue it returns 0 otherwise it returns 1. We use a while loop to process each event in turn.
while(SDL_PollEvent(&test_event)) {
The SDL_PollEvent function take a pointer to an SDL_Event structure that is to be filled with event information. We know that if SDL_PollEvent removes an event from the queue then the event information will be placed in our test_event structure, but we also know that the type of event will be placed in the type member of test_event. So to handle each event type seperately we use a switch statement.
  switch(test_event.type) {
We need to know what kind of events we're looking for and the event type's of those events. So lets assume we want to detect where the user is moving the mouse pointer within our application. We look through our event types and notice that SDL_MOUSEMOTION is, more than likely, the event we're looking for. A little more research tells use that SDL_MOUSEMOTION events are handled within the SDL_MouseMotionEvent structure which is the motion member of SDL_Event. We can check for the SDL_MOUSEMOTION event type within our switch statement like so:
    case SDL_MOUSEMOTION:
All we need do now is read the information out of the motion member of test_event.
      printf("We got a motion event.\n");
      printf("Current mouse position is: (%d, %d)\n", test_event.motion.x, test_event.motion.y);
      break;
    default:
      printf("Unhandled Event!\n");
      break;
  }
}
printf("Event queue empty.\n");

It is also possible to push events onto the event queue and so use it as a two-way communication path. Both SDL_PushEvent and SDL_PeepEvents allow you to place events onto the event queue. This is usually used to place a SDL_USEREVENT on the event queue, however you could use it to post fake input events if you wished. Creating your own events is a simple matter of choosing the event type you want, setting the type member and filling the appropriate member structure with information.

SDL_Event user_event;

user_event.type=SDL_USEREVENT;
user_event.user.code=2;
user_event.user.data1=NULL;
user_event.user.data2=NULL;
SDL_PushEvent(&user_event);

See Also

SDL_PollEvent, SDL_PushEvent, SDL_PeepEvents

Prev | Home | Next
SDL Event Structures. | Up | SDL_ActiveEvent


SDL_ActiveEvent

Name

SDL_ActiveEvent -- Application visibility event structure

Structure Definition

typedef struct{
  Uint8 type;
  Uint8 gain;
  Uint8 state;
} SDL_ActiveEvent;

Structure Data

type | SDL_ACTIVEEVENT.
gain | 0 if the event is a loss or 1 if it is a gain.
state | SDL_APPMOUSEFOCUS if mouse focus was gained or lost, SDL_APPINPUTFOCUS if input focus was gained or lost, or SDL_APPACTIVE if the application was iconified (gain=0) or restored(gain=1).

Description

SDL_ActiveEvent is a member of the SDL_Event union and is used when an event of type SDL_ACTIVEEVENT is reported.

When the mouse leaves or enters the window area a SDL_APPMOUSEFOCUS type activation event occurs, if the mouse entered the window then gain will be 1, otherwise gain will be 0. A SDL_APPINPUTFOCUS type activation event occurs when the application loses or gains keyboard focus. This usually occurs when another application is made active. Finally, a SDL_APPACTIVE type event occurs when the application is either minimised/iconified (gain=0) or restored.

Note: This event does not occur when an application window is first created.

See Also

SDL_Event, SDL_GetAppState

Prev | Home | Next
SDL_Event | Up | SDL_KeyboardEvent


SDL_KeyboardEvent

Name

SDL_KeyboardEvent -- Keyboard event structure

Structure Definition

typedef struct{
  Uint8 type;
  Uint8 state;
  SDL_keysym keysym;
} SDL_KeyboardEvent;

Structure Data

type | SDL_KEYDOWN or SDL_KEYUP
state | SDL_PRESSED or SDL_RELEASED
keysym | Contains key press information

Description

SDL_KeyboardEvent is a member of the SDL_Event union and is used when an event of type SDL_KEYDOWN or SDL_KEYUP is reported.

The type and state actually report the same information, they just use different values to do it! A keyboard event occurs when a key is released (type=SDK_KEYUP or state=SDL_RELEASED) and when a key is pressed (type=SDL_KEYDOWN or state=SDL_PRESSED). The information on what key was pressed or released is in the keysym structure.

Note: Repeating SDL_KEYDOWN events will occur if key repeat is enabled (see SDL_EnableKeyRepeat).

See Also

SDL_Event, SDL_keysym, SDL_EnableKeyRepeat, SDL_EnableUNICODE

Prev | Home | Next
SDL_ActiveEvent | Up | SDL_MouseMotionEvent


SDL_MouseMotionEvent

Name

SDL_MouseMotionEvent -- Mouse motion event structure

Structure Definition

typedef struct{
  Uint8 type;
  Uint8 state;
  Uint16 x, y;
  Sint16 xrel, yrel;
} SDL_MouseMotionEvent;

Structure Data

type | SDL_MOUSEMOTION
state | The current button state
x, y | The X/Y coordinates of the mouse
xrel, yrel | Relative motion in the X/Y direction

Description

SDL_MouseMotionEvent is a member of the SDL_Event union and is used when an event of type SDL_MOUSEMOTION is reported.

Simply put, a SDL_MOUSEMOTION type event occurs when a user moves the mouse within the application window or when SDL_WarpMouse is called. Both the absolute (x and y) and relative (xrel and yrel) coordinates are reported along with the current button states (state). The button state can be interpreted using the SDL_BUTTON macro (see SDL_GetMouseState).

If the cursor is hidden (SDL_ShowCursor(0)) and the input is grabbed (SDL_WM_GrabInput(SDL_GRAB_ON)), then the mouse will give relative motion events even when the cursor reaches the edge fo the screen. This is currently only implemented on Windows and Linux/Unix-a-likes.

See Also

SDL_Event, SDL_MouseButtonEvent

Prev | Home | Next
SDL_KeyboardEvent | Up | SDL_MouseButtonEvent


SDL_MouseButtonEvent

Name

SDL_MouseButtonEvent -- Mouse button event structure

Structure Definition

typedef struct{
  Uint8 type;
  Uint8 button;
  Uint8 state;
  Uint16 x, y;
} SDL_MouseButtonEvent;

Structure Data

type | SDL_MOUSEBUTTONDOWN or SDL_MOUSEBUTTONUP
button | The mouse button index (SDL_BUTTON_LEFT, SDL_BUTTON_MIDDLE, SDL_BUTTON_RIGHT)
state | SDL_PRESSED or SDL_RELEASED
x, y | The X/Y coordinates of the mouse at press/release time

Description

SDL_MouseButtonEvent is a member of the SDL_Event union and is used when an event of type SDL_MOUSEBUTTONDOWN or SDL_MOUSEBUTTONUP is reported.

When a mouse button press or release is detected then number of the button pressed (from 1 to 255, with 1 usually being the left button and 2 the right) is placed into button, the position of the mouse when this event occured is stored in the x and the y fields. Like SDL_KeyboardEvent, information on whether the event was a press or a release event is stored in both the type and state fields, but this should be obvious.

See Also

SDL_Event, SDL_MouseMotionEvent

Prev | Home | Next
SDL_MouseMotionEvent | Up | SDL_JoyAxisEvent


SDL_JoyAxisEvent

Name

SDL_JoyAxisEvent -- Joystick axis motion event structure

Structure Definition

typedef struct{
  Uint8 type;
  Uint8 which;
  Uint8 axis;
  Sint16 value;
} SDL_JoyAxisEvent;

Structure Data

type | SDL_JOYAXISMOTION
which | Joystick device index
axis | Joystick axis index
value | Axis value (range: -32768 to 32767)

Description

SDL_JoyAxisEvent is a member of the SDL_Event union and is used when an event of type SDL_JOYAXISMOTION is reported.

A SDL_JOYAXISMOTION event occurs when ever a user moves an axis on the joystick. The field which is the index of the joystick that reported the event and axis is the index of the axis (for a more detailed explaination see the Joystick section). value is the current position of the axis.

See Also

SDL_Event, Joystick Functions, SDL_JoystickEventState, SDL_JoystickGetAxis

Prev | Home | Next
SDL_MouseButtonEvent | Up | SDL_JoyButtonEvent


SDL_JoyButtonEvent

Name

SDL_JoyButtonEvent -- Joystick button event structure

Structure Definition

typedef struct{
  Uint8 type;
  Uint8 which;
  Uint8 button;
  Uint8 state;
} SDL_JoyButtonEvent;

Structure Data

type | SDL_JOYBUTTONDOWN or SDL_JOYBUTTONUP
which | Joystick device index
button | Joystick button index
state | SDL_PRESSED or SDL_RELEASED

Description

SDL_JoyButtonEvent is a member of the SDL_Event union and is used when an event of type SDL_JOYBUTTONDOWN or SDL_JOYBUTTONUP is reported.

A SDL_JOYBUTTONDOWN or SDL_JOYBUTTONUP event occurs when ever a user presses or releases a button on a joystick. The field which is the index of the joystick that reported the event and button is the index of the button (for a more detailed explaination see the Joystick section). state is the current state or the button which is either SDL_PRESSED or SDL_RELEASED.

See Also

SDL_Event, Joystick Functions, SDL_JoystickEventState, SDL_JoystickGetButton

Prev | Home | Next
SDL_JoyAxisEvent | Up | SDL_JoyHatEvent


SDL_JoyHatEvent

Name

SDL_JoyHatEvent -- Joystick hat position change event structure

Structure Definition

typedef struct{
  Uint8 type;
  Uint8 which;
  Uint8 hat;
  Uint8 value;
} SDL_JoyHatEvent;

Structure Data

type | SDL_JOY
which | Joystick device index
hat | Joystick hat index
value | Hat position

Description

SDL_JoyHatEvent is a member of the SDL_Event union and is used when an event of type SDL_JOYHATMOTION is reported.

A SDL_JOYHATMOTION event occurs when ever a user moves a hat on the joystick. The field which is the index of the joystick that reported the event and hat is the index of the hat (for a more detailed exlaination see the Joystick section). value is the current position of the hat. It is a logically OR'd combination of the following values (whose meanings should be pretty obvious:) :

SDL_HAT_CENTERED
SDL_HAT_UP
SDL_HAT_RIGHT
SDL_HAT_DOWN
SDL_HAT_LEFT

The following defines are also provided:

SDL_HAT_RIGHTUP
SDL_HAT_RIGHTDOWN
SDL_HAT_LEFTUP
SDL_HAT_LEFTDOWN

See Also

SDL_Event, Joystick Functions, SDL_JoystickEventState, SDL_JoystickGetHat

Prev | Home | Next
SDL_JoyButtonEvent | Up | SDL_JoyBallEvent


SDL_JoyBallEvent

Name

SDL_JoyBallEvent -- Joystick trackball motion event structure

Structure Definition

typedef struct{
  Uint8 type;
  Uint8 which;
  Uint8 ball;
  Sint16 xrel, yrel;
} SDL_JoyBallEvent;

Structure Data

type | SDL_JOYBALLMOTION
which | Joystick device index
ball | Joystick trackball index
xrel, yrel | The relative motion in the X/Y direction

Description

SDL_JoyBallEvent is a member of the SDL_Event union and is used when an event of type SDL_JOYBALLMOTION is reported.

A SDL_JOYBALLMOTION event occurs when a user moves a trackball on the joystick. The field which is the index of the joystick that reported the event and ball is the index of the trackball (for a more detailed explaination see the Joystick section). Trackballs only return relative motion, this is the change in position on the ball since it was last polled (last cycle of the event loop) and it is stored in xrel and yrel.

See Also

SDL_Event, Joystick Functions, SDL_JoystickEventState, SDL_JoystickGetBall

Prev | Home | Next
SDL_JoyHatEvent | Up | SDL_ResizeEvent


SDL_ResizeEvent

Name

SDL_ResizeEvent -- Window resize event structure

Structure Definition

typedef struct{
  Uint8 type;
  int w, h;
} SDL_ResizeEvent;

Structure Data

type | SDL_VIDEORESIZE
w, h | New width and height of the window

Description

SDL_ResizeEvent is a member of the SDL_Event union and is used when an event of type SDL_VIDEORESIZE is reported.

When SDL_RESIZABLE is passed as a flag to SDL_SetVideoMode the user is allowed to resize the applications window. When the window is resized an SDL_VIDEORESIZE is report, with the new window width and height values stored in w and h, respectively. When an SDL_VIDEORESIZE is recieved the window should be resized to the new dimensions using SDL_SetVideoMode.

See Also

SDL_Event, SDL_SetVideoMode

Prev | Home | Next
SDL_JoyBallEvent | Up | SDL_ExposeEvent


SDL_ExposeEvent

Name

SDL_ExposeEvent -- Quit requested event

Structure Definition

typedef struct{
  Uint8 type
} SDL_ExposeEvent;

Structure Data

type SDL_QUIT

Description

SDL_ExposeEvent is a member of the SDL_Event union and is used whan an event of type SDL_VIDEOEXPOSE is reported.

A VIDEOEXPOSE event is triggered when the screen has been modified outside of the application, usually by the window manager and needs to be redrawn.

See Also

SDL_Event, SDL_SetEventFilter

Prev | Home | Next
SDL_ResizeEvent | Up | SDL_SysWMEvent


SDL_SysWMEvent

Name

SDL_SysWMEvent -- Platform-dependent window manager event.

Description

The system window manager event contains a pointer to system-specific information about unknown window manager events. If you enable this event using SDL_EventState(), it will be generated whenever unhandled events are received from the window manager. This can be used, for example, to implement cut-and-paste in your application.

typedef struct {
         Uint8 type;   /* Always SDL_SysWM */
 } SDL_SysWMEvent;
If you want to obtain system-specific information about the window manager, you can fill the version member of a SDL_SysWMinfo structure (details can be found in SDL_syswm.h, which must be included) using the SDL_VERSION() macro found in SDL_version.h, and pass it to the function:

int SDL_GetWMInfo(SDL_SysWMinfo *info);

See Also

SDL_EventState

Prev | Home | Next
SDL_ExposeEvent | Up | SDL_UserEvent


SDL_UserEvent

Name

SDL_UserEvent -- A user-defined event type

Structure Definition

typedef struct{
  Uint8 type;
  int code;
  void *data1;
  void *data2;
} SDL_UserEvent;

Structure Data

type | SDL_USEREVENT through to SDL_NUMEVENTS-1
code | User defined event code
data1 | User defined data pointer
data2 | User defined data pointer

Description

SDL_UserEvent is in the user member of the structure SDL_Event. This event is unique, it is never created by SDL but only by the user. The event can be pushed onto the event queue using SDL_PushEvent. The contents of the structure members or completely up to the programmer, the only requirement is that type is a value from SDL_USEREVENT to SDL_NUMEVENTS-1 (inclusive).

Examples

SDL_Event event;

event.type = SDL_USEREVENT;
event.user.code = my_event_code;
event.user.data1 = significant_data;
event.user.data2 = 0;
SDL_PushEvent(&event);

See Also

SDL_Event, SDL_PushEvent

Prev | Home | Next
SDL_SysWMEvent | Up | SDL_QuitEvent


SDL_QuitEvent

Name

SDL_QuitEvent -- Quit requested event

Structure Definition

typedef struct{
  Uint8 type
} SDL_QuitEvent;

Structure Data

type SDL_QUIT

Description

SDL_QuitEvent is a member of the SDL_Event union and is used whan an event of type SDL_QUIT is reported.

As can be seen, the SDL_QuitEvent structure serves no useful purpose. The event itself, on the other hand, is very important. If you filter out or ignore a quit event then it is impossible for the user to close the window. On the other hand, if you do accept a quit event then the application window will be closed, and screen updates will still report success event though the application will no longer be visible.

Note: The macro SDL_QuitRequested will return non-zero if a quit event is pending

See Also

SDL_Event, SDL_SetEventFilter

Prev | Home | Next
SDL_UserEvent | Up | SDL_keysym


SDL_keysym

Name

SDL_keysym -- Keysym structure

Structure Definition

typedef struct{
  Uint8 scancode;
  SDLKey sym;
  SDLMod mod;
  Uint16 unicode;
} SDL_keysym;

Structure Data

scancode | Hardware specific scancode
sym | SDL virtual keysym
mod | Current key modifiers
unicode | Translated character

Description

The SDL_keysym structure is used by reporting key presses and releases since it is a part of the SDL_KeyboardEvent.

The scancode field should generally be left alone, it is the hardware dependent scancode returned by the keyboard. The sym field is extremely useful. It is the SDL-defined value of the key (see SDL Key Syms. This field is very useful when you are checking for certain key presses, like so:

.
.
while(SDL_PollEvent(&event)){
  switch(event.type){
    case SDL_KEYDOWN:
      if(event.key.keysym.sym==SDLK_LEFT)
        move_left();
      break;
    .
    .
    .
  }
}
.
.
mod stores the current state of the keyboard modifiers as explained in SDL_GetModState. The unicode is only used when UNICODE translation is enabled with SDL_EnableUNICODE. If unicode is non-zero then this a the UNICODE character corresponding to the keypress. If the high 9 bits of the character are 0, then this maps to the equivalent ASCII character:
char ch;
if ( (keysym.unicode & 0xFF80) == 0 ) {
  ch = keysym.unicode & 0x7F;
}
else {
  printf("An International Character.\n");
}
UNICODE translation does have a slight overhead so don't enable it unless its needed.

See Also

SDLKey

Prev | Home | Next
SDL_QuitEvent | Up | SDLKey


SDLKey

Name

SDLKey -- Keysym definitions.

Description

Table 8-1. SDL Keysym definitions

SDLKey | ASCII value | Common name
SDLK_BACKSPACE | '\b' | backspace
SDLK_TAB | '\t' | tab
SDLK_CLEAR |  | clear
SDLK_RETURN | '\r' | return
SDLK_PAUSE |  | pause
SDLK_ESCAPE | '^[' | escape
SDLK_SPACE | ' ' | space
SDLK_EXCLAIM | '!' | exclaim
SDLK_QUOTEDBL | '"' | quotedbl
SDLK_HASH | '#' | hash
SDLK_DOLLAR | '$' | dollar
SDLK_AMPERSAND | '&' | ampersand
SDLK_QUOTE | ''' | quote
SDLK_LEFTPAREN | '(' | left parenthesis
SDLK_RIGHTPAREN | ')' | right parenthesis
SDLK_ASTERISK | '*' | asterisk
SDLK_PLUS | '+' | plus sign
SDLK_COMMA | ',' | comma
SDLK_MINUS | '-' | minus sign
SDLK_PERIOD | '.' | period
SDLK_SLASH | '/' | forward slash
SDLK_0 | '0' | 0
SDLK_1 | '1' | 1
SDLK_2 | '2' | 2
SDLK_3 | '3' | 3
SDLK_4 | '4' | 4
SDLK_5 | '5' | 5
SDLK_6 | '6' | 6
SDLK_7 | '7' | 7
SDLK_8 | '8' | 8
SDLK_9 | '9' | 9
SDLK_COLON | ':' | colon
SDLK_SEMICOLON | ';' | semicolon
SDLK_LESS | '<' | less-than sign
SDLK_EQUALS | '=' | equals sign
SDLK_GREATER | '>' | greater-than sign
SDLK_QUESTION | '?' | question mark
SDLK_AT | '@' | at
SDLK_LEFTBRACKET | '[' | left bracket
SDLK_BACKSLASH | '\' | backslash
SDLK_RIGHTBRACKET | ']' | right bracket
SDLK_CARET | '^' | caret
SDLK_UNDERSCORE | '_' | underscore
SDLK_BACKQUOTE | '`' | grave
SDLK_a | 'a' | a
SDLK_b | 'b' | b
SDLK_c | 'c' | c
SDLK_d | 'd' | d
SDLK_e | 'e' | e
SDLK_f | 'f' | f
SDLK_g | 'g' | g
SDLK_h | 'h' | h
SDLK_i | 'i' | i
SDLK_j | 'j' | j
SDLK_k | 'k' | k
SDLK_l | 'l' | l
SDLK_m | 'm' | m
SDLK_n | 'n' | n
SDLK_o | 'o' | o
SDLK_p | 'p' | p
SDLK_q | 'q' | q
SDLK_r | 'r' | r
SDLK_s | 's' | s
SDLK_t | 't' | t
SDLK_u | 'u' | u
SDLK_v | 'v' | v
SDLK_w | 'w' | w
SDLK_x | 'x' | x
SDLK_y | 'y' | y
SDLK_z | 'z' | z
SDLK_DELETE | '^?' | delete
SDLK_KP0 |  | keypad 0
SDLK_KP1 |  | keypad 1
SDLK_KP2 |  | keypad 2
SDLK_KP3 |  | keypad 3
SDLK_KP4 |  | keypad 4
SDLK_KP5 |  | keypad 5
SDLK_KP6 |  | keypad 6
SDLK_KP7 |  | keypad 7
SDLK_KP8 |  | keypad 8
SDLK_KP9 |  | keypad 9
SDLK_KP_PERIOD | '.' | keypad period
SDLK_KP_DIVIDE | '/' | keypad divide
SDLK_KP_MULTIPLY | '*' | keypad multiply
SDLK_KP_MINUS | '-' | keypad minus
SDLK_KP_PLUS | '+' | keypad plus
SDLK_KP_ENTER | '\r' | keypad enter
SDLK_KP_EQUALS | '=' | keypad equals
SDLK_UP |  | up arrow
SDLK_DOWN |  | down arrow
SDLK_RIGHT |  | right arrow
SDLK_LEFT |  | left arrow
SDLK_INSERT |  | insert
SDLK_HOME |  | home
SDLK_END |  | end
SDLK_PAGEUP |  | page up
SDLK_PAGEDOWN |  | page down
SDLK_F1 |  | F1
SDLK_F2 |  | F2
SDLK_F3 |  | F3
SDLK_F4 |  | F4
SDLK_F5 |  | F5
SDLK_F6 |  | F6
SDLK_F7 |  | F7
SDLK_F8 |  | F8
SDLK_F9 |  | F9
SDLK_F10 |  | F10
SDLK_F11 |  | F11
SDLK_F12 |  | F12
SDLK_F13 |  | F13
SDLK_F14 |  | F14
SDLK_F15 |  | F15
SDLK_NUMLOCK |  | numlock
SDLK_CAPSLOCK |  | capslock
SDLK_SCROLLOCK |  | scrollock
SDLK_RSHIFT |  | right shift
SDLK_LSHIFT |  | left shift
SDLK_RCTRL |  | right ctrl
SDLK_LCTRL |  | left ctrl
SDLK_RALT |  | right alt
SDLK_LALT |  | left alt
SDLK_RMETA |  | right meta
SDLK_LMETA |  | left meta
SDLK_LSUPER |  | left windows key
SDLK_RSUPER |  | right windows key
SDLK_MODE |  | mode shift
SDLK_HELP |  | help
SDLK_PRINT |  | print-screen
SDLK_SYSREQ |  | SysRq
SDLK_BREAK |  | break
SDLK_MENU |  | menu
SDLK_POWER |  | power
SDLK_EURO |  | euro

Table 8-2. SDL modifier definitions

SDL Modifier | Meaning
KMOD_NONE | No modifiers applicable
KMOD_NUM | Numlock is down
KMOD_CAPS | Capslock is down
KMOD_LCTRL | Left Control is down
KMOD_RCTRL | Right Control is down
KMOD_RSHIFT | Right Shift is down
KMOD_LSHIFT | Left Shift is down
KMOD_RALT | Right Alt is down
KMOD_LALT | Left Alt is down
KMOD_CTRL | A Control key is down
KMOD_SHIFT | A Shift key is down
KMOD_ALT | An Alt key is down

Prev | Home | Next
SDL_keysym | Up | Event Functions.


Event Functions.

Table of Contents
SDL_PumpEvents — Pumps the event loop, gathering events from the input devices.
SDL_PeepEvents — Checks the event queue for messages and optionally returns them.
SDL_PollEvent — Polls for currently pending events.
SDL_WaitEvent — Waits indefinitely for the next available event.
SDL_PushEvent — Pushes an event onto the event queue
SDL_SetEventFilter — Sets up a filter to process all events before they are posted to the event queue.
SDL_GetEventFilter — Retrieves a pointer to he event filter
SDL_EventState — This function allows you to set the state of processing certain events.
SDL_GetKeyState — Get a snapshot of the current keyboard state
SDL_GetModState — Get the state of modifier keys.
SDL_SetModState — Set the current key modifier state
SDL_GetKeyName — Get the name of an SDL virtual keysym
SDL_EnableUNICODE — Enable UNICODE translation
SDL_EnableKeyRepeat — Set keyboard repeat rate.
SDL_GetMouseState — Retrieve the current state of the mouse
SDL_GetRelativeMouseState — Retrieve the current state of the mouse
SDL_GetAppState — Get the state of the application
SDL_JoystickEventState — Enable/disable joystick event polling

SDL_PumpEvents | Pumps the event loop, gathering events from the input devices
SDL_PeepEvents | Checks the event queue for messages and optionally returns them
SDL_PollEvent | Polls for currently pending events
SDL_WaitEvent | Waits indefinitely for the next available event
SDL_PushEvent | Pushes an event onto the event queue
SDL_SetEventFilter | Sets up a filter to process all events
SDL_EventState | Allows you to set the state of processing certain events
SDL_GetKeyState | Get a snapshot of the current keyboard state
SDL_GetModState | Get the state of modifier keys
SDL_SetModState | Set the state of modifier keys
SDL_GetKeyName | Get the name of an SDL virtual keysym
SDL_EnableUNICODE | Enable UNICODE translation
SDL_EnableKeyRepeat | Set keyboard repeat rate
SDL_GetMouseState | Retrieve the current state of the mouse
SDL_GetRelativeMouseState | Retrieve the current state of the mouse
SDL_GetAppState | Get the state of the application
SDL_JoystickEventState | Enable/disable joystick event polling

Prev | Home | Next
SDLKey | Up | SDL_PumpEvents


SDL_PumpEvents

Name

SDL_PumpEvents -- Pumps the event loop, gathering events from the input devices.

Synopsis

#include "SDL.h"

void SDL_PumpEvents(void);

Description

Pumps the event loop, gathering events from the input devices.

SDL_PumpEvents gathers all the pending input information from devices and places it on the event queue. Without calls to SDL_PumpEvents no events would ever be placed on the queue. Often calls the need for SDL_PumpEvents is hidden from the user since SDL_PollEvent and SDL_WaitEvent implicitly call SDL_PumpEvents. However, if you are not polling or waiting for events (e.g. your filtering them), then you must call SDL_PumpEvents to force an event queue update.

Note: You can only call this function in the thread that set the video mode.

See Also

SDL_PollEvent

Prev | Home | Next
Event Functions. | Up | SDL_PeepEvents


SDL_WaitEvent

Name

SDL_WaitEvent -- Waits indefinitely for the next available event.

Synopsis

#include "SDL.h"

int SDL_WaitEvent(SDL_Event *event);

Description

Waits indefinitely for the next available event, returning 1, or 0 if there was an error while waiting for events.

If event is not NULL, the next event is removed from the queue and stored in that area.

See Also

SDL_Event, SDL_PollEvent

Prev | Home | Next
SDL_PollEvent | Up | SDL_PushEvent


SDL_PeepEvents

Name

SDL_PeepEvents -- Checks the event queue for messages and optionally returns them.

Synopsis

#include "SDL.h"

int SDL_PeepEvents(SDL_Event *events, int numevents, SDL_eventaction action, Uint32 mask);

Description

Checks the event queue for messages and optionally returns them.

If action is SDL_ADDEVENT, up to numevents events will be added to the back of the event queue.

If action is SDL_PEEKEVENT, up to numevents events at the front of the event queue, matching mask, will be returned and will not be removed from the queue.

If action is SDL_GETEVENT, up to numevents events at the front of the event queue, matching mask, will be returned and will be removed from the queue.

This function is thread-safe.

Return Value

This function returns the number of events actually stored, or -1 if there was an error.

See Also

SDL_Event, SDL_PollEvent, SDL_PushEvent

Prev | Home | Next
SDL_PumpEvents | Up | SDL_PollEvent


SDL_PollEvent

Name

SDL_PollEvent -- Polls for currently pending events.

Synopsis

#include "SDL.h"

int SDL_PollEvent(SDL_Event *event);

Description

Polls for currently pending events, and returns 1 if there are any pending events, or 0 if there are none available.

If event is not NULL, the next event is removed from the queue and stored in that area.

Examples

SDL_Event event; /* Event structure */

.
.
.
/* Check for events */
while(SDL_PollEvent(&event)){  /* Loop until there are no events left on the queue */
  switch(event.type){  /* Process the appropiate event type */
    case SDL_KEYDOWN:  /* Handle a KEYDOWN event */
      printf("Oh! Key press\n");
      break;
    case SDL_MOUSEMOTION:
      .
      .
      .
    default: /* Report an unhandled event */
      printf("I don't know what this event is!\n");
  }
}

See Also

SDL_Event, SDL_WaitEvent, SDL_PeepEvents

Prev | Home | Next
SDL_PeepEvents | Up | SDL_WaitEvent


SDL_PushEvent

Name

SDL_PushEvent -- Pushes an event onto the event queue

Synopsis

#include "SDL.h"

int SDL_PushEvent(SDL_Event *event);

Description

The event queue can actually be used as a two way communication channel. Not only can events be read from the queue, but the user can also push their own events onto it. event is a pointer to the event structure you wish to push onto the queue.

Note: Pushing device input events onto the queue doesn't modify the state of the device within SDL.

Return Value

Returns 0 on success or -1 if the event couldn't be pushed.

Examples

See SDL_Event.

See Also

SDL_PollEvent, SDL_PeepEvents, SDL_Event

Prev | Home | Next
SDL_WaitEvent | Up | SDL_SetEventFilter


SDL_SetEventFilter

Name

SDL_SetEventFilter -- Sets up a filter to process all events before they are posted to the event queue.

Synopsis

#include "SDL.h"

void SDL_SetEventFilter(SDL_EventFilter filter);

Description

This function sets up a filter to process all events before they are posted to the event queue. This is a very powerful and flexible feature. The filter is prototyped as:

typedef int (*SDL_EventFilter)(const SDL_Event *event);
If the filter returns 1, then the event will be added to the internal queue. If it returns 0, then the event will be dropped from the queue. This allows selective filtering of dynamically.

There is one caveat when dealing with the SDL_QUITEVENT event type. The event filter is only called when the window manager desires to close the application window. If the event filter returns 1, then the window will be closed, otherwise the window will remain open if possible. If the quit event is generated by an interrupt signal, it will bypass the internal queue and be delivered to the application at the next event poll.

Note: Events pushed onto the queue with SDL_PushEvent or SDL_PeepEvents do not get passed through the event filter.

Note: Be Careful! The event filter function may run in a different thread so be careful what you do within it.

See Also

SDL_Event, SDL_GetEventFilter, SDL_PushEvent

Prev | Home | Next
SDL_PushEvent | Up | SDL_GetEventFilter


SDL_GetEventFilter

Name

SDL_GetEventFilter -- Retrieves a pointer to he event filter

Synopsis

#include "SDL.h"

SDL_EventFilter SDL_GetEventFilter(void);

Description

This function retrieces a pointer to the event filter that was previously set using SDL_SetEventFilter. An SDL_EventFilter function is defined as:

typedef int (*SDL_EventFilter)(const SDL_Event *event);

Return Value

Returns a pointer to the event filter or NULL if no filter has been set.

See Also

SDL_Event, SDL_SetEventFilter

Prev | Home | Next
SDL_SetEventFilter | Up | SDL_EventState


SDL_EventState

Name

SDL_EventState -- This function allows you to set the state of processing certain events.

Synopsis

#include "SDL.h"

Uint8 SDL_EventState(Uint8 type, int state);

Description

This function allows you to set the state of processing certain event type's.

If state is set to SDL_IGNORE, that event type will be automatically dropped from the event queue and will not be filtered.

If state is set to SDL_ENABLE, that event type will be processed normally.

If state is set to SDL_QUERY, SDL_EventState will return the current processing state of the specified event type.

A list of event type's can be found in the SDL_Event section.

See Also

SDL_Event

Prev | Home | Next
SDL_GetEventFilter | Up | SDL_GetKeyState


SDL_GetKeyState

Name

SDL_GetKeyState -- Get a snapshot of the current keyboard state

Synopsis

#include "SDL.h"

Uint8 *SDL_GetKeyState(int *numkeys);

Description

Gets a snapshot of the current keyboard state. The current state is return as a pointer to an array, the size of this array is stored in numkeys. The array is indexed by the SDLK_* symbols. A value of 1 means the key is pressed and a value of 0 means its not. The pointer returned is a pointer to an internal SDL array and should not be freed by the caller.

Note: Use SDL_PumpEvents to update the state array.

Example

Uint8 *keystate = SDL_GetKeyState(NULL);
if ( keystate[SDLK_RETURN] ) printf("Return Key Pressed.\n");

See Also

SDL Key Symbols, SDL_PumpEvents

Prev | Home | Next
SDL_EventState | Up | SDL_GetModState


SDL_GetModState

Name

SDL_GetModState -- Get the state of modifier keys.

Synopsis

#include "SDL.h"

SDLMod SDL_GetModState(void);

Description

Returns the current state of the modifier keys (CTRL, ALT, etc.).

Return Value

The return value can be an OR'd combination of the SDLMod enum.

SDLMod

typedef enum {
  KMOD_NONE  = 0x0000,
  KMOD_LSHIFT= 0x0001,
  KMOD_RSHIFT= 0x0002,
  KMOD_LCTRL = 0x0040,
  KMOD_RCTRL = 0x0080,
  KMOD_LALT  = 0x0100,
  KMOD_RALT  = 0x0200,
  KMOD_LMETA = 0x0400,
  KMOD_RMETA = 0x0800,
  KMOD_NUM   = 0x1000,
  KMOD_CAPS  = 0x2000,
  KMOD_MODE  = 0x4000,
} SDLMod;
SDL also defines the following symbols for convenience:
#define KMOD_CTRL (KMOD_LCTRL|KMOD_RCTRL)
#define KMOD_SHIFT  (KMOD_LSHIFT|KMOD_RSHIFT)
#define KMOD_ALT  (KMOD_LALT|KMOD_RALT)
#define KMOD_META (KMOD_LMETA|KMOD_RMETA)

See Also

SDL_GetKeyState

Prev | Home | Next
SDL_GetKeyState | Up | SDL_SetModState


SDL_SetModState

Name

SDL_SetModState -- Set the current key modifier state

Synopsis

#include "SDL.h"

void SDL_SetModState(SDLMod modstate);

Description

The inverse of SDL_GetModState, SDL_SetModState allows you to impose modifier key states on your application.

Simply pass your desired modifier states into modstate. This value my be a logical OR'd combination of the following:

typedef enum {
  KMOD_NONE  = 0x0000,
  KMOD_LSHIFT= 0x0001,
  KMOD_RSHIFT= 0x0002,
  KMOD_LCTRL = 0x0040,
  KMOD_RCTRL = 0x0080,
  KMOD_LALT  = 0x0100,
  KMOD_RALT  = 0x0200,
  KMOD_LMETA = 0x0400,
  KMOD_RMETA = 0x0800,
  KMOD_NUM   = 0x1000,
  KMOD_CAPS  = 0x2000,
  KMOD_MODE  = 0x4000,
} SDLMod;

See Also

SDL_GetModState

Prev | Home | Next
SDL_GetModState | Up | SDL_GetKeyName


SDL_GetKeyName

Name

SDL_GetKeyName -- Get the name of an SDL virtual keysym

Synopsis

#include "SDL.h"

char *SDL_GetKeyName(SDLKey key);

Description

Returns the SDL-defined name of the SDLKey key.

See Also

SDLKey

Prev | Home | Next
SDL_SetModState | Up | SDL_EnableUNICODE


SDL_EnableUNICODE

Name

SDL_EnableUNICODE -- Enable UNICODE translation

Synopsis

#include "SDL.h"

int SDL_EnableUNICODE(int enable);

Description

Enables/Disables Unicode keyboard translation.

To obtain the character codes corresponding to received keyboard events, Unicode translation must first be turned on using this function. The translation incurs a slight overhead for each keyboard event and is therefore disabled by default. For each subsequently received key down event, the unicode member of the SDL_keysym structure will then contain the corresponding character code, or zero for keysyms that do not correspond to any character code.

A value of 1 for enable enables Unicode translation; 0 disables it, and -1 leaves it unchanged (useful for querying the current translation mode).

Note that only key press events will be translated, not release events.

Return Value

Returns the previous translation mode (0 or 1).

See Also

SDL_keysym

Prev | Home | Next
SDL_GetKeyName | Up | SDL_EnableKeyRepeat


SDL_EnableKeyRepeat

Name

SDL_EnableKeyRepeat -- Set keyboard repeat rate.

Synopsis

#include "SDL.h"

int SDL_EnableKeyRepeat(int delay, int interval);

Description

Enables or disables the keyboard repeat rate. delay specifies how long the key must be pressed before it begins repeating, it then repeats at the speed specified by interval. Both delay and interval are expressed in milliseconds.

Setting delay to 0 disables key repeating completely. Good default values are SDL_DEFAULT_REPEAT_DELAY and SDL_DEFAULT_REPEAT_INTERVAL.

Return Value

Returns 0 on success and -1 on failure.

Prev | Home | Next
SDL_EnableUNICODE | Up | SDL_GetMouseState


SDL_GetMouseState

Name

SDL_GetMouseState -- Retrieve the current state of the mouse

Synopsis

#include "SDL.h"

Uint8 SDL_GetMouseState(int *x, int *y);

Description

The current button state is returned as a button bitmask, which can be tested using the SDL_BUTTON(X) macros, and x and y are set to the current mouse cursor position. You can pass NULL for either x or y.

Example

SDL_PumpEvents();
if(SDL_GetMouseState(NULL, NULL)&SDL_BUTTON(1))
  printf("Mouse Button 1(left) is pressed.\n");

See Also

SDL_GetRelativeMouseState, SDL_PumpEvents

Prev | Home | Next
SDL_EnableKeyRepeat | Up | SDL_GetRelativeMouseState


SDL_GetRelativeMouseState

Name

SDL_GetRelativeMouseState -- Retrieve the current state of the mouse

Synopsis

#include "SDL.h"

Uint8 SDL_GetRelativeMouseState(int *x, int *y);

Description

The current button state is returned as a button bitmask, which can be tested using the SDL_BUTTON(X) macros, and x and y are set to the change in the mouse position since the last call to SDL_GetRelativeMouseState or since event initialization. You can pass NULL for either x or y.

See Also

SDL_GetMouseState

Prev | Home | Next
SDL_GetMouseState | Up | SDL_GetAppState


SDL_GetAppState

Name

SDL_GetAppState -- Get the state of the application

Synopsis

#include "SDL.h"

Uint8 SDL_GetAppState(void);

Description

This function returns the current state of the application. The value returned is a bitwise combination of:

SDL_APPMOUSEFOCUS | The application has mouse focus.
SDL_APPINPUTFOCUS | The application has keyboard focus
SDL_APPACTIVE | The application is visible

See Also

SDL_ActiveEvent

Prev | Home | Next
SDL_GetRelativeMouseState | Up | SDL_JoystickEventState


SDL_JoystickEventState

Name

SDL_JoystickEventState -- Enable/disable joystick event polling

Synopsis

#include "SDL.h"

int SDL_JoystickEventState(int state);

Description

This function is used to enable or disable joystick event processing. With joystick event processing disabled you will have to update joystick states with SDL_JoystickUpdate and read the joystick information manually. state is either SDL_QUERY, SDL_ENABLE or SDL_IGNORE.

Note: Joystick event handling is prefered

Return Value

If state is SDL_QUERY then the current state is returned, otherwise the new processing state is returned.

See Also

SDL Joystick Functions, SDL_JoystickUpdate, SDL_JoyAxisEvent, SDL_JoyBallEvent, SDL_JoyButtonEvent, SDL_JoyHatEvent

Prev | Home | Next
SDL_GetAppState | Up | Joystick


Joystick

SDL Library Documentation
Prev Next

Chapter 9. Joystick

Table of Contents
SDL_NumJoysticks — Count available joysticks.
SDL_JoystickName — Get joystick name.
SDL_JoystickOpen — Opens a joystick for use.
SDL_JoystickOpened — Determine if a joystick has been opened
SDL_JoystickIndex — Get the index of an SDL_Joystick.
SDL_JoystickNumAxes — Get the number of joystick axes
SDL_JoystickNumBalls — Get the number of joystick trackballs
SDL_JoystickNumHats — Get the number of joystick hats
SDL_JoystickNumButtons — Get the number of joysitck buttons
SDL_JoystickUpdate — Updates the state of all joysticks
SDL_JoystickGetAxis — Get the current state of an axis
SDL_JoystickGetHat — Get the current state of a joystick hat
SDL_JoystickGetButton — Get the current state of a given button on a given joystick
SDL_JoystickGetBall — Get relative trackball motion
SDL_JoystickClose — Closes a previously opened joystick

Joysticks, and other similar input devices, have a very strong role in game playing and SDL provides comprehensive support for them. Axes, Buttons, POV Hats and trackballs are all supported.

Joystick support is initialized by passed the SDL_INIT_JOYSTICK flag to SDL_Init. Once initilized joysticks must be opened using SDL_JoystickOpen.

While using the functions describe in this secton may seem like the best way to access and read from joysticks, in most cases they aren't. Ideally joysticks should be read using the event system. To enable this, you must set the joystick event processing state with SDL_JoystickEventState. Joysticks must be opened before they can be used of course.

Note: If you are not handling the joystick via the event queue then you must explicitly request a joystick update by calling SDL_JoystickUpdate.

Note: Force Feedback is not yet support. Sam (slouken@libsdl.org) is soliciting suggestions from people with force-feedback experience on the best wat to desgin the API.

Prev | Home | Next
SDL_JoystickEventState | Up | SDL_NumJoysticks


SDL_NumJoysticks

Name

SDL_NumJoysticks -- Count available joysticks.

Synopsis

#include "SDL.h"

int SDL_NumJoysticks(void);

Description

Counts the number of joysticks attached to the system.

Return Value

Returns the number of attached joysticks

See Also

SDL_JoystickName, SDL_JoystickOpen

Prev | Home | Next
Joystick | Up | SDL_JoystickName


SDL_JoystickName

Name

SDL_JoystickName -- Get joystick name.

Synopsis

#include "SDL.h"

const char *SDL_JoystickName(int index);

Description

Get the implementation dependent name of joystick. The index parameter refers to the N'th joystick on the system.

Return Value

Returns a char pointer to the joystick name.

Examples

/* Print the names of all attached joysticks */
int num_joy, i;
num_joy=SDL_NumJoysticks();
printf("%d joysticks found\n", num_joy);
for(i=0;i<num_joy;i++)
  printf("%s\n", SDL_JoystickName(i));

See Also

SDL_JoystickOpen

Prev | Home | Next
SDL_NumJoysticks | Up | SDL_JoystickOpen


SDL_JoystickOpen

Name

SDL_JoystickOpen -- Opens a joystick for use.

Synopsis

#include "SDL.h"

SDL_Joystick *SDL_JoystickOpen(int index);

Description

Opens a joystick for use within SDL. The index refers to the N'th joystick in the system. A joystick must be opened before it game be used.

Return Value

Returns a SDL_Joystick structure on success. NULL on failure.

Examples

SDL_Joystick *joy;
// Check for joystick
if(SDL_NumJoysticks()>0){
  // Open joystick
  joy=SDL_JoystickOpen(0);

  if(joy)
  {
    printf("Opened Joystick 0\n");
    printf("Name: %s\n", SDL_JoystickName(0));
    printf("Number of Axes: %d\n", SDL_JoystickNumAxes(joy));
    printf("Number of Buttons: %d\n", SDL_JoystickNumButtons(joy));
    printf("Number of Balls: %d\n", SDL_JoystickNumBalls(joy));
  }
  else
    printf("Couldn't open Joystick 0\n");

  // Close if opened
  if(SDL_JoystickOpened(0))
    SDL_JoystickClose(joy);
}

See Also

SDL_JoystickClose

Prev | Home | Next
SDL_JoystickName | Up | SDL_JoystickOpened


SDL_JoystickOpened

Name

SDL_JoystickOpened -- Determine if a joystick has been opened

Synopsis

#include "SDL.h"

int SDL_JoystickOpened(int index);

Description

Determines whether a joystick has already been opened within the application. index refers to the N'th joystick on the system.

Return Value

Returns 1 if the joystick has been opened, or 0 if it has not.

See Also

SDL_JoystickOpen, SDL_JoystickClose

Prev | Home | Next
SDL_JoystickOpen | Up | SDL_JoystickIndex


SDL_JoystickIndex

Name

SDL_JoystickIndex -- Get the index of an SDL_Joystick.

Synopsis

#include "SDL.h"

int SDL_JoystickIndex(SDL_Joystick *joystick);

Description

Returns the index of a given SDL_Joystick structure.

Return Value

Index number of the joystick.

See Also

SDL_JoystickOpen

Prev | Home | Next
SDL_JoystickOpened | Up | SDL_JoystickNumAxes


SDL_JoystickNumAxes

Name

SDL_JoystickNumAxes -- Get the number of joystick axes

Synopsis

#include "SDL.h"

int SDL_JoystickNumAxes(SDL_Joystick *joystick);

Description

Return the number of axes available from a previously opened SDL_Joystick.

Return Value

Number of axes.

See Also

SDL_JoystickGetAxis, SDL_JoystickOpen

Prev | Home | Next
SDL_JoystickIndex | Up | SDL_JoystickNumBalls


SDL_JoystickNumBalls

Name

SDL_JoystickNumBalls -- Get the number of joystick trackballs

Synopsis

#include "SDL.h"

int SDL_JoystickNumBalls(SDL_Joystick *joystick);

Description

Return the number of trackballs available from a previously opened SDL_Joystick.

Return Value

Number of trackballs.

See Also

SDL_JoystickGetBall, SDL_JoystickOpen

Prev | Home | Next
SDL_JoystickNumAxes | Up | SDL_JoystickNumHats


SDL_JoystickNumHats

Name

SDL_JoystickNumHats -- Get the number of joystick hats

Synopsis

#include "SDL.h"

int SDL_JoystickNumHats(SDL_Joystick *joystick);

Description

Return the number of hats available from a previously opened SDL_Joystick.

Return Value

Number of hats.

See Also

SDL_JoystickGetHat, SDL_JoystickOpen

Prev | Home | Next
SDL_JoystickNumBalls | Up | SDL_JoystickNumButtons


SDL_JoystickNumButtons

Name

SDL_JoystickNumButtons -- Get the number of joysitck buttons

Synopsis

#include "SDL.h"

int SDL_JoystickNumButtons(SDL_Joystick *joystick);

Description

Return the number of buttons available from a previously opened SDL_Joystick.

Return Value

Number of buttons.

See Also

SDL_JoystickGetButton, SDL_JoystickOpen

Prev | Home | Next
SDL_JoystickNumHats | Up | SDL_JoystickUpdate


SDL_JoystickUpdate

Name

SDL_JoystickUpdate -- Updates the state of all joysticks

Synopsis

#include "SDL.h"

void SDL_JoystickUpdate(void);

Description

Updates the state(position, buttons, etc.) of all open joysticks. If joystick events have been enabled with SDL_JoystickEventState then this is called automatically in the event loop.

See Also

SDL_JoystickEventState

Prev | Home | Next
SDL_JoystickNumButtons | Up | SDL_JoystickGetAxis


SDL_JoystickGetAxis

Name

SDL_JoystickGetAxis -- Get the current state of an axis

Synopsis

#include "SDL.h"

Sint16 SDL_JoystickGetAxis(SDL_Joystick *joystick, int axis);

Description

SDL_JoystickGetAxis returns the current state of the given axis on the given joystick.

On most modern joysticks the X axis is usually represented by axis 0 and the Y axis by axis 1. The value returned by SDL_JoystickGetAxis is a signed integer (-32768 to 32768) representing the current position of the axis, it maybe necessary to impose certain tolerances on these values to account for jitter. It is worth noting that some joysticks use axes 2 and 3 for extra buttons.

Return Value

Returns a 16-bit signed integer representing the current position of the axis.

Examples

Sint16 x_move, y_move;
SDL_Joystick *joy1;
.
.
x_move=SDL_JoystickGetAxis(joy1, 0);
y_move=SDL_JoystickGetAxis(joy1, 1);

See Also

SDL_JoystickNumAxes

Prev | Home | Next
SDL_JoystickUpdate | Up | SDL_JoystickGetHat


SDL_JoystickGetHat

Name

SDL_JoystickGetHat -- Get the current state of a joystick hat

Synopsis

#include "SDL.h"

Uint8 SDL_JoystickGetHat(SDL_Joystick *joystick, int hat);

Description

SDL_JoystickGetHat returns the current state of the given hat on the given joystick.

Return Value

The current state is returned as a Uint8 which is defined as an OR'd combination of one or more of the following

SDL_HAT_CENTERED
SDL_HAT_UP
SDL_HAT_RIGHT
SDL_HAT_DOWN
SDL_HAT_LEFT
SDL_HAT_RIGHTUP
SDL_HAT_RIGHTDOWN
SDL_HAT_LEFTUP
SDL_HAT_LEFTDOWN

See Also

SDL_JoystickNumHats

Prev | Home | Next
SDL_JoystickGetAxis | Up | SDL_JoystickGetButton


SDL_JoystickGetButton

Name

SDL_JoystickGetButton -- Get the current state of a given button on a given joystick

Synopsis

#include "SDL.h"

Uint8 SDL_JoystickGetButton(SDL_Joystick *joystick, int button);

Description

SDL_JoystickGetButton returns the current state of the given button on the given joystick.

Return Value

1 if the button is pressed. Otherwise, 0.

See Also

SDL_JoystickNumButtons

Prev | Home | Next
SDL_JoystickGetHat | Up | SDL_JoystickGetBall


SDL_JoystickGetBall

Name

SDL_JoystickGetBall -- Get relative trackball motion

Synopsis

#include "SDL.h"

int SDL_JoystickGetBall(SDL_Joystick *joystick, int ball, int *dx, int *dy);

Description

Get the ball axis change.

Trackballs can only return relative motion since the last call to SDL_JoystickGetBall, these motion deltas a placed into dx and dy.

Return Value

Returns 0 on success or -1 on failure

Examples

int delta_x, delta_y;
SDL_Joystick *joy;
.
.
.
SDL_JoystickUpdate();
if(SDL_JoystickGetBall(joy, 0, &delta_x, &delta_y)==-1)
  printf("TrackBall Read Error!\n");
printf("Trackball Delta- X:%d, Y:%d\n", delta_x, delta_y);

See Also

SDL_JoystickNumBalls

Prev | Home | Next
SDL_JoystickGetButton | Up | SDL_JoystickClose


SDL_JoystickClose

Name

SDL_JoystickClose -- Closes a previously opened joystick

Synopsis

#include "SDL.h"

void SDL_JoystickClose(SDL_Joystick *joystick);

Description

Close a joystick that was previously opened with SDL_JoystickOpen.

See Also

SDL_JoystickOpen, SDL_JoystickOpened

Prev | Home | Next
SDL_JoystickGetBall | Up | Audio


Audio

SDL Library Documentation
Prev Next

Chapter 10. Audio

Table of Contents
SDL_AudioSpec — Audio Specification Structure
SDL_OpenAudio — Opens the audio device with the desired parameters.
SDL_PauseAudio — Pauses and unpauses the audio callback processing
SDL_GetAudioStatus — Get the current audio state
SDL_LoadWAV — Load a WAVE file
SDL_FreeWAV — Frees previously opened WAV data
SDL_AudioCVT — Audio Conversion Structure
SDL_BuildAudioCVT — Initializes a SDL_AudioCVT structure for conversion
SDL_ConvertAudio — Convert audio data to a desired audio format.
SDL_MixAudio — Mix audio data
SDL_LockAudio — Lock out the callback function
SDL_UnlockAudio — Unlock the callback function
SDL_CloseAudio — Shuts down audio processing and closes the audio device.

Sound on the computer is translated from waves that you hear into a series of values, or samples, each representing the amplitude of the wave. When these samples are sent in a stream to a sound card, an approximation of the original wave can be recreated. The more bits used to represent the amplitude, and the greater frequency these samples are gathered, the closer the approximated sound is to the original, and the better the quality of sound.

This library supports both 8 and 16 bit signed and unsigned sound samples, at frequencies ranging from 11025 Hz to 44100 Hz, depending on the underlying hardware. If the hardware doesn't support the desired audio format or frequency, it can be emulated if desired (See SDL_OpenAudio())

A commonly supported audio format is 16 bits per sample at 22050 Hz.

Prev | Home | Next
SDL_JoystickClose | Up | SDL_AudioSpec


SDL_AudioSpec

Name

SDL_AudioSpec -- Audio Specification Structure

Structure Definition

typedef struct{
  int freq;
  Uint16 format;
  Uint8 channels;
  Uint8 silence;
  Uint16 samples;
  Uint32 size;
  void (*callback)(void *userdata, Uint8 *stream, int len);
  void *userdata;
} SDL_AudioSpec;

Structure Data

freq | Audio frequency in samples per second
format | Audio data format
channels | Number of channels: 1 mono, 2 stereo
silence | Audio buffer silence value (calculated)
samples | Audio buffer size in samples
size | Audio buffer size in bytes (calculated)
callback(..) | Callback function for filling the audio buffer
userdata | Pointer the user data which is passed to the callback function

Description

The SDL_AudioSpec structure is used to describe the format of some audio data. This structure is used by SDL_OpenAudio and SDL_LoadWAV. While all fields are used by SDL_OpenAudio only freq, format, samples and channels are used by SDL_LoadWAV. We will detail these common members here.

freq

The number of samples sent to the sound device every second. Common values are 11025, 22050 and 44100. The higher the better.

format

Specifies the size and type of each sample element

AUDIO_U8

Unsigned 8-bit samples

AUDIO_S8

Signed 8-bit samples

AUDIO_U16 or AUDIO_U16LSB

Unsigned 16-bit little-endian samples

AUDIO_S16 or AUDIO_S16LSB

Signed 16-bit little-endian samples

AUDIO_U16MSB

Unsigned 16-bit big-endian samples

AUDIO_S16MSB

Signed 16-bit big-endian samples

AUDIO_U16SYS

Either AUDIO_U16LSB or AUDIO_U16MSB depending on you systems endianness

AUDIO_S16SYS

Either AUDIO_S16LSB or AUDIO_S16MSB depending on you systems endianness

channels | The number of seperate sound channels. 1 is mono (single channel), 2 is stereo (dual channel).
samples | When used with SDL_OpenAudio this refers to the size of the audio buffer in samples. A sample a chunk of audio data of the size specified in format mulitplied by the number of channels. When the SDL_AudioSpec is used with SDL_LoadWAV samples is set to 4096.

See Also

SDL_OpenAudio, SDL_LoadWAV

Prev | Home | Next
Audio | Up | SDL_OpenAudio


SDL_OpenAudio

Name

SDL_OpenAudio -- Opens the audio device with the desired parameters.

Synopsis

#include "SDL.h"

int SDL_OpenAudio(SDL_AudioSpec *desired, SDL_AudioSpec *obtained);

Description

This function opens the audio device with the desired parameters, and returns 0 if successful, placing the actual hardware parameters in the structure pointed to by obtained. If obtained is NULL, the audio data passed to the callback function will be guaranteed to be in the requested format, and will be automatically converted to the hardware audio format if necessary. This function returns -1 if it failed to open the audio device, or couldn't set up the audio thread.

To open the audio device a desired SDL_AudioSpec must be created.

SDL_AudioSpec *desired;
.
.
desired=(SDL_AudioSpec *)malloc(sizeof(SDL_AudioSpec));
You must then fill this structure with your desired audio specifications.

desired->freq

The desired audio frequency in samples-per-second.

desired->format

The desired audio format (see SDL_AudioSpec)

desired->samples

The desired size of the audio buffer in samples. This number should be a power of two, and may be adjusted by the audio driver to a value more suitable for the hardware. Good values seem to range between 512 and 8192 inclusive, depending on the application and CPU speed. Smaller values yield faster response time, but can lead to underflow if the application is doing heavy processing and cannot fill the audio buffer in time. A stereo sample consists of both right and left channels in LR ordering. Note that the number of samples is directly related to time by the following formula: ms = (samples*1000)/freq

desired->callback

This should be set to a function that will be called when the audio device is ready for more data. It is passed a pointer to the audio buffer, and the length in bytes of the audio buffer. This function usually runs in a separate thread, and so you should protect data structures that it accesses by calling SDL_LockAudio and SDL_UnlockAudio in your code. The callback prototype is:

void callback(void *userdata, Uint8 *stream, int len);
userdata is the pointer stored in userdata field of the SDL_AudioSpec. stream is a pointer to the audio buffer you want to fill with information and len is the length of the audio buffer in bytes.

desired->userdata

This pointer is passed as the first parameter to the callback function.

SDL_OpenAudio reads these fields from the desired SDL_AudioSpec structure pass to the function and attempts to find an audio configuration matching your desired. As mentioned above, if the obtained parameter is NULL then SDL with convert from your desired audio settings to the hardware settings as it plays.

If obtained is NULL then the desired SDL_AudioSpec is your working specification, otherwise the obtained SDL_AudioSpec becomes the working specification and the desirec specification can be deleted. The data in the working specification is used when building SDL_AudioCVT's for converting loaded data to the hardware format.

SDL_OpenAudio calculates the size and silence fields for both the desired and obtained specifications. The size field stores the total size of the audio buffer in bytes, while the silence stores the value used to represent silence in the audio buffer

The audio device starts out playing silence when it's opened, and should be enabled for playing by calling SDL_PauseAudio(0) when you are ready for your audio callback function to be called. Since the audio driver may modify the requested size of the audio buffer, you should allocate any local mixing buffers after you open the audio device.

Examples

/* Prototype of our callback function */
void my_audio_callback(void *userdata, Uint8 *stream, int len);

/* Open the audio device */
SDL_AudioSpec *desired, *obtained;
SDL_AudioSpec *hardware_spec;

/* Allocate a desired SDL_AudioSpec */
desired=(SDL_AudioSpec *)malloc(sizeof(SDL_AudioSpec));

/* Allocate space for the obtained SDL_AudioSpec */
obtained=(SDL_AudioSpec *)malloc(sizeof(SDL_AudioSpec));

/* 22050Hz - FM Radio quality */
desired->freq=22050;

/* 16-bit signed audio */
desired->format=AUDIO_S16LSB;

/* Mono */
desired->channels=0;

/* Large audio buffer reduces risk of dropouts but increases response time */
desired->samples=8192;

/* Our callback function */
desired->callback=my_audio_callback;

desired->userdata=NULL;

/* Open the audio device */
if ( SDL_OpenAudio(desired, obtained) < 0 ){
  fprintf(stderr, "Couldn't open audio: %s\n", SDL_GetError());
  exit(-1);
}
/* desired spec is no longer needed */
free(desired);
hardware_spec=obtained;
.
.
/* Prepare callback for playing */
.
.
.
/* Start playing */
SDL_PauseAudio(0);

See Also

SDL_AudioSpec, SDL_LockAudio, SDL_UnlockAudio, SDL_PauseAudio

Prev | Home | Next
SDL_AudioSpec | Up | SDL_PauseAudio


SDL_PauseAudio

Name

SDL_PauseAudio -- Pauses and unpauses the audio callback processing

Synopsis

#include "SDL.h"

void SDL_PauseAudio(int pause_on);

Description

This function pauses and unpauses the audio callback processing. It should be called with pause_on=0 after opening the audio device to start playing sound. This is so you can safely initialize data for your callback function after opening the audio device. Silence will be written to the audio device during the pause.

See Also

SDL_GetAudioStatus, SDL_OpenAudio

Prev | Home | Next
SDL_OpenAudio | Up | SDL_GetAudioStatus


SDL_GetAudioStatus

Name

SDL_GetAudioStatus -- Get the current audio state

Synopsis

#include "SDL.h"

SDL_audiostatusSDL_GetAudioStatus(void);

Description

typedef enum{
  SDL_AUDIO_STOPPED,
  SDL_AUDIO_PAUSED,
  SDL_AUDIO_PLAYING
} SDL_audiostatus;

Returns either SDL_AUDIO_STOPPED, SDL_AUDIO_PAUSED or SDL_AUDIO_PLAYING depending on the current audio state.

See Also

SDL_PauseAudio

Prev | Home | Next
SDL_PauseAudio | Up | SDL_LoadWAV


SDL_LoadWAV

Name

SDL_LoadWAV -- Load a WAVE file

Synopsis

#include "SDL.h"

SDL_AudioSpec *SDL_LoadWAV(const char *file, SDL_AudioSpec *spec, Uint8 **audio_buf, Uint32 *audio_len);

Description

SDL_LoadWAV This function loads a WAVE file into memory.

If this function succeeds, it returns the given SDL_AudioSpec, filled with the audio data format of the wave data, and sets audio_buf to a malloc'd buffer containing the audio data, and sets audio_len to the length of that audio buffer, in bytes. You need to free the audio buffer with SDL_FreeWAV when you are done with it.

This function returns NULL and sets the SDL error message if the wave file cannot be opened, uses an unknown data format, or is corrupt. Currently raw, MS-ADPCM and IMA-ADPCM WAVE files are supported.

Example

SDL_AudioSpec wav_spec;
Uint32 wav_length;
Uint8 *wav_buffer;

/* Load the WAV */
if( SDL_LoadWAV("test.wav", &wav_spec, &wav_buffer, &wav_length) == NULL ){
  fprintf(stderr, "Could not open test.wav: %s\n", SDL_GetError());
  exit(-1);
}
.
.
.
/* Do stuff with the WAV */
.
.
/* Free It */
SDL_FreeWAV(wav_buffer);

See Also

SDL_AudioSpec, SDL_OpenAudio, SDL_FreeWAV

Prev | Home | Next
SDL_GetAudioStatus | Up | SDL_FreeWAV


SDL_FreeWAV

Name

SDL_FreeWAV -- Frees previously opened WAV data

Synopsis

#include "SDL.h"

void SDL_FreeWAV(Uint8 *audio_buf);

Description

After a WAVE file has been opened with SDL_LoadWAV its data can eventually be freed with SDL_FreeWAV. audio_buf is a pointer to the buffer created by SDL_LoadWAV.

See Also

SDL_LoadWAV

Prev | Home | Next
SDL_LoadWAV | Up | SDL_AudioCVT


SDL_AudioCVT

Name

SDL_AudioCVT -- Audio Conversion Structure

Structure Definition

typedef struct{
  int needed;
  Uint16 src_format;
  Uint16 dest_format;
  double rate_incr;
  Uint8 *buf;
  int len;
  int len_cvt;
  int len_mult;
  double len_ratio;
  void (*filters[10])(struct SDL_AudioCVT *cvt, Uint16 format);
  int filter_index;
} SDL_AudioCVT;

Structure Data

needed | Set to one if the conversion is possible
src_format | Audio format of the source
dest_format | Audio format of the destination
rate_incr | Rate conversion increment
buf | Audio buffer
len | Length of the original audio buffer in bytes
len_cvt | Length of converted audio buffer in bytes (calculated)
len_mult | buf must be len*len_mult bytes in size(calculated)
len_ratio | Final audio size is len*len_ratio
filters[10](..) | Pointers to functions needed for this conversion
filter_index | Current conversion function

Description

The SDL_AudioCVT is used to convert audio data between different formats. A SDL_AudioCVT structure is created with the SDL_BuildAudioCVT function, while the actual conversion is done by the SDL_ConvertAudio function.

Many of the fields in the SDL_AudioCVT structure should be considered private and their function will not be discussed here.

Uint8 *buf

This points to the audio data that will be used in the conversion. It is both the source and the destination, which means the converted audio data overwrites the original data. It also means that the converted data may be larger than the original data (if you were converting from 8-bit to 16-bit, for instance), so you must ensure buf is large enough. See below.

int len

This is the length of the original audio data in bytes.

int len_mult

As explained above, the audio buffer needs to be big enough to store the converted data, which may be bigger than the original audio data. The length of buf should be len*len_mult.

double len_ratio

When you have finished converting your audio data, you need to know how much of your audio buffer is valid. len*len_ratio is the size of the converted audio data in bytes. This is very similar to len_mult, however when the convert audio data is shorter than the original len_mult would be 1. len_ratio, on the other hand, would be a fractional number between 0 and 1.

See Also

SDL_BuildAudioCVT, SDL_ConvertAudio, SDL_AudioSpec

Prev | Home | Next
SDL_FreeWAV | Up | SDL_BuildAudioCVT


SDL_BuildAudioCVT

Name

SDL_BuildAudioCVT -- Initializes a SDL_AudioCVT structure for conversion

Synopsis

#include "SDL.h"

int SDL_BuildAudioCVT(SDL_AudioCVT *cvt, Uint16 src_format, Uint8 src_channels, int src_rate, Uint16 dst_format, Uint8 dst_channels, int dst_rate);

Description

Before an SDL_AudioCVT structure can be used to convert audio data it must be initialized with source and destination information.

src_format and dst_format are the source and destination format of the conversion. (For information on audio formats see SDL_AudioSpec). src_channels and dst_channels are the number of channels in the source and destination formats. Finally, src_rate and dst_rate are the frequency or samples-per-second of the source and destination formats. Once again, see SDL_AudioSpec.

Return Values

Returns -1 if the filter could not be built or 1 if it could.

Examples

See SDL_ConvertAudio.

See Also

SDL_ConvertAudio, SDL_AudioCVT

Prev | Home | Next
SDL_AudioCVT | Up | SDL_ConvertAudio


SDL_ConvertAudio

Name

SDL_ConvertAudio -- Convert audio data to a desired audio format.

Synopsis

#include "SDL.h"

int SDL_ConvertAudio(SDL_AudioCVT *cvt);

Description

SDL_ConvertAudio takes one parameter, cvt, which was previously initilized. Initilizing a SDL_AudioCVT is a two step process. First of all, the structure must be passed to SDL_BuildAudioCVT along with source and destination format parameters. Secondly, the cvt->buf and cvt->len fields must be setup. cvt->buf should point to the audio data and cvt->len should be set to the length of the audio data in bytes. Remember, the length of the buffer pointed to by buf show be len*len_mult bytes in length.

Once the SDL_AudioCVTstructure is initilized then we can pass it to SDL_ConvertAudio, which will convert the audio data pointer to by cvt->buf. If SDL_ConvertAudio returned 0 then the conversion was completed successfully, otherwise -1 is returned.

If the conversion completed successfully then the converted audio data can be read from cvt->buf. The amount of valid, converted, audio data in the buffer is equal to cvt->len*cvt->len_ratio.

Examples

/* Converting some WAV data to hardware format */
void my_audio_callback(void *userdata, Uint8 *stream, int len);

SDL_AudioSpec *desired, *obtained;
SDL_AudioSpec wav_spec;
SDL_AudioCVT  wav_cvt;
Uint32 wav_len;
Uint8 *wav_buf;
int ret;

/* Allocated audio specs */
desired=(SDL_AudioSpec *)malloc(sizeof(SDL_AudioSpec));
obtained=(SDL_AudioSpec *)malloc(sizeof(SDL_AudioSpec));

/* Set desired format */
desired->freq=22050;
desired->format=AUDIO_S16LSB;
desired->samples=8192;
desired->callback=my_audio_callback;
desired->userdata=NULL;

/* Open the audio device */
if ( SDL_OpenAudio(desired, obtained) < 0 ){
  fprintf(stderr, "Couldn't open audio: %s\n", SDL_GetError());
  exit(-1);
}

free(desired);

/* Load the test.wav */
if( SDL_LoadWAV("test.wav", &wav_spec, &wav_buf, &wav_len) == NULL ){
  fprintf(stderr, "Could not open test.wav: %s\n", SDL_GetError());
  SDL_CloseAudio();
  free(obtained);
  exit(-1);
}

/* Build AudioCVT */
ret = SDL_BuildAudioCVT(&wav_cvt,
                        wav_spec.format, wav_spec.channels, wav_spec.freq,
                        obtained->format, obtained->channels, obtained->freq);

/* Check that the convert was built */
if(ret==-1){
  fprintf(stderr, "Couldn't build converter!\n");
  SDL_CloseAudio();
  free(obtained);
  SDL_FreeWAV(wav_buf);
}

/* Setup for conversion */
wav_cvt.buf=(Uint8 *)malloc(wav_len*wav_cvt.len_mult);
wav_cvt.len=wav_len;
memcpy(wav_cvt.buf, wav_buf, wav_len);

/* We can delete to original WAV data now */
SDL_FreeWAV(wav_buf);

/* And now we're ready to convert */
SDL_ConvertAudio(&wav_cvt);

/* do whatever */
.
.
.
.

See Also

SDL_BuildAudioCVT, SDL_AudioCVT

Prev | Home | Next
SDL_BuildAudioCVT | Up | SDL_MixAudio


SDL_MixAudio

Name

SDL_MixAudio -- Mix audio data

Synopsis

#include "SDL.h"

void SDL_MixAudio(Uint8 *dst, Uint8 *src, Uint32 len, int volume);

Description

This function takes two audio buffers of len bytes each of the playing audio format and mixes them, performing addition, volume adjustment, and overflow clipping. The volume ranges from 0 to SDL_MIX_MAXVOLUME and should be set to the maximum value for full audio volume. Note this does not change hardware volume. This is provided for convenience -- you can mix your own audio data.

Note: Do not use this function for mixing together more than two streams of sample data. The output from repeated application of this function may be distorted by clipping, because there is no accumulator with greater range than the input (not to mention this being an inefficient way of doing it). Use mixing functions from SDL_mixer, OpenAL, or write your own mixer instead.

See Also

SDL_OpenAudio

Prev | Home | Next
SDL_ConvertAudio | Up | SDL_LockAudio


SDL_LockAudio

Name

SDL_LockAudio -- Lock out the callback function

Synopsis

#include "SDL.h"

void SDL_LockAudio(void);

Description

The lock manipulated by these functions protects the callback function. During a LockAudio period, you can be guaranteed that the callback function is not running. Do not call these from the callback function or you will cause deadlock.

See Also

SDL_OpenAudio

Prev | Home | Next
SDL_MixAudio | Up | SDL_UnlockAudio


SDL_UnlockAudio

Name

SDL_UnlockAudio -- Unlock the callback function

Synopsis

#include "SDL.h"

void SDL_UnlockAudio(void);

Description

Unlocks a previous SDL_LockAudio call.

See Also

SDL_OpenAudio

Prev | Home | Next
SDL_LockAudio | Up | SDL_CloseAudio


SDL_CloseAudio

Name

SDL_CloseAudio -- Shuts down audio processing and closes the audio device.

Synopsis

#include "SDL.h"

void SDL_CloseAudio(void);

Description

This function shuts down audio processing and closes the audio device.

See Also

SDL_OpenAudio

Prev | Home | Next
SDL_UnlockAudio | Up | CD-ROM


CD-ROM

SDL Library Documentation
Prev Next

Chapter 11. CD-ROM

Table of Contents
SDL_CDNumDrives — Returns the number of CD-ROM drives on the system.
SDL_CDName — Returns a human-readable, system-dependent identifier for the CD-ROM.
SDL_CDOpen — Opens a CD-ROM drive for access.
SDL_CDStatus — Returns the current status of the given drive.
SDL_CDPlay — Play a CD
SDL_CDPlayTracks — Play the given CD track(s)
SDL_CDPause — Pauses a CDROM
SDL_CDResume — Resumes a CDROM
SDL_CDStop — Stops a CDROM
SDL_CDEject — Ejects a CDROM
SDL_CDClose — Closes a SDL_CD handle
SDL_CD — CDROM Drive Information
SDL_CDtrack — CD Track Information Structure

SDL supports audio control of up to 32 local CD-ROM drives at once.

You use this API to perform all the basic functions of a CD player, including listing the tracks, playing, stopping, and ejecting the CD-ROM. (Currently, multi-changer CD drives are not supported.)

Before you call any of the SDL CD-ROM functions, you must first call "SDL_Init(SDL_INIT_CDROM)", which scans the system for CD-ROM drives, and sets the program up for audio control. Check the return code, which should be 0, to see if there were any errors in starting up.

After you have initialized the library, you can find out how many drives are available using the SDL_CDNumDrives() function. The first drive listed is the system default CD-ROM drive. After you have chosen a drive, and have opened it with SDL_CDOpen(), you can check the status and start playing if there's a CD in the drive.

A CD-ROM is organized into one or more tracks, each consisting of a certain number of "frames". Each frame is ~2K in size, and at normal playing speed, a CD plays 75 frames per second. SDL works with the number of frames on a CD, but this can easily be converted to the more familiar minutes/seconds format by using the FRAMES_TO_MSF() macro.

Prev | Home | Next
SDL_CloseAudio | Up | SDL_CDNumDrives


SDL_CDNumDrives

Name

SDL_CDNumDrives -- Returns the number of CD-ROM drives on the system.

Synopsis

#include "SDL.h"

int SDL_CDNumDrives(void);

Description

Returns the number of CD-ROM drives on the system.

See Also

SDL_CDOpen

Prev | Home | Next
CD-ROM | Up | SDL_CDName


SDL_CDName

Name

SDL_CDName -- Returns a human-readable, system-dependent identifier for the CD-ROM.

Synopsis

#include "SDL.h"

const char *SDL_CDName(int drive);

Description

Returns a human-readable, system-dependent identifier for the CD-ROM. drive is the index of the drive. Drive indices start to 0 and end at SDL_CDNumDrives()-1.

Examples

  • "/dev/cdrom"

  • "E:"

  • "/dev/disk/ide/1/master"

See Also

SDL_CDNumDrives

Prev | Home | Next
SDL_CDNumDrives | Up | SDL_CDOpen


SDL_CDOpen

Name

SDL_CDOpen -- Opens a CD-ROM drive for access.

Synopsis

#include "SDL.h"

SDL_CD *SDL_CDOpen(int drive);

Description

Opens a CD-ROM drive for access. It returns a SDL_CD structure on success, or NULL if the drive was invalid or busy. This newly opened CD-ROM becomes the default CD used when other CD functions are passed a NULL CD-ROM handle.

Drives are numbered starting with 0. Drive 0 is the system default CD-ROM.

Examples

SDL_CD *cdrom;
int cur_track;
int min, sec, frame;
SDL_Init(SDL_INIT_CDROM);
atexit(SDL_Quit);

/* Check for CD drives */
if(!SDL_CDNumDrives()){
  /* None found */
  fprintf(stderr, "No CDROM devices available\n");
  exit(-1);
}

/* Open the default drive */
cdrom=SDL_CDOpen(0);

/* Did if open? Check if cdrom is NULL */
if(!cdrom){
  fprintf(stderr, "Couldn't open drive: %s\n", SDL_GetError());
  exit(-1);
}

/* Print Volume info */
printf("Name: %s\n", SDL_CDName(0));
printf("Tracks: %d\n", cdrom->numtracks);
for(cur_track=0;cur_track < cdrom->numtracks; cur_track++){
  FRAMES_TO_MSF(cdrom->track[cur_track].length, &min, &sec, &frame);
  printf("\tTrack %d: Length %d:%d\n", cur_track, min, sec);
}

SDL_CDClose(cdrom);

See Also

SDL_CD, SDL_CDtrack, SDL_CDClose

Prev | Home | Next
SDL_CDName | Up | SDL_CDStatus


SDL_CDStatus

Name

SDL_CDStatus -- Returns the current status of the given drive.

Synopsis

#include "SDL.h"

CDstatus SDL_CDStatus(SDL_CD *cdrom);

/* Given a status, returns true if there's a disk in the drive */
#define CD_INDRIVE(status)      ((int)status > 0)

Description

This function returns the current status of the given drive. Status is described like so:

typedef enum {
  CD_TRAYEMPTY,
  CD_STOPPED,
  CD_PLAYING,
  CD_PAUSED,
  CD_ERROR = -1
} CDstatus;

If the drive has a CD in it, the table of contents of the CD and current play position of the CD will be stored in the SDL_CD structure.

The macro CD_INDRIVE is provided for convenience, and given a status returns true if there's a disk in the drive.

Note: SDL_CDStatus also updates the SDL_CD structure passed to it.

Example

int playTrack(int track)
{
  int playing = 0;

  if ( CD_INDRIVE(SDL_CDStatus(cdrom)) ) {
  /* clamp to the actual number of tracks on the CD */
    if (track >= cdrom->numtracks) {
      track = cdrom->numtracks-1;
    }

    if ( SDL_CDPlayTracks(cdrom, track, 0, 1, 0) == 0 ) {
      playing = 1;
    }
  }
  return playing;
}

See Also

SDL_CD

Prev | Home | Next
SDL_CDOpen | Up | SDL_CDPlay


SDL_CDPlay

Name

SDL_CDPlay -- Play a CD

Synopsis

#include "SDL.h"

int SDL_CDPlay(SDL_CD *cdrom, int start, int length);

Description

Plays the given cdrom, starting a frame start for length frames.

Return Values

Returns 0 on success, or -1 on an error.

See Also

SDL_CDPlayTracks, SDL_CDStop

Prev | Home | Next
SDL_CDStatus | Up | SDL_CDPlayTracks


SDL_CDPlayTracks

Name

SDL_CDPlayTracks -- Play the given CD track(s)

Synopsis

#include "SDL.h"

int SDL_CDPlayTracks(SDL_CD *cdrom, int start_track, int start_frame, int ntracks, int nframes));

Description

SDL_CDPlayTracks plays the given CD starting at track start_track, for ntracks tracks.

start_frame is the frame offset, from the beginning of the start_track, at which to start. nframes is the frame offset, from the beginning of the last track (start_track+ntracks), at which to end playing.

SDL_CDPlayTracks should only be called after calling SDL_CDStatus to get track information about the CD.

Note: Data tracks are ignored.

Return Value

Returns 0, or -1 if there was an error.

Examples

/* assuming cdrom is a previously opened device */
/* Play the entire CD */
if(CD_INDRIVE(SDL_CDStatus(cdrom)))
  SDL_CDPlayTracks(cdrom, 0, 0, 0, 0);

/* Play the first track */
if(CD_INDRIVE(SDL_CDStatus(cdrom)))
  SDL_CDPlayTracks(cdrom, 0, 0, 1, 0);

/* Play first 15 seconds of the 2nd track */
if(CD_INDRIVE(SDL_CDStatus(cdrom)))
  SDL_CDPlayTracks(cdrom, 1, 0, 0, CD_FPS*15);

See Also

SDL_CDPlay, SDL_CDStatus, SDL_CD

Prev | Home | Next
SDL_CDPlay | Up | SDL_CDPause


SDL_CDPause

Name

SDL_CDPause -- Pauses a CDROM

Synopsis

#include "SDL.h"

int SDL_CDPause(SDL_CD *cdrom);

Description

Pauses play on the given cdrom.

Return Value

Returns 0 on success, or -1 on an error.

See Also

SDL_CDPlay, SDL_CDResume

Prev | Home | Next
SDL_CDPlayTracks | Up | SDL_CDResume


SDL_CDResume

Name

SDL_CDResume -- Resumes a CDROM

Synopsis

#include "SDL.h"

int SDL_CDResume(SDL_CD *cdrom);

Description

Resumes play on the given cdrom.

Return Value

Returns 0 on success, or -1 on an error.

See Also

SDL_CDPlay, SDL_CDPause

Prev | Home | Next
SDL_CDPause | Up | SDL_CDStop


SDL_CDStop

Name

SDL_CDStop -- Stops a CDROM

Synopsis

#include "SDL.h"

int SDL_CDStop(SDL_CD *cdrom);

Description

Stops play on the given cdrom.

Return Value

Returns 0 on success, or -1 on an error.

See Also

SDL_CDPlay,

Prev | Home | Next
SDL_CDResume | Up | SDL_CDEject


SDL_CDEject

Name

SDL_CDEject -- Ejects a CDROM

Synopsis

#include "SDL.h"

int SDL_CDEject(SDL_CD *cdrom);

Description

Ejects the given cdrom.

Return Value

Returns 0 on success, or -1 on an error.

See Also

SDL_CD

Prev | Home | Next
SDL_CDStop | Up | SDL_CDClose


SDL_CDClose

Name

SDL_CDClose -- Closes a SDL_CD handle

Synopsis

#include "SDL.h"

void SDL_CDClose(SDL_CD *cdrom);

Description

Closes the given cdrom handle.

See Also

SDL_CDOpen, SDL_CD

Prev | Home | Next
SDL_CDEject | Up | SDL_CD


SDL_CD

Name

SDL_CD -- CDROM Drive Information

Structure Definition

typedef struct{
  int id;
  CDstatus status;
  int numtracks;
  int cur_track;
  int cur_frame;
  SDL_CDtrack track[SDL_MAX_TRACKS+1];
} SDL_CD;

Structure Data

id | Private drive identifier
status | Drive status
numtracks | Number of tracks on the CD
cur_track | Current track
cur_frame | Current frame offset within the track
track[SDL_MAX_TRACKS+1] | Array of track descriptions. (see SDL_CDtrack)

Description

An SDL_CD structure is returned by SDL_CDOpen. It represents an opened CDROM device and stores information on the layout of the tracks on the disc.

A frame is the base data unit of a CD. CD_FPS frames is equal to 1 second of music. SDL provides two macros for converting between time and frames: FRAMES_TO_MSF(f, M,S,F) and MSF_TO_FRAMES.

Examples

int min, sec, frame;
int frame_offset;

FRAMES_TO_MSF(cdrom->cur_frame, &min, &sec, &frame);
printf("Current Position: %d minutes, %d seconds, %d frames\n", min, sec, frame);

frame_offset=MSF_TO_FRAMES(min, sec, frame);

See Also

SDL_CDOpen, SDL_CDtrack

Prev | Home | Next
SDL_CDClose | Up | SDL_CDtrack


SDL_CDtrack

Name

SDL_CDtrack -- CD Track Information Structure

Structure Definition

typedef struct{
  Uint8 id;
  Uint8 type;
  Uint32 length;
  Uint32 offset;
} SDL_CDtrack;

Structure Data

id | Track number (0-99)
type | SDL_AUDIO_TRACK or SDL_DATA_TRACK
length | Length, in frames, of this track
offset | Frame offset to the beginning of this track

Description

SDL_CDtrack stores data on each track on a CD, its fields should be pretty self explainatory. It is a member a the SDL_CD structure.

Note: Frames can be converted to standard timings. There are CD_FPS frames per second, so SDL_CDtrack.length/CD_FPS=length_in_seconds.

See Also

SDL_CD

Prev | Home | Next
SDL_CD | Up | Multi-threaded Programming


Multi-threaded Programming

SDL Library Documentation
Prev Next

Chapter 12. Multi-threaded Programming

Table of Contents
SDL_CreateThread — Creates a new thread of execution that shares its parent's properties.
SDL_ThreadID — Get the 32-bit thread identifier for the current thread.
SDL_GetThreadID — Get the SDL thread ID of a SDL_Thread
SDL_WaitThread — Wait for a thread to finish.
SDL_KillThread — Gracelessly terminates the thread.
SDL_CreateMutex — Create a mutex
SDL_DestroyMutex — Destroy a mutex
SDL_mutexP — Lock a mutex
SDL_mutexV — Unlock a mutex
SDL_CreateSemaphore — Creates a new semaphore and assigns an initial value to it.
SDL_DestroySemaphore — Destroys a semaphore that was created by SDL_CreateSemaphore.
SDL_SemWait — Lock a semaphore and suspend the thread if the semaphore value is zero.
SDL_SemTryWait — Attempt to lock a semaphore but don't suspend the thread.
SDL_SemWaitTimeout — Lock a semaphore, but only wait up to a specified maximum time.
SDL_SemPost — Unlock a semaphore.
SDL_SemValue — Return the current value of a semaphore.
SDL_CreateCond — Create a condition variable
SDL_DestroyCond — Destroy a condition variable
SDL_CondSignal — Restart a thread wait on a condition variable
SDL_CondBroadcast — Restart all threads waiting on a condition variable
SDL_CondWait — Wait on a condition variable
SDL_CondWaitTimeout — Wait on a condition variable, with timeout

SDL provides functions for creating threads, mutexes, semphores and condition variables.

In general, you must be very aware of concurrency and data integrity issues when writing multi-threaded programs. Some good guidelines include:

  • Don't call SDL video/event functions from separate threads

  • Don't use any library functions in separate threads

  • Don't perform any memory management in separate threads

  • Lock global variables which may be accessed by multiple threads

  • Never terminate threads, always set a flag and wait for them to quit

  • Think very carefully about all possible ways your code may interact

Note: SDL's threading is not implemented on MacOS, due to that lack of preemptive thread support (MacOS X dos nt suffer from this problem)

Prev | Home | Next
SDL_CDtrack | Up | SDL_CreateThread


SDL_CreateThread

Name

SDL_CreateThread -- Creates a new thread of execution that shares its parent's properties.

Synopsis

#include "SDL.h"
#include "SDL_thread.h"

SDL_Thread *SDL_CreateThread(int (*fn)(void *), void *data);

Description

SDL_CreateThread creates a new thread of execution that shares all of its parent's global memory, signal handlers, file descriptors, etc, and runs the function fn passed the void pointer data The thread quits when this function returns.

See Also

SDL_KillThread

Prev | Home | Next
Multi-threaded Programming | Up | SDL_ThreadID


SDL_ThreadID

Name

SDL_ThreadID -- Get the 32-bit thread identifier for the current thread.

Synopsis

#include "SDL.h"
#include "SDL_thread.h"

Uint32 SDL_ThreadID(void);

Description

Get the 32-bit thread identifier for the current thread.

Prev | Home | Next
SDL_CreateThread | Up | SDL_GetThreadID


SDL_GetThreadID

Name

SDL_GetThreadID -- Get the SDL thread ID of a SDL_Thread

Synopsis

#include "SDL.h"
#include "SDL_thread.h"

Uint32 SDL_GetThreadID(SDL_Thread *thread);

Description

Returns the ID of a SDL_Thread created by SDL_CreateThread.

See Also

SDL_CreateThread

Prev | Home | Next
SDL_ThreadID | Up | SDL_WaitThread


SDL_WaitThread

Name

SDL_WaitThread -- Wait for a thread to finish.

Synopsis

#include "SDL.h"
#include "SDL_thread.h"

void SDL_WaitThread(SDL_Thread *thread, int *status);

Description

Wait for a thread to finish (timeouts are not supported).

Return Value

The return code for the thread function is placed in the area pointed to by status, if status is not NULL.

See Also

SDL_CreateThread

Prev | Home | Next
SDL_GetThreadID | Up | SDL_KillThread


SDL_KillThread

Name

SDL_KillThread -- Gracelessly terminates the thread.

Synopsis

#include "SDL.h"
#include "SDL_thread.h"

void SDL_KillThread(SDL_Thread *thread);

Description

SDL_KillThread gracelessly terminates the thread associated with thread. If possible, you should use some other form of IPC to signal the thread to quit.

See Also

SDL_CreateThread, SDL_WaitThread

Prev | Home | Next
SDL_WaitThread | Up | SDL_CreateMutex


SDL_CreateMutex

Name

SDL_CreateMutex -- Create a mutex

Synopsis

#include "SDL.h"
#include "SDL_thread.h"

SDL_mutex *SDL_CreateMutex(void);

Description

Create a new, unlocked mutex.

Examples

SDL_mutex *mut;

mut=SDL_CreateMutex();
.
.
if(SDL_mutexP(mut)==-1){
  fprintf(stderr, "Couldn't lock mutex\n");
  exit(-1);
}
.
/* Do stuff while mutex is locked */
.
.
if(SDL_mutexV(mut)==-1){
  fprintf(stderr, "Couldn't unlock mutex\n");
  exit(-1);
}

SDL_DestroyMutex(mut);

See Also

SDL_mutexP, SDL_mutexV, SDL_DestroyMutex

Prev | Home | Next
SDL_KillThread | Up | SDL_DestroyMutex


SDL_DestroyMutex

Name

SDL_DestroyMutex -- Destroy a mutex

Synopsis

#include "SDL.h"
#include "SDL_thread.h"

void SDL_DestroyMutex(SDL_mutex *mutex);

Description

Destroy a previously created mutex.

See Also

SDL_CreateMutex

Prev | Home | Next
SDL_CreateMutex | Up | SDL_mutexP


SDL_mutexP

Name

SDL_mutexP -- Lock a mutex

Synopsis

#include "SDL.h"
#include "SDL_thread.h"

int SDL_mutexP(SDL_mutex *mutex);

Description

Locks the mutex, which was previously created with SDL_CreateMutex. If the mutex is already locked then SDL_mutexP will not return until it is unlocked. Returns 0 on success, or -1 on an error.

SDL also defines a macro #define SDL_LockMutex(m) SDL_mutexP(m).

See Also

SDL_CreateMutex, SDL_mutexV

Prev | Home | Next
SDL_DestroyMutex | Up | SDL_mutexV


SDL_mutexV

Name

SDL_mutexV -- Unlock a mutex

Synopsis

#include "SDL.h"
#include "SDL_thread.h"

int SDL_mutexV(SDL_mutex *mutex);

Description

Unlocks the mutex, which was previously created with SDL_CreateMutex. Returns 0 on success, or -1 on an error.

SDL also defines a macro #define SDL_UnlockMutex(m) SDL_mutexV(m).

See Also

SDL_CreateMutex, SDL_mutexP

Prev | Home | Next
SDL_mutexP | Up | SDL_CreateSemaphore


SDL_CreateSemaphore

Name

SDL_CreateSemaphore -- Creates a new semaphore and assigns an initial value to it.

Synopsis

#include "SDL.h"
#include "SDL_thread.h"

SDL_sem *SDL_CreateSemaphore(Uint32 initial_value);

Description

SDL_CreateSemaphore() creates a new semaphore and initializes it with the value initial_value. Each locking operation on the semaphore by SDL_SemWait, SDL_SemTryWait or SDL_SemWaitTimeout will atomically decrement the semaphore value. The locking operation will be blocked if the semaphore value is not positive (greater than zero). Each unlock operation by SDL_SemPost will atomically increment the semaphore value.

Return Value

Returns a pointer to an initialized semaphore or NULL if there was an error.

Examples

SDL_sem *my_sem;

my_sem = SDL_CreateSemaphore(INITIAL_SEM_VALUE);

if (my_sem == NULL) {
        return CREATE_SEM_FAILED;
}

See Also

SDL_DestroySemaphore, SDL_SemWait, SDL_SemTryWait, SDL_SemWaitTimeout, SDL_SemPost, SDL_SemValue

Prev | Home | Next
SDL_mutexV | Up | SDL_DestroySemaphore


SDL_DestroySemaphore

Name

SDL_DestroySemaphore -- Destroys a semaphore that was created by SDL_CreateSemaphore.

Synopsis

#include "SDL.h"
#include "SDL_thread.h"

void SDL_DestroySemaphore(SDL_sem *sem);

Description

SDL_DestroySemaphore destroys the semaphore pointed to by sem that was created by SDL_CreateSemaphore. It is not safe to destroy a semaphore if there are threads currently blocked waiting on it.

Examples

if (my_sem != NULL) {
        SDL_DestroySemaphore(my_sem);
        my_sem = NULL;
}

See Also

SDL_CreateSemaphore, SDL_SemWait, SDL_SemTryWait, SDL_SemWaitTimeout, SDL_SemPost, SDL_SemValue

Prev | Home | Next
SDL_CreateSemaphore | Up | SDL_SemWait


SDL_SemWait

Name

SDL_SemWait -- Lock a semaphore and suspend the thread if the semaphore value is zero.

Synopsis

#include "SDL.h"
#include "SDL_thread.h"

int SDL_SemWait(SDL_sem *sem);

Description

SDL_SemWait() suspends the calling thread until either the semaphore pointed to by sem has a positive value, the call is interrupted by a signal or error. If the call is successful it will atomically decrement the semaphore value.

After SDL_SemWait() is successful, the semaphore can be released and its count atomically incremented by a successful call to SDL_SemPost.

Return Value

Returns 0 if successful or -1 if there was an error (leaving the semaphore unchanged).

Examples

if (SDL_SemWait(my_sem) == -1) {
        return WAIT_FAILED;
}

...

SDL_SemPost(my_sem);

See Also

SDL_CreateSemaphore, SDL_DestroySemaphore, SDL_SemTryWait, SDL_SemWaitTimeout, SDL_SemPost, SDL_SemValue

Prev | Home | Next
SDL_DestroySemaphore | Up | SDL_SemTryWait


SDL_SemTryWait

Name

SDL_SemTryWait -- Attempt to lock a semaphore but don't suspend the thread.

Synopsis

#include "SDL.h"
#include "SDL_thread.h"

int SDL_SemTryWait(SDL_sem *sem);

Description

SDL_SemTryWait is a non-blocking varient of SDL_SemWait. If the value of the semaphore pointed to by sem is positive it will atomically decrement the semaphore value and return 0, otherwise it will return SDL_MUTEX_TIMEOUT instead of suspending the thread.

After SDL_SemTryWait is successful, the semaphore can be released and its count atomically incremented by a successful call to SDL_SemPost.

Return Value

Returns 0 if the semaphore was successfully locked or either SDL_MUTEX_TIMEOUT or -1 if the thread would have suspended or there was an error, respectivly.

If the semaphore was not successfully locked, the semaphore will be unchanged.

Examples

res = SDL_SemTryWait(my_sem);

if (res == SDL_MUTEX_TIMEOUT) {
        return TRY_AGAIN;
}
if (res == -1) {
        return WAIT_ERROR;
}

...

SDL_SemPost(my_sem);

See Also

SDL_CreateSemaphore, SDL_DestroySemaphore, SDL_SemWait, SDL_SemWaitTimeout, SDL_SemPost, SDL_SemValue

Prev | Home | Next
SDL_SemWait | Up | SDL_SemWaitTimeout


SDL_SemWaitTimeout

Name

SDL_SemWaitTimeout -- Lock a semaphore, but only wait up to a specified maximum time.

Synopsis

#include "SDL.h"
#include "SDL_thread.h"

int SDL_SemWaitTimeout(SDL_sem *sem, Uint32 timeout);

Description

SDL_SemWaitTimeout() is a varient of SDL_SemWait with a maximum timeout value. If the value of the semaphore pointed to by sem is positive (greater than zero) it will atomically decrement the semaphore value and return 0, otherwise it will wait up to timeout milliseconds trying to lock the semaphore. This function is to be avoided if possible since on some platforms it is implemented by polling the semaphore every millisecond in a busy loop.

After SDL_SemWaitTimeout() is successful, the semaphore can be released and its count atomically incremented by a successful call to SDL_SemPost.

Return Value

Returns 0 if the semaphore was successfully locked or either SDL_MUTEX_TIMEOUT or -1 if the timeout period was exceeded or there was an error, respectivly.

If the semaphore was not successfully locked, the semaphore will be unchanged.

Examples

res = SDL_SemWaitTimeout(my_sem, WAIT_TIMEOUT_MILLISEC);

if (res == SDL_MUTEX_TIMEOUT) {
        return TRY_AGAIN;
}
if (res == -1) {
        return WAIT_ERROR;
}

...

SDL_SemPost(my_sem);

See Also

SDL_CreateSemaphore, SDL_DestroySemaphore, SDL_SemWait, SDL_SemTryWait, SDL_SemPost, SDL_SemValue

Prev | Home | Next
SDL_SemTryWait | Up | SDL_SemPost


SDL_SemPost

Name

SDL_SemPost -- Unlock a semaphore.

Synopsis

#include "SDL.h"
#include "SDL_thread.h"

int SDL_SemPost(SDL_sem *sem);

Description

SDL_SemPost unlocks the semaphore pointed to by sem and atomically increments the semaphores value. Threads that were blocking on the semaphore may be scheduled after this call succeeds.

SDL_SemPost should be called after a semaphore is locked by a successful call to SDL_SemWait, SDL_SemTryWait or SDL_SemWaitTimeout.

Return Value

Returns 0 if successful or -1 if there was an error (leaving the semaphore unchanged).

Examples

SDL_SemPost(my_sem);

See Also

SDL_CreateSemaphore, SDL_DestroySemaphore, SDL_SemWait, SDL_SemTryWait, SDL_SemWaitTimeout, SDL_SemValue

Prev | Home | Next
SDL_SemWaitTimeout | Up | SDL_SemValue


SDL_SemValue

Name

SDL_SemValue -- Return the current value of a semaphore.

Synopsis

#include "SDL.h"
#include "SDL/SDL_thread.h"

Uint32 SDL_SemValue(SDL_sem *sem);

Description

SDL_SemValue() returns the current semaphore value from the semaphore pointed to by sem.

Return Value

Returns current value of the semaphore.

Examples

  sem_value = SDL_SemValue(my_sem);

See Also

SDL_CreateSemaphore, SDL_DestroySemaphore, SDL_SemWait, SDL_SemTryWait, SDL_SemWaitTimeout, SDL_SemPost

Prev | Home | Next
SDL_SemPost | Up | SDL_CreateCond


SDL_CreateCond

Name

SDL_CreateCond -- Create a condition variable

Synopsis

#include "SDL.h"
#include "SDL_thread.h"

SDL_cond *SDL_CreateCond(void);

Description

Creates a condition variable.

Examples

SDL_cond *cond;

cond=SDL_CreateCond();
.
.
/* Do stuff */

.
.
SDL_DestroyCond(cond);

See Also

SDL_DestroyCond, SDL_CondWait, SDL_CondSignal

Prev | Home | Next
SDL_SemValue | Up | SDL_DestroyCond


SDL_DestroyCond

Name

SDL_DestroyCond -- Destroy a condition variable

Synopsis

#include "SDL.h"
#include "SDL_thread.h"

void SDL_DestroyCond(SDL_cond *cond);

Description

Destroys a condition variable.

See Also

SDL_CreateCond

Prev | Home | Next
SDL_CreateCond | Up | SDL_CondSignal


SDL_CondSignal

Name

SDL_CondSignal -- Restart a thread wait on a condition variable

Synopsis

#include "SDL.h"
#include "SDL_thread.h"

int SDL_CondSignal(SDL_cond *cond);

Description

Restart one of the threads that are waiting on the condition variable, cond. Returns 0 on success of -1 on an error.

See Also

SDL_CondWait, SDL_CondBroadcast

Prev | Home | Next
SDL_DestroyCond | Up | SDL_CondBroadcast


SDL_CondBroadcast

Name

SDL_CondBroadcast -- Restart all threads waiting on a condition variable

Synopsis

#include "SDL.h"
#include "SDL_thread.h"

int SDL_CondBroadcast(SDL_cond *cond);

Description

Restarts all threads that are waiting on the condition variable, cond. Returns 0 on success, or -1 on an error.

See Also

SDL_CondSignal, SDL_CondWait

Prev | Home | Next
SDL_CondSignal | Up | SDL_CondWait


SDL_CondWait

Name

SDL_CondWait -- Wait on a condition variable

Synopsis

#include "SDL.h"
#include "SDL_thread.h"

int SDL_CondWait(SDL_cond *cond, SDL_mutex *mut);

Description

Wait on the condition variable cond and unlock the provided mutex. The mutex must the locked before entering this function. Returns 0 when it is signalled, or -1 on an error.

See Also

SDL_CondWaitTimeout, SDL_CondSignal, SDL_mutexP

Prev | Home | Next
SDL_CondBroadcast | Up | SDL_CondWaitTimeout


SDL_CondWaitTimeout

Name

SDL_CondWaitTimeout -- Wait on a condition variable, with timeout

Synopsis

#include "SDL.h"
#include "SDL_thread.h"

int SDL_CondWaitTimeout(SDL_cond *cond, SDL_mutex *mutex, Uint32 ms);

Description

Wait on the condition variable cond for, at most, ms milliseconds. mut is unlocked so it must be locked when the function is called. Returns SDL_MUTEX_TIMEDOUT if the condition is not signalled in the allotted time, 0 if it was signalled or -1 on an error.

See Also

SDL_CondWait

Prev | Home | Next
SDL_CondWait | Up | Time


Time

SDL Library Documentation
Prev Next

Chapter 13. Time

Table of Contents
SDL_GetTicks — Get the number of milliseconds since the SDL library initialization.
SDL_Delay — Wait a specified number of milliseconds before returning.
SDL_AddTimer — Add a timer which will call a callback after the specified number of milliseconds has elapsed.
SDL_RemoveTimer — Remove a timer which was added with SDL_AddTimer.
SDL_SetTimer — Set a callback to run after the specified number of milliseconds has elapsed.

SDL provides several cross-platform functions for dealing with time. It provides a way to get the current time, a way to wait a little while, and a simple timer mechanism. These functions give you two ways of moving an object every x milliseconds:

  • Use a timer callback function. This may have the bad effect that it runs in a seperate thread or uses alarm signals, but it's easier to implement.

  • Or you can get the number of milliseconds passed, and move the object if, for example, 30 ms passed.

Prev | Home | Next
SDL_CondWaitTimeout | Up | SDL_GetTicks


SDL_GetTicks

Name

SDL_GetTicks -- Get the number of milliseconds since the SDL library initialization.

Synopsis

#include "SDL.h"

Uint32 SDL_GetTicks(void);

Description

Get the number of milliseconds since the SDL library initialization. Note that this value wraps if the program runs for more than ~49 days.

See Also

SDL_Delay

Prev | Home | Next
Time | Up | SDL_Delay


SDL_Delay

Name

SDL_Delay -- Wait a specified number of milliseconds before returning.

Synopsis

#include "SDL.h"

void SDL_Delay(Uint32 ms);

Description

Wait a specified number of milliseconds before returning. SDL_Delay will wait at least the specified time, but possible longer due to OS scheduling.

Note: Count on a delay granularity of at least 10 ms. Some platforms have shorter clock ticks but this is the most common.

See Also

SDL_AddTimer

Prev | Home | Next
SDL_GetTicks | Up | SDL_AddTimer


SDL_AddTimer

Name

SDL_AddTimer -- Add a timer which will call a callback after the specified number of milliseconds has elapsed.

Synopsis

#include "SDL.h"

SDL_TimerID SDL_AddTimer(Uint32 interval, SDL_NewTimerCallback callback, void *param);

Callback

/* type definition for the "new" timer callback function */
typedef Uint32 (*SDL_NewTimerCallback)(Uint32 interval, void *param);

Description

Adds a callback function to be run after the specified number of milliseconds has elapsed. The callback function is passed the current timer interval and the user supplied parameter from the SDL_AddTimer call and returns the next timer interval. If the returned value from the callback is the same as the one passed in, the periodic alarm continues, otherwise a new alarm is scheduled.

To cancel a currently running timer call SDL_RemoveTimer with the timer ID returned from SDL_AddTimer.

The timer callback function may run in a different thread than your main program, and so shouldn't call any functions from within itself. You may always call SDL_PushEvent, however.

The granularity of the timer is platform-dependent, but you should count on it being at least 10 ms as this is the most common number. This means that if you request a 16 ms timer, your callback will run approximately 20 ms later on an unloaded system. If you wanted to set a flag signaling a frame update at 30 frames per second (every 33 ms), you might set a timer for 30 ms (see example below). If you use this function, you need to pass SDL_INIT_TIMER to SDL_Init.

Return Value

Returns an ID value for the added timer or NULL if there was an error.

Examples

my_timer_id = SDL_AddTimer((33/10)*10, my_callbackfunc, my_callback_param);

See Also

SDL_RemoveTimer, SDL_PushEvent

Prev | Home | Next
SDL_Delay | Up | SDL_RemoveTimer


SDL_RemoveTimer

Name

SDL_RemoveTimer -- Remove a timer which was added with SDL_AddTimer.

Synopsis

#include "SDL.h"

SDL_bool SDL_RemoveTimer(SDL_TimerID id);

Description

Removes a timer callback previously added with SDL_AddTimer.

Return Value

Returns a boolean value indicating success.

Examples

SDL_RemoveTimer(my_timer_id);

See Also

SDL_AddTimer

Prev | Home | Next
SDL_AddTimer | Up | SDL_SetTimer


SDL_SetTimer

Name

SDL_SetTimer -- Set a callback to run after the specified number of milliseconds has elapsed.

Synopsis

#include "SDL.h"

int SDL_SetTimer(Uint32 interval, SDL_TimerCallback callback);

Callback

/* Function prototype for the timer callback function */ typedef Uint32 (*SDL_TimerCallback)(Uint32 interval);

Description

Set a callback to run after the specified number of milliseconds has elapsed. The callback function is passed the current timer interval and returns the next timer interval. If the returned value is the same as the one passed in, the periodic alarm continues, otherwise a new alarm is scheduled.

To cancel a currently running timer, call SDL_SetTimer(0, NULL);

The timer callback function may run in a different thread than your main constant, and so shouldn't call any functions from within itself.

The maximum resolution of this timer is 10 ms, which means that if you request a 16 ms timer, your callback will run approximately 20 ms later on an unloaded system. If you wanted to set a flag signaling a frame update at 30 frames per second (every 33 ms), you might set a timer for 30 ms (see example below).

If you use this function, you need to pass SDL_INIT_TIMER to SDL_Init().

Note: This function is kept for compatibility but has been superseded by the new timer functions SDL_AddTimer and SDL_RemoveTimer which support multiple timers.

Examples

SDL_SetTimer((33/10)*10, my_callback);

See Also

SDL_AddTimer

Prev | Home
SDL_RemoveTimer | Up


Exemple

SDL Game Series

  • SDL Game Framework Series
  • SDL Tutorial Basics
  • SDL Coordinates and Bliting
  • SDL Events
  • SDL Tutorial - Tic Tac Toe
  • SDL Animation
  • SDL Entities
  • SDL Maps
  • SDL Collision
  • SDL Collision Events
 b SDL tutoriel en

SDL Tutorials

  • SDL Tutorials
  • SDL Image
  • SDL SoundBank
  • SDL Sokoban
  • SDL Scale Surface
  • SDL Net - Part 1
  • SDL Net - Part 2
  • SDL Per Pixel Collision
  • SDL + OpenGL Tutorial Basics
  • SDL TTF
  • SDL App States


 Tutoriel SDL

SDL Tutorials

Category

SDL App States

Tim Jones | October 19th, 2011 | 28 comments
In this muchly desired side tutorial we are going to look at how to implement something most commonly called Game States (or as I call them, App States). This tutorial will be based upon the code from the SDL Collision Events tutorial.

To simply explain what an app state is, try to think of your game (or application) as a parent with many child applications. In other words, your primary application can yield control to a single child when needed.
SDL Tutorials
App States
Game States

SDL TTF

Alexander Bock | September 8th, 2011 | 12 comments
Text is an important part of almost all games. From informing the player of the awesome combo that they just got, to menu titles, button captions and epic storylines. In this tutorial, we will take a look at SDL_ttf, an addon library for SDL that uses the FreeType library to load fonts and render text.
SDL Tutorials
Fonts

SDL + OpenGL Tutorial Basics

Tim Jones | June 5th, 2010 | 50 comments
In this new tutorial series, we will be looking at using SDL along with OpenGL. SDL as it stands is nice for basic 2D graphics/composition, but when i
SDL Tutorials
SDL + OpenGL Tutorials

SDL Per Pixel Collision

Jason Norris | January 28th, 2010 | 22 comments
Are bounding box and circle collisions just not cutting it? Well, now we're going to dive right down into pixel precision!

Before we start, I'd like to say that this is an advanced SDL tutorial. You should have experience with creating SDL applications and some basic knowledge of getting a program with it's libraries up and running.

First, we'll start off with a very simple Game class to build the foundation of our demo project.
SDL Tutorials

SDL Net - Part 1

Tim Jones | January 7th, 2010 | 5 comments
The following user tutorial was created by Kahshoo-heem, for the purpose of expanding upon the SDLTutorials.com series, and expounding upon the use of
SDL Tutorials

SDL Net - Part 2

Tim Jones | January 7th, 2010 | 12 comments
The following user tutorial was created by Kahshoo-heem, for the purpose of expanding upon the SDLTutorials.com series, and expounding upon the use of
SDL Tutorials

SDL Scale Surface

Tim Jones | July 15th, 2008 | 14 comments
The following user tutorial was created by Mod J., for the purpose of expanding upon the SDLTutorials.com series, and expounding upon the use of SDL.
SDL Tutorials

SDL Sokoban

Tim Jones | June 9th, 2008 | 6 comments
The following user tutorial was created by Arseniy B., for the purpose of expanding upon the SDLTutorials.com series, and expounding upon the use of S
SDL Tutorials

SDL SoundBank

Tim Jones | April 11th, 2008 | 25 comments
In this side tutorial we are going to be adding a soundbank that will load all of our sounds, and then we call play them via an ID whenever we want. T
SDL Tutorials

SDL Image

Tim Jones | March 14th, 2008 | 76 comments
This side tutorial is rather simple, short, and sweet. I am going to show you how to stop using those pesky bitmap (BMP) files that are too big and do
SDL Tutorials
RECENT TUTORIALS

First Foray into SDL/OpenGL

OpenGL handles the drawing of models in a window but it does not provide the window, or handle events; that is why SDL is required. The prime purpose of this tutorial is to set up the SDL window and events mechanism. It also sets up the OpenGL environment that uses the window, the game loop, and a rotating cube.

Huh? My Pong clone uses 100% CPU and is still slow?

Your program is being a CPU hog! In some cases, this situation can actually make your program run slowly! Your code needs to give some time back to the OS (i.e. delay or sleep).

Setting up MonoDevelop to run Tao

A new series from Stephen Jones demonstrating how to use C#, Mono, and Tao to develop SDL / OpenGL games.


Tutoriel SDL

SDL Image
This side tutorial is rather simple, short, and sweet. I am going to show you how to stop using those pesky bitmap (BMP) files that are too big and don't support alpha transparency, and to start using other file formats for you surfaces (I personally like PNG). If you have not read my SDL Coordinates and Bliting tutorial, I encourage you to do so now. We will be building off of that tutorial, modifying the OnLoad function of the CSurface class.

The first thing you need to do is download SDL_image, the latest version, from the main SDL website. You can also download this library from the "Libraries" section of this website, under SDL. If you don't want to worry about finding all these libraries, the SDL package I provide is good for most SDL beginners, as it provides SDL_image, and some other useful libraries. Be sure to put the include files in the same directory as your SDL include files, and your lib files in the same directory as your SDL lib files to make things easier.

Start by opening your project, and going under the linker settings. Add SDL_image after SDLmain, something like:

mingw32
SDLmain
SDL_image
SDL

If you don't remember where to find all these settings, hope on over to the first tutorial SDL Tutorial Basics to get a refresher on linking.

First, open up CSurface.h, so that we can include an extra header file:
#ifndef _CSURFACE_H_
    #define _CSURFACE_H_

#include <SDL.h>
#include <SDL_image.h>
Once that is all done, open up CSurface.cpp and look for the OnLoad function. Currently, this is what our code looks like:
SDL_Surface* CSurface::OnLoad(char* File) {
    SDL_Surface* Surf_Temp = NULL;
    SDL_Surface* Surf_Return = NULL;

    if((Surf_Temp = SDL_LoadBMP(File)) == NULL) {
        return NULL;
    }

    Surf_Return = SDL_DisplayFormat(Surf_Temp);
    SDL_FreeSurface(Surf_Temp);

    return Surf_Return;
}
There are two changes to make here, the first is changing SDL_LoadBMP to IMG_Load. The second is changing SDL_DisplayFormat to SDL_DisplayFormatAlpha. Remember I said BMPs don't support alpha layers? Well, PNGs do! And if you are wanting to keep that alpha layer you'll need to use SDL_DisplayFormatAlpha instead of SDL_DisplayFormat. So, our code should look like this now:
SDL_Surface* CSurface::OnLoad(char* File) {
    SDL_Surface* Surf_Temp = NULL;
    SDL_Surface* Surf_Return = NULL;

    if((Surf_Temp = IMG_Load(File)) == NULL) {
        return NULL;
    }

    Surf_Return = SDL_DisplayFormatAlpha(Surf_Temp);
    SDL_FreeSurface(Surf_Temp);

    return Surf_Return;
}
Finally, don't forget to change the filename that the Test Surface is trying to load in CApp_OnInit.cpp:
if((Surf_Test = CSurface::OnLoad("myimage.png")) == NULL) {
    return false;
}
And we are done! I told you it would be short. Try compiling, and trying out different file formats. Some common formats supported by SDL_image are:

BMP (Go figure)
GIF
PNG
JPG
PCX
TIF
and others...

Please note that there are additional DLL files associated with SDL image that are needed now (about 5 of them). So be sure to include those with your project.

You can download these DLL files here, http://www.libsdl.org/projects/SDL_image/ under Binary -> Win32 -> and the Win32 zip (not the VC8). Also, the DLL files are also included in the project files below.


Tutoriel SDL

SDL SoundBank
In this side tutorial we are going to be adding a soundbank that will load all of our sounds, and then we call play them via an ID whenever we want. This tutorial will only deal with sounds, not music, and mind you it's a very basic tutorial that gets the job done. There is much that can be added to this class, for channels, groups and such, but we're dealing with basics here. We'll base this tutorial off of my SDL Events tutorial. So use those project files if you need something to work off of.

The first thing you need to do is download SDL mixer from the SDL website. It's also included in the SDL library I provide on my website. Be sure to put the include files in the same directory as your SDL include files, and your lib files in the same directory as your SDL lib files to make things easier.

Start by opening your project, and going under the linker settings. Add SDL_mixer after SDLmain, something like:

mingw32
SDLmain
SDL_mixer
SDL

If you don't remember where to find all these settings, hope on over to the first tutorial SDL Tutorial Basics to get a refresher on linking. If you have a .lib file, instead of a .a library file, use the browse button to find the library file that way.

Just a little side note here about sounds. SDL_mixer treats any sound in memory as a chunk. This chunk basically contains the frequency data. What's nice about SDL_mixer is that it takes this data and sends it to the audio output, meaning it has taken care of all the hard work. So all that we have to do is load our data, from a WAV file for instance, and then send it to SDL_mixer to be played.

First, create two new files, CSoundBank.h and CSoundBank.cpp. Open up the header file first:
#ifndef _CSOUNDBANK_H_
#define _CSOUNDBANK_H_
#include <SDL.h>
#include <SDL_mixer.h>
#include <vector>

class CSoundBank {
    public:
        static CSoundBank           SoundControl;

        std::vector<Mix_Chunk*>     SoundList;

    public:
        CSoundBank();

        int OnLoad(char* File);

        void OnCleanup();

    public:
        void Play(int ID);
};

#endif
We have a static control object first, this lets us add sounds and play sounds from anywhere in our program. Think of this as your master control for adding/playing sounds. We then have our SoundList, this is a list of SDL mixer sounds. Now, a Mix_Chunk object contains the information needed to play a sound. What we will do later on is load a wav file into a Mix_Chunk object. Think of it as the sound equivalent to SDL_Surface. Then we have some basic functions for loading, cleanup up, and playing a sound. Simple enough.

Next, open up CSoundBank.cpp:
#include "CSoundBank.h"

CSoundBank CSoundBank::SoundControl;

CSoundBank::CSoundBank() {
}

int CSoundBank::OnLoad(char* File) {
    Mix_Chunk* TempSound = NULL;

    if((TempSound = Mix_LoadWAV(File)) == NULL) {
        return -1;
    }

    SoundList.push_back(TempSound);

    return (SoundList.size() - 1);
}

void CSoundBank::OnCleanup() {
    for(int i = 0;i < SoundList.size();i++) {
        Mix_FreeChunk(SoundList[i]);
    }

    SoundList.clear();
}

void CSoundBank::Play(int ID) {
    if(ID < 0 || ID >= SoundList.size()) return;
    if(SoundList[ID] == NULL) return;

    Mix_PlayChannel(-1, SoundList[ID], 0);
}
This is also pretty straight forward. We have our static control, our empty constructor, and then our loading function. We basically take the filename of the sound to be loaded, and try to load it via Mix_LoadWAV into the Mix_Chunk object. Notice I am making a temporary object first, and then throwing it into the list. At the very end of the function, I return the ID of the last inserted sound. So, for instance, say I loaded SoundA and SoundB:
int SoundA = CSoundBank::SoundControl.OnLoad("sounda.wav");
int SoundB = CSoundBank::SoundControl.OnLoad("soundb.wav");
The ID for playing this sounds later would be returned by the OnLoad function.

Okay, next we have our cleanup function which goes through the list and frees the sound chunks. Simple enough, just like SDL_FreeSurface.

Lastly, we have our Play function that actually plays the sound. We pass an ID (the one we got returned from the Load function) first. We do a little boundary check to make sure the ID is valid, and then actually try to play the sound. The Mix_PlayChannel takes three arguments:

The first specifies which channel to play the sound on, -1 means use the first available.
The second specifies which sound chunk to play.
The last specifies how many times to play this sound (loop). -1 would be infinite.

We haven't done much with channels in this class, but the class is easily changeable to add that functionality. Basically, a channel allows you to set certain settings to certain sounds. For instance, you can set volume or panning on the first channel, so any sounds played on that channel would be affected.

So lets do a little test now. Open up CApp.h and add the header file, and add some variables and functions to the class:
#include "CSoundBank.h"

//...

class CApp {
    public:
        int SoundA;
        int SoundB;

        //... other code

    public:
        void OnKeyDown(SDLKey sym, SDLMod mod, Uint16 unicode);
};
Now, open up CApp_Init.cpp so we can load some sounds:
if((SoundA = CSoundBank::SoundControl.OnLoad("sounda.wav")) == -1) {
    return false;
}

if((SoundB = CSoundBank::SoundControl.OnLoad("soundb.wav")) == -1) {
    return false;
}
Now, to make sounds when we press a key, open up CApp_OnEvent.cpp and add the function:
void CApp::OnKeyDown(SDLKey sym, SDLMod mod, Uint16 unicode) {
    if(sym == SDLK_1) {
        CSoundBank::SoundControl.Play(SoundA);
    }

    if(sym == SDLK_2) {
        CSoundBank::SoundControl.Play(SoundB);
    }
}
We aren't done yet, we have to do a little bit to start SDL_mixer. This is just like starting up SDL, but instead we need to start up SDL_mixer to access sounds. Open up CApp_Init.cpp and add the following somewhere after SDL_Init:
if(Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 4096) < 0) {
    return false;
}
Mix_OpenAudio basically Inits SDL_mixer. This function takes 4 arguments. The first is used to specify the frequency, 44100 is usually a good number. But depending on your wav files, you may have to change this. Another common number is 22050. I won't get into detail about frequencies and such, just leave it alone unless you really need to change it. The next is the format, again, it's best to leave it unless you know much about audio (it's basically the size of the samples used, 8bit or 16bit). The next is the number of channels, 1 = mono, 2 = stereo. Note, these channels are different than the other channels I was talking about before. The other channels are "mixing channels," basically streams that I can push audio through. These channels though, are the number of audio outputs. Think of it like the number of speakers that you might have. 2 speakers, 2 channels. The last number is used to set the size of the samples (in bytes). Again, unless you really know what you are doing, it's best to leave these numbers alone.

To tidy things up, we need to make sure we stop the SDL_mixer service when we are done, as well as freeing any sounds we may have loaded. So open up CApp_OnCleanup.cpp and add the following:
CSoundBank::SoundControl.OnCleanup();

Mix_CloseAudio();
And we are done! Try compiling, and pressing the 1 and 2 keys to here some sounds. If you need some WAV files, download the project files below to grab them.


Tutoriel SDL

SDL Sokoban
The following user tutorial was created by Arseniy B., for the purpose of expanding upon the SDLTutorials.com series, and expounding upon the use of SDL. This tutorial, though not purposely a part of the SDLTutorials.com or created for the series, may be a branch or addition to the series. Please read notes by the author for any additional code and/or framework used by the author. If you wish to submit your own tutorial to this site, please visit the "User Tutorials" page.
IMPORTANT NOTE: I DON'T NAME SURFACES LIKE TIM DOES. WHEN HE CALLS IT Surf_Display, MINE IS SIMPLY SurfDisplay, WITHOUT THE UNDERSCORE. MAKE SURE YOU KNOW THIS, AND ADAPT TO THE TUTORIAL ACCORDINGLY. ALSO, WHEN I MAKE PROTOTYPES IN CLASS DEFINITIONS, THE ONLY STUFF I WRITE IN THE PARAMETERS AREA (I.E. THE PARENTHESES) IS THE TYPE OF DATA TAKEN, NOT THE NAME OF THE DATA TAKEN. THAT NAME IS ONLY IN THE ACTUAL FUNCTION DEFINITION, AT LEAST FOR ME.

This tutorial is based on everything you've learned by now, if you've been following the tutorials in order and have just finished the Maps tutorial. Get ready to get a glimpse of creating your first real game! (Not counting that fabulous tic-tac-toe.) Similarly, the code this comes from is from all the code you've written up to the Maps tutorial, and including it. It would be wise to make a new Code::Blocks project (or a new DevC++ project, or whatever), because this tutorial will delve a different way for a bit, and you don't want to mess up your existing code for the upcoming Collision Detection tutorial. Speaking of that, this tutorial will introduce you to very simple collision detection, on a tile-based board.

If you're all ready, open up your [new] project and let's begin!

First, we need an introduction to Sokoban. For those of you who have never played this unusually fun and addicting puzzle game, here's how it goes. There's a player that you control with the arrow keys, and he pushes boxes on tiles to their 'home' locations. Those locations are special tiles on the board. Once all the boxes are at 'home', the level ends. The challenge, though, is that the boxes are positioned initially in such a way that makes it a puzzle. A box can't be pushed into a wall, or out of a corner, etc., so it becomes harder. That's all there is to it!

Here we go. This tutorial, like the last few, will only instruct you as to where to put the code I write; there will be no full documents, unless they're new. First and foremost, we need to modify CTile. Add another element to the typedef enum statement at the top.

typedef enum {TILE_TYPE_NONE = 0, TILE_TYPE_NORMAL, TILE_TYPE_BLOCK, TILE_TYPE_HOMEOFBLOCK};
You'll see why soon.

Also, we need to make a new function in the CMap class that checks what the TypeID of a given tile is. Add the appropriate prototype in the class definition, and add this function in CMap.cpp:

int CMap::TypeOfTileAt(int X, int Y) {
    int CheckID = (X / TILE_SIZE) + ((Y / TILE_SIZE) * MAP_WIDTH);
    return TileList[CheckID].TypeID;
}
Simple enough.

Sokoban needs blocks. Let's make a block! The CBlock class is going to extend CEntity. We're going to make this new class for the block inherit all the attributes (variables) and methods (functions) of the CEntity class. It also has its own attributes and methods that don't exist in the CEntity class alone. A good example to illustrate this is geometry. The 'parent' class is the Shape class. It has the attributes of area, color, and perimeter. A child is Square, which has those same attributes due to inheritance, but also has side (because it's a square), and Shape doesn't have side because it could be a shape with no straight lines. Circle is also a child, because it inherits area, color, and perimeter, but it has a radius attribute for itself; etc.

So open up two new files, CBlock.h and CBlock.cpp.
#ifndef CBLOCK_H_INCLUDED
#define CBLOCK_H_INCLUDED
#include "CEntity.h"
#include "CArea.h"
#include <vector>

using namespace std;

class CBlock : public CEntity {
    public:
        static vector<CBlock*> BlockList;
        bool OnHomeTile;
        CBlock();
        void CheckForHome();
        void OnMove(int, int);
};

#endif // CBLOCK_H_INCLUDED
And...
#include "CBlock.h"

vector<CBlock*> CBlock::BlockList;

CBlock::CBlock() {
    OnHomeTile = false;
    SurfEntity = NULL;
    X = Y = 0.0f;
    Width = Height = 32;
    AnimState = 0;
}

void CBlock::CheckForHome() {
    if (CArea::AreaControl.MapList[0].TypeOfTileAt(X, Y) == 3) {
        OnHomeTile = true;
    } else {
        OnHomeTile = false;
    }
}

void CBlock::OnMove(int MoveX, int MoveY) {
    X += MoveX;
    Y += MoveY;
}

After the class definition, we have " : public CEntity." This is what tells the compiler that this new class extends and inherits the attributes and methods of the CEntity class. This class, much like the CEntity class, has its own vector of CBlocks. Sure, it makes sense to just use the CEntity::EntityList vector in its place, but we're going to need a vector of just CBlocks (not including the CEntity that will be CPlayer).

Next, CBlock has an attribute that says whether or not it's located on its 'home' tile. The constructor, CBlock(), sets that value to false, and the rest of the values which it inherits from its parent class, CEntity, are set to their default values, as well. The next function, CheckForHome(), sets the OnHomeTile boolean value to true, if the TypeID of the tile under the block is TILE_TYPE_HOMEOFBLOCK. Otherwise, it's false. There isn't a return statement because this function just deals with CBlock's personal variables. The final function, OnMove, is very simple. It takes in two integer variables, MoveX and MoveY, and increments the block's X and the block's Y by those amounts, respectively. (Remember, even though the class didn't set an X or a Y variable, CBlock inherits those two from CEntity, so they're allowed to be manipulated. And they exist.)

Next, we need to implement a player, in kinda the same way.

CPlayer, a child of CEntity, will inherit every one of its attributes and methods, and we will define our own and maybe even override the inherited methods. Make two new files, CPlayer.cpp and CPlayer.h, and open up CPlayer.h.

#ifndef CPLAYER_H_INCLUDED
#define CPLAYER_H_INCLUDED
#include "CEntity.h"
#include "CBlock.h"
#include "CArea.h"

typedef enum {YES = 0, NO, BLOCK};

class CPlayer : public CEntity {
    public:
        CPlayer();
        void OnMove(int MoveX, int MoveY);

        int CanPlayerMoveLeft();
        int CanPlayerMoveUp();
        int CanPlayerMoveRight();

        int CanPlayerMoveDown();
};

#endif CPLAYER_H_INCLUDED
Then, open CPlayer.cpp and let's define the functions.

#include "CPlayer.h"

CPlayer::CPlayer() {
    SurfEntity = NULL;
    X = Y = 0.0f;
    Width = Height = 0;
    AnimState = 0;
}

void CPlayer::OnMove(int MoveX, int MoveY) {
    X += MoveX;
    Y += MoveY;
}

int CPlayer::CanPlayerMoveLeft() {
    if (CArea::AreaControl.MapList[0].TypeOfTileAt(X - 32, Y) == 2) return NO;

    for (int i = 0; i < CBlock::BlockList.size(); i++) {
        if (CBlock::BlockList[i]->X == X - 32 && CBlock::BlockList[i]->Y == Y) {
            if (CArea::AreaControl.MapList[0].TypeOfTileAt(X - 64, Y) == 2) return NO;
            for (int k = 0; k < CBlock::BlockList.size(); k++) {
                if (CBlock::BlockList[k]->Y == Y && CBlock::BlockList[k]->X == X - 64) return NO;
            }

            return BLOCK;
        }
    }

    return YES;
}

int CPlayer::CanPlayerMoveUp() {
    if (CArea::AreaControl.MapList[0].TypeOfTileAt(X, Y - 32) == 2) return NO;

    for (int i = 0; i < CBlock::BlockList.size(); i++) {
        if (CBlock::BlockList[i]->Y == Y - 32 && CBlock::BlockList[i]->X == X) {
            if (CArea::AreaControl.MapList[0].TypeOfTileAt(X, Y - 64) == 2) return NO;
            for (int k = 0; k < CBlock::BlockList.size(); k++) {
                if (CBlock::BlockList[k]->Y == Y - 64 && CBlock::BlockList[k]->X == X) return NO;
            }
            return BLOCK;
        }
    }

    return YES;
}

int CPlayer::CanPlayerMoveRight() {
    if (CArea::AreaControl.MapList[0].TypeOfTileAt(X + 32, Y) == 2) return NO;

    for (int i = 0; i < CBlock::BlockList.size(); i++) {
        if (CBlock::BlockList[i]->X == X + 32 && CBlock::BlockList[i]->Y == Y) {
            if (CArea::AreaControl.MapList[0].TypeOfTileAt(X + 64, Y) == 2) return NO;
            for (int k = 0; k < CBlock::BlockList.size(); k++) {
                if (CBlock::BlockList[k]->Y == Y && CBlock::BlockList[k]->X == X + 64) return NO;
            }
            return BLOCK;
        }
    }

    return YES;
}

int CPlayer::CanPlayerMoveDown() {
    if (CArea::AreaControl.MapList[0].TypeOfTileAt(X, Y + 32) == 2) return NO;

    for (int i = 0; i < CBlock::BlockList.size(); i++) {
        if (CBlock::BlockList[i]->Y == Y + 32 && CBlock::BlockList[i]->X == X) {
            if (CArea::AreaControl.MapList[0].TypeOfTileAt(X, Y + 64) == 2) return NO;
            for (int k = 0; k < CBlock::BlockList.size(); k++) {
                if (CBlock::BlockList[k]->Y == Y + 64 && CBlock::BlockList[k]->X == X) return NO;
            }
            return BLOCK;
        }
    }

    return YES;
}
A little explanation. Just like with CBlock, " : public CEntity" is to extend the CEntity class.

In the CPlayer class, we're defining five new functions, OnMove and the four "can he move in a certain direction" functions. OnMove is simple: two integers are passed, and the CPlayer's X and Y coordinates increment by exactly that much. (Remember, CPlayer has the X and Y attributes, even though they weren't defined in CPlayer.h, because he INHERITED them from CEntity.) Here's where it gets a little trickier. Those four other functions are the collision detection functions. They check whether the player can move left, up, right, or down, and return what the result of the check was. So let's examine CanPlayerMoveLeft() closely.

First, look at the top of CPlayer.h. The typedef enum statement, like others before, makes YES equal to 0, NO equal to 1, and BLOCK equal to 2; this is because it's much easier to remember YES, NO, and BLOCK, than it is to remember that 0 is the same thing as YES, etc.

The function first checks if the TypeID of the tile to the left of the player is TILE_TYPE_BLOCK. If so, then the function returns NO, meaning the player cannot move left (because a wall is blocking his movement). Then, the function checks if there's a block there. It does this by looping through every one of the CBlocks in the static BlockList (which are automatically added when the CBlocks are loaded...we'll get to that a bit later). Every time it loops, it checks the X and Y coordinates of the block, and if a block is to the left of the player, then the function returns BLOCK, which will tell the program that the player has a block to the left of him. There are two caveats, though. First, if there is a block to the left of the player, but there is also a TILE_TYPE_BLOCK (i.e. a wall) to the left of that block, the player cannot move, and the function returns NO. Similarly, second, if there are two blocks to the left of him, the player can't move (he's not THAT strong!), and the function returns NO. Otherwise, it returns BLOCK. However, if NONE of those are true, then the function returns a simple YES, because the player has a free spot to his left.

That repeats for each direction. The purpose of this function is so that, when the OnKeyDown() function is called (in CApp), before moving the character we test if he can move. That said, let's move on to the implementation of these functions.

We need a way to load the blocks. Just like with the maps and areas, we're going to have a directory next to your exe called blocks, with (so far) one file in it: 1.blocks. Make that file, position it accordingly in the directory tree, and open it. Our first level will have blocks at these coordinates:

(160, 128) (160, 192) (224, 160) (224, 192) (160, 288) (64, 288)

Make two more new files called CBlockControl.h and CBlockControl.cpp, and open up the header.
#ifndef CBLOCKCONTROL_H_INCLUDED
#define CBLOCKCONTROL_H_INCLUDED
#include "CBlock.h"
#include <vector>

using namespace std;

class CBlockControl {
    public:
        static vector<CBlock> BlockList;
        static bool LoadBlocksFromFile(char*);
};

#endif // CBLOCKCONTROL_H_INCLUDED
And...
#include "CBlockControl.h"

vector<CBlock> CBlockControl::BlockList;

bool CBlockControl::LoadBlocksFromFile(char* File) {
    FILE* FileHandle = fopen(File, "r");

    if (FileHandle == NULL) {
        return false;
    }

    int BlockCount;

    fscanf(FileHandle, "%d\n", &BlockCount);

    for (int i = 0; i < BlockCount; i++) {
        CBlock TmpBlock;
        int X, Y;
        fscanf(FileHandle, "%d ", &X);
        fscanf(FileHandle, "%d\n", &Y);
        TmpBlock.X = X;
        TmpBlock.Y = Y;
        TmpBlock.OnLoad("gfx/block.png", 32, 32, 1, false, 64, 64, 0);
        CBlockControl::BlockList.push_back(TmpBlock);
    }

    fclose(FileHandle);

    return true;
}
This is a class that will load every one of the blocks and their locations from a text file (namely, blocks/x.blocks, where x is the level number). When the static function is called, it takes in a char array. As we've done before with Tim's tutorials, it reads that file. That file contains a number (in level one's case, 6), followed by a next-line/return character. That number is stored in the integer BlockCount. Then, the function loops through every one of the blocks in the file (with the for loop), and loads their respective X and Y coordinates. Inside the for loop, a TmpBlock (TeMPoraryBLOCK) is created and its X and Y coordinates (attributes set by CEntity, and inherited by the CBlock class, remember) are set as the values in the X and Y integers in the for loop. Then, the TmpBlock is inserted into CBlockControl's static vector of CBlocks (CBlockControl::BlockList).

So, it seems that now we have everything needed to actually implement everything we've just written; to bring it all together and make a level. Well, first we have to accomodate for one slight change. There is no longer a CCamera. We don't need one as the map can't get bigger than the one level. So we have to change something in CArea. Remember the function CArea::OnRender(SDL_Surface* SurfDisplay, int CameraX, int CameraY)? Well since we don't need a camera anymore, we don't need that function anymore. But don't toss it! Who knows, we might later need the function for any additions you decide to make. So let's make a new function:

void CArea::OnRender(SDL_Surface* SurfDisplay);

Add the prototype to the class definition (make sure it's public), and write the function in CArea.cpp:

void CArea::OnRender(SDL_Surface* SurfDisplay) {
    MapList[0].OnRender(SurfDisplay, 0, 0);
}
We now have two functions with the same name, but they have different parameters, which is allowed. It does the same thing, except there's one map to render so there's no need to figure out which is the first map to render. Duh.

Finally, open up CApp. We're gonna put this thing together. In CApp.h, add the following things:

private:
    SDL_Surface* SurfBackground;
    CPlayer ActionJackson;
    int Moves;
public:
    void OnKeyDown(SDLKey, SDLMod, Uint16);
Remember, this isn't the whole thing; this is just what you need to add in the appropriate places. I think you already have the new function, but make sure it's there, in any case.

So then, in CApp.cpp... In the constructor, CApp::CApp(), add statements that define Moves as 0, and SurfBackground as NULL (like SurfDisplay). In CApp::OnInit(), we need to almost completely rewrite it:

bool CApp::OnInit() {
    if(SDL_Init(SDL_INIT_EVERYTHING) < 0) {
        return false;
    }

    if ((SurfDisplay = SDL_SetVideoMode(WWIDTH, WHEIGHT, 32, SDL_HWSURFACE | SDL_DOUBLEBUF)) == NULL) {
        return false;
    }

    if ((SurfBackground = CSurface::OnLoad("gfx/background.png")) == false) {
        return false;
    }

    if (CArea::AreaControl.OnLoad("maps/1.area") == false) {
        return false;
    }

    if (ActionJackson.OnLoad("gfx/actionjackson.png", 32, 32, 1, false, 64, 64, 0) == false) {
        return false;
    }

    CEntity::EntityList.push_back(&ActionJackson);

    if (CBlockControl::LoadBlocksFromFile("blocks/1.blocks") == false) {
        return false;
    }

    for (int i = 0; i < CBlockControl::BlockList.size(); i++) {
        CBlock::BlockList.push_back(&CBlockControl::BlockList[i]);
        CEntity::EntityList.push_back(&CBlockControl::BlockList[i]);
    }

    ActionJackson.X = 352;
    ActionJackson.Y = 320;

    return true;
}

Let's talk about this. The first two if's start SDL, like we know already. The third is just something I thought would look nice. I have this cool red graphic that I made, and I put it in gfx, and called it background.png. This statement uses our CSurface function OnLoad and puts it on SurfBackground. Next, an area is loaded. I'll get to what's in that area file in a second. The next statement loads the player. Remember, CPlayer inherits everything from CEntity, and since CEntity had an OnLoad function that loaded the entity from an image file, so does CPlayer. And thus, ActionJackon (which we declared in the class definition) has it. NOTE: MY PARAMETERS MAY BE DIFFERENT FROM YOURS, SO CHANGE THEM AS IS NECESSARY. THERE'S NO NEED FOR TRANSPARENCY, AT LEAST NOT YET, SO JUST REMEMBER THAT.

ActionJackson is pushed back in the static EntityList vector, and then CBlockControl loads all the blocks from a file. Remember, it's static, so we can run it without having declared an object/instance of CBlockControl. Then, like I promised WAAAAAY back, CBlock's static BlockList vector would get those blocks. And, like we need to do and like we did for ActionJackson, we need to push the blocks back in EntityList, too. Finally, we just need to set initial X and Y coordinates for ActionJackson, and that's all for OnInit().

Go to OnRender(). Rewrite it as this:

void CApp::OnRender() {
    CSurface::OnDraw(SurfDisplay, SurfBackground, 0, 0);

    CArea::AreaControl.OnRender(SurfDisplay);

    for (int i = 0; i < CEntity::EntityList.size(); i++) {
        if (!CEntity::EntityList[i]) continue;
        CEntity::EntityList[i]->OnRender(SurfDisplay);
    }

    SDL_Flip(SurfDisplay);
}
Since we loaded a background to SurfBackground, we have to draw SurfBackground on SurfDisplay. And this MUST be the first render statement because otherwise, the background will be drawn OVER the other things. Also, the new function we created for CArea is called, taking only the filename and no camera coordinates (since we no longer need a camera).

Finally, the meat of the game. Go to CApp::OnKeyDown(SDLKey sym, SDLMod mod, Uint16 unicode). Erase all that stuff involving camera movement, and rewrite the function as this:

void CApp::OnKeyDown(SDLKey sym, SDLMod mod, Uint16 unicode) {
    switch(sym) {
        case SDLK_UP:
            if (ActionJackson.CanPlayerMoveUp() == BLOCK) {
                for (int i = 0; i < CBlock::BlockList.size(); i++) {
                    if (CBlock::BlockList[i]->X == ActionJackson.X && CBlock::BlockList[i]->Y == ActionJackson.Y - 32) {
                        CBlock::BlockList[i]->OnMove(0, -32);
                    }
                }
                ActionJackson.OnMove(0, -32);
                Moves++;
            }
            if (ActionJackson.CanPlayerMoveUp() == YES) {
                ActionJackson.OnMove(0, -32);
                Moves++;
            }
            break;

        case SDLK_DOWN:
            if (ActionJackson.CanPlayerMoveDown() == BLOCK) {
                for (int i = 0; i < CBlock::BlockList.size(); i++) {
                    if (CBlock::BlockList[i]->X == ActionJackson.X && CBlock::BlockList[i]->Y == ActionJackson.Y + 32) {
                        CBlock::BlockList[i]->OnMove(0, 32);
                    }
                }
                ActionJackson.OnMove(0, 32);
                Moves++;
            }
            if (ActionJackson.CanPlayerMoveDown() == YES) {
                ActionJackson.OnMove(0, 32);
                Moves++;
            }
            break;

        case SDLK_LEFT:
            if (ActionJackson.CanPlayerMoveLeft() == BLOCK) {
                for (int i = 0; i < CBlock::BlockList.size(); i++) {
                    if (CBlock::BlockList[i]->X == ActionJackson.X - 32 && CBlock::BlockList[i]->Y == ActionJackson.Y) {
                        CBlock::BlockList[i]->OnMove(-32, 0);
                    }
                }
                ActionJackson.OnMove(-32, 0);
                Moves++;
            }
            if (ActionJackson.CanPlayerMoveLeft() == YES) {
                ActionJackson.OnMove(-32, 0);
                Moves++;
            }
            break;

        case SDLK_RIGHT:
            if (ActionJackson.CanPlayerMoveRight() == BLOCK) {
                for (int i = 0; i < CBlock::BlockList.size(); i++) {
                    if (CBlock::BlockList[i]->X == ActionJackson.X + 32 && CBlock::BlockList[i]->Y == ActionJackson.Y) {
                        CBlock::BlockList[i]->OnMove(32, 0);
                    }
                }
                ActionJackson.OnMove(32, 0);
                Moves++;
            }
            if (ActionJackson.CanPlayerMoveRight() == YES) {
                ActionJackson.OnMove(32, 0);
                Moves++;
            }
            break;

        case SDLK_ESCAPE:
            Running = false;
            break;

        default: break;
    }
}
WOW. Let's go through that bunch of code. The same switch statement remains as before, but the inside is different. Let's take just the case of SDLK_UP. If the UP key is pressed: First, the function checks if the player can move, and that is done by calling the CanPlayerMove...() function and checking its return value. If there is a block (i.e. if CanPlayerMove...() returns BLOCK), then a for loop is run. For every block in CBlock's static BlockList vector, there's an if statement that checks if that block is the block that is next to the player. Otherwise, this function could just move any random block up, and that would obviously not be good. So this just figures out which block is next to the player. Once that's figured out, that block moves up, and the for loop continues. It's not going to return to the same block again, so it just keeps running until it's out of options. Then, ActionJackson moves up, sort of "following" the block. And the Move count increments up by one.

If ActionJackson.CanPlayerMove...() doesn't return BLOCK, none of that happens. The next if statement checks if that function returned YES. In this case, ActionJackson very simply moves up, not disturbing anything else, and the Move count increments up by one. Finally, if CanPlayerMove...() returns NO, nothing happens, so when up is pressed and the player can't move up, he DOESN'T.

This repeats for every arrow key. There we go! The game works! Now let's just make a use for CBlock's boolean attribute, OnHomeTile. Go to CApp::OnLoop(), and rewrite it as this:

void CApp::OnLoop() {
    for (int i = 0; i < CEntity::EntityList.size(); i++) {
        if (!CEntity::EntityList[i]) continue;
        CEntity::EntityList[i]->OnLoop();
    }

    bool BlocksAreHome = true;

    for (int i = 0; i < CBlock::BlockList.size(); i++) {
        CBlock::BlockList[i]->CheckForHome();
        if (BlocksAreHome && !CBlock::BlockList[i]->OnHomeTile) {
            BlocksAreHome = false;
        } else { continue; }
    }

    if (BlocksAreHome) {
        Running = false;
    }
}
After the existing first for loop, a new boolean value is created. By default, BlocksAreHome is set to true, but that changes immediately if it isn't true. The next for loop runs through every block in CBlock's BlockList vector. Each of the blocks the loop runs through calls its CheckForHome() function, and accordingly sets its OnHomeTile boolean to true/false. Then, the block in the for loop is checked to be on its home tile with the if statement. If it's not on its home tile, BlocksAreHome is set to false. Otherwise, the for loop continues, leaving BlocksAreHome true. Finally, after the for loop, if BlocksAreHome is true, Running is set to false, and the game ends. This is just all I've written for the end of the level; feel free to add your own end-of-game stuff. Now the game is perfect. All you need to do is make the area, map, blocks, and graphics files. Thankfully, they're all packaged below. Also, in Define.h, set TILE_SIZE to 32, MAP_WIDTH to 20, and MAP_HEIGHT to 15.

The directory tree should be as follows (numbered by sublevel):
1. Sokoban (directory)
2. blocks (directory) gfx (directory), maps (directory), tilesets (directory).

In the blocks directory, put the files in the following .zip archive:
MediaFire - blocks.zip

In the gfx directory, put the files in the following .zip archive:
MediaFire - gfx.zip

In the maps directory, put the files in the following .zip archive:
MediaFire - maps.zip

In the tilesets directory, put the files in the following .zip archive:
MediaFire - tilesets.zip

COPYRIGHT:
Copyright 2008 Arseniy Banayev

This may be not be reproduced under any circumstances except for personal, private use. It may not be placed on any web site or otherwise distributed publicly without advance written permission. Use of this guide on any other web site or as a part of any public display is strictly prohibited, and a violation of copyright. This guide is only to be used on SDLTutorials.com, and only by its host, Tim Jones. If found anywhere else, the proper web law enforcement authorities will be notified for violating copyright.

All trademarks and copyrights contained in this document are owned by their respective trademark and copyright holders.

If you have any questions, comments, or misinformation concerning this guide, please leave comments or, in the rarest case, email me at xboteb13@gmail.com. Any electronic mail that I judge to be spam before opening the contents will be deleted automatically. Any malicious software attached to the electronic mail will not only be ignored and pointless, thanks to Ubuntu's security, but will be promptly reported to the appropriate law enforcement authorities and legal partners.

DISCLAIMER:
I will not be held responsible for any damages, reversible or irreversible, to your operating system, personal files, applications, or the physical computer itself, brought upon as a result of compiling and executing the given code or opening the provided files in the above packages.


Tutoriel SDL

SDL Scale Surface
The following user tutorial was created by Mod J., for the purpose of expanding upon the SDLTutorials.com series, and expounding upon the use of SDL. This tutorial, though not purposely a part of the SDLTutorials.com or created for the series, may be a branch or addition to the series. Please read notes by the author for any additional code and/or framework used by the author. If you wish to submit your own tutorial to this site, please visit the "User Tutorials" page.
Today we're going to learn something about extending the capabilities of SDL_Surfaces. If we take a look at OpenGL, what happens when we load a Surface and we render it specifying a width and height bigger than the Surface's original width and height? It gets automatically scaled (unless you specify you want to be repeated). In SDL there is no automatic way of scaling surfaces, so we're going to make a (relatively simple) routine that does the trick.

First, the prototype of the function:
SDL_Surface* SDL_ScaleSurface(SDL_Surface* Surface, Uint16 Width, Uint16 Height);
What are these parameters? Well:
Surface: The original surface the caller of the function wants to be scaled.
Width: The width the caller wants the scaled surface to be (we use Uint16 because this is SDL's standard for width).
Height: The height the caller wants the scaled surface to be (we use Uint16 because this is SDL's standard for height).

Got that? If you didn't, try reading it over again; if you did, then let's move on.

First, we have to check if the Surface's width and height are valid; this is simple to accomplish:
SDL_Surface* SDL_ScaleSurface(SDL_Surface* Surface, Uint16 Width, Uint16 Height)
{
    if(!Surface || !Width || !Height)
        return 0;
}
We don't want a null pointer specified, since it will make BAD things happen when we try to read data from the surface. Secondly, a surface with 0 width or 0 height wouldn't be a good idea, so let's just return a null pointer when either of these things happen.

Now we need to create a surface that suits our needs; we'll do this using the SDL_CreateRGBSurface function:
SDL_Surface* SDL_ScaleSurface(SDL_Surface* Surface, Uint16 Width, Uint16 Height)
{
    SDL_Surface *_ret = SDL_CreateRGBSurface(Surface->flags, Width, Height, Surface->format->BitsPerPixel,
        Surface->format->Rmask, Surface->format->Gmask, Surface->format->Bmask, Surface->format->Amask);
}
Here we're declaring and defining the return surface. We want its specifications to be the same as the surface we got from the caller, so we do so, and then the width and the height should be the same as the ones the caller specified. Looks pretty simple to me ;-).

Now comes the math part. We need to calculate the scaling (or stretching) factor to be applied when we copy the surface's pixels. It's not as hard as it seems, we just divide the width the caller wants by the width of the surface, what have we calculated then? If you calculate the scaling factor for the widths, you'll get the number of pixels you need for each individual source pixel, in other words, each pixel you read from the source surface has to be drawn SCALING_FACTOR_X times over the width in order to stretch it (the same goes for the height).

Calculations:
SDL_Surface* SDL_ScaleSurface(SDL_Surface* Surface, Uint16 Width, Uint16 Height)
{
    double    _stretch_factor_x = (static_cast<double>(Width)  / static_cast<double>(Surface->w)),
        _stretch_factor_y = (static_cast<double>(Height) / static_cast<double>(Surface->h));
}
Casting might be bad to some of you, but it's necessary here since we need the scaling factors as a double. This probably could have been done better, like checking for w = 0 and h = 0, but this is just a simple tutorial ;-). We're just doing here what I explained before.

Now we need to do the actual drawing. This might look overwhelming, but it's not as hard as it seems. We're just going to make 2 for-loops to run across all the pixels on the source surface, and then 2 more loops for drawing STRETCHING_FACTOR pixels for each pixel that gets read from the source surface.

Here we go!
SDL_Surface* SDL_ScaleSurface(SDL_Surface *Surface, Uint16 Width, Uint16 Height)
{
    for(Sint32 y = 0; y < Surface->h; y++) //Run across all Y pixels.
        for(Sint32 x = 0; x < Surface->w; x++) //Run across all X pixels.
            for(Sint32 o_y = 0; o_y < _stretch_factor_y; ++o_y) //Draw _stretch_factor_y pixels for each Y pixel.
                for(Sint32 o_x = 0; o_x < _stretch_factor_x; ++o_x) //Draw _stretch_factor_x pixels for each X pixel.
                    DrawPixel(_ret, static_cast<Sint32>(_stretch_factor_x * x) + o_x,
                        static_cast<Sint32>(_stretch_factor_y * y) + o_y, ReadPixel(Surface, x, y));
}
I'm not going to specify the DrawPixel and ReadPixel functions here, as they can be gotten from the SDL documentation wiki (http://www.libsdl.org/cgi/docwiki.cgi/Pixel_Access), though my prototype might be a little different, they work like this:
void DrawPixel(SDL_Surface* Target, Sint16 X, Sint16 Y, Uint16 Color); //Draws the specified color at the specified X and Y on the target surface.
Uint32 ReadPixel(SDL_Surface* Source, Sint16 X, Sint16 Y); //Returns the color of the specified surface on the specified X and Y coordinates.
The principle is pretty easy:
for(each_source_pixel) -> draw(scaling_factor_amount_of_pixels_on_the_return_value)
The DrawPixel call might look weird to you, but actually what happens inside is this: Draw a pixel on(the return value, stretching_factor_x * source_x because the destination moves each time and then blit the amount of pixels we need, same for y, what color do we need? The color that's on the source surface at the x and y position, because we need to blit it scaling_factor amount of times).

Why the strange X and Y coordinates? Well, imagine we need to make the width twice as bug, look at this crappy drawing:
Source surface:
---------------------------------------------
|x,y       |0,0       |1,0       |2,0       |
---------------------------------------------

Destination:
---------------------------------------------
|x,y       |0,0 & 1,0 |2,0 & 3,0 |4,0 & 5,0 |
---------------------------------------------
See? When at pixel (0,0) we need to draw a pixel at (0,0) and (1,0) because we need 2 pixels on the destination for each 1 pixel on the source (following the X-axis because it's the width), the same goes for the Y. We're actually just skipping what we've already drawn, _stretch_factor_x * x is actually 'Move to the corresponding pixel on the destination. It makes the (2,0) on the drawing say that it should begin its drawing at (4,0) on the destination, then it loops o_x untill it's smaller than 2, drawing first at (4,0) and then when o_x increments at (5,0). It's THAT easy.

Finally we return the desired surface:
SDL_Surface *SDL_ScaleSurface(SDL_Surface *Surface, Uint16 Width, Uint16 Height)
{
    return _ret;
}
Here an overview of the full function:
SDL_Surface *ScaleSurface(SDL_Surface *Surface, Uint16 Width, Uint16 Height)
{
    if(!Surface || !Width || !Height)
        return 0;

    SDL_Surface *_ret = SDL_CreateRGBSurface(Surface->flags, Width, Height, Surface->format->BitsPerPixel,
        Surface->format->Rmask, Surface->format->Gmask, Surface->format->Bmask, Surface->format->Amask);

    double    _stretch_factor_x = (static_cast<double>(Width)  / static_cast<double>(Surface->w)),
        _stretch_factor_y = (static_cast<double>(Height) / static_cast<double>(Surface->h));

    for(Sint32 y = 0; y < Surface->h; y++)
        for(Sint32 x = 0; x < Surface->w; x++)
            for(Sint32 o_y = 0; o_y < _stretch_factor_y; ++o_y)
                for(Sint32 o_x = 0; o_x < _stretch_factor_x; ++o_x)
                    DrawPixel(_ret, static_cast<Sint32>(_stretch_factor_x * x) + o_x,
                        static_cast<Sint32>(_stretch_factor_y * y) + o_y, ReadPixel(Surface, x, y));

    return _ret;
}
I hope you learned something with this tutorial, maybe I'll write more tutorials in the future (perhaps something on texture repeating? ;-)).

See ya!
Creature


Tutoriel SDL

SDL Net - Part 1

User Tutorial
The following user tutorial was created by Kahshoo-heem, for the purpose of expanding upon the SDLTutorials.com series, and expounding upon the use of SDL. This tutorial, though not purposely a part of the SDLTutorials.com or created for the series, may be a branch or addition to the series. Please read notes by the author for any additional code and/or framework used by the author. If you wish to submit your own tutorial to this site, please visit the "User Tutorials" page.

In this tutorial, I will show you a simple c++ library to add network resources to your game.

Firstly, we have to add the SDL_net headers and libraries to your desktop environment. If you are using a linux distro like Ubuntu, it is enough to download the libsdl-netx.x-dev package from the repositories. If not, the process is analogous of including sdl mixer, image, etc, better explained by Tim's Tutorials. Don't forget, as well, to set the linker options in your IDE, adding the SDL_net library.

So, let's create our two files, CNet.h and CNet.cpp, that will build our framework that we'll help us at making network games. It's necessary to say that this library, for the time being, only works with TCP/IP protocol.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <string>
#include <cstring>
#include "SDL_net.h"

typedef char charbuf [256];

class CNet{
  public:
    static bool Init(); //Initialize SDL_net
    static void Quit(); //Exit SDL_net
};
Here is our header file, and, after the includes and the charbuf declaration, our first class. Its aim is initialize and end the network environment, with two static functions. However, they are unnecessary, if we're using SDL_Init(SDL_INIT_EVERYTHING).
class CNetMessage {
  protected:
    charbuf buffer; //message stored in buffer, limited to 256 bytes
    enum bufstates { //message states
    EMPTY,
    READING,
    WRITING,
    FULL };
    bufstates state; //the message state
    void reset(); // reset message: fulfill it with zeroes and change its state to EMPTY
  public:
    CNetMessage(); //constructor
//Virtual member function that indicates how many bytes may have to be loaded into the instance. By default, it works with strings that have a maximum
//of 255 characters.
    virtual int NumToLoad();

//Virtual member function that indicates how many bytes may have to be downloaded from the instance.
    virtual int NumToUnLoad();

    void LoadBytes(charbuf& inputbuffer, int n); //Load a char set into the message buffer
    void UnLoadBytes(charbuf& destbuffer); //Unload a char set from the buffer
    void finish(); //set the state object to full. No more data to be loaded
};
The class above provides the needed interface for the application to deal with the packets sent through the network. It's limited to a maximum of 256 bytes.

Regardless this class uses a buffer with a fixed length, the packets sent through the network can be much more smaller, according to the program's needs. In our TIC TAC TOE game, only one byte is sent. To work with this customization, we put some message states, to help the framework user to build a derived class.

The LoadBytes and UnloadBytes member functions load and unload in a buffer the wanted chars. They were determined by the virtual functions NumToLoad and NumToUnload, each of which indicates how many bytes have to be loaded or unloaded. By default, this class is able to work with strings that have a maximum of 255 characters. If you want to work with other data structures, as we'll do at the game making, you may override this member function.

Last, the finish() member function, which sets the class ready to be used. So, we can send through the socket, in the same packet, a message with two different data structures, avoiding unnecessary network pollution. Below, the implementation code of this and the latter classes:
#include "CNet.h"

bool CNet::Init() {
    if (SDLNet_Init() < 0) {
        std::cerr << "SDLNet_Init: " << SDLNet_GetError() << std::endl;
          return false;
        }
        else
            return true;
}

void CNet::Quit() {
    SDLNet_Quit();
}

CNetMessage::CNetMessage() {
    reset();
}

void CNetMessage::reset() {
    for (int i =0; i < 256; i++)
        buffer [i] = 0;
    state = EMPTY;
}

void CNetMessage::finish() {
    if (state == READING)
        state = FULL;
}

int CNetMessage::NumToLoad() {
    if (state == EMPTY)
        return 255;
    else
        return 0;
}

int CNetMessage::NumToUnLoad() {
    if (state == FULL)
        return strlen(buffer) + 1;
    else
        return 0;
}

void CNetMessage::LoadBytes(charbuf& inputbuffer, int n) {
    for (int i = 0; i < n; i++)
        buffer[i] = inputbuffer[i];
    state = READING;
}

void CNetMessage::UnLoadBytes(charbuf& destbuffer) {
    for (int i=0; i < this->NumToUnLoad(); i++)
        destbuffer[i] = buffer[i];
    reset();
}
Now, back to the header file. Let's take a look at the CIpAddress class:
class CIpAddress {
  private:
    IPaddress m_Ip; //the IPaddress structure
  public:
    CIpAddress(); //constructor
    CIpAddress (Uint16 port); //create and associate a port to the instance
    CIpAddress (std::string host, Uint16 port); //create and associate a port and a host to the instance
    void SetIp (IPaddress sdl_ip); //set a CIpAddress object from an existing SDL IPaddress
    bool Ok() const; //True if the object have a port and a host associated to it
    IPaddress GetIpAddress() const; //return a SDL_net IPaddress structure
    Uint32 GetHost() const; //return the host
    Uint16 GetPort() const; //return the port
};
Basically, that is a C++ interface for the SDL NET structure IPaddress. It has informations about the host IP address, and the port which the program is associated to. Here is the definition of the class members, at CNet.cpp:
CIpAddress::CIpAddress (Uint16 port) {
    if (SDLNet_ResolveHost(&m_Ip, NULL, port) < 0){
        std::cerr << "SDLNet_ResolveHost: " << SDLNet_GetError() << std::endl;
        m_Ip.host = 0;
        m_Ip.port = 0;
    }
}

CIpAddress::CIpAddress (std::string host, Uint16 port) {
    if (SDLNet_ResolveHost(&m_Ip, host.c_str(), port) < 0) {
        std::cerr << "SDLNet_ResolveHost: " << SDLNet_GetError() << std::endl;
        m_Ip.host = 0;
        m_Ip.port = 0;
    }
}

CIpAddress::CIpAddress() {
    m_Ip.host = 0;
    m_Ip.port = 0;
}

bool CIpAddress::Ok() const {
    return !(m_Ip.port == 0);
}

void CIpAddress::SetIp (IPaddress sdl_ip) { //sets a CTcpSocket object from a existing SDL socket
    m_Ip = sdl_ip;
}

IPaddress CIpAddress::GetIpAddress() const {
    return m_Ip;
}

Uint32 CIpAddress::GetHost() const {
    return m_Ip.host;
}

Uint16 CIpAddress::GetPort() const {
    return m_Ip.port;
}
Again, back to header, let's analyze CTcpSocket, a framework to the SDL NET TCP socket structure. In a few words, a socket is a connection, a "pipe" between two computers, that delivers messages to them, in and out. In a TCP socket, there are necessarily two fixed computers in each pipe's end, and messages always are got in the right order that they have sent.
class CTcpSocket {
  protected:
    TCPsocket m_Socket; //the TCP socket structure
    SDLNet_SocketSet set; //a set of sockets. Used here only to check existing packets
  public:
    CTcpSocket();
    virtual ~CTcpSocket();
    virtual void SetSocket (TCPsocket  the_sdl_socket); //set a CTcpSocket object from a existing SDL socket
    bool Ok() const; //indicate if theres is a TCPsocket associated to the instance
    bool Ready() const; //True if there are bytes ready to be read
    virtual void OnReady(); //pure virtual
};
In the code above, the member function Ready() plays an important role: it checks whether there is or not activity in the socket, freeing the program's flow to do what they have to do, without waiting messages from the other side of the pipe. Below, the code in CNet.cpp:
CTcpSocket::CTcpSocket() {
    m_Socket = NULL;
    set = SDLNet_AllocSocketSet(1);
}

CTcpSocket::~CTcpSocket() {
    if (!(m_Socket == NULL)) {
          SDLNet_TCP_DelSocket(set,m_Socket);
        SDLNet_FreeSocketSet(set);
        SDLNet_TCP_Close (m_Socket);
    }
}

void CTcpSocket::SetSocket (TCPsocket the_sdl_socket) {
    if (!(m_Socket == NULL)) {
          SDLNet_TCP_DelSocket(set,m_Socket);
          SDLNet_TCP_Close (m_Socket);
      }
    m_Socket = the_sdl_socket;
    if(SDLNet_TCP_AddSocket(set,m_Socket)==-1) {
        std::cerr << "SDLNet_TCP_AddSocket: " << SDLNet_GetError() << std::endl;
        m_Socket = NULL;
    }
}

bool CTcpSocket::Ok() const {
    return !(m_Socket == NULL);
}

bool CTcpSocket::Ready() const {
    bool rd = false;
    int numready = SDLNet_CheckSockets(set, 0);
    if (numready == -1)
            std::cerr << "SDLNet_CheckSockets: " << SDLNet_GetError() << std:: endl;
      else
            if (numready)
                rd = SDLNet_SocketReady (m_Socket);
      return rd;
}

void CTcpSocket::OnReady() {

}
From this class, we will derive CHostSocket, according to the piece of code of CNet.h:
class CClientSocket;

class CHostSocket : public CTcpSocket {
  public:
    CHostSocket (CIpAddress& the_ip_address); //create and open a new socket, with an existing CIpAddress object

    CHostSocket (Uint16 port); //create and open a new socket with the desired port
    bool Accept (CClientSocket&); //set a client CTcpSocket object after listening to the port
    virtual void OnReady(); //pure virtual
};
A host socket is a socket whose utility is listen to a certain port, to check whether there is someone wanting to connect to the service provided by the program, in the machine it runs. If found, we have to use the member Accept to associate another socket to the client. Here is the implementation:
CHostSocket::CHostSocket (CIpAddress& the_ip_address) {
    CTcpSocket::CTcpSocket();
    IPaddress iph = the_ip_address.GetIpAddress();
    if (!(m_Socket = SDLNet_TCP_Open(&iph))) {
            SDLNet_FreeSocketSet(set);
            std::cerr << "SDLNet_TCP_Open: " << SDLNet_GetError() << std::endl;
    }
}

CHostSocket::CHostSocket (Uint16 port) {
    CIpAddress iplistener (port);
    if (!iplistener.Ok()) {
        m_Socket = NULL;
    }
    else {
        CTcpSocket::CTcpSocket();
        IPaddress iph = iplistener.GetIpAddress();
        if (!(m_Socket = SDLNet_TCP_Open(&iph))) {
            SDLNet_FreeSocketSet(set);
            std::cerr << "SDLNet_TCP_Open: " << SDLNet_GetError() << std::endl;
        }
    }
}

bool CHostSocket::Accept (CClientSocket& the_client_socket) {
    TCPsocket cs;
    if ((cs = SDLNet_TCP_Accept(m_Socket))) {
        the_client_socket.SetSocket(cs);
        return true;
    }
    else
        return false;
}

void CHostSocket::OnReady() {

}
So, a tcp socket client is a socket whose aim is to communicate with a remote host, who, in turn, is using a tcp client to send and receive messages too. Below, the header file for this class:
class CClientSocket : public CTcpSocket {
  private:
    CIpAddress m_RemoteIp; //the CIpAddress object corresponding to the remote host
  public:
    CClientSocket(); //constructor
    CClientSocket (std::string host, Uint16 port); //Create the object and connect to a host, in a given port
    bool Connect (CIpAddress& remoteip); //make a connection to communicate with a remote host
    bool Connect (CHostSocket& the_listener_socket); //make a connection to communicate with a client
    void SetSocket (TCPsocket  the_sdl_socket); //set a CTcpSocket object from an existing SDL_net socket
    CIpAddress getIpAddress () const; //return a CIpAddress object associated to the remote host
    virtual void OnReady(); //pure virtual
    bool Receive(CNetMessage& rData); //receive data and load it into a CNetMessage object
    bool Send (CNetMessage& sData); //send data from a CNetMessage object
};
The same way CHostSocket is, this class is derived from CTcpSocket, and uses the constructor or Connect() to make a connection to the host. If so, the object uses the Send() and Receive() member functions to throw and get messages through the network. Below, the implementation:
CClientSocket::CClientSocket() {
    CTcpSocket::CTcpSocket();
}

CClientSocket::CClientSocket (std::string host, Uint16 port) {
    CIpAddress remoteip (host.c_str(), port);
    if (!remoteip.Ok()) {
        m_Socket = NULL;
    }
    else {
        CTcpSocket::CTcpSocket();
        Connect(remoteip);
    }
}

bool CClientSocket::Connect (CHostSocket& the_listener_socket) {
    return false;
}

bool CClientSocket::Connect (CIpAddress& remoteip) {
    TCPsocket cs;
    IPaddress ip = remoteip.GetIpAddress();
    if ((cs = SDLNet_TCP_Open(&ip)))
    {
        SetSocket(cs);
        return true;
    }
    else {
        std::cerr << "SDLNet_TCP_Open: " << SDLNet_GetError() << std::endl;
        return false;
    }
}

void CClientSocket::SetSocket (TCPsocket the_sdl_socket) {
    CTcpSocket::SetSocket (the_sdl_socket);
    IPaddress* ips;
    if ((ips = SDLNet_TCP_GetPeerAddress(m_Socket))) {
    /* Print the address, converting it onto the host format */
        m_RemoteIp.SetIp(*ips);
        Uint32 hbo = m_RemoteIp.GetHost();
        Uint16 pbo = m_RemoteIp.GetPort();
        std::cout << "Client connected: " << SDLNet_Read32(&hbo) << ' '
            << SDLNet_Read16 (&pbo) << std::endl;
    }
    else
        std::cerr << "SDLNet_TCP_GetPeerAddress: " << SDLNet_GetError() << std::endl;
}

CIpAddress CClientSocket::getIpAddress () const {
    return m_RemoteIp;
}

bool CClientSocket::Receive(CNetMessage& rData) {
//Firstly, check if there is a socket
    if (m_Socket == NULL)
        return false;
    charbuf buf;

//Check if the instance can receive bytes, if it can, load the number of bytes specified by NumToLoad() virtual function
    while (rData.NumToLoad() > 0) {
        if (SDLNet_TCP_Recv(m_Socket, buf, rData.NumToLoad()) > 0) {
            rData.LoadBytes (buf, rData.NumToLoad());
        }
        else {
            return false;
        }
    }
    rData.finish();
    return true;
}

bool CClientSocket::Send (CNetMessage& sData) {
//check if there is a socket
    if (m_Socket == NULL)
        return false;
    charbuf buf;
    int len;

//Check if the instance can send bytes, if it can, unload the number of bytes specified by NumToLoad() virtual function
    while ((len = sData.NumToUnLoad()) > 0) {
        sData.UnLoadBytes (buf);
        if (SDLNet_TCP_Send(m_Socket, (void *)buf, len) < len) {
            std::cerr << "SDLNet_TCP_Send: " << SDLNet_GetError() << std::endl;
            return false;
        }
    }
    return true;
}

void CClientSocket::OnReady() {

}


Tutoriel SDL

SDL Net - Part 2

User Tutorial
The following user tutorial was created by Kahshoo-heem, for the purpose of expanding upon the SDLTutorials.com series, and expounding upon the use of SDL. This tutorial, though not purposely a part of the SDLTutorials.com or created for the series, may be a branch or addition to the series. Please read notes by the author for any additional code and/or framework used by the author. If you wish to submit your own tutorial to this site, please visit the "User Tutorials" page.

Once we have made our common library, it's time to think on designing the game. Network games can have many architectures, but, normally, there are always at least two applications: a server and a client.

A server can be dedicated, like most online multiplayer games over the internet. But, in this case, the server will be playing too. To make things simpler, the server always will be "X", and the player that starts the game, too. It'll be responsible for waiting clients to connect, too.

At each player turn, the program sets the grid status, refreshes the screen, and sends a message to the other player. Receiving this message, the other player is now able to make valid inputs, as the first one can only exit or minimize his application.

At this point, we may have to create a state for the application: connected. Without it, one can start the game alone, and send messages to a person who is not online.

Now, let's take a look at the messages the program has to make. As we said in the first tutorial, we will send only a byte, that corresponds to the ID of the grid cell clicked. So we have to modify our CNetMessage class, as this byte can be zero, and the program can take the message as an empty string. Therefore, we will make the class CNetMessageApp. Here is the header file, CNetApp.h:
#include "CNet.h"

class CNetMessageApp : public CNetMessage {

 private:

//Virtual function that indicates how many bytes may have to be loaded onto the object. Overrides the parent class function member to work with only a byte

   virtual int NumToLoad();

//Virtual function that indicates how many bytes may have to be downloaded from the object. Overrides the parent class function member to work with only a byte

   virtual int NumToUnLoad();

public:

//Function simpler than LoadBytes(), inherited from the parent class, as it only works with one byte

   void LoadByte(char);

//Function simpler than UnLoadBytes(), inherited from the parent class, as it only works with one byte

   char UnLoadByte();

};
Here comes the implementation, at CNetApp.cpp:
#include "CNetApp.h"

int CNetMessageApp::NumToLoad(){

if (state == EMPTY)

 return 1;

else

 return 0;

}

int CNetMessageApp::NumToUnLoad() {

if (state == FULL)

 return 1;

else

 return 0;

}

void CNetMessageApp::LoadByte(char ID) {

   charbuf c;

   c[0] = ID;

LoadBytes(c, 1);

finish();

}

char CNetMessageApp::UnLoadByte() {

   charbuf c;

   UnLoadBytes (c);

   return c[0];

}
As we can see, very simple. Finally, we can modify the original files of Tim's TIC TAC TOE game.

At the header file CApp.h, in the client side, we will have to add the following lines:
//adding the network library

#include "CNetApp.h"

 .

 .

 .

   private:

// Network status indicator

       bool Connected;

// Network objects

       CClientSocket* tcpclient;

       CIpAddress* remoteip;

       CNetMessageApp msg;
And, in the server side, we don't need the help of CIpAddress object, so it can be deleted. However, we have to add a tcp socket that looks for - or, better saying, listen to - clients:
//network objects

       CHostSocket* tcplistener;
And we are able to get into the implementation code. Firstly, let's set this members when creating Capp object. At CApp.cpp, in the server side, we'll have to add these lines to the constructor:
   tcplistener = NULL;

   tcpclient = NULL;

   Connected = false;
And, in the client side,
   Connected = false;

   tcpclient = NULL;

   remoteip = NULL;
Now, let's see CApp_Oninit.cpp. We may have only to create the objects above, except CNetMessage, that is created automatically by CApp. In the server side, the code of CApp::OnInit() will be added by these lines:
//Creating host socket

   tcplistener = new CHostSocket (1234);

   if (!tcplistener->Ok())

       exit(EXIT_FAILURE);

//Creating client socket

   tcpclient = new CClientSocket();
And in the client side,
//Creating Ip and Socket objects

   tcpclient = new CClientSocket ();

   remoteip = new CIpAddress ("192.168.0.102", 1234);
Note that the client, in this example, will communicate with a determined machine. In my home network, "192.168.0.102" will be the server's address, and "1234" will be the port we have chosen to use. Otherwise, in a LAN, there is a manner to broadcast a required connection, but this method uses UDP sockets. Thus, someone can use this kind of protocol to get the server address, and, after that, work with TCP sockets to make reliable and simple communication over the network.

On the other hand, that is the code on CApp_OnCleanup - it will destroy these objects:

Server side:
   delete tcplistener;

   delete tcpclient;
Client side:
   delete remoteip;

   delete tcpclient;
Now, we can examine what happens when we click the mouse in our application. At CApp::OnLButtonDown() we'll change these lines:
if(CurrentPlayer == 0) {
 SetCell(ID, GRID_TYPE_X);
CurrentPlayer = 1;
 }else{
  SetCell(ID, GRID_TYPE_O);
 CurrentPlayer = 0;
 }
}
to these: CApp_OnEvent - Server side:
//Only starts to play once connected

   if (Connected) {

//Player == 0 will always be Server. Always starts to play and always be "X"

       if(CurrentPlayer == 0) {

           SetCell(ID, GRID_TYPE_X);

           CurrentPlayer = 1;

//Send a message to the client, telling the ID of the clicked cell

           msg.LoadByte((char) ID);

           tcpclient->Send(msg);

       }

   }
CApp_OnEvent - Client side:
//Player == 0 will always be Server. Always starts to play and always be "X". So if

//CurrentPlayer is not zero, the game is necessarily connected

if(CurrentPlayer != 0) {

 SetCell(ID, GRID_TYPE_O);

 CurrentPlayer = 0;

//Send a message to the server, telling the ID of the clicked cell

 msg.LoadByte((char) ID);

  tcpclient->Send(msg);

}
As we can see, no much changes have been made. If it's not the application's turn, it does nothing. If it is, the application sets the grid and player status, like it did priorly, and sends a message to the remote application. There's no need to modify the rendering behavior.

Lastly, we have to write CApp OnLoop() member function. We have to do two things: try to connect, or, if yet connected, receive the messages sent over the network. Below, the code of CApp_OnLoop.cpp, server side:
#include "CApp.h"

void CApp::OnLoop() {

//if not connected, listen to the port to detect if there is a client waiting there

   if (!Connected) {

       if (tcplistener->Accept (*tcpclient)) {

           Connected = true;

       }

   }

   else {

//if connected, checks the socket for messages ready to be read

       if (tcpclient->Ready()) {

// if there is a message, try to receive it. In case of disconnection, the TCP protocol sends

// a message with no bytes

           if (tcpclient->Receive (msg)) {

               //if there is a valid message, we can set the grid and player status

               SetCell((int) msg.UnLoadByte(), GRID_TYPE_O);

               CurrentPlayer = 0;

           }

           else

               Connected = false;

       }

   }

}
The commented lines say by themselves the program's flow. On the other side, the client's code:
//if not connected, try to connect to the server

   if (!Connected) {

       if (tcpclient->Connect(*remoteip)) {

           if (tcpclient->Ok()) {

               Connected = true;

           }

       }

}

   else {

//if connected, checks the socket for messages ready to be read

       if (tcpclient->Ready()){

// if there is a message, try to receive it. In case of disconnection, the TCP protocol sends

// a message with no bytes

           if (tcpclient->Receive (msg)) {

               //if there is a valid message, we can set the grid and player status

               SetCell((int) msg.UnLoadByte(), GRID_TYPE_X);

               CurrentPlayer = 1;

           }

           else {

               Connected = false;

           }

       }

   }

}


Tutoriel SDL

SDL Per Pixel Collision
Are bounding box and circle collisions just not cutting it? Well, now we're going to dive right down into pixel precision!

Before we start, I'd like to say that this is an advanced SDL tutorial. You should have experience with creating SDL applications and some basic knowledge of getting a program with it's libraries up and running.

First, we'll start off with a very simple Game class to build the foundation of our demo project.

Game.h
#ifndef GAME_H
#define GAME_H

#include "SDL.h"

class Game
{
    public:
        Game();
        ~Game();

        void OnExecute();

        void OnThink();
        void OnUpdate();
        void OnRender();

    private:
        int ScreenWidth;
        int ScreenHeight;
        int ScreenBPP;
        Uint32 ScreenFlags;

        bool done;

        double thisTime;
        double lastTime;
        double deltaTime;

        SDL_Surface* screen;
        SDL_Event event;
};

#endif
Game.cpp
#include "Game.h"

Game::Game()
{
    ScreenWidth = 800;
    ScreenHeight = 600;
    ScreenBPP = 32;
     ScreenFlags = SDL_HWSURFACE | SDL_DOUBLEBUF;

    thisTime = 0.0;
    lastTime = 0.0;
    deltaTime = 0.0;

    done = false;

    SDL_Init(SDL_INIT_EVERYTHING);

    SDL_WM_SetCaption("SDL Per Pixel Collision", NULL);
    SDL_ShowCursor(0);

    screen = SDL_SetVideoMode(ScreenWidth, ScreenHeight, ScreenBPP, ScreenFlags);
}

Game::~Game()
{
    SDL_Quit();
}

void Game::OnExecute()
{
    while(!done)
    {
        OnThink();
        OnUpdate();
        OnRender();
    }
}

void Game::OnThink()
{
    while(SDL_PollEvent(&event))
    {
        if(event.type == SDL_QUIT)
            done = true;

        if(event.type == SDL_KEYDOWN)
            if(event.key.keysym.sym == SDLK_ESCAPE)
                done = true;
    }
}

void Game::OnUpdate()
{
    lastTime = thisTime;
    thisTime = SDL_GetTicks();
    deltaTime = (thisTime - lastTime) / 1000.0;
}

void Game::OnRender()
{
    SDL_FillRect(screen, NULL, 0);

    SDL_Flip(screen);
}

int main(int argc, char* args[])
{
    Game game;
    game.OnExecute();

    return 0;
}
You should be able to compile and run at this point. Now we have a nice 800x600 window prepared for our bidding! The three double variables are used at keep track of elapsed time each tick. You can simply substitute in your favorite Timer class if you wish. I also turn off cursor visibility since we'll be controlling an object with the mouse and we don't want it to obscure our view.

Now we'll need some sort of objects that we can collide together!

Entity.h
#ifndef ENTITY_H
#define ENTITY_H
#include "SDL.h"
#include "SDL_image.h"

struct Vector
{
    float X;
    float Y;

    Vector()
    {
        X = 0.0f;
        Y = 0.0f;
    }
};

class Entity
{
    public:
        Entity(char* filename, int tilesX, int tilesY, double frameRate);
        ~Entity();

    public:
        Vector position;
        Vector origin;

    public:
        SDL_Surface* surface;

        int tilesX;
        int tilesY;
        int tileWidth;
        int tileHeight;

        int frame;
        int frameCount;

        double frameInterval;
        double frameTimeRemaining;

    public:
        void Update(double deltaTime);
        void Render(SDL_Surface* screen);
};

#endif
Simple enough, I hope. For each Entity we create, we'll take in a filename for the for the image we'll be loading. If you didn't notice, SDL_image is included, as I'll be working with PNG files with alpha transparency. Next, we take in the tiles across and tiles down in the spritesheet we'll be using. Finally, a frame rate to determine how fast the sprite will animate.

Next, the member variables. I've created a simple Vector struct to hold position and origin information. Position is where the Entity will sit on the screen and origin is the center of the tile currently being drawn. This will make things simpler when we want to draw a sprite centered on it's position. Of course, an SDL_Surface pointer to hold the image data we'll be using. And last, variables to hold information about the spritesheet we'll be using and handling animation.

Finally, two methods to update and render the given Entity. Now let's implement these!

Entity.cpp
#include "Entity.h"

Entity::Entity(char* filename, int tilesX, int tilesY, double frameRate)
{
    this->surface = IMG_Load(filename);

    this->tilesX = tilesX;
    this->tilesY = tilesY;
    this->tileWidth = surface->w / tilesX;
    this->tileHeight = surface->h / tilesY;
    this->frame = 0;
    this->frameCount = tilesX * tilesY;
    this->frameInterval = 1 / frameRate;
    this->frameTimeRemaining = frameInterval;

    this->origin.X = tileWidth / 2.0f;
    this->origin.Y = tileHeight / 2.0f;
}

Entity::~Entity()
{
    SDL_FreeSurface(surface);
}

void Entity::Update(double deltaTime)
{
    frameTimeRemaining -= deltaTime;
    if(frameTimeRemaining < 0)
    {
        frame++;
        frame %= frameCount;
        frameTimeRemaining = frameInterval;
    }
}

void Entity::Render(SDL_Surface* screen)
{
    SDL_Rect location = GetBounds();
    SDL_Rect clip = GetFrameBounds();

    SDL_BlitSurface(surface, &clip, screen, &location);
}
The constructor loads in our image using IMG_Load() and stores all our data about the sprite sheet. The destructor ensures we have no memory leaks by freeing our SDL_Surface. Update chips away at our frameTimeRemaining with a given deltaTime. Once we hit or are under zero, we increase the frame, wrap around to the beginning if need be, and reset our frameTimeRemaining with our given frameInterval. Render finds our location and clip rectangles and then blits the surface to the screen. But wait, we haven't added GetBounds or GetFrameBounds() yet. I'm sure those will be useful to have! Let's get to that!

Add these method definitions in:

Entity.h
    public:
        SDL_Rect GetBounds();
        SDL_Rect GetFrameBounds();
And now we'll implement them:

Entity.cpp
SDL_Rect Entity::GetBounds()
{
    SDL_Rect bounds;
    bounds.x = (Sint16)(position.X - origin.X);
    bounds.y = (Sint16)(position.Y - origin.Y);
    bounds.w = (Sint16)(tileWidth);
    bounds.h = (Sint16)(tileHeight);

    return bounds;
}

SDL_Rect Entity::GetFrameBounds()
{
    SDL_Rect frameBounds;
    frameBounds.x = (Sint16)(frame % tilesX * tileWidth);
    frameBounds.y = (Sint16)(frame / tilesX * tileHeight);
    frameBounds.w = (Sint16)(tileWidth);
    frameBounds.h = (Sint16)(tileHeight);

    return frameBounds;
}
GetBounds() takes our position and shifts it over by our origin. This places the position at the center of the sprite, rather than the top left. GetFramesBound() checks what frame we're on and finds the matching coordinates. Very important for animation!

Now lets slap a couple of these suckers into our demo!

Game.h
#include "Entity.h"

class Game
{
    ...

    private:
        Entity* ship;
        Entity* asteroid;
};
Make sure you include Entity.h so we can actually use the class and we'll add two Entity pointers to our Game class.

Game.cpp
Game::Game()
{
    ...

    ship = new Entity("images/ship.png", 4, 3, 10);
    asteroid = new Entity("images/asteroid.png", 5, 6, 30);

    asteroid->position.X = (float)(ScreenWidth / 2);
    asteroid->position.Y = (float)(ScreenHeight / 2);
}

Game::~Game()
{
    delete ship;
    delete asteroid;

    ...
}
Image: Ship
Image: Asteroid
(Special thanks to 3DBuzz for these images.)

We'll now dynamically allocate memory for our two new Entity objects. I have my images sitting in an "images" folder, but you may do whatever you want! I then position the asteroid at the center of the screen. And because we're mindful programmers, we'll delete the allocated memory once we're all done.

Game.cpp
void Game::OnThink()
{
    while(SDL_PollEvent(&event))
    {
        if(event.type == SDL_QUIT)
            done = true;

        if(event.type == SDL_KEYDOWN)
            if(event.key.keysym.sym == SDLK_ESCAPE)
                done = true;

        if(event.type == SDL_MOUSEMOTION)
        {
            ship->position.X = event.motion.x;
            ship->position.Y = event.motion.y;
        }
    }
}

void Game::OnUpdate()
{
    lastTime = thisTime;
    thisTime = SDL_GetTicks();
    deltaTime = (thisTime - lastTime) / 1000.0;

    ship->Update(deltaTime);
    asteroid->Update(deltaTime);
}

void Game::OnRender()
{
    SDL_FillRect(screen, NULL, 0);

    ship->Render(screen);
    asteroid->Render(screen);

    SDL_Flip(screen);
}
In OnThink, we'll mouse movement to our ship by looking for an SDL_MOUSEMOTION event while polling for events. In OnUpdate, we'll simply call each Entity's Update method with the deltaTime we found. Lastly, in OnRender, we'll just call the Render method by passing our screen.

Compile, run, and give it a whirl! Weeee! We now have two animated sprites, one centered in the screen and one controlled by our mouse. But where's the collision? Oh yeah, we haven't implemented it yet!

We'll need a bit more functionally in our Entity class, so let's work on that. This one's a doozy, so hold on tight! But don't worry, we'll take it one step at a time...

Entity.h
class Entity
{
    ...

    public:
        SDL_Rect NormalizeBounds(const SDL_Rect& rect);

        static SDL_Rect Intersection(const SDL_Rect& boundsA, const SDL_Rect& boundsB);
        static bool CheckCollision(Entity* entityA, Entity* entityB);
        static bool GetAlphaXY(Entity* entity, int x, int y);
};
Entity.cpp
SDL_Rect Entity::NormalizeBounds(const SDL_Rect& rect)
{
    SDL_Rect normalized;
    normalized.x = rect.x - (Sint16)position.X + (Sint16)origin.X + GetFrameBounds().x;
    normalized.y = rect.y - (Sint16)position.Y + (Sint16)origin.Y + GetFrameBounds().y;
    normalized.w = rect.w;
    normalized.h = rect.h;

    return normalized;
}
Given the SDL_Rect of a collision rectangle, we want to find what pixels on the sprite sheet correlate to it. A little messy, but it gets the job done.

Entity.cpp
SDL_Rect Entity::Intersection(const SDL_Rect& boundsA, const SDL_Rect& boundsB)
{
    int x1 = Maximum(boundsA.x, boundsB.x);
    int y1 = Maximum(boundsA.y, boundsB.y);
    int x2 = Minimum(boundsA.x + boundsA.w, boundsB.x + boundsB.w);
    int y2 = Minimum(boundsA.y + boundsA.h, boundsB.y + boundsB.h);

    int width = x2 - x1;
    int height = y2 - y1;

    if(width > 0 && height > 0)
    {
        SDL_Rect intersect = {x1, y1, width, height};
        return intersect;
    }
    else
    {
        SDL_Rect intersect = {0, 0, 0, 0};
        return intersect;
    }
}
Given the two bounds of two entities, we want to find how much they intersect, if at all. If they do, we're given their intersection rectangle, if not, we're given an empty rectangle. For Max and Min, you can either include "cmath" or use a couple macros such as this:
#define Maximum(a, b) ((a > b) ? a : b)
#define Minimum(a, b) ((a < b) ? a : b)
Entity.cpp
bool Entity::CheckCollision(Entity* entityA, Entity* entityB)
{
    SDL_Rect collisionRect = Intersection(entityA->GetBounds(), entityB->GetBounds());

    if(collisionRect.w == 0 && collisionRect.h == 0)
        return false;

    SDL_Rect normalA = entityA->NormalizeBounds(collisionRect);
    SDL_Rect normalB = entityB->NormalizeBounds(collisionRect);

    for(int y = 0; y < collisionRect.h; y++)
        for(int x = 0; x < collisionRect.w; x++)
            if(GetAlphaXY(entityA, normalA.x + x, normalA.y + y) && GetAlphaXY(entityB, normalB.x + x, normalB.y + y))
                return true;

    return false;
}
This is where bulk of our per pixel collision check happens. First, we ensure there's even a bounding box collision. No use going through all the trouble when we know nothing will happen. Then we normalize the bounds of each entity with the found collision rectangle. This will give us the region of pixels we'll be comparing against each other. You'll notice this GetAlphaXY() function. This is where we'll get down into the pixels and determine if they're solid enough for collision. You'll see that it's called twice in one if-statement. This is because we're searching for a solid enough pixel in each surface at the same position. Once that is found, we'll confirm a hit!

Entity.cpp
bool Entity::GetAlphaXY(Entity* entity, int x, int y)
{
    int bpp = entity->surface->format->BytesPerPixel;
    Uint8* p = (Uint8*)entity->surface->pixels + y * entity->surface->pitch + x * bpp;
    Uint32 pixelColor;

    switch(bpp)
    {
        case(1):
            pixelColor = *p;
            break;
        case(2):
            pixelColor = *(Uint16*)p;
            break;
        case(3):
            if(SDL_BYTEORDER == SDL_BIG_ENDIAN)
                pixelColor = p[0] << 16 | p[1] << 8 | p[2];
            else
                pixelColor = p[0] | p[1] << 8 | p[2] << 16;
            break;
        case(4):
            pixelColor = *(Uint32*)p;
            break;
    }

    Uint8 red, green, blue, alpha;
    SDL_GetRGBA(pixelColor, entity->surface->format, &red, &green, &blue, &alpha);

    return alpha > 200;
}
Oh my, some crazy looking stuff here! It's not that bad. Let me explain a few things here.

We're passing in a pointer to an Entity and an (x,y) coordinate. All we're doing in this function is finding the pixel at that coordinate and determining what colors its composed of. The first two lines give us a pointer to the pixel in question. The funky looking switch-statement is finding the color composition depending on the image's bytes-per-pixel format, either 8-bit, 16-bit, 24-bit, or 32-bit. In our case, it will always land on case(4), but this should give you an idea on how to expand to other formats.

Finally, we're grabbing the RGBA components of the pixel using SDL_GetRGBA. All we care about is alpha, but you can experiment with collision where hits will only happen when a pixel is red or blue enough. Alpha will range between 0 and 255. I wanted collision to happen with not just pixels that were completely opaque, but not too transparent. I found 200 to be a nice number, but you're free to tweak as you please.

Well that's it! Our Entity class has the functionally to test for per pixel collision... so lets show it off!

Game.cpp
void Game::OnRender()
{
    SDL_FillRect(screen, NULL, 0);

    SDL_Rect collisionRect = Entity::Intersection(ship->GetBounds(), asteroid->GetBounds());

    if(collisionRect.w != 0 && collisionRect.h != 0)
        if(Entity::CheckCollision(ship, asteroid))
            SDL_FillRect(screen, &collisionRect, SDL_MapRGB(screen->format, 255, 0, 0));
        else
            SDL_FillRect(screen, &collisionRect, SDL_MapRGB(screen->format, 0, 255, 0));

    ship->Render(screen);
    asteroid->Render(screen);

    SDL_Flip(screen);
}
Adding this checks for collision between our ship and asteroid. When bounding box collision happens, a green box will be drawn behind them. When per pixel collision happens, the box will turn red!

Well, that's about it. Run, compile, and enjoy! You now have per pixel collision checks between your two Entity objects! Feel free to tweak numbers, use different images, or control them in different ways! I've given you the foundation, now go out there and create some interesting implementations!

Here's the final code in case you may have some errors and need to check against my own.

Game.h
#ifndef GAME_H
#define GAME_H
#include "Entity.h"

class Game
{
    public:
        Game();
        ~Game();

        void OnExecute();

        void OnThink();
        void OnUpdate();
        void OnRender();

    private:
        int ScreenWidth;
        int ScreenHeight;
        int ScreenBPP;
        Uint32 ScreenFlags;

        bool done;

        double thisTime;
        double lastTime;
        double deltaTime;

        SDL_Surface* screen;
        SDL_Event event;

        Entity* ship;
        Entity* asteroid;
};

#endif

Game.cpp

#include "Game.h"

Game::Game()
{
    ScreenWidth = 800;
    ScreenHeight = 600;
    ScreenBPP = 32;
    ScreenFlags = SDL_HWSURFACE | SDL_DOUBLEBUF;

    done = false;

    thisTime = 0.0;
    lastTime = 0.0;
    deltaTime = 0.0;

    SDL_Init(SDL_INIT_EVERYTHING);

    SDL_WM_SetCaption("SDL Per Pixel Collision", NULL);
    SDL_ShowCursor(0);

    screen = SDL_SetVideoMode(ScreenWidth, ScreenHeight, ScreenBPP, ScreenFlags);

    ship = new Entity("images/ship.png", 4, 3, 10);
    asteroid = new Entity("images/asteroid.png", 5, 6, 30);

    asteroid->position.X = (float)(ScreenWidth / 2);
    asteroid->position.Y = (float)(ScreenHeight / 2);
}

Game::~Game()
{
    delete ship;
    delete asteroid;

    SDL_Quit();
}

void Game::OnExecute()
{
    while(!done)
    {
        OnThink();
        OnUpdate();
        OnRender();
    }
}

void Game::OnThink()
{
    while(SDL_PollEvent(&event))
    {
        if(event.type == SDL_QUIT)
            done = true;

        if(event.type == SDL_KEYDOWN)
            if(event.key.keysym.sym == SDLK_ESCAPE)
                done = true;

        if(event.type == SDL_MOUSEMOTION)
        {
            ship->position.X = event.motion.x;
            ship->position.Y = event.motion.y;
        }
    }
}

void Game::OnUpdate()
{
    lastTime = thisTime;
    thisTime = SDL_GetTicks();
    deltaTime = (thisTime - lastTime) / 1000.0;

    ship->Update(deltaTime);
    asteroid->Update(deltaTime);
}

void Game::OnRender()
{
    SDL_FillRect(screen, NULL, 0);

    if(collisionRect.w != 0 && collisionRect.h != 0)
        if(Entity::CheckCollision(ship, asteroid))
            SDL_FillRect(screen, &collisionRect, SDL_MapRGB(screen->format, 255, 0, 0));
        else
            SDL_FillRect(screen, &collisionRect, SDL_MapRGB(screen->format, 0, 255, 0));

    ship->Render(screen);
    asteroid->Render(screen);

    SDL_Flip(screen);
}

int main(int argc, char* args[])
{
    Game game;
    game.OnExecute();

    return 0;
}
Entity.h
#ifndef ENTITY_H
#define ENTITY_H

#include "SDL.h"
#include "SDL_image.h"

struct Vector
{
    float X;
    float Y;

    Vector()
    {
        X = 0.0f;
        Y = 0.0f;
    }
};

class Entity
{
    public:
        Entity(char* filename, int tilesX, int tilesY, double frameRate);
        ~Entity();

    public:
        Vector position;
        Vector origin;

    public:
        SDL_Surface* surface;

        int tilesX;
        int tilesY;
        int tileWidth;
        int tileHeight;

        int frame;
        int frameCount;

        double frameInterval;
        double frameTimeRemaining;

    public:
        void Update(double deltaTime);
        void Render(SDL_Surface* screen);

    public:
        SDL_Rect GetBounds();
        SDL_Rect GetFrameBounds();
        SDL_Rect NormalizeBounds(const SDL_Rect& rect);

        static SDL_Rect Intersection(const SDL_Rect& boundsA, const SDL_Rect& boundsB);
        static bool CheckCollision(Entity* entityA, Entity* entityB);
        static bool GetAlphaXY(Entity* entity, int x, int y);
};

#endif
Entity.cpp
#include "Entity.h"
#define Maximum(a, b) ((a > b) ? a : b)
#define Minimum(a, b) ((a < b) ? a : b)

Entity::Entity(char* filename, int tilesX, int tilesY, double frameRate)
{
    this->surface = IMG_Load(filename);

    this->tilesX = tilesX;
    this->tilesY = tilesY;
    this->tileWidth = surface->w / tilesX;
    this->tileHeight = surface->h / tilesY;
    this->frame = 0;
    this->frameCount = tilesX * tilesY;
    this->frameInterval = 1 / frameRate;
    this->frameTimeRemaining = frameInterval;

    this->origin.X = tileWidth / 2.0f;
    this->origin.Y = tileHeight / 2.0f;
}

Entity::~Entity()
{
    SDL_FreeSurface(surface);
}

void Entity::Update(double deltaTime)
{
    frameTimeRemaining -= deltaTime;
    if(frameTimeRemaining < 0)
    {
        frame++;
        frame %= frameCount;
        frameTimeRemaining = frameInterval;
    }
}

void Entity::Render(SDL_Surface* screen)
{
    SDL_Rect location = GetBounds();
    SDL_Rect clip = GetFrameBounds();

    SDL_BlitSurface(surface, &clip, screen, &location);
}

SDL_Rect Entity::GetBounds()
{
    SDL_Rect bounds;
    bounds.x = (Sint16)(position.X - origin.X);
    bounds.y = (Sint16)(position.Y - origin.Y);
    bounds.w = (Sint16)(tileWidth);
    bounds.h = (Sint16)(tileHeight);

    return bounds;
}

SDL_Rect Entity::GetFrameBounds()
{
    SDL_Rect frameBounds;
    frameBounds.x = (Sint16)(frame % tilesX * tileWidth);
    frameBounds.y = (Sint16)(frame / tilesX * tileHeight);
    frameBounds.w = (Sint16)(tileWidth);
    frameBounds.h = (Sint16)(tileHeight);

    return frameBounds;
}

SDL_Rect Entity::NormalizeBounds(const SDL_Rect& rect)
{
    SDL_Rect normalized;
    normalized.x = rect.x - (Sint16)position.X + (Sint16)origin.X + GetFrameBounds().x;
    normalized.y = rect.y - (Sint16)position.Y + (Sint16)origin.Y + GetFrameBounds().y;
    normalized.w = rect.w;
    normalized.h = rect.h;

    return normalized;
}

SDL_Rect Entity::Intersection(const SDL_Rect& boundsA, const SDL_Rect& boundsB)
{
    int x1 = Maximum(boundsA.x, boundsB.x);
    int y1 = Maximum(boundsA.y, boundsB.y);
    int x2 = Minimum(boundsA.x + boundsA.w, boundsB.x + boundsB.w);
    int y2 = Minimum(boundsA.y + boundsA.h, boundsB.y + boundsB.h);

    int width = x2 - x1;
    int height = y2 - y1;

    if(width > 0 && height > 0)
    {
        SDL_Rect intersect = {x1, y1, width, height};
        return intersect;
    }
    else
    {
        SDL_Rect intersect = {0, 0, 0, 0};
        return intersect;
    }
}

bool Entity::CheckCollision(Entity* entityA, Entity* entityB)
{
    SDL_Rect collisionRect = Intersection(entityA->GetBounds(), entityB->GetBounds());

    if(collisionRect.w == 0 && collisionRect.h == 0)
        return false;

    SDL_Rect normalA = entityA->NormalizeBounds(collisionRect);
    SDL_Rect normalB = entityB->NormalizeBounds(collisionRect);

    for(int y = 0; y < collisionRect.h; y++)
        for(int x = 0; x < collisionRect.w; x++)
            if(GetAlphaXY(entityA, normalA.x + x, normalA.y + y) && GetAlphaXY(entityB, normalB.x + x, normalB.y + y))
                return true;

    return false;
}

bool Entity::GetAlphaXY(Entity* entity, int x, int y)
{
    int bpp = entity->surface->format->BytesPerPixel;
    Uint8* p = (Uint8*)entity->surface->pixels + y * entity->surface->pitch + x * bpp;
    Uint32 pixelColor;

    switch(bpp)
    {
        case(1):
            pixelColor = *p;
            break;
        case(2):
            pixelColor = *(Uint16*)p;
            break;
        case(3):
            if(SDL_BYTEORDER == SDL_BIG_ENDIAN)
                pixelColor = p[0] << 16 | p[1] << 8 | p[2];
            else
                pixelColor = p[0] | p[1] << 8 | p[2] << 16;
            break;
        case(4):
            pixelColor = *(Uint32*)p;
            break;
    }

    Uint8 red, green, blue, alpha;
    SDL_GetRGBA(pixelColor, entity->surface->format, &red, &green, &blue, &alpha);

    return alpha > 200;
}


Tutoriel SDL

SDL + OpenGL Tutorial Basics

Beginning OpenGL
In this new tutorial series, we will be looking at using SDL along with OpenGL. SDL as it stands is nice for basic 2D graphics/composition, but when it comes to more intensive gaming applications or 3D graphics, we need to leverage the use of other libraries. SDL works so well with OpenGL because we can use OpenGL to manage only graphical rendering, and leave SDL for everything else (Events, Window management, etc).

So, lets get started. We'll be basing our code off of the "SDL Tutorial Basics" tutorial - so head on over there first if you haven't had a chance to look at it. Primarily what we'll be doing here is making any necessary changes to the code for use with OpenGL. What's nice about SDL is that it provides an easy way to setup OpenGL. On the technical side of things, whenever a window is created on an operating system, an OpenGL context must also be created (with certain properties) and linked to that window. SDL makes this rather easy for us.

Before we get started with modifying our code, open up your project file (the one from SDL Tutorial Basics). Because we are using another library now, we are going to need to link to it. So, open up your Project Properties, and add "opengl32", "glu32" to your linker settings (this is the same area that has mingw32, SDLmain, SDL). If you are another operating system besides Windows, your library names will most likely be "opengl", "glu." Most people already have the OpenGL library installed (as it usually comes with CodeBlocks). If you don't, make sure you downloaded the CodeBlocks with mingw. If you are on some Linux flavor, try installing Mesa. Also, as a last note, put the new libraries after the SDL libraries.

That's it for project settings, lets move on to modifying our code. Open up CApp.h and include our OpenGL header file.
#ifndef _CAPP_H_
    #define _CAPP_H_

#include <SDL.h>
#include <gl/gl.h>
#include <gl/glu.h>

// ... other code below
If you are wondering what the difference between gl and glu, gl contains the primary functions from the OpenGL specification, and glu contains several helper/utility functions for creating complex shapes, moving the camera, etc. So now that we have that, lets actually get OpenGL up and running. Open up CApp_OnInit.cpp. Right now this is what we have:
#include "CApp.h"

bool CApp::OnInit() {
    if(SDL_Init(SDL_INIT_EVERYTHING) < 0) {
        return false;
    }

    if((Surf_Display = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE | SDL_DOUBLEBUF)) == NULL) {
        return false;
    }

    return true;
}
The first thing we need to do is tell SDL that we are going to be using OpenGL. Thankfully, SDL provides an easy way to do this. Add the flag SDL_OPENGL just after SDL_DOUBLEBUF, and change SDL_DOUBLEBUF to SDL_GL_DOUBLEBUFFER:
    if((Surf_Display = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE | SDL_GL_DOUBLEBUFFER | SDL_OPENGL)) == NULL) {
        return false;
    }
Now, there are several settings you can set for OpenGL. By default, most are fine just as is, but I want to make you aware of them:
SDL_GL_SetAttribute(SDL_GL_RED_SIZE,        8);
SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE,      8);
SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE,       8);
SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE,      8);

SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE,      16);
SDL_GL_SetAttribute(SDL_GL_BUFFER_SIZE,        32);

SDL_GL_SetAttribute(SDL_GL_ACCUM_RED_SIZE,    8);
SDL_GL_SetAttribute(SDL_GL_ACCUM_GREEN_SIZE,    8);
SDL_GL_SetAttribute(SDL_GL_ACCUM_BLUE_SIZE,    8);
SDL_GL_SetAttribute(SDL_GL_ACCUM_ALPHA_SIZE,    8);

SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS,  1);
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES,  2);
These are all basic memory sizes for how much data OpenGL will store (except SDL_GL_MULTISAMPLEBUFFERS/SDL_GL_MULTISAMPLESAMPLES, those are for something else). By increasing these, you will effectively increase how much memory OpenGL will use for color information, and the buffer. As I mentioned before, the default on all of these is usually just fine. If you wish to set these, the values I have up above are pretty good. Be sure to call SDL_GL_SetAttribute before calling SDL_SetVideoMode. One thing I want to mention is on SDL_GL_MULTISAMPLEBUFFERS. This is basically your anti-aliasing flag. By turning this on, you are turning on anti-aliasing. SDL_GL_MULTISAMPLESAMPLES is how much to anti-alias. A value of 2 is pretty standard if turned on, 4 is also commonly used. Just keep in mind that this can be resource intensive for some systems. Also, not every computer is able to perform anti-aliasing. And one last note! Make sure your SDL_GL_DEPTH_SIZE is a power of two (16, 32 works), otherwise anti-aliasing may not work for you.

Moving on. Now we basically have OpenGL setup with our window, but the problem is that OpenGL requires additional information about the rendering area. What I mean by this is that because OpenGL is a 3D rendering library, it needs to know some basic things like where the camera is, what our 3D world looks like, what color is our rendering area, etc.

So, lets add a few more lines of code after SDL_SetVideoMode.
glClearColor(0, 0, 0, 0);
glClearDepth(1.0f);

glViewport(0, 0, 640, 480);

glMatrixMode(GL_PROJECTION);
glLoadIdentity();

glOrtho(0, 640, 480, 0, 1, -1);

glMatrixMode(GL_MODELVIEW);

glEnable(GL_TEXTURE_2D);

glLoadIdentity();
Okay, this is where we actually get to start using OpenGL. First, note that OpenGL is a C library (like SDL), and that all function calls begin with gl* (much like all SDL function calls begin with SDL_*). So what are we doing here exactly? The first function call, glClearColor, tells OpenGL what color to render the view when the color buffer is cleared. The color buffer is the buffer OpenGL uses to render pixels to the screen. glClearDepth does something similar. It tells OpenGL what value to reset the depth buffer when it is cleared. The depth buffer is used to determine the z-axis of a pixel. There are other buffers as well, but only worry about these two for now. The 4 arguments are basically Red, Green, Blue, Alpha. With 1.0f being full color, and 0.0f being no color. If you want to define values as regular RGB 255 values, do this:
glClearColor((128.0f / 255.0f), 1.0f, 1.0f, 1.0f); //Cyan color

Next, we setup the viewport for opengl. This, in essence, is the window area OpenGL will use. If you set it smaller to the SDL window size you'll have black borders, and if you set it larger then the SDL window, you'll have your view clipped. Usually, this will always be the same as the regular window size, but there are certain cases you would want it smaller (you can actually create multiple viewports, and have them all display different camera angles of a single scene). Arguments are X, Y, Width, Height.

After that, we tell OpenGL to change to GL_PROJECTION mode. OpenGL contains certain matrix modes that affect certain things. I don't want to confuse you with technical details, but you'll only ever want to use GL_PROJECTION when you are setting up the "projection" of the scene (which is what the glOrtho call does). Other possible projection functions are glLoadIdentity / gluPerspective / glFrustum / gluOrtho2D. Otherwise, you'll almost always want to be in GL_MODELVIEW mode.

So, after we are in projection mode, we call glLoadIdentity (which is basically a "reset matrix" call). Then, we call glOrtho. Now, to take a quick aside for a moment, we have been working in 2D thus far in all the tutorials on this website. This tutorial is no different, so we are going to use glOrtho to setup a "2D Perspective." An ortho, or orthogonal, perspective is one that flattens a 3D environment into a 2D one. This more or less elmiminates any 3D look, and makes it render like a regular 2D game. Please note though, you can still using the z-axis for layering. On the flip side, if you wanted a 3D environment, you would use gluPerspective or similar to setup a "3D Perspective." So, in simple basic terms, for 2D us glOrtho, and for 3D use gluPerspective. Regarding the arguments of the function, they are Left, Right, Bottom, Top, Near, Far. So, by setting Left to 0, and Right to 640, I am defining the perspective area. If you set these values to something different than the window size, it'd basically stretch / shrink everything.

Moving on, we switch to GL_MODELVIEW mode, reset it, and also enable GL_TEXTURE_2D (By default, textures are not enabled). What is a texture? A texture is very much like an SDL_Surface. It's an image stored in memory that can be rendered to the display, but unlike SDL_Surface's, Textures cannot be rendered to the screen on their own. They must be attached to some sort of primitive. Meaning, if you want to render a basic texture like an SDL_Surface, you have to "Bind" the texture (make it active), render a square, and then attach the texture to the four corners of that square. It sounds more complex then it really is, and we'll take a look at doing that in future tutorials.

This is how our OnInit methods ends up looking like:
bool CApp::OnInit() {
    if(SDL_Init(SDL_INIT_EVERYTHING) < 0) {
        return false;
    }

    SDL_GL_SetAttribute(SDL_GL_RED_SIZE,            8);
    SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE,          8);
    SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE,           8);
    SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE,          8);

    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE,          16);
    SDL_GL_SetAttribute(SDL_GL_BUFFER_SIZE,            32);

    SDL_GL_SetAttribute(SDL_GL_ACCUM_RED_SIZE,        8);
    SDL_GL_SetAttribute(SDL_GL_ACCUM_GREEN_SIZE,    8);
    SDL_GL_SetAttribute(SDL_GL_ACCUM_BLUE_SIZE,        8);
    SDL_GL_SetAttribute(SDL_GL_ACCUM_ALPHA_SIZE,    8);

    SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS,  1);

    SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES,  2);

    if((Surf_Display = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE | SDL_GL_DOUBLEBUFFER | SDL_OPENGL)) == NULL) {
        return false;
    }

    glClearColor(0, 0, 0, 0);

    glViewport(0, 0, 640, 480);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();

    glOrtho(0, 640, 480, 0, 1, -1);

    glMatrixMode(GL_MODELVIEW);

    glEnable(GL_TEXTURE_2D);

    glLoadIdentity();

    return true;
}

Okay, that's it for our OpenGL setup. Please note that there are other setup options I haven't mentioned, but I am sure we will get to them in future tutorials.

Jump on over to OnRender and lets get something to show up on the screen.
void CApp::OnRender() {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glLoadIdentity();

    glBegin(GL_QUADS);
        glColor3f(1, 0, 0); glVertex3f(0, 0, 0);
        glColor3f(1, 1, 0); glVertex3f(100, 0, 0);
        glColor3f(1, 0, 1); glVertex3f(100, 100, 0);
        glColor3f(1, 1, 1); glVertex3f(0, 100, 0);
    glEnd();

    SDL_GL_SwapBuffers();
}

We should be able to get through this fairly quickly. The first line clears our color and depth buffer. For all simplistic purposes, this does a type of reset, and makes our screen blank again. This is similar to calling SDL_FillRect on SDL's primary display surface. Keep in mind though it does more than that. Then, we reset our GL_MODELVIEW matrix. Why? Whenever you move around in the 2D / 3D world in OpenGL to render things you are modifying the model view matrix. This matrix is what OpenGL uses to determine where and how to render something to the screen. So, if you are moving around rendering things, you need to reset it on every loop.

Next is where the magic starts to happen. glBegin is used to signal the start of the rendering of a primitive. The parameter is used to simply define what primitive you are going to render. In this case, we are going to render a Rectangle (called a Quad - 4 sides). glEnd is used to signal the end you rendering that primitive.

Now, when we go to render our primitive what we have to do is define each corner of that primitive. So, we start with the first corner (or vertex), specify the coordinates, and then move to the next, and so on. At the same time, I am defining the colors of each corner (each corner will have a different color). What OpenGL will do for us is color the entire primitive blending those four corner's colors together. It ends up looking like this:

Lastly, we call SDL_GL_SwapBuffers which basically takes the buffer and moves it to the primary display - much like SDL_Flip.

And that's it! I hope you see how easy it is to setup OpenGL. There's quiet a bit to memorize at first, but once you have it down, it's not that bad at all.

If you want more information about specific functions, check out the docs here:
http://www.opengl.org/sdk/docs/man/


Tutoriel SDL

SDL TTF

Fonts, and how to use them

Text is an important part of almost all games. From informing the player of the awesome combo that they just got, to menu titles, button captions and epic storylines. In this tutorial, we will take a look at SDL_ttf, an addon library for SDL that uses the FreeType library to load fonts and render text. SDL_ttf adheres to the C-style syntax of SDL, so if you are familiar with SDL (which you hopefully are) it won't be too hard to get used to SDL_ttf's calling conventions. You can get the newest version as of this writing (2.0.10-1 to be exact) here.

Let's start by defining some font terms which we will use throughout this tutorial:

Font face: A style of a font. For example bold or italic. Font files can contain more than one font face.
Point size: The font measure originally used in typography. Some but not all digital fonts use point sizes and one of these is roughly 1/72 of an inch (~2.54cm), but this is by no means a guarantee.
Glyph: A single character of a font, like 'a'.

Initialization, shutdown and library versions

Loading and unloading SDL_ttf is very similar to that of SDL. All SDL_ttf functions are prefixed with 'TTF_'. Before we can use any of these functions, however, we need to initialize the library:
if (TTF_Init() < 0) {
    // Handle error...
}
TTF_Init() returns -1 if an error occurred and zero if the initialization was successful. Similar to SDL_WasInit(...), you can check if SDL_ttf was successfully initialized like this:
int was_init = TTF_WasInit();

if (was_init == 1)
    // SDL_ttf was already initialized
else if (was_init == 0)
    // SDL_ttf was not already initialized
When you are done with your sweet text-rendering business, SDL_ttf is shut down like this:
void TTF_Quit();
Alternatively, you can use the atexit(...) function to ensure it is automatically called when the program terminates:
#include <cstdlib> // The function is defined in this header

// ...

atexit(TTF_Quit); // Ensure TTF_Quit() is called when we exit
You can also check the version of the linked SDL_ttf library you are using at runtime. Unlike the other functions provided by SDL_ttf, these two do not need any prior initialization to be used:
const SDL_version *TTF_Linked_Version();
void SDL_TTF_VERSION(SDL_version *compile_version);
Here's an example:
#include <iostream>

const SDL_version *linked_version = TTF_Linked_Version();
SDL_version compiled_version;
SDL_TTF_VERSION(&compiled_version);

std::cout << "Linked version:\n"
        << linked_version->major << "." << linked_version->minor << "." << linked_version->patch;

std::cout << "Compiled version:\n"
        << compiled_verison.major << "." << compiled_version.minor << "." << compiled_version.patch << std::endl << std::endl;
Note: I am using the standard I/O (input/output) library to print text to the console window. This gives the same result as using the C function printf(...)

Opening and closing fonts

SDL_ttf uses a single struct to represent its fonts: TTF_Font. The documentation warns not to directly access its contents as they may change in future releases and deprecate projects using older versions. Use the available functions instead. SDL_ttf supports loading of TrueType fonts (with the '.ttf' extension) and opening a font is quite simple. There are four ways of doing it and this is the first one:
TTF_Font* TTF_OpenFont(const char* file, int ptsize);
The first function takes a TrueType font file and a point size and returns the loaded TTF_Font pointer or NULL if loading failed. The point size is a somewhat vague term as mentioned earlier on. It used to be a measure of the height of the metal frame of characters used in typography in olden times, but in digital fonts it is a bit more complicated. I suggest that you experiment with the size until you get the one you want instead of worrying about point sizes because you seldom know what size you want beforehand anyways. If you still want to know about point sizes, take a look in the suggested reading section at the bottom of this tutorial.

The second function looks like this:
TTF_Font* TTF_OpenFontIndex(const char* file, int ptsize, long index);
It is identical to the previous function apart from the last argument. This function loads a specific font face from the file, and is useful if your font file contains several font faces. A font index of 0 is always the first font in the file.

The third function is:
TTF_Font* TTF_OpenFontRW(SDL_RWops *src, int freesrc, int ptsize);
You may or may not have seen the SDL_RWops struct before. It is an undocumented feature of SDL and so is rarely seen (I personally have never seen nor used it). It is capable of taking a pointer to memory where some data is stored, but files are supported too. Since this tutorial is about SDL_ttf and not SDL_RWops, I've added a suggested reading entry at the bottom if you want to know more. If you pass a non-zero value as the second argument the memory used by the SDL_RWops struct is automatically freed before the function returns. The third and last argument is the point size.

The final function does the same thing as the previous one, but loads a specific font face from the SDL_RWops struct.
TTF_Font* TTF_OpenFontIndexRW(SDL_RWops *src, int freesrc, int ptsize, long index);
Just like SDL_Surfaces, there is a single function to free a TTF_Font:
void TTF_CloseFont(TTF_Font *font);
When a function fails to load, you may use TTF_SetError and TTF_GetError just as you use the equivalent functions of SDL:
void TTF_SetError(const char *fmt, ...)
char *TTF_GetError()
These functions are actually defined macros to SDL_SetError(...) and SDL_GetError() and so work just like them:
if (/* loading failed */) {
    TTF_SetError("Loading failed :( (code: %d)", 142);
    std::cout << "Error: " << TTF_GetError() << std::endl;
}

Rendering fonts

Let's get to the rendering part and get some cool fonts onto the screen. There are twelve functions int total available for rendering which I have chosen to split into three categories: Solid-, shaded- and blended rendering. Here are the first four for solid rendering:
SDL_Surface *TTF_RenderGlyph_Solid(TTF_Font* font, Uint16 ch, SDL_Color fg);
SDL_Surface *TTF_RenderText_Solid(TTF_Font *font, const char *text, SDL_Color fg);
SDL_Surface *TTF_RenderUTF8_Solid(TTF_Font *font, const char *text, SDL_Color fg);
SDL_Surface *TTF_RenderUNICODE_Solid(TTF_Font *font, const Uint16 *text, SDL_Color fg);

Solid rendering is the fastest type of text rendering but gives you the lowest quality. It is best used for text that is expected to change frequently. The first function renders a single glyph while the others render full text strings. However, the last two functions render with text encoding. If you are unfamiliar with the UNICODE standard or UTF-8, I've added entries in the suggested reading section, but I'll give a short introduction to both of them. UNICODE is a standard for text encoding. It defines a set of rules for characters. UNICODE text encoding can be used with different character encodings, one of the most popularly being UTF-8. UTF stands for Unicode transformation format, and the number denotes the bit-size used for the encoding. For example, the character 'T' encodes as U+0054. This is properly a bit confusing, so I advise you to go read the suggested reading entries for a more thorough explanation.

Next up is shaded rendering:

SDL_Surface *TTF_RenderGlyph_Shaded(TTF_Font* font, Uint16 ch, SDL_Color fg, SDL_Color bg);
SDL_Surface *TTF_RenderText_Shaded(TTF_Font *font, const char *text, SDL_Color fg, SDL_Color bg);
SDL_Surface *TTF_RenderUTF8_Shaded(TTF_Font *font, const char *text, SDL_Color fg, SDL_Color bg);
SDL_Surface *TTF_RenderUNICODE_Shaded(TTF_Font *font, const Uint16 *text, SDL_Color fg, SDL_Color bg);
Shaded rendering gives a nicer anti-aliased result, but is slower. Additionally, the text will have a box around it. The color of this box is the bg (background) color you pass to the function. If you're unsure what anti-aliased means, it's when an image (like text) is surrounded at its edges by pixels shaded roughly the same color as that part of the image. This results in a more smooth and nicer-looking image. Here's an example to explain what I mean:

Text with and without anti-aliasing

The last four functions are:
SDL_Surface *TTF_RenderGlyph_Blended(TTF_Font *font, Uint16 ch, SDL_Color fg);
SDL_Surface *TTF_RenderText_Blended(TTF_Font *font, const char *text, SDL_Color fg);
SDL_Surface *TTF_RenderUTF8_Blended(TTF_Font *font, const char *text, SDL_Color fg);
SDL_Surface *TTF_RenderUNICODE_Blended(TTF_Font *font, const Uint16 *text, SDL_Color fg);
Blended rendering yields the best results and as you might suspect it is the slowest of all the rendering functions though not really that much slower relative to shaded rendering. The returned SDL_Surface is a 32-bit ARGB surface with alpha transparency and anti-aliasing. Because this surface will blit slower than either solid or shaded rendered surfaces, you should use it for text that isn't expected to change a lot.

Here's an image of three types of rendering so you can see the differences:

Solid, shaded and blended rendering

Font properties

Fonts have properties and attributes that you can both set and retrieve. One is the font style. Bold, italic and underline are examples of a font style. Additionally, the newest version of SDL_ttf provides the strikethrough style as well, which puts a line right through the middle of the text. To set a font's style you use the following function:
void TTF_SetFontStyle(TTF_Font *font, int style);
The first argument is a pointer to the font for which you wish to change the style. The second is an integer that denotes which style you want to change to. This argument can be any of the below:
    TTF_STYLE_BOLD
    TTF_STYLE_ITALIC
    TTF_STYLE_UNDERLINE
    TTF_STYLE_STRIKETHROUGH
    TTF_STYLE_NORMAL
These are all bitmasks and so can be combined just like the entity flags used in the SDL Collision tutorial:
TTF_SetFontStyle(myFont, TTF_STYLE_BOLD | TTF_STYLE_UNDERLINE);
Use TTF_STYLE_NORMAL to reset all styles applied to a font. As you may expect, there's an accompaniying get function for getting the styles applied to a font:
int TTF_GetFontStyle(TTF_Font *font);
This returns the bitmask flags set for the font you pass to the function. It returns TTF_STYLE_NORMAL if no style applies. If you don't know how to use bitmasks, fear not! This is how:
int myFontStyles = TTF_GetFontStyle(myFont);

if (myFontStyles & TTF_STYLE_BOLD) {
    // Font is bold
}
If you want your font to have an outline, that's possible too. An outline is an additional border around the font. Here's an example:

Example of an outlined font

Just as with most of these functions, there is a set/get function pair available for font outlines as well:
void TTF_SetFontOutline(TTF_Font *font, int outline);
int TTF_GetFontOutline(TTF_Font *font);
The second parameter to TTF_SetFontOutline is the outline width in pixels. Passing zero turns outlines off. The second function simply returns the current outline width.

SDL_ttf also allows you to turn kerning on and off. What is kerning you ask? Let's look at a string like "AV". Here's what it will look like with and without kerning:

Text with and without kerning

See the difference? In the first image, the 'V' is rendered besides the 'A' such that you could draw a line between the letters that will just touch the edge of each letter. In the second, the letters have been moved closer together. This essentially "compresses" the string because it uses the "free space" that is not available without kerning. Note that this is not always possible everytime. For example, in the string "AA" you can't move either 'A' further towards each other.

The SDL_ttf set/get function pair is:
void TTF_SetFontKerning(TTF_Font *font, int allowed);
int TTF_GetFontKerning(TTF_Font *font);
Passing zero to the second parameter of TTF_SetFontKerning disables kerning. A non-zero integer value turns kerning on. The second function returns zero if kerning is disabled and a non-zero value if kerning is on.

Note: For newly loaded fonts, the default kerning is 1, i.e. on.

Now we take a look at some functions that deal more with the actual physical size of the text:
int TTF_FontHeight(const TTF_Font *font);
int TTF_FontAscent(const TTF_Font *font);
int TTF_FontDescent(const TTF_Font *font);
int TTF_FontLineSkip(const TTF_Font *font);
int TTF_SizeText(TTF_Font *font, const char *text, int *w, int *h);
int TTF_SizeUTF8(TTF_Font *font, const char *text, int *w, int *h);
int TTF_SizeUNICODE(TTF_Font *font, const Uint16 *text, int *w, int *h);
Refer to this picture for glyph metric conventions:

Glyph metric convention

Let's work our way through all these functions from top to bottom. The first function returns the maximum pixel height of the glyphs loaded for the particular font. That is, the pixel height of the tallest glyph contained within the font.

The second returns the maximum pixel ascent of the font. The ascent is the distance from the top of the glyph to its baseline.

The third function returns the maxmum pixel descent of the font. This is the distance from the baseline to the bottom of the font.

Although you can use TTF_FontHeight to space multi-line text, it is better to provide some space between each line. This space is what the fourth function TTF_FontLineSkip returns.

Note: TTF_FontLineSkip returns only a suggested value for the size of the line gap. Not necessarily the "correct" one.

The final three functions return information about the size of a text string if it was rendered. TTF_SizeText fills the w and h variables with the correct values. The function returns zero if the third and fourth parameters were successfully filled, -1 otherwise. The other two functions with similar names do the same but for UTF8- and UNICODE-encoded strings.

Here's an example for the first function:
#include <iostream>

int w = 0;
int h = 0;

if ((TTF_SizeText(myFont, "SDL_ttf is awesome!", &w, &h) != -1) {
    // Print out the width and height of the string if I render it with myFont
    std::cout << "Width : " << w << "\nHeight: " << h << std::endl;
} else {
    // Error...
}
Next is font hinting. Font hinting "hints" to SDL_ttf how to draw the font using mathematical formulas. The proper details are beyond the scope of this tutorial though.
void TTF_SetFontHinting(TTF_Font *font, int hinting);
int TTF_GetFontHinting(TTF_Font *font);
The second parameter of TTF_SetFontHinting can be either of the following:
    TTF_HINTING_NORMAL
    TTF_HINTING_LIGHT
    TTF_HINTING_MONO
    TTF_HINTING_NONE
The documentation advises you to experiment with these settings, as do I. Be careful though! Calling this function flushes the internal cache of pre-rendered glyphs. This is true even if you pass the same hinting parameter already set for the font. Therefore, you should check the font's hinting parameter with TTF_GetFontHinting before you decide to change it.

There are also a couple of functions available if you want to get some information about the font face you are using:
long TTF_FontFaces(const TTF_Font *font);
int TTF_FontFaceIsFixedWidth(const TTF_Font *font);
char * TTF_FontFaceFamilyName(const TTF_Font *font);
char * TTF_FontFaceStyleName(const TTF_Font *font);
The first function returns the number of font faces contained in the given font.
The second function returns zero if the font face is not fixed width and a value above zero if it is. A font with fixed width means that all its characters (glyphs) have the exact same width.
The third gets the family name of a font. These can be names like: Arial or Verdana that you use in a word processor.
The fourth function returns the name of this font's style. Some fonts are all in bold or italic and this is what you'll get from this function. If this function returns NULL, no information could be retrieved.

As for information about glyphs, SDL_ttf provides these two:
int TTF_GlyphIsProvided(const TTF_Font *font, Uint16 ch);
int TTF_GlyphMetrics(TTF_Font *font, Uint16 ch, int *minx, int *maxx, int *miny, int *maxy, int *advance);
The first tells you whether or not the given character is available for the font:
int temp = TTF_GlyphIsProvided(myFont, 'a');

if (temp)
    // glyph is available :D
else
    // glyph is unavailable :(
Note that if the glyph is available the returned integer is actually the glyph index in the font.

The second function fills in minx, maxx, miny, maxy and advance with the proper values if possible. These values correspond those on the image I showed you earlier for glyph metric conventions. Here's an example:
int minx = 0;
int maxx = 0;
int miny = 0;
int maxy = 0;
int advance = 0;

int success = TTF_GlyphMetrics(myFont, 'a', &minx, &maxx, &miny, &maxy, &advance);

if (success == 0) {
    // Yay! No errors
    std::cout << "Min X: " << minx
            << "\nMax X: " << maxx
            << "\nMin Y: " << miny
            << "\nMax Y: " << maxy
            << "\nAdvance: " << advance << std::endl;
} else if (success == -1) {
    // Ohoh...
}
If the function returns -1 for failure, your values get filled with NULL, so watch out!

And so we now take a look at the last function of the SDL_ttf, phew!
void TTF_ByteSwappedUNICODE(int swapped);
This function tells SDL_ttf if UNICODE text is byte swapped or not. What? Well, this has to do with the endianness of your computer. Umm...what? Okok, without going into too much detail, endianness deals with the ordering of bytes in a computer. Hence another word for endianness is "byte order". There's an entry in suggested reading section about endianness.

As a final note, almost all of SDL_ttf's functions that take a pointer to a TTF_Font cause a segfault error if you pass NULL instead, so check the validity of your font pointer before you use it.
if (myFont)
    // Font is OK!
else
    // Font is not OK!

Entering text

Here's a quick example of how to get text entered by the user, that I picked up a while back from the Lazyfoo SDL tutorial site.
std::string text = "";
SDL_EnableUNICODE(SDL_ENABLE);

void OnKeyDown(SDLKey key, SDLMod modifier, Uint16 unicode) {

    if(key == SDLK_BACKSPACE) {
        if(!text.empty())
            text.erase(text.name.length() - 1);
    } else if(key == SDLK_RETURN) {
        if(!text.empty()) {
            // Do stuff with the text
        }
    } else {
        if(text.length() < MAX_NAME_SIZE) {
            if(unicode == ' ' ||
               unicode >= 'a' && unicode <= 'z' ||
               unicode >= 'A' && unicode <= 'Z' ||
               unicode >= '0' && unicode <= '9')
                text.name += (char)unicode;
        }
    }
}

SDL_EnableUNICODE(SDL_DISABLE);

Closing words

And that's all the functions provided by SDL_ttf. Hopefully, you now understand how to use the library or discovered some interesting things you didn't know about it before. I wish you good luck in all your future font-related endeavours :)

As an added bonus, I've included a small sample for you where you can try some the things we've talked in the tutorial for yourself. Enter the directory path of the font you want to load in the "fontname.txt" file and try it out. The sample has only been tested on a Windows computer, but should hopefully work on the other platforms. The controls are:

[s]: Solid rendering
[d]: Shaded rendering
[b]: Blended rendering
[k]: Toggle kerning
[o] + [0-3]: Set outline width
[Esc]: Quit the application

This is my first tutorial ever! So please bear with me if there are some mistakes or pecularities in the text. Hopefully you enjoyed it anyway! I would appreciate any feedback you guys have on how to improve, including any corrections.

Suggested reading

SDL_ttf documentation
UNICODE article entry on Wikipedia
UTF-8 article entry on Wikipedia
SDL_RWops
Endianness

Downloadable fonts (always check how "free" fonts are):
http://www.1001freefonts.com/
http://www.urbanfonts.com/
http://www.dafont.com/


Tutoriel SDL

SDL App States

Or Game States, whatever you prefer
In this muchly desired side tutorial we are going to look at how to implement something most commonly called Game States (or as I call them, App States). This tutorial will be based upon the code from the SDL Collision Events tutorial.

To simply explain what an app state is, try to think of your game (or application) as a parent with many child applications. In other words, your primary application can yield control to a single child when needed.

This is probably best explained with an example. Say we are working on a typical game - like a simple side scroller (such as Mario). What would the different application states be in this case?

1. Splash Screen
2. Menu
3. World Overview
4. Game
5. Credits

So, to tie into my previous explanation, when the parent application launches it first loads the child app state Splash Screen and yields control to it. This allows the Splash Screen to render a neat graphic, animate, and do its thing. Once it's done, it turns over control to the Menu. The menu then captures input events, and allows the user to make some choices. If the user clicks Play (or whatever), the menu app state will then yield control over to the World Overview app state. I think you get the picture.

The basic premise of app states is to turn over control to another app state when certain criteria is met (usually a timer or input event).

Just by understanding what's above, we could simply have a pointer that points to the active app state, and then switch between objects. And that does work. But we're going to take that a step further and introduce what I call an App State Manager. The purpose of the manager is to handle switching between app states. Why would we want something to manage that? Well, for example, when we switch between a splash screen and the main menu we could do some neat fade out / fade in effect. Without an app state manager, that would still be possible, but not nearly as neat.

Here's how that class would look (CAppStateManager.h):
#ifndef __CAPPSTATEMANAGER_H__
    #define __CAPPSTATEMANAGER_H__

#include "CAppState.h"

enum {
    // Add your Other App States Here
    APPSTATE_NONE,
    APPSTATE_INTRO,
    APPSTATE_GAME
};

class CAppStateManager {
    private:
        static CAppState* ActiveAppState;

    public:
        static void OnEvent(SDL_Event* Event);

        static void OnLoop();

        static void OnRender(SDL_Surface* Surf_Display);

    public:
        static void SetActiveAppState(int AppStateID);

        static CAppState* GetActiveAppState();
};

#endif
Notice that every member is static. Why? Basically because this class is a singleton, and we I want it easy to use. So, instead of coding something like CAppStateManager::GetInstance()->OnRender(), we can simply do CAppStateManager::OnRender(). Which looks better to you?

Notice the enum and the 3 App States that I created. We are going to create an Intro App State and Game App State for this tutorial. When you want to add more app states, you'll need to add them to this enum.

So lets continue and implement the rest of the code for this class (CAppStateManager.cpp).
#include "CAppStateManager.h"

// Refer to your Other App States Here
#include "CAppStateIntro.h"
#include "CAppStateGame.h"

CAppState* CAppStateManager::ActiveAppState = 0;

void CAppStateManager::OnEvent(SDL_Event* EventHolder) {
    if(ActiveAppState) ActiveAppState->OnEvent(EventHolder);
}

void CAppStateManager::OnLoop() {
    if(ActiveAppState) ActiveAppState->OnLoop();
}

void CAppStateManager::OnRender(SDL_Surface* Surf_Display) {
    if(ActiveAppState) ActiveAppState->OnRender(Surf_Display);
}

void CAppStateManager::SetActiveAppState(int AppStateID) {
    if(ActiveAppState) ActiveAppState->OnDeactivate();

    // Also, add your App State Here so that the Manager can switch to it
    if(AppStateID == APPSTATE_NONE)        ActiveAppState = 0;
    if(AppStateID == APPSTATE_INTRO)    ActiveAppState = CAppStateIntro::GetInstance();
    if(AppStateID == APPSTATE_GAME)        ActiveAppState = CAppStateGame::GetInstance();

    if(ActiveAppState) ActiveAppState->OnActivate();
}

CAppState* CAppStateManager::GetActiveAppState() {
    return ActiveAppState;
}
One important side note before we continue, notice how I am including the other app state classes. Just like in the header file, you'd want to include the header files for your other app states there, as well as inside the SetActiveAppState method. But, more importantly, notice how I am including the other app states inside the .cpp file instead of the header file. This is to avoid something called circular dependencies. When we begin to creat the other app state files we are going to include this CAppStateManager class. If the CAppStateManager class was including the header files for the other app states inside its header, you'd run into an include loop.

To better explain this:
#ifndef __CAPPSTATEMANAGER_H__
    #define __CAPPSTATEMANAGER_H__

#include "CAppStateIntro.h"

class CAppStateManager {
    // Other Code
};

#endif

#ifndef __CAPPSTATEINTRO_H__
    #define __CAPPSTATEINTRO_H__

#include "CAppStateManager.h"

class CAppStateIntro {
    // Other Code
};

#endif
Do you see the issue? Each is including the other and the includes will never end (This statement is not quite right. Because of the #define's I have in each header file, the files will only be compiled once in the project. However, you'll still run into issues, so the following still applies.). To remedy this we refer to the class we need inside the .cpp file.

Lets continue on and go through this class line by line. First we have a static member that refers to the current active app state. This pointer will always point to some other app state class. It's what we'll use when we want an app state to be active.

Next we have the familiar OnEvent, OnLoop, and OnRender methods. Inside those all we're simply doing is calling the respective method of the active app state. So, if the Intro app state is active, we'll let the OnEvent, OnLoop, and OnRender methods of that app state run.

Next we have SetActiveAppState which will simply do what it says, set the active app state. We pass an enum value to it from our header file. We then let the active app state know it's being deactivated. Then, find the appropriate app state matched to the enum value, and let it know it's being activated. Simple enough.

Lastly we have a simple method to retrieve the active app state.

Now that we have our manager class up and working, lets go ahead and create some app states. First, we're going to need to create a class that all our app states can inherit from. Appropriately, we'll call this class CAppState (CAppState.h):
#ifndef __CAPPSTATE_H__
#define __CAPPSTATE_H__
#include "CEvent.h"

class CAppState : public CEvent {
    public:
        CAppState();

    public:
        virtual void OnActivate() = 0;

        virtual void OnDeactivate() = 0;

        virtual void OnLoop() = 0;

        virtual void OnRender(SDL_Surface* Surf_Display) = 0;
};

#endif
The class itself is pretty simple. All we're doing is laying out the common methods for an app state, and requiring those to be defined (that's what the '= 0' does, it requires a virtual method to be defined in a child class). Also notice that the CAppState class is inheriting CEvent.

Lets look inside the class (CAppState.cpp):
#include "CAppState.h"

CAppState::CAppState() {
}
Now, don't think about this too hard. Remember all the methods are virtual and required to be defined by classes that inherit CAppState. So, we don't really have to define much of anything inside the class itself.

Okay, now lets actually create our two app states. We'll create our Intro app state first (CAppStateIntro.h).
#ifndef __CAPPSTATEINTRO_H__
    #define __CAPPSTATEINTRO_H__

#include "CAppState.h"
#include "CSurface.h"

class CAppStateIntro : public CAppState {
    private:
        static CAppStateIntro Instance;

        SDL_Surface* Surf_Logo;

        int StartTime;

    private:
        CAppStateIntro();

    public:
        void OnActivate();

        void OnDeactivate();

        void OnLoop();

        void OnRender(SDL_Surface* Surf_Display);

    public:
        static CAppStateIntro* GetInstance();
};

#endif
This class should be pretty straightforward. Remember, in the end, our app states are going to look and behave much like our regular CApp class. So, first we have our instance of this class (which the CAppStateManager uses to set the active app state), a logo to display, and StartTime variable. We'll be using the StartTime variable to do a simple timer to show the splash and then advance to the next app state. You can grab the Logo from the Files area on the sidebar.

Here's inside the class (CAppStateIntro.cpp).
#include "CAppStateIntro.h"

#include "CAppStateManager.h"

CAppStateIntro CAppStateIntro::Instance;

CAppStateIntro::CAppStateIntro() {
    Surf_Logo = NULL;
}

void CAppStateIntro::OnActivate() {
    // Load Simple Logo
    Surf_Logo = CSurface::OnLoad("splash.png");

    StartTime = SDL_GetTicks();
}

void CAppStateIntro::OnDeactivate() {
    if(Surf_Logo) {
        SDL_FreeSurface(Surf_Logo);
        Surf_Logo = NULL;
    }
}

void CAppStateIntro::OnLoop() {
    if(StartTime + 3000 < SDL_GetTicks()) {
        CAppStateManager::SetActiveAppState(APPSTATE_GAME);
    }
}

void CAppStateIntro::OnRender(SDL_Surface* Surf_Display) {
    if(Surf_Logo) {
        CSurface::OnDraw(Surf_Display, Surf_Logo, 0, 0);
    }
}

CAppStateIntro* CAppStateIntro::GetInstance() {
    return &Instance;
}
Inside of OnActivate we load our logo, and then start the timer. Inside of OnDeactivate we free our logo. Inside of OnLoop all we're going is checking if 3 seconds have passed; if so, we change the app state. Inside of OnRender we simply blit the logo. And, of course, our GetInstance returns the Instance to the class.

Lets create your Game app state now (CAppStateGame.h).
#ifndef __CAPPSTATEGAME_H__
#define __CAPPSTATEGAME_H__
#include "CAppState.h"
#include "CArea.h"
#include "CCamera.h"
#include "CEntity.h"
#include "CSurface.h"
#include "CPlayer.h"

class CAppStateGame : public CAppState {
    private:
        static CAppStateGame Instance;

        CPlayer    Player;
        CPlayer    Player2;

    private:
        CAppStateGame();

    public:
        void OnKeyDown(SDLKey sym, SDLMod mod, Uint16 unicode);

        void OnKeyUp(SDLKey sym, SDLMod mod, Uint16 unicode);

    public:
        void OnActivate();

        void OnDeactivate();

        void OnLoop();

        void OnRender(SDL_Surface* Surf_Display);

    public:
        static CAppStateGame* GetInstance();
};

#endif
Lots more going on here? Not really, I want you to notice that I simply moved over a lot of the code from CApp.h over to this new file. This is what our CApp.h looks like now:
#ifndef _CAPP_H_
#define _CAPP_H_
#include <SDL.h>
#include "Define.h"
#include "CAppStateManager.h"
#include "CFPS.h"
#include "CEvent.h"
#include "CSurface.h"

class CApp : public CEvent {
    private:
        bool            Running;

        SDL_Surface*    Surf_Display;

    public:
        CApp();

        int OnExecute();

    public:
        bool OnInit();

        void OnEvent(SDL_Event* Event);

            void OnExit();

        void OnLoop();

        void OnRender();

        void OnCleanup();
};

#endif
I basically moved over OnKeyDown, OnKeyUp, the Player objects, and some #include directives (you may notice though that I added #include "CAppStateManager.h" to CApp.h).

Lets create the code for inside our class now (CAppStateGame.cpp).
#include "CAppStateGame.h"

CAppStateGame CAppStateGame::Instance;

CAppStateGame::CAppStateGame() {
}

void CAppStateGame::OnKeyDown(SDLKey sym, SDLMod mod, Uint16 unicode) {
    switch(sym) {
        case SDLK_LEFT: {
            Player.MoveLeft = true;
            break;
        }

        case SDLK_RIGHT: {
            Player.MoveRight = true;
            break;
        }

        case SDLK_SPACE: {
            Player.Jump();
            break;
        }

        default: {
        }
    }
}

void CAppStateGame::OnKeyUp(SDLKey sym, SDLMod mod, Uint16 unicode) {
    switch(sym) {
        case SDLK_LEFT: {
            Player.MoveLeft = false;
            break;
        }

        case SDLK_RIGHT: {
            Player.MoveRight = false;
            break;
        }

        default: {
        }
    }
}

void CAppStateGame::OnActivate() {
    if(CArea::AreaControl.OnLoad("./maps/1.area") == false) {
        return;
    }

    if(Player.OnLoad("yoshi.png", 64, 64, 8) == false) {
        return;
    }

    if(Player2.OnLoad("yoshi.png", 64, 64, 8) == false) {
        return;
    }

    Player2.X = 100;

    CEntity::EntityList.push_back(&Player);
    CEntity::EntityList.push_back(&Player2);

    CCamera::CameraControl.TargetMode = TARGET_MODE_CENTER;
    CCamera::CameraControl.SetTarget(&Player.X, &Player.Y);
}

void CAppStateGame::OnDeactivate() {
    CArea::AreaControl.OnCleanup();

    for(int i = 0;i < CEntity::EntityList.size();i++) {
        if(!CEntity::EntityList[i]) continue;

        CEntity::EntityList[i]->OnCleanup();
    }

    CEntity::EntityList.clear();
}

void CAppStateGame::OnLoop() {
    for(int i = 0;i < CEntity::EntityList.size();i++) {
        if(!CEntity::EntityList[i]) continue;

        CEntity::EntityList[i]->OnLoop();
    }

    //Collision Events
    for(int i = 0;i < CEntityCol::EntityColList.size();i++) {
        CEntity* EntityA = CEntityCol::EntityColList[i].EntityA;
        CEntity* EntityB = CEntityCol::EntityColList[i].EntityB;

        if(EntityA == NULL || EntityB == NULL) continue;

        if(EntityA->OnCollision(EntityB)) {
            EntityB->OnCollision(EntityA);
        }
    }

    CEntityCol::EntityColList.clear();
}

void CAppStateGame::OnRender(SDL_Surface* Surf_Display) {
    SDL_Rect Rect;
    Rect.x = 0;
    Rect.y = 0;
    Rect.w = WWIDTH;
    Rect.h = WHEIGHT;

    SDL_FillRect(Surf_Display, &Rect, 0);

    CArea::AreaControl.OnRender(Surf_Display, -CCamera::CameraControl.GetX(), -CCamera::CameraControl.GetY());

    for(int i = 0;i < CEntity::EntityList.size();i++) {
        if(!CEntity::EntityList[i]) continue;

        CEntity::EntityList[i]->OnRender(Surf_Display);
    }
}

CAppStateGame* CAppStateGame::GetInstance() {
    return &Instance;
}
You'll notice all the same things from our Intro class, and you'll also notice all the code that used to be inside of the CApp_* files. OnActivate code was taken from CApp_OnInit, OnLoop code was taken from CApp_OnLoop, OnRender code was taken from CApp_OnRender, and OnDeactivate code was taken from CApp_OnCleanup. We also moved over the code for the keyboard controls for Yoshi. We moved all the code over because it's the job of the Game app state now to actually run the game.

So, lets take a look at the CApp_* files.

CApp_OnInit:
#include "CApp.h"

bool CApp::OnInit() {
    if(SDL_Init(SDL_INIT_EVERYTHING) < 0) {
        return false;
    }

    if((Surf_Display = SDL_SetVideoMode(WWIDTH, WHEIGHT, 32, SDL_HWSURFACE | SDL_DOUBLEBUF)) == NULL) {
        return false;
    }

    SDL_EnableKeyRepeat(1, SDL_DEFAULT_REPEAT_INTERVAL / 3);

    CAppStateManager::SetActiveAppState(APPSTATE_INTRO);

    return true;
}
CApp_OnEvent:
#include "CApp.h"

void CApp::OnEvent(SDL_Event* Event) {
    CEvent::OnEvent(Event);

    CAppStateManager::OnEvent(Event);
}

void CApp::OnExit() {
    Running = false;
}
CApp_OnLoop:
#include "CApp.h"

void CApp::OnLoop() {
    CAppStateManager::OnLoop();

    CFPS::FPSControl.OnLoop();

    char Buffer[255];
    sprintf(Buffer, "FPS: %d", CFPS::FPSControl.GetFPS());
    SDL_WM_SetCaption(Buffer, Buffer);
}
CApp_OnRender:
#include "CApp.h"

void CApp::OnRender() {
    CAppStateManager::OnRender(Surf_Display);

    SDL_Flip(Surf_Display);
}
CApp_OnCleanup:
#include "CApp.h"

void CApp::OnCleanup() {
    CAppStateManager::SetActiveAppState(APPSTATE_NONE);

    SDL_FreeSurface(Surf_Display);
    SDL_Quit();
}
All of this should be self-explanatory, but to help you out, all we're doing really is calling the appropriate method inside of the CAppStateManager class. This will in turn call the appropriate class inside of the app state itself.

And that's it! Try making the splash screen fade, jump, or do other crazy things. Also, try adding animations for when app states change.

x


 Tutoriel SDL

SDL Tutorial Basics

We all have to start somewhere
These tutorials are meant to teach someone with some experience using C++, or perhaps another programming language. If you have a hard time following certain ideas related to the code itself and not conceptual ideas (those relating to games), then I urge you to first read our tutorials that explain the C++ programming language. It's not vital to understand everything about C++, but every little bit will help later on.

In these tutorials we will be referncing to CodeBlocks as our IDE of choice, with gcc and mingw for compilation. If you wish to use another IDE and compiler that is up to you, but it may be harder to follow along if you have limited experience with linking libraries. If you wish to download CodeBlocks, you can do so for free at http://www.codeblocks.org (download the one including the mingw package). We recommend you use the stable version, unless you want to spend the extra time and use the nightly builds.

These tutorials will be heavily centered around SDL (Simple DirectMedia Layer), a 2D cross-platform graphics library. This library is going to allow us to draw fancy graphics to the screen, and do all sorts of fun stuff that allows us to make a game. You're going to need to download this library at http://www.libsdl.org; be sure to download the Mingw32 tar file under "Development Libraries" and "Win32" and the zip file under "Runtime Libraries" and "Win32." If you are using Visual Studio download the appropriate version instead of the Mingw32 file. Once downloaded, we recommend you put the .dll found in the zip in your system32 folder. This way whenever we open an SDL application it can find the dll file it needs.

Open the tar file now (the one that was listed under "Development Libraries") and decompress the contents to a folder (such as C:\SDL). Now, open up CodeBlocks so we can change some settings. Click "Settings" on the menu bar, and then click "Search Directories" tab. You'll need to add C:\SDL\include to the "Compiler" tab, and C:\SDL\lib to the "Linker" tab (change C:\SDL to wherever you decompressed the files to). Once done, click Okay.

Important Note: I use #include <SDL.h> below (and in all other tutorials), when it really should be #include <SDL/SDL.h>. I moved all of my header files up a directory. So, if you want to follow my code exactly, move all your header files up a directory so they are in the include folder, not the SDL folder. Otherwise, use #include instead. Sorry for all the confusion this has caused.

Start a new "blank" project, calling it whatever you want. Save it some place. Click on "Project" on the menu bar, and then click on "Properties." A dialog will open; click on the "Project's build options..." button on the bottom right. Click on the "Linker Settings" tab and add the following to the list under "Link libraries":

mingw32
SDLmain
SDL

The order is important, so use the arrows to rearrange the list until it matches what is shown above. If you are having a little trouble figuring out what we are doing, we're simply "linking" code together, or, in other words, we're taking the SDL code and combining it with our own. We do this by using the include files to compile, and the lib files to link. And once done, our code is put together to make one application.

Click Ok twice, and you should be all set to go!

Let's create two new files called CApp.h, and CApp.cpp; these will serve as the core for our program. First, open CApp.h and add the following code, in here is where our tutorial will really begin:

#ifndef _CAPP_H_
    #define _CAPP_H_

#include <SDL.h>

class CApp {

    public:

        CApp();

        int OnExecute();

};

#endif
Now, open up CApp.cpp and add the following code:

#include "CApp.h"

CApp::CApp() {
}

int CApp::OnExecute() {
    return 0;
}

int main(int argc, char* argv[]) {
    CApp theApp;

    return theApp.OnExecute();
}
The CApp class is setting the stage for our entire program. Let me step aside to take special note of how games are typically setup. Most games consist of 5 functions that handle how the game processes. These processes are typically:

Initialize
This function handles all the loading of data, whether it be textures, maps, NPCs, or whatever.

Event
This function handles all input events from the mouse, keyboard, joysticks, or other devices.

Loop
This function handles all the data updates, such as a NPCs moving across the screen, decreasing your health bar, or whatever

Render
This function handles all the rendering of anything that shows up on the screen. It does NOT handle data manipulation, as this is what the Loop function is supposed to handle.

Cleanup
This function simply cleans up any resources loaded, and insures a peaceful quitting of the game.

It's important to understand that games are one gigantic loop. Within this loop we find events, update data, and render pictures. Thus, the basic structure could be thought of as:

Initialize();

while(true) {
    Events();
    Loop();
    Render();
}

Cleanup();
Each iteration of the loop we do something with data, and render accordingly. Events are extra, as a means for the user to manipulate data as well. In that sense, Events aren't required to make a game, but are needed when you want the user to be able to manipulate data (such as move a NPC to the left).

Let's clarify this idea with an example. Say we have our Knight, the hero of the game. All we want to do is simply have him move around. If I press left, he goes left. We need to figure out how to do this within a loop. Firstly, we know we want to check for events (keyboard events). Since Events are a means to manipulate data, we also know we'll need some sort of variables to modify. Then we can use these variables to render our Knight appropriatly on the screen. We could have:

if(Key == LEFT) X--;
if(Key == RIGHT) X++;
if(Key == UP) Y--;
if(Key == DOWN) Y++;//... somewhere else in our code ...

RenderImage(KnightImage, X, Y);
This works because each loop it checks if the key is LEFT, RIGHT, etc., and if so, we decrease or increase a variable. So, if our game is running at 30 frames per second and we press LEFT, then our guy would move to the left 30 pixels per second. If you don't understand the game loop, you soon will. Games need them to function correctly.

Going back to our conceptual code (the 5 functions), we can add these additional pages to our project:

CApp_OnInit.cpp CApp_OnEvent.cpp CApp_OnLoop.cpp CApp_OnRender.cpp CApp_OnCleanup.cpp

Go back to CApp.h, and add the following functions and variables:

#ifndef _CAPP_H_
    #define _CAPP_H_

#include <SDL.h>

class CApp {
    private:
        bool    Running;

    public:
        CApp();

        int OnExecute();

    public:

        bool OnInit();

        void OnEvent(SDL_Event* Event);

        void OnLoop();

        void OnRender();

        void OnCleanup();
};

#endif
Go through each of the 5 files we just created and create the function themselves:

#include "CApp.h"

bool CApp::OnInit() {
    return true;
}
#include "CApp.h"

void CApp::OnEvent(SDL_Event* Event) {
}
#include "CApp.h"

void CApp::OnLoop() {
}
#include "CApp.h"

void CApp::OnRender() {
}
#include "CApp.h"

void CApp::OnCleanup() {
}

Now lets go back to our CApp.cpp code to link all of these functions together:

#include "CApp.h"

CApp::CApp() {
    Running = true;
}

int CApp::OnExecute() {
    if(OnInit() == false) {
        return -1;
    }

    SDL_Event Event;

    while(Running) {
        while(SDL_PollEvent(&Event)) {
            OnEvent(&Event);
        }

        OnLoop();
        OnRender();
    }

    OnCleanup();

    return 0;
}

int main(int argc, char* argv[]) {
    CApp theApp;

    return theApp.OnExecute();
}
You'll notice some new variables, but let's look at what is happening first. First, we try to Initialize our game, if it fails we return -1 (an error code), thus closing our program. If everything is good, we continue on to the game loop. Within the game loop we use SDL_PollEvent to check for events, and pass them one at a time to OnEvent. Once done with Events, we go to OnLoop for move data around and what not, and then render our game. We repeat this indefinitly. If the user exits the game, we proceed to OnCleanup cleaning up any resources. Simple enough.

Now, lets look at SDL_Event and SDL_PollEvent. The first is a structure that holds information about events. The second is a function that will grab any events waiting in the queue. This queue can have any number of events, which is the reason why we have to loop through them. So, for example, lets say the user presses A and moves the mouse during the OnRender() function. SDL will detect this and put two events in the queue, one for a key press and one for a mouse move. We can grab this event from the queue by using the SDL_PollEvent, and then passing it to OnEvent to handle it accordingly. Once there are no more events in the queue, SDL_PollEvent will return false, thus exiting out of the Event queue loop.

The other variable added, Running, is our own. This is our exit out of the game loop. When this is set to false, it will end the program, and in turn exit the program. So, for example, if the user presses the Escape key we can set this variable to false, quitting the game.

You should be able to compile everything so far just fine, but you may notice you can't exit out. You'll probably have to use the task manager to end the program.

Now that everything is setup, let's start by creating the window our game will be drawn in. Jump over to CApp.h and add a SDL surface variable to the code:

#ifndef _CAPP_H_
    #define _CAPP_H_

#include <SDL.h>

class CApp {
    private:
        bool            Running;

        SDL_Surface*    Surf_Display;

    public:
        CApp();

        int OnExecute();

    public:
        bool OnInit();

        void OnEvent(SDL_Event* Event);

        void OnLoop();

        void OnRender();

        void OnCleanup();
};

#endif
I suppose now would be a good time to explain exactly what a SDL Surface is. An SDL Surface is anything that can be drawn, or drawn to. Say we have a blank piece of paper, a pencil, and some stickers; this paper could be called our display "surface." We can draw to it, put stickers on it, or whatever. The stickers we have are also surfaces; we can draw on them and put other stickers on them as well. So, Surf_Display is simply our "piece of blank paper" that we will draw all our stuff on.

Now, lets jump on over to CApp_OnInit to actually create this surface:

#include "CApp.h"

bool CApp::OnInit() {
    if(SDL_Init(SDL_INIT_EVERYTHING) < 0) {
        return false;
    }

    if((Surf_Display = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE | SDL_DOUBLEBUF)) == NULL) {
        return false;
    }

    return true;
}
The first thing we need to do is start up SDL itself, so we can access its functions. We are telling SDL to Initialize everything it has; there are other parameters you can pass, but understanding them at this point is not important. The next function we use is SDL_SetVideoMode. This bad boy is what creates our window, and our surface. It takes 4 parameters: The width of the window, the height of the window, the bit resolution of the window (recommended to be 16 or 32), and then display flags. There are quite a few display flags, but the ones shown above are fine for now. The first flag tells SDL to use hardware memory for storing our images and such, and the second flag tells SDL to use double buffering (which is important if you don't want flickering on your screen). Another flag that may interest you now is SDL_FULLSCREEN, which makes the window go fullscreen.

Now that our display is setup, lets cleanup a little bit to make sure everything works smoothly. Open up CApp_OnCleanup.cpp and add the following:

#include "CApp.h"

void CApp::OnCleanup() {
    SDL_Quit();
}
We basically quit out of SDL. You should take note that in this function is where you would free other surfaces as well. This keeps all your code centralized to the function its performing.

To keep things tidy, lets also set the Surf_Display pointer to NULL on the class constructor. Open up CApp.cpp and add the following:

CApp::CApp() {
    Surf_Display = NULL;

    Running = true;
}
Try compiling your code, and watching it work. You should have a nice blank window popup. You'll notice you can't close it though, so you'll need to use the task manager to do so.

Now that we have a window all setup, we're going to need a way to close it. Open up the CApp_OnEvent.cpp file, add the following:

#include "CApp.h"

void CApp::OnEvent(SDL_Event* Event) {
    if(Event->type == SDL_QUIT) {
        Running = false;
    }
}
The SDL event structure is broken down into types. These types can range from keypresses, to mouse movements; what we simply are doing here is checking the event type. The type above we are looking for is the request to close the window (i.e., when the user clicks the X button). If that event happens to take place, we set Running to false, thus ending our program. Simple enough. We'll look more into events in a later lesson.

Now, you should everything setup, and a good structure to work with later. It may be a good idea to take this project and turn it into an "SDL template" within CodeBlocks. I won't go over how to do that, but feel free to Google it.


Tutoriel SDL

SDL Coordinates and Bliting
Using the first tutorial as our base, we'll delve more into the world of SDL surfaces. As I attempted to explain in the last lesson, SDL Surfaces are basically images stored in memory. Imagine we have a blank 320x240 pixel surface. Illustrating the SDL coordinate system, we have something like this:

SDL Coordinate System

This coordinate system is quite different than the normal one you are familiar with. Notice how the Y coordinate increases going down, and the X coordinate increases going right. Understanding the SDL coordinate system is important in order to properly draw images on the screen.

Since we already have the main surface (Surf_Display) setup and ready, we are going to need a way to draw images on it. This process is called Blitting, where we basically transfer one image onto another. But before we can do that, we have to have a way to load these images into memory. SDL offers a simple function to do this called SDL_LoadBMP. Some pseudocode might look like this:
SDL_Surface* Surf_Temp;

if((Surf_Temp = SDL_LoadBMP("mypicture.bmp")) == NULL) {
    //Error!
}
It's rather simple, SDL_LoadBMP takes a single argument, the file you want to load, and returns a surface. If the functions returns NULL, either the file is not found, corrupted, or some other error. Unfortunately, for the sake of effeciency, this method is not enough. Often the image loaded will be in a different pixel format than the display. Thus, when we draw an image to the display we can incurr performance loss, lose image colors, and the like. But, thanks to SDL, it offers a quick solution around this, SDL_DisplayFormat. This function takes a surface already loaded, and returns a new surface using the same format as the display.

Let's take this process and throw it into a reusable class. Use SDL Tutorial 1 as the basis for you code, and add the following two new files: CSurface.h, CSurface.cpp. Open up CSurface.h and add the following:
#ifndef _CSURFACE_H_
#define _CSURFACE_H_
#include <SDL.h>

class CSurface {
    public:
        CSurface();

    public:
        static SDL_Surface* OnLoad(char* File);
};

#endif
We've created a simple static function, OnLoad, that will load a surface for us. Now open CSurface.cpp:
#include "CSurface.h"

CSurface::CSurface() {
}

SDL_Surface* CSurface::OnLoad(char* File) {
    SDL_Surface* Surf_Temp = NULL;
    SDL_Surface* Surf_Return = NULL;

    if((Surf_Temp = SDL_LoadBMP(File)) == NULL) {
        return NULL;
    }

    Surf_Return = SDL_DisplayFormat(Surf_Temp);
    SDL_FreeSurface(Surf_Temp);

    return Surf_Return;
}
There are a couple of important things to note here. Firstly, always remember that when you make a pointer to set it to NULL, or 0. Many problems can come along later if you fail to do this. Secondly, notice how SDL_DisplayFormat returns a new Surface, and doesn't overwrite the original. This important to remember because since it creates a new surface, we have to free the old one. Otherwise, we have a surface floating around in memory.

Now that we have a way of loading surfaces into memory, we are also going to need a way to draw them onto other surfaces. Just like SDL offers a function to load images, it also offers a function to draw (blit) images: SDL_BlitSurface. Unfortunately, this function is not as easy to use as the SDL_LoadBMP, but nonetheless, it's simple enough. Open back up CSurface.h and add the following function prototype:
#ifndef _CSURFACE_H_
    #define _CSURFACE_H_

#include <SDL.h>

class CSurface {
    public:
        CSurface();

    public:
        static SDL_Surface* OnLoad(char* File);

        static bool OnDraw(SDL_Surface* Surf_Dest, SDL_Surface* Surf_Src, int X, int Y);
};

#endif
Now, open back up CSurface.cpp, and add the following:
#include "CSurface.h"

CSurface::CSurface() {
}

SDL_Surface* CSurface::OnLoad(char* File) {
    SDL_Surface* Surf_Temp = NULL;
    SDL_Surface* Surf_Return = NULL;

    if((Surf_Temp = SDL_LoadBMP(File)) == NULL) {
        return NULL;
    }

    Surf_Return = SDL_DisplayFormat(Surf_Temp);
    SDL_FreeSurface(Surf_Temp);

    return Surf_Return;
}

bool CSurface::OnDraw(SDL_Surface* Surf_Dest, SDL_Surface* Surf_Src, int X, int Y) {
    if(Surf_Dest == NULL || Surf_Src == NULL) {
        return false;
    }

    SDL_Rect DestR;

    DestR.x = X;
    DestR.y = Y;

    SDL_BlitSurface(Surf_Src, NULL, Surf_Dest, &DestR);

    return true;
}
First of all, look at the arguments being passed to the OnDraw function. We have two surfaces, and two int variables. The first surface is the destination surface, or the surface we are going to draw on. The second surface is the source surface, or the surface we going to use to draw on another surface. Basically, we are putting Surf_Src on top of Surf_Dest. The X, Y variables is the position on the Surf_Dest where we are drawing this surface to.

The start of the function makes sure we have valid surfaces, if we don't, return false. Next, we find SDL_Rect. This is a SDL structure that basically has four members: x, y, w, h. This, of course, creates the dimensions for a rectangle. We are only worried about where we are drawing to, not the size. So we assign X, Y coordinates to the destination surface. If you are wondering what NULL is within SDL_BlitSurface, it's another parameter for a SDL_Rect. We'll get to this later on in this lesson.

Lastly, we actualy call the function to draw the image, and then return true.

Now, to make sure all of this works, let's create a test surface. Open up CApp.h, and create a new surface, and include the new created CSurface.h:
#ifndef _CAPP_H_
#define _CAPP_H_
#include <SDL.h>
#include "CSurface.h"

class CApp {
    private:
        bool            Running;

        SDL_Surface*    Surf_Display;

        SDL_Surface*    Surf_Test;

    public:
        CApp();

        int OnExecute();

    public:
        bool OnInit();

        void OnEvent(SDL_Event* Event);

        void OnLoop();

        void OnRender();

        void OnCleanup();
};

#endif
Also, initialize the surface to NULL in the constructor:
CApp::CApp() {
    Surf_Test = NULL;
    Surf_Display = NULL;

    Running = true;
}
And, remember to cleanup:
#include "CApp.h"

void CApp::OnCleanup() {
    SDL_FreeSurface(Surf_Test);
    SDL_FreeSurface(Surf_Display);
    SDL_Quit();
}
Now, lets actually load an image. Open up CApp_OnInit.cpp, and add the code to load a surface:
#include "CApp.h"

bool CApp::OnInit() {
    if(SDL_Init(SDL_INIT_EVERYTHING) < 0) {
        return false;
    }

    if((Surf_Display = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE | SDL_DOUBLEBUF)) == NULL) {
        return false;
    }

    if((Surf_Test = CSurface::OnLoad("myimage.bmp")) == NULL) {
        return false;
    }

    return true;
}
Be sure to replace "myimage.bmp" with an actual bitmap image you have. If you don't have one, open mspaint and draw something real quick, and save it in the same folder where your executable goes. Now that we have the image loaded, lets actually draw it. Open up CApp_OnRender.cpp and add the following:
#include "CApp.h"

void CApp::OnRender() {
    CSurface::OnDraw(Surf_Display, Surf_Test, 0, 0);

    SDL_Flip(Surf_Display);
}
Notice a new function here SDL_Flip. This basically refreshes the buffer and displays Surf_Display onto the screen. This is called double buffering. It's the process of drawing everything into memory, and then finally drawing everything to the screen. If we didn't do this, we would have images flickering on the screen. Remember the SDL_DOUBLEBUF flag? This is what turns double buffering on.

Compile your code, and make sure everything works correctly. You should see your image on the top left hand corner of the screen. If so, congratulations, you are one step closer to an actual game. If not, make sure you have the myimage.bmp in the same folder as your executable. Also insure it's a valid bitmap file.

Draw Surface

Now lets take this process a little bit further. While it's nice and all to draw images to the screen, often we'll need to draw only parts of an image. Take, for example, a tileset:

Tileset

Though this is one single image, we only want to draw a part of it. Open back up CSurface.h, and add the following code:
#ifndef _CSURFACE_H_
    #define _CSURFACE_H_

#include <SDL.h>

class CSurface {
    public:
        CSurface();

    public:
        static SDL_Surface* OnLoad(char* File);

        static bool OnDraw(SDL_Surface* Surf_Dest, SDL_Surface* Surf_Src, int X, int Y);

        static bool OnDraw(SDL_Surface* Surf_Dest, SDL_Surface* Surf_Src, int X, int Y, int X2, int Y2, int W, int H);
};

#endif
Open back up CSurface.cpp, and add the following function (Important, we are adding a second OnDraw function, we are not replacing the first one):
bool CSurface::OnDraw(SDL_Surface* Surf_Dest, SDL_Surface* Surf_Src, int X, int Y, int X2, int Y2, int W, int H) {
    if(Surf_Dest == NULL || Surf_Src == NULL) {
        return false;
    }

    SDL_Rect DestR;

    DestR.x = X;
    DestR.y = Y;

    SDL_Rect SrcR;

    SrcR.x = X2;
    SrcR.y = Y2;
    SrcR.w = W;
    SrcR.h = H;

    SDL_BlitSurface(Surf_Src, &SrcR, Surf_Dest, &DestR);

    return true;
}
Notice that it's basically the same function as our first one, except we've added another SDL_Rect. This source rect allows use to specify what pixels from the source to copy over to the destination. If we specified 0, 0, 50, 50 as parameters for X2...H, it would only draw upper left part of the surface (a 50x50 square). Draw Surface 2

Lets also test this new function out, open back up CApp_OnRender.cpp, and add the following:
#include "CApp.h"

void CApp::OnRender() {
    CSurface::OnDraw(Surf_Display, Surf_Test, 0, 0);
    CSurface::OnDraw(Surf_Display, Surf_Test, 100, 100, 0, 0, 50, 50);

    SDL_Flip(Surf_Display);
}


Tutoriel SDL

SDL Events
Alongside the basics of game development is something called Events. All videogames, from pong to the highly complex PC games and console titles, use events to interact with the player. These events can come from keyboards, mice, joysticks, gamepads, and so on, or events from our operating system. It's important to understand how events work if we are to appropriately interact a user with a game. We've already been using events, but only for closing our window, now we'll look at how to receive events from the user.

If you haven't caught on, each tutorial builds off the previous, so we'll be using the code from the previous lesson. In order to track all these events and throw them into nice functions, lets create a new class. Create two files called CEvent.h and CEvent.cpp. These two files will handle our events, and call the appropriate function. CApp will inherit this class, so when we need to catch an event we simply override a function.

Open up CEvent.h and add the following code:
#ifndef _CEVENT_H_
    #define _CEVENT_H_

#include <SDL.h>

class CEvent {
    public:
        CEvent();

        virtual ~CEvent();

        virtual void OnEvent(SDL_Event* Event);

        virtual void OnInputFocus();

        virtual void OnInputBlur();

        virtual void OnKeyDown(SDLKey sym, SDLMod mod, Uint16 unicode);

        virtual void OnKeyUp(SDLKey sym, SDLMod mod, Uint16 unicode);

        virtual void OnMouseFocus();

        virtual void OnMouseBlur();

        virtual void OnMouseMove(int mX, int mY, int relX, int relY, bool Left,bool Right,bool Middle);

        virtual void OnMouseWheel(bool Up, bool Down);    //Not implemented

        virtual void OnLButtonDown(int mX, int mY);

        virtual void OnLButtonUp(int mX, int mY);

        virtual void OnRButtonDown(int mX, int mY);

        virtual void OnRButtonUp(int mX, int mY);

        virtual void OnMButtonDown(int mX, int mY);

        virtual void OnMButtonUp(int mX, int mY);

        virtual void OnJoyAxis(Uint8 which, Uint8 axis, Sint16 value);

        virtual void OnJoyButtonDown(Uint8 which, Uint8 button);

        virtual void OnJoyButtonUp(Uint8 which, Uint8 button);

        virtual void OnJoyHat(Uint8 which, Uint8 hat, Uint8 value);

        virtual void OnJoyBall(Uint8 which, Uint8 ball, Sint16 xrel, Sint16 yrel);

        virtual void OnMinimize();

        virtual void OnRestore();

        virtual void OnResize(int w,int h);

        virtual void OnExpose();

        virtual void OnExit();

        virtual void OnUser(Uint8 type, int code, void* data1, void* data2);
};

#endif
Pretty hefty class right? Alright, open up CEvent.cpp, and add the following code:
#include "CEvent.h"

CEvent::CEvent() {
}

CEvent::~CEvent() {
    //Do nothing
}

void CEvent::OnEvent(SDL_Event* Event) {
    switch(Event->type) {
        case SDL_ACTIVEEVENT: {
            switch(Event->active.state) {
                case SDL_APPMOUSEFOCUS: {
                    if ( Event->active.gain )    OnMouseFocus();
                    else                OnMouseBlur();

                    break;
                }
                case SDL_APPINPUTFOCUS: {
                    if ( Event->active.gain )    OnInputFocus();
                    else                OnInputBlur();

                    break;
                }
                case SDL_APPACTIVE:    {
                    if ( Event->active.gain )    OnRestore();
                    else                OnMinimize();

                    break;
                }
            }
            break;
        }

        case SDL_KEYDOWN: {
            OnKeyDown(Event->key.keysym.sym,Event->key.keysym.mod,Event->key.keysym.unicode);
            break;
        }

        case SDL_KEYUP: {
            OnKeyUp(Event->key.keysym.sym,Event->key.keysym.mod,Event->key.keysym.unicode);
            break;
        }

        case SDL_MOUSEMOTION: {
            OnMouseMove(Event->motion.x,Event->motion.y,Event->motion.xrel,Event->motion.yrel,(Event->motion.state&SDL_BUTTON(SDL_BUTTON_LEFT))!=0,(Event->motion.state&SDL_BUTTON(SDL_BUTTON_RIGHT))!=0,(Event->motion.state&SDL_BUTTON(SDL_BUTTON_MIDDLE))!=0);
            break;
        }

        case SDL_MOUSEBUTTONDOWN: {
            switch(Event->button.button) {
                case SDL_BUTTON_LEFT: {
                    OnLButtonDown(Event->button.x,Event->button.y);
                    break;
                }
                case SDL_BUTTON_RIGHT: {
                    OnRButtonDown(Event->button.x,Event->button.y);
                    break;
                }
                case SDL_BUTTON_MIDDLE: {
                    OnMButtonDown(Event->button.x,Event->button.y);
                    break;
                }
            }
            break;
        }

        case SDL_MOUSEBUTTONUP:    {
            switch(Event->button.button) {
                case SDL_BUTTON_LEFT: {
                    OnLButtonUp(Event->button.x,Event->button.y);
                    break;
                }
                case SDL_BUTTON_RIGHT: {
                    OnRButtonUp(Event->button.x,Event->button.y);
                    break;
                }
                case SDL_BUTTON_MIDDLE: {
                    OnMButtonUp(Event->button.x,Event->button.y);
                    break;
                }
            }
            break;
        }

        case SDL_JOYAXISMOTION: {
            OnJoyAxis(Event->jaxis.which,Event->jaxis.axis,Event->jaxis.value);
            break;
        }

        case SDL_JOYBALLMOTION: {
            OnJoyBall(Event->jball.which,Event->jball.ball,Event->jball.xrel,Event->jball.yrel);
            break;
        }

        case SDL_JOYHATMOTION: {
            OnJoyHat(Event->jhat.which,Event->jhat.hat,Event->jhat.value);
            break;
        }
        case SDL_JOYBUTTONDOWN: {
            OnJoyButtonDown(Event->jbutton.which,Event->jbutton.button);
            break;
        }

        case SDL_JOYBUTTONUP: {
            OnJoyButtonUp(Event->jbutton.which,Event->jbutton.button);
            break;
        }

        case SDL_QUIT: {
            OnExit();
            break;
        }

        case SDL_SYSWMEVENT: {
            //Ignore
            break;
        }

        case SDL_VIDEORESIZE: {
            OnResize(Event->resize.w,Event->resize.h);
            break;
        }

        case SDL_VIDEOEXPOSE: {
            OnExpose();
            break;
        }

        default: {
            OnUser(Event->user.type,Event->user.code,Event->user.data1,Event->user.data2);
            break;
        }
    }
}

void CEvent::OnInputFocus() {
    //Pure virtual, do nothing
}

void CEvent::OnInputBlur() {
    //Pure virtual, do nothing
}

void CEvent::OnKeyDown(SDLKey sym, SDLMod mod, Uint16 unicode) {
    //Pure virtual, do nothing
}

void CEvent::OnKeyUp(SDLKey sym, SDLMod mod, Uint16 unicode) {
    //Pure virtual, do nothing
}

void CEvent::OnMouseFocus() {
    //Pure virtual, do nothing
}

void CEvent::OnMouseBlur() {
    //Pure virtual, do nothing
}

void CEvent::OnMouseMove(int mX, int mY, int relX, int relY, bool Left,bool Right,bool Middle) {
    //Pure virtual, do nothing
}

void CEvent::OnMouseWheel(bool Up, bool Down) {
    //Pure virtual, do nothing
}

void CEvent::OnLButtonDown(int mX, int mY) {
    //Pure virtual, do nothing
}

void CEvent::OnLButtonUp(int mX, int mY) {
    //Pure virtual, do nothing
}

void CEvent::OnRButtonDown(int mX, int mY) {
    //Pure virtual, do nothing
}

void CEvent::OnRButtonUp(int mX, int mY) {
    //Pure virtual, do nothing
}

void CEvent::OnMButtonDown(int mX, int mY) {
    //Pure virtual, do nothing
}

void CEvent::OnMButtonUp(int mX, int mY) {
    //Pure virtual, do nothing
}

void CEvent::OnJoyAxis(Uint8 which,Uint8 axis,Sint16 value) {
    //Pure virtual, do nothing
}

void CEvent::OnJoyButtonDown(Uint8 which,Uint8 button) {
    //Pure virtual, do nothing
}

void CEvent::OnJoyButtonUp(Uint8 which,Uint8 button) {
    //Pure virtual, do nothing
}

void CEvent::OnJoyHat(Uint8 which,Uint8 hat,Uint8 value) {
    //Pure virtual, do nothing
}

void CEvent::OnJoyBall(Uint8 which,Uint8 ball,Sint16 xrel,Sint16 yrel) {
    //Pure virtual, do nothing
}

void CEvent::OnMinimize() {
    //Pure virtual, do nothing
}

void CEvent::OnRestore() {
    //Pure virtual, do nothing
}

void CEvent::OnResize(int w,int h) {
    //Pure virtual, do nothing
}

void CEvent::OnExpose() {
    //Pure virtual, do nothing
}

void CEvent::OnExit() {
    //Pure virtual, do nothing
}

void CEvent::OnUser(Uint8 type, int code, void* data1, void* data2) {
    //Pure virtual, do nothing
}
Lots of code, but all the SDL events should be covered. What we basically are doing is taking an SDL_Event pointer, and switching through the types, and then calling the appropriate function. It just looks like a lot since they are quite a bit of events.

Now that this has been all setup, let's jump over to CApp.h and link the two together:
#ifndef _CAPP_H_
    #define _CAPP_H_

#include <SDL.h>

#include "CEvent.h"
#include "CSurface.h"

class CApp : public CEvent {
    private:
        bool            Running;

        SDL_Surface*    Surf_Display;

        SDL_Surface*    Surf_Test;

    public:
        CApp();

        int OnExecute();

    public:
        bool OnInit();

        void OnEvent(SDL_Event* Event);

        void OnLoop();

        void OnRender();

        void OnCleanup();
};

#endif
Everything should compile just fine. We have the event class setup, now lets actually link events to our new class structure. Open up CApp_OnEvent.cpp and edit the following function:
#include "CApp.h"

void CApp::OnEvent(SDL_Event* Event) {
    CEvent::OnEvent(Event);
}
We're passing the Event structure to our class now, it will take care of the correct function calls. Now when we want to check for an event, we override the function. Since we got rid of checking for the SDL_QUIT event, lets use the function instead. Open up CApp.h again, and add the following function:
#ifndef _CAPP_H_
#define _CAPP_H_
#include <SDL.h>
#include "CEvent.h"
#include "CSurface.h"

class CApp : public CEvent {
    private:
        bool            Running;

        SDL_Surface*    Surf_Display;

        SDL_Surface*    Surf_Test;

    public:
        CApp();

        int OnExecute();

    public:
        bool OnInit();

        void OnEvent(SDL_Event* Event);

        void OnExit();

        void OnLoop();

        void OnRender();

        void OnCleanup();
};

#endif
The OnExit function handles the SDL_QUIT events. Now that we have the prototype, lets define what it does. Open up CApp_OnEvent.cpp, and add the following:
#include "CApp.h"

void CApp::OnEvent(SDL_Event* Event) {
    CEvent::OnEvent(Event);
}

void CApp::OnExit() {
    Running = false;
}
Recompile, and try it out. You should be able to close the application just like before.

I encourage you to look through some of the various events that we can check. Later on we'll use some of these events in our games, but for now try to familiarize yourself with them.


Tutoriel SDL

SDL Tutorial - Tic Tac Toe
Up to this point we have been laying the foundation for developing a game. So far we've setup a basic structure to handle common routines, we've setup a special class to handle events, and we've also setup a class to handle a few surface functions. In this tutorial we'll take all those things, combine them, and create a tic-tac-toe game. Don't worry, things should be pretty simple. Use the last tutorial to build off of.

The first thing we are going to need to do is plan our game. From experience, we know that tic-tac-toe has a 3x3 grid, where you place X's and O's. So, we know that we will need 3 graphics, one for the grid, one for the X, and one for the O. We don't need multiples of the X or O, because we can draw them in the program as many times as we like. Lets eliminate this first step. Our grid is going to be 600x600, and our X's and O's will be 200x200 (1/3 of the area).

Grid
O X

Now that we have our images, we are going to need a way to load them into our program. Open up CApp.h and make some modifications. Remove the Test Surface, and add three new surfaces.
#ifndef _CAPP_H_
    #define _CAPP_H_

#include <SDL.h>

#include "CEvent.h"
#include "CSurface.h"

class CApp : public CEvent {
    private:
        bool            Running;

        SDL_Surface*    Surf_Display;

    private:
        SDL_Surface*    Surf_Grid;

        SDL_Surface*    Surf_X;
        SDL_Surface*    Surf_O;

    public:
        CApp();

        int OnExecute();

    public:
        bool OnInit();

        void OnEvent(SDL_Event* Event);

                void OnExit();

        void OnLoop();

        void OnRender();

        void OnCleanup();
};

#endif
Also, open up CApp.cpp and make some modifications. Remove the Test Surface again, and add the three new ones again.
#include "CApp.h"

CApp::CApp() {
    Surf_Grid = NULL;
    Surf_X = NULL;
    Surf_O = NULL;

    Surf_Display = NULL;

    Running = true;
}

int CApp::OnExecute() {
    if(OnInit() == false) {
        return -1;
    }

    SDL_Event Event;

    while(Running) {
        while(SDL_PollEvent(&Event)) {
            OnEvent(&Event);
        }

        OnLoop();
        OnRender();
    }

    OnCleanup();

    return 0;
}

int main(int argc, char* argv[]) {
    CApp theApp;

    return theApp.OnExecute();
}
And, you guessed it, open up CApp_OnCleanup.cpp to make some modifications there too. Just like before, get rid of the Test Surface and add the three new ones.
#include "CApp.h"

void CApp::OnCleanup() {
    SDL_FreeSurface(Surf_Grid);
    SDL_FreeSurface(Surf_X);
    SDL_FreeSurface(Surf_O);
    SDL_FreeSurface(Surf_Display);
    SDL_Quit();
}
Now that we have the surfaces setup, lets load them into memory. Open up CApp_OnInit.cpp, and make some changes. Get rid of the test surface (again), and load the three new ones. Be sure to put the correct filenames. Also, change the dimensions of the window to 600x600, the size of the grid. This make sure we don't have any blank space around the window we aren't using.
#include "CApp.h"

bool CApp::OnInit() {
    if(SDL_Init(SDL_INIT_EVERYTHING) < 0) {
        return false;
    }

    if((Surf_Display = SDL_SetVideoMode(600, 600, 32, SDL_HWSURFACE | SDL_DOUBLEBUF)) == NULL) {
        return false;
    }

    if((Surf_Grid = CSurface::OnLoad("./gfx/grid.bmp")) == NULL) {
    return false;
    }

    if((Surf_X = CSurface::OnLoad("./gfx/x.bmp")) == NULL) {
    return false;
    }

    if((Surf_O = CSurface::OnLoad("./gfx/o.bmp")) == NULL) {
    return false;
    }

    return true;
}
You may have noticed a change I made in the filenames. I added ./gfx/ before the filename to specify which folder the graphics are in. As games begin to grow it's very practical to have all the files in one folder. Because of such, from hereon, all images will be put in the gfx folder. Now, let's get the grid showing up on the screen. Open up CApp_OnRender.cpp and change the test surface rendering to make the grid render.
#include "CApp.h"

void CApp::OnRender() {
    CSurface::OnDraw(Surf_Display, Surf_Grid, 0, 0);

    SDL_Flip(Surf_Display);
}
Try compiling your program, and if successful, you should see the grid show up. Remember, there are basically 5 steps to using a surface: declare it, set it to NULL, load it, draw it, and then free it. It's good practice to learn all these 5 steps now, because later on if you neglect one of these steps if can cause problems. For example, neglecting to set a surface to NULL can cause undefined behavior, or neglecting to free a surface can cause a memory leak.

You may have noticed something odd about the graphics we are using, the X and O contain a pink background. There is a reason for it, we are going to implement transparency onto these surfaces. Basically, wherever there is pink, it will show through; we will make the pink color transparent. SDL offers a simple function to do this, SDL_SetColorKey. To implement this, open up CSurface.h so we can add a new function.
#ifndef _CSURFACE_H_
    #define _CSURFACE_H_

#include <SDL.h>

class CSurface {
    public:
        CSurface();

    public:
        static SDL_Surface* OnLoad(char* File);

        static bool OnDraw(SDL_Surface* Surf_Dest, SDL_Surface* Surf_Src, int X, int Y);

        static bool OnDraw(SDL_Surface* Surf_Dest, SDL_Surface* Surf_Src, int X, int Y, int X2, int Y2, int W, int H);

        static bool Transparent(SDL_Surface* Surf_Dest, int R, int G, int B);
};

#endif
Now, to implement this function, open up CSurface.cpp and add the function:
bool CSurface::Transparent(SDL_Surface* Surf_Dest, int R, int G, int B) {
    if(Surf_Dest == NULL) {
        return false;
    }

    SDL_SetColorKey(Surf_Dest, SDL_SRCCOLORKEY | SDL_RLEACCEL, SDL_MapRGB(Surf_Dest->format, R, G, B));

    return true;
}
Notice 3 extra arguments being passed besides the surface. These are the 3 color values that we want to make transparent, it doesn't have to be just pink. For instance, it we wanted red to be transparent it would be 255, 0, 0.

This function first checks to see if we have a valid surface. If so, we set a color key (transparency) for a color. The first argument is the surface to apply the color key to, the second is some flags telling SDL how to perform the operation, and the third is the color to make transparent. The flags being applied are basic, the first tells SDL to apply the color key to the source (the surface being passed) and the second tells SDL to try to use RLE acceleration (basically, try to make drawing later on faster). The third argument is a little bit more complex; we are using SDL_MapRGB in order to create a color. SDL_MapRGB takes a surface, and your requested color (R, G, B), and tries to match it as close as it can to that surface. You might be thinking why this is useful. Not all surfaces have the same color palette. Remember the old NES days where there was only a few colors that could be used? Same idea here, SDL_MapRGB takes a color and matches it with the closest color on that surface palette.

Lets apply this new function to our surfaces now, open up CApp_OnInit.cpp and make the following changes:
#include "CApp.h"

bool CApp::OnInit() {
    if(SDL_Init(SDL_INIT_EVERYTHING) < 0) {
        return false;
    }

    if((Surf_Display = SDL_SetVideoMode(600, 600, 32, SDL_HWSURFACE | SDL_DOUBLEBUF)) == NULL) {
        return false;
    }

    if((Surf_Grid = CSurface::OnLoad("./gfx/grid.bmp")) == NULL) {
        return false;
    }

    if((Surf_X = CSurface::OnLoad("./gfx/x.bmp")) == NULL) {
        return false;
    }

    if((Surf_O = CSurface::OnLoad("./gfx/o.bmp")) == NULL) {
        return false;
    }

    CSurface::Transparent(Surf_X, 255, 0, 255);
    CSurface::Transparent(Surf_O, 255, 0, 255);

    return true;
}
Everything for the surfaces should be set. The next thing we have to do is figure out a way to draw these X's and O's. We can't just draw them everywhere on the grid because they won't always be in the same spots. What we are going to have to do is make an array of 9 containers, the values in this array will tell us the values for each cell on the grid. So, spot 0 would be the top left, 1 would be the top middle, 2 the top right, 3 the middle left, and so on. Create this array by adding it to CApp.h:
#ifndef _CAPP_H_
    #define _CAPP_H_

#include <SDL.h>

#include "CEvent.h"
#include "CSurface.h"

class CApp : public CEvent {
    private:
        bool            Running;

        SDL_Surface*    Surf_Display;

    private:
        SDL_Surface*    Surf_Grid;

        SDL_Surface*    Surf_X;
        SDL_Surface*    Surf_O;

    private:
        int        Grid[9];

    public:
        CApp();

        int OnExecute();

    public:
        bool OnInit();

        void OnEvent(SDL_Event* Event);

            void OnExit();

        void OnLoop();

        void OnRender();

        void OnCleanup();
};

#endif
We know that each cell can have three possible values: Empty, X, and O. These tell us what is currently in that cell. To make things a little neater than having 0, 1, 2 as values, we'll use an enum instead. If you are umfamiliar with how an enum works, try finding a quick tutorial on them. Just know that GRID_TYPE_NONE = 0, GRID_TYPE_X = 1, and GRID_TYPE_O = 2. Go back to CApp.h and add the following underneath the Grid array:
enum {
    GRID_TYPE_NONE = 0,
    GRID_TYPE_X,
    GRID_TYPE_O
};
Note, up to this point I have been displaying practically all the code when I am referring to different files. From hereon, I expect you to know where code goes. Most of the time I will tell you where to place it by, and sometimes I might display all the code. Now that we have a way to know what a cell is, we're going to need a way to reset the board. Let's create a new function at the bottom inside of CApp.h called Reset.
public:
    void Reset();
Open up CApp.cpp and add the following function just before int main:
void CApp::Reset() {
    for(int i = 0;i < 9;i++) {
        Grid[i] = GRID_TYPE_NONE;
    }
}
This loop will set every cell in the grid to GRID_TYPE_NONE, meaning that all the cells are empty. We're going to have to do this at the very beginning when our program is loaded, so make a call to this function within the CApp_OnInit.cpp file:
//...

CSurface::Transparent(Surf_X, 255, 0, 255);
CSurface::Transparent(Surf_O, 255, 0, 255);

Reset();
So far so good. The next thing we are going to have to do is make the ability to place X's and O's on the screen. Lets create a new function that will handle this. Open up CApp.h again and add the function just below Reset.
void SetCell(int ID, int Type);
Now, open back up CApp.cpp and add the function:
void CApp::SetCell(int ID, int Type) {
    if(ID < 0 || ID >= 9) return;
    if(Type < 0 || Type > GRID_TYPE_O) return;

    Grid[ID] = Type;
}
This function takes two arguments, the first is the cell ID to change, and the second is the type to change it to. We have to conditions here, the first is to make sure we don't go outside the bounds of the array (if we did it would likely crash our program), and the second is to make sure we passed an appropriate type. Simple enough. Now, let's implement a way to draw the X's and O'. Open up CApp_OnRender and add the following code just after the grid:
for(int i = 0;i < 9;i++) {
    int X = (i % 3) * 200;
    int Y = (i / 3) * 200;

    if(Grid[i] == GRID_TYPE_X) {
        CSurface::OnDraw(Surf_Display, Surf_X, X, Y);
    }else
    if(Grid[i] == GRID_TYPE_O) {
        CSurface::OnDraw(Surf_Display, Surf_O, X, Y);
    }
}
This is a little bit more complex than we have been used to so far. Firstly, we are looping through each cell in the grid. Next, we are translating that grid ID over to X and Y coordinates. We do this in two different ways. To find X, we take the remainder of i to 3. This will give us 0 when i is 0, 1 when i is 1, 2 when i is 2, 0 when i is 3, and so on. We multiply it by 200 because each cell is 200x200 pixels. To find Y, we divide by 3, this causes Y to be 0 when i is 0, 1, 2, Y to be 1 when i is 3, 4, 5, and so on. We then multiply that by 200 as well. I encourage you to really try to understand what is going on here because this sort of method is used for tile based games.

Next, all we have to do is check the cell type, and then draw the correct surface at that cell.

Now that we have our surfaces drawing, we'll need a way to communicate from the user to the computer. We'll use mouse events for this. When the users clicks a cell it will set the cell appropriately. We are going to need to overload one of the CEvent functions for this. Open up CApp.h and add the following function just below OnEvent and next to OnExit.
void OnLButtonDown(int mX, int mY);
Now, open up CApp_OnEvent.cpp and add the function:
void CApp::OnLButtonDown(int mX, int mY) {
    int ID    = mX / 200;
    ID = ID + ((mY / 200) * 3);

    if(Grid[ID] != GRID_TYPE_NONE) {
        return;
    }

    if(CurrentPlayer == 0) {
        SetCell(ID, GRID_TYPE_X);
        CurrentPlayer = 1;
    }else{
        SetCell(ID, GRID_TYPE_O);
        CurrentPlayer = 0;
    }
}
First, we are doing the reverse of what we did with translating to X and Y from an ID, this time we are translating to an ID. We then make sure that that cell hasn't already been taken, if it has, we return out of the function. Next, we are checking which players turn it is, set the cell appropriately, and then switch turns. CurrentPlayer is a new variable to specify whose turn it is, so we'll need to add this. Open up CApp.h and add the variable below the grid array:
int CurrentPlayer;
Also, set the default value for this variable in CApp.cpp:
CApp::CApp() {
    CurrentPlayer = 0;

    Surf_Grid = NULL;
    Surf_X = NULL;
    Surf_O = NULL;

    Surf_Display = NULL;

    Running = true;
}
Try compiling the program and you should have a mostly working version of tic-tac-toe. Congratulations!

This is where you take the rest of it yourself. We have a solid foundation for our game, and most the work done. I encourage you to take this a step further. Try adding a "X Won", "O Won", and "Draw" at the end of each game (extra images here). Think, how are you going to check who won (a function for this purpose would fit well)? Try adding a way to reset the game after it's done. If you are brave, try to add some generic AI that will play against the user. And if you are even braver, try adding the ability to play player vs. player, or player vs. computer.


Tutoriel SDL

SDL Animation
The last tutorial we took our chance at making a Tic-Tac-Toe game. Hopefully most of you were successful in getting it to work. If not, don't fret, you'll get the hang of all of this eventually.

In this tutorial we are going to take our hand at SDL Animation. As before, we'll be building on top of the previous SDL lessons (but not including the Tic Tac Toe one). So lets get started.

We'll be creating a new class to handle Animation, and in the next tutorial we will create a class to handle Entities. Please keep in mind that these two things are seperate, and while I know they could be one class, I don't wish to take that approach. So please hold back your criticism.

Create two new files (I am sure you are seeing a pattern now, hopefully), called CAnimation.h and CAnimation.cpp. Eventually, our CEntity class will inherit this class, but for now we'll be testing through an object we create later. And before we get started add the include directive within CApp.h (before #include "CEvent.h" is just fine).
#include "CAnimation.h"
Now open up CAnimation.h and lets start coding. Add the following basic class structure to your file.
#ifndef _CANIMATION_H_
    #define _CANIMATION_H_

#include <SDL.h>

class CAnimation {
    private:
        int    CurrentFrame;

        int     FrameInc;

    private:
        int     FrameRate; //Milliseconds

        long    OldTime;

    public:
        int    MaxFrames;

        bool    Oscillate;

    public:
        CAnimation();

        void OnAnimate();

    public:
        void SetFrameRate(int Rate);

        void SetCurrentFrame(int Frame);

        int GetCurrentFrame();
};

#endif
Now, open up CAnimation.cpp and add this code:
#include "CAnimation.h"

CAnimation::CAnimation() {
    CurrentFrame    = 0;
    MaxFrames       = 0;
    FrameInc        = 1;

    FrameRate       = 100; //Milliseconds
    OldTime         = 0;

    Oscillate       = false;
}

void CAnimation::OnAnimate() {
    if(OldTime + FrameRate > SDL_GetTicks()) {
        return;
    }

    OldTime = SDL_GetTicks();

    CurrentFrame += FrameInc;

    if(Oscillate) {
        if(FrameInc > 0) {
            if(CurrentFrame >= MaxFrames) {
                FrameInc = -FrameInc;
            }
        }else{
            if(CurrentFrame <= 0) {
                FrameInc = -FrameInc;
            }
        }
    }else{
        if(CurrentFrame >= MaxFrames) {
            CurrentFrame = 0;
        }
    }
}

void CAnimation::SetFrameRate(int Rate) {
    FrameRate = Rate;
}

void CAnimation::SetCurrentFrame(int Frame) {
    if(Frame < 0 || Frame >= MaxFrames) return;

    CurrentFrame = Frame;
}

int CAnimation::GetCurrentFrame() {
    return CurrentFrame;
}
Some explanation on what this class is all about now. There is one basic element of animation that we need to handle, that is the current frame of the animation. Take the Yoshi image below for example (we'll be using him in this tutorial). You'll notice we have 8 frames of Yoshi on one image. Each frame would then be labeled 0, 1, 2 from Top to Bottom.

Remember the second tutorial where we create a function to draw part of an image? Well, if we take that function in conjunction with the frame of the animation, voila!

So the first variable I want you to see is CurrentFrame. This is the current frame of the animation that we are going to draw on the screen. Whatever value it has, will determine what part of the surface we will draw to the screen. So, when we call our draw function, we would do something like this:
CSurface::OnDraw(Surf_Display, Surf_Image, 0, 0, 0, Anim_Yoshi.GetCurrentFrame() * 64, 64, 64);
Since our Yoshi is really 64 x 64 pixels, that's the width and height we will grab, and is also how we grab the frame. Look at the image below for an illustration.

When the CurrentFrame increases by 1, we jump down 64 pixels (the height of Yoshi's frame), and draw that frame.

The other part of this class is that we need to know how many frames this animation is, thus the MaxFrames. The last vital part to know is how many frames per second, or rather, how fast this animation is going to display. In order to determine that we find use this bit of code found in the OnAnimate function.
if(OldTime + FrameRate > SDL_GetTicks()) {
    return;
}
By taking the Old Time in milliseconds plus the desired frame rate, we can check it against how long the SDL has been running. So, for example, we just started our program. SDL_GetTicks is 0, and OldTime is 0. Our desired frame rate is 1 frame per second. So FrameRate = 1000 (milliseconds). So, is 0 + 1000 greater than 0? Yes, thus we will skip over the function and wait. But once 0 + 1000 is less than SDL_GetTicks, that must mean 1 second has passed. So we increment the frame, and then reset OldTime to the current time, and start over.

The next interesting tid bit is Oscillate and FrameInc. Not that I wanted to confuse anyone by adding this, but I feel it's necessary for certain things later on. Basically, when the Oscillate flag is true, the animation will increase frames, and then decrease frames. If we had an animation with 10 frames, it would do something like this:

0 1 2 3 4 5 6 7 8 9 8 7 6 5 4 3 2 1 2 ...

You see, it goes up to 9, and then decreases back down to 0, and so on. There are some interesting uses for this, but we'll get into that in other lessons. So how does this work? Take a look at the OnAnimate function.
void CAnimation::OnAnimate() {
    if(OldTime + FrameRate > SDL_GetTicks()) {
        return;
    }

    OldTime = SDL_GetTicks();

    CurrentFrame += FrameInc;

    if(Oscillate) {
        if(FrameInc > 0) {
            if(CurrentFrame >= MaxFrames) {
                FrameInc = -FrameInc;
            }
        }else{
            if(CurrentFrame <= 0) {
                FrameInc = -FrameInc;
            }
        }
    }else{
        if(CurrentFrame >= MaxFrames) {
            CurrentFrame = 0;
        }
    }
}

Bug Fix: if(CurrentFrame >= MaxFrames - 1) needs to be if(CurrentFrame >= MaxFrames). Kudos to everyone in the comments mentioning this.

Bug Fix: There was an error with placing CurrentFrame += FrameInc; after the if statement, it should have been placed before the if statement. What would happen is if the animation had MaxFrames = 0, it would still increase the CurrentFrame to 1. Thanks Alexander Mangel for helping find this bug!

We already know what the OldTime and such do, but what about the rest? For now, look at the else statement of the Oscillate if statement. You'll notice we're simply checking if the CurrentFrame has exceeded the Max Frames. If it has, reset back to 0. Pretty simple. Then below that, outside of the block, we increase to the next frame.

Now, the more confusing part is the Oscillate if statement. This is where the FrameInc variable comes in. Basically, the FrameInc is set to 1 or -1, depending on how we are increasing or decreasing the frames. Remember, Oscillating causes the frames to go from 0 to 9 back to 0. So if the FrameInc is greater than 0, we are increasing the frames, otherwise we are decreasing frames. The innermost if statements basically inverse the FrameInc if we reached the end of 0, or MaxFrames.

Now that that is all taken care of, lets put this class into action. Create a new CAnimation object within CApp.h:
CAnimation      Anim_Yoshi;
Now, lets set the MaxFrames, add this to CApp_OnInit:
Anim_Yoshi.MaxFrames = 8;
If you want to see your animation Oscillate, set this:
Anim_Yoshi.Oscillate = true;
Okay, now to make our animation loop, add this to CApp_OnLoop:
Anim_Yoshi.OnAnimate();
Now, the last thing, to make it actually animate; add this to CApp_OnRender:
CSurface::OnDraw(Surf_Display, Surf_Test, 290, 220, 0, Anim_Yoshi.GetCurrentFrame() * 64, 64, 64);
Now try compiling, and watch your Yoshi workout! Make sure you replace the myimage.bmp with the Yoshi image.


Tutoriel SDL

SDL Entities
In this new tutorial, as I had promised before, we are going to take our hand at creating entities. Entities, for all gaming purposes, are anything that can be interacted with in any way, shape, or form. Some examples might be a monster or a treasure chest that you can open. In this sense, practically everything within the game that moves is an Entity. A rock that is part of a map, which never moves, is not an entity. But if you wanted that rock to move for whatever reason, then we'd make it an Entity. This tutorial will be split into 3 different tutorials. The first, this one you are reading, will deal with a basic Entity class structure. The next tutorial will veer off slightly to build a Map class via a tileset. Then, the last tutorial, which is what a lot of people have trouble with, will deal with Entity to Map collision, and Entity to Entity Collision.

Update:
- Fixed class below to have a virtual destructor. (Thanks Andras!)

Lets get started by creating two new files called CEntity.cpp and CEntity.h. Open up the header file and add the following:
#include <vector>
#include "CAnimation.h"
#include "CSurface.h"

class CEntity {
    public:
        static std::vector<CEntity*>    EntityList;

    protected:
        CAnimation      Anim_Control;

        SDL_Surface*    Surf_Entity;

    public:
        float           X;
        float           Y;

        int             Width;
        int             Height;

        int             AnimState;

    public:
        CEntity();

        virtual ~CEntity();

    public:
        virtual bool OnLoad(char* File, int Width, int Height, int MaxFrames);

        virtual void OnLoop();

        virtual void OnRender(SDL_Surface* Surf_Display);

        virtual void OnCleanup();
};
Now, open up the cpp file and add the following:
#include "CEntity.h"

std::vector<CEntity*> CEntity::EntityList;

CEntity::CEntity() {
    Surf_Entity = NULL;

    X = Y = 0.0f;

    Width = Height = 0;

    AnimState = 0;
}

CEntity::~CEntity() {
}

bool CEntity::OnLoad(char* File, int Width, int Height, int MaxFrames) {
    if((Surf_Entity = CSurface::OnLoad(File)) == NULL) {
        return false;
    }

    CSurface::Transparent(Surf_Entity, 255, 0, 255);

    this->Width = Width;
    this->Height = Height;

    Anim_Control.MaxFrames = MaxFrames;

    return true;
}

void CEntity::OnLoop() {
    Anim_Control.OnAnimate();
}

void CEntity::OnRender(SDL_Surface* Surf_Display) {
    if(Surf_Entity == NULL || Surf_Display == NULL) return;

    CSurface::OnDraw(Surf_Display, Surf_Entity, X, Y, AnimState * Width, Anim_Control.GetCurrentFrame() * Height, Width, Height);
}

void CEntity::OnCleanup() {
    if(Surf_Entity) {
        SDL_FreeSurface(Surf_Entity);
    }

    Surf_Entity = NULL;
}
Okay, now for some basic explanation. What we are doing here is encapsulating the basic 5 components I mentioned within the first lesson (excluding Events, which will be handled in a later lesson). This allows us to handle Entities within the game much more easily, rather than clumping them together with everything else in the game within the main CApp class. This will also be the way we handle other things as well. The first thing you may notice is a static vector called EntityList. This vector will hold all of our entities, easily accessible through CEntity::EntityList, because it's declared as a static. I should make a special note here: we declare this EntityList within CEntity because it prevents from circular dependencies later on. An example of this is trying to get a Map to communicate with Entities, and Entities to get to communicate with the Map. Such as CMap declaring a CEntity member, and CEntity declaring a CMap member. It would cause problems on the compile level.

So this vector contains all of our Entities within the game. Notice that each member of the vector is a pointer. This is because later on we are going to inherit this CEntity class for Entity specific classes. So, for example, if we were going to make a Megaman game, we would have a CMegaMan class inheriting the CEntity class. And, via polymorphism, we can store that CMegaMan class within the EntityList. This is the very reason why we declared the functions above as virtuals, and certain members as protected.

Next, we have basic information about the Entity, common to all Entities, coordinates, dimensions, and a surface for its image. Next, we have a loading function that basically takes a filename, and loads the image. By default, we have it setting a transparent color. I'd like to step aside here for a moment to let you all know that certain things I do aren't set in stone. You can, and are encourage, to take this code and modify to your liking. You may want more parameters on your OnLoad function, or you may want less. You may not want a default transparent color, who knows. I encourage you to test different things. Don't worry, my code will still be here if you mess things up.

Next, we have a basic OnLoop function that handles basic calculations. Right now we are only calculating Animation. Also please note that we have only set the MaxFrames for the Animation, and left the defaults in place. Next, we have the OnRender function. Instead of making it render to the display only, I've allowed a parameter to specify where to render this entity. This could be any surface you want. So you could, if you wanted, render one entity onto another.

Lastly, we have an OnCleanup function that restores memory and all that stuff.

Like I mentioned in the beginning, this is a basic Entity class structure, it basically doesn't do much yet, but don't fret, it soon will in coming lessons. So lets get it working. Open up CApp.h and add the header file to the top, and declare two Entities:
#include "CEntity.h"

//...

private:
    CEntity         Entity1;
    CEntity         Entity2;
Now, lets load these two Entities. Open up CApp_OnInit.cpp and add the following:
if(Entity1.OnLoad("./entity1.bmp", 64, 64, 8) == false) {
    return false;
}

if(Entity2.OnLoad("./entity2.bmp", 64, 64, 8) == false) {
    return false;
}

Entity2.X = 100;

CEntity::EntityList.push_back(&Entity1);
CEntity::EntityList.push_back(&Entity2);
Now, depending on the images you use, you need to set the values appropriately on the OnLoad function. I've reused the yoshi image from the previous lesson, and if you still need it:

Now, remember how I stated we are basically encapsulating the basic functions of a game within the Entity class? We have to call those functions now in the respective CApp functions. So, open up CApp_OnLoop.cpp and add the following:
for(int i = 0;i < CEntity::EntityList.size();i++) {
    if(!CEntity::EntityList[i]) continue;

    CEntity::EntityList[i]->OnLoop();
}
We are basically running through each Entity in our vector, and calling the OnLoop function. Simple enough! (And we're doing an error checking so we don't call any NULL pointers). Now, lets do the same things in CApp_OnRender.cpp:
for(int i = 0;i < CEntity::EntityList.size();i++) {
    if(!CEntity::EntityList[i]) continue;

    CEntity::EntityList[i]->OnRender(Surf_Display);
}
And the same thing in CApp_OnCleanup.cpp:
for(int i = 0;i < CEntity::EntityList.size();i++) {
    if(!CEntity::EntityList[i]) continue;

    CEntity::EntityList[i]->OnCleanup();
}

CEntity::EntityList.clear();

Note that I added the clear function call, which clears out the vector to nothing. Basically a reset.


Tutoriel SDL

SDL Maps
As I stated in the last lesson, we're going to be looking at making a Map class that will be tile based. In addition to the maps, we'll be creating Areas that enhouse many Maps. While we could create one giant map, it's far easier to manage many smaller maps, and also opens the possibility of tiling maps as well. By the way, head on over to the SDL Image tutorial if you haven't already done so, we will be making the switch over to SDL Image and stop using SDL_LoadBMP. No more chatter, lets get started.

Known Bug: The camera settings for this tutorial are backwards, and are reversed in the SDL Collision tutorial (and following). Changes are not put in place yet, to get the fixes view the SDL Collision tutorial.

We're going to need several files in the beginning, Define.h, CArea.h, CArea.cpp, CMap.h, CMap.cpp, CTile.h, and CTile.cpp. So create these blank files. What we are going to do is have one Area object, this Area object will have many children Map objects in a vector (like the Entities in a vector). Within each of these Map objects, they will have a vector of each individual tile. So basically, we have a bunch of tiles that make up a map, and a bunch of maps make up an area.

Lets start by opening up Define.h. This file is going to hold some constant values, as defines, that will define things like map width and height, screen width and height, and tile size. These are going to be some values that we will be using throughout making our Maps and such. Add the following below to define.h.
#ifndef _DEFINE_H_
    #define _DEFINE_H_

#define MAP_WIDTH    40
#define MAP_HEIGHT    40

#define TILE_SIZE    16

#define WWIDTH        640
#define    WHEIGHT        480

#endif
We'll get back to what these values mean later, but WWIDTH and WHEIGHT are the values set in SDL_SetVideoMode. So open up CApp_Init and change that function call to the following:
if((Surf_Display = SDL_SetVideoMode(WWIDTH, WHEIGHT, 32, SDL_HWSURFACE | SDL_DOUBLEBUF)) == NULL) {
    return false;
}
Simple enough. Next, open up CTile.h. This class is going to define a single tile on a map. In gaming terms, a tile is basically a single square graphic that we draw to the screen. Remember the animation tutorial, each frame of Yoshi could be thought of as a tile. So when I say we have a map made up of many tiles, we are taking these tiles and repeating them in a grid sequence.

And since we have a single graphic containg all of our tiles, we only have to load one graphic, and not several for each tile. Each tile then needs some properties. The most obvious is which graphical tile to use. Take a look at an example tileset below:

You'll notice that the very top left has an ID of 0, and going to the left it increments in 1s. This is called the Tile ID. By using the ID, and the size of each tile (TILE_SIZE), we can know which tile to draw to the screen. The next important thing is TypeID. This determines what type of tile it is. Some examples might be, invisible (don't draw a tile), block (like a wall), or ground (player can walk on it). You can define whatever TypeIDs you want, but these are the most basic (other examples might be water, fire, ice, etc...). That's basically it, this Tile class only has two members (for now, later on it will contain information for animated tiles). So lets push this class together inside of CTile.h:
#ifndef _CTILE_H_
#define _CTILE_H_
#include "Define.h"

enum {
    TILE_TYPE_NONE = 0,

    TILE_TYPE_NORMAL,
    TILE_TYPE_BLOCK
};

class CTile {
    public:
        int     TileID;
        int     TypeID;

    public:
        CTile();
};

#endif
Notice the enum used. This lets us assign or check the TypeID, and know what it is at the same time. So if I did TypeID == TILE_TYPE_BLOCK, I would be able to notice in my code that I am checking for a block type of tile. TypeID == 2 isn't as easy to interpret. If you never used enums, think of them like const variables. The value never changes. Also, the starting value defines the rest, notice I have = 0 on TILE_TYPE_NONE. From there the rest of the variables would automatically be assigned values in increments of 1.

Now, lets open up CTile.cpp and finish the code:
#include "CTile.h"

CTile::CTile() {
    TileID = 0;
    TypeID = TILE_TYPE_NONE;
}
Pretty straight forward. Next, lets look at making some maps. The first thing we need to consider is that our maps will be text files. So we need to come up with a file format, that way we can easily edit our maps outside of our code. And possibly later you could create a map editor.

Each map is going to have a width and height, defining the number of tiles. So a map of 10x10, would have 100 tiles in it. We are going to make all of our maps the same width and height, so we don't have to define that within the map files (MAP_WIDTH and MAP_HEIGHT). The part that we do need to add though is the TileID and TypeID for each individual tile. We are going to use the file format that I came up with, as it is relatively simple. Take a look at the 5x5 map example below:

0:0 0:0 0:0 0:0 0:0
1:0 1:0 1:0 0:0 0:0
1:0 1:0 1:0 0:0 0:0
1:0 1:0 1:0 0:0 0:0
1:0 1:0 1:0 0:0 0:0

Each tile within the file would consist of 0:0, effectively being the TileID:TypeID. A space would be the deliminator between the tiles. A concept rendering if this map may look like this:

For this tutorial we will be using 40x40 maps, since that is a pretty decent size. And instead of printing an example out for you to copy, click on this link here:

Example Map First create a folder in the same directory as your exe called maps. This folder is where all your maps will be. Save that map as 1.map. Also, make another folder called tilesets in the same directory as your exe, and save the tileset above as 1.png.

Now that we have a file format for our maps, lets start designing the class that will render this thing. Open up CMap.h:
#ifndef _CMAP_H_
#define _CMAP_H_
#include <SDL.h>
#include <vector>
#include "CTile.h"
#include "CSurface.h"

class CMap {
    public:
        SDL_Surface* Surf_Tileset;

    private:
        std::vector<CTile> TileList;

    public:
        CMap();

    public:
        bool OnLoad(char* File);

        void OnRender(SDL_Surface* Surf_Display, int MapX, int MapY);
};

#endif
A few of the basic things are here, OnLoad and OnRender. OnLoad does what you expect, loads a map from a file, and populates the TileList. The OnRender also does what you expect, properly puts each tile on the screen and draws them using the Surf_Tileset. Lets define what these functions do, now open up CMap.cpp:
#include "CMap.h"

CMap::CMap() {
    Surf_Tileset = NULL;
}

bool CMap::OnLoad(char* File) {
    TileList.clear();

    FILE* FileHandle = fopen(File, "r");

    if(FileHandle == NULL) {
        return false;
    }

    for(int Y = 0;Y < MAP_HEIGHT;Y++) {
        for(int X = 0;X < MAP_WIDTH;X++) {
            CTile tempTile;

            fscanf(FileHandle, "%d:%d ", &tempTile.TileID, &tempTile.TypeID);

            TileList.push_back(tempTile);
        }
        fscanf(FileHandle, "\n");
    }

    fclose(FileHandle);

    return true;
}

void CMap::OnRender(SDL_Surface* Surf_Display, int MapX, int MapY) {
    if(Surf_Tileset == NULL) return;

    int TilesetWidth  = Surf_Tileset->w / TILE_SIZE;
    int TilesetHeight = Surf_Tileset->h / TILE_SIZE;

    int ID = 0;

    for(int Y = 0;Y < MAP_HEIGHT;Y++) {
        for(int X = 0;X < MAP_WIDTH;X++) {
            if(TileList[ID].TypeID == TILE_TYPE_NONE) {
                ID++;
                continue;
            }

            int tX = MapX + (X * TILE_SIZE);
            int tY = MapY + (Y * TILE_SIZE);

            int TilesetX = (TileList[ID].TileID % TilesetWidth) * TILE_SIZE;
            int TilesetY = (TileList[ID].TileID / TilesetWidth) * TILE_SIZE;

            CSurface::OnDraw(Surf_Display, Surf_Tileset, tX, tY, TilesetX, TilesetY, TILE_SIZE, TILE_SIZE);

            ID++;
        }
    }
}

Bug Fix: There was a bug in the continue within the loop, we should be incrementing the ID before continuing. Thanks Zakalwe for helping find this bug!
Bug Fix: We should been dividing by TilesetWidth for TilesetY, otherwise it will not correctly grab the correct Y coordinate unless the Tileset is square. Thanks Anime for helping find this bug!

Lets start at the top. The constructor sets the Tileset to NULL, obviously. Next we have the OnLoad function. First, we are clearing out any old tiles, that way if we load twice, it won't have double the amount of tiles, effectively loading a new map. We then open up a FileHandle, and try to open the requested map file. Now, we go through the map file and grab each tile. This is accomplished by using the two loops. The outer loop is the Y axis loop, going from the top row to the bottom row of the map file. The inner loop is the X axis loop, going from the left most tile, to the right most tile. Pay attention to how this loop works, because it is also used below in the OnRender function. It basically goes from the top left tile, to the bottom right tile, 1 tile at a time. Inside of those loops we create a temporary tile, and load the file information into it. We then push that into our TileList, effectively saving the tile. We close the filehandle, and we are done.

Next, we have the function to render a map. Notice the MapX and MapY arguments. These tell use where to render this map on the screen. This is useful later on for moving maps around. First in the function we check for a valid tileset, because we are going to access this tileset directly and don't want to cause a crash. We then grab the TilesetWidth and TilesetHeight in Tiles. This is important, because we need to know how many tiles a tileset contains, not its actual width and height. That way we can match up a TileID to a tileset. So, a Tileset with a Width and Height, in tiles, of 2x2, would have 4 tiles on it, but in reality it would be 32x32 pixels. So, a TileID of 0 would match the first tile, a TileID of 1 would be the next, and so on. TileIDs repeat on the next row, from left to right.

Okay, next we have that same loop again, going through each tile (Please note that these X and Y coordinates here in the loop are also in number of tiles, they aren't actual pixels). This time, however, we have an ID variable. This ID is incremented by one each loop, and will allow us to grab each tile in the map. So, first we check to see if this Tile should even be drawn, by checking the TypeID. If it is TILE_TYPE_NONE, we skip over it with continue. After that we figure out where to draw this tile on the screen. This is done by taking the MapX and MapY (effectively offset coordinates, because they offset a map from 0, 0 to somewhere, which makes it seem like a map is moving), and taking the X and Y coordinates in the loop. We have to convert these X and Y coordinates back to pixel coordinates, which is done by multiplying it by the size of a tile.

After that, we do something with the Tileset. What we are doing is calculating where on the Tileset to grab the appropriate tile. This is by grabing the TileID of the tile first, and then converting that to a Tile coordinate. A little bit of explanation here. We have our 2x2 tileset, and a TileID of 1. Figuring out X, we would get 1 % 2, which would be 1 still. We when multiply that by the TILE_SIZE, and get 16. That is the correct X coordinate for the Tile. Same with the Y, we put 1 / 2, which is 0.5. Since this an integer operation, the .5 is automatically dropped. Thus we are left with 0. Which is also the correct row. Now, say we had a TileID of 2. 2 % 2 = 0, and 2 / 2 = 1. See how the X repeats in a pattern? 0, 1, 0, 1... And notice how Y increases every time it goes past the Tileset Width? 0, 0, 1, 1. I hope this is clear, as it is somewhat hard to explain.

Next, we actually draw the tile to the screen using the coordinates we just calculated, and then increase the ID to go to the next tile. A little side note here, we could, for the sake of speed, create an OnRender_Cache function that would perform this same operation, but would draw to a Surface defined in the CMap class. Something like SDL_Surface* Surf_Map. Then, the OnRender function would render the Surf_Map only, and not perform any operations. But also take note that that method does not necessarely work later on when we want to animate tiles.

Okay, cool! I know that is quite a bit to take in at once, but don't fret! It's really not too bad once you get the hang of it. I want to say that right now we could create Map objects and render maps just fine. This would be ideal for games like Mario and Megaman, as they are just one big map. But for games like Zelda and Metroid, it won't work too well. That is where Areas come in. Just like maps having file formats, each area will have its own file and file format. Each area is going to have a tileset to load, the size of the area, and the maps that are to be loaded in each area. Here is the area we are going to be using below:

./tilesets/1.png
3
./maps/1.map ./maps/1.map ./maps/1.map
./maps/1.map ./maps/1.map ./maps/1.map
./maps/1.map ./maps/1.map ./maps/1.map

Save this into a file called 1.area, and save it in your maps directory. Just like maps, this area will tile maps. I won't get into detail because I think you all have the point by now.

Open up CArea.h and lets begin:
#ifndef _CAREA_H_
#define _CAREA_H_
#include "CMap.h"

class CArea {
    public:
        static CArea            AreaControl;

    public:
        std::vector<CMap>       MapList;

    private:
        int                     AreaSize;

        SDL_Surface*        Surf_Tileset;

    public:
        CArea();

        bool    OnLoad(char* File);

        void    OnRender(SDL_Surface* Surf_Display, int CameraX, int CameraY);

        void    OnCleanup();
};

#endif
This class is going to similar to the Map class in some ways, but different in others. Similarly, we have MapList that will hold our maps, just like the Map class had a list of tiles. Also, as have a Load function and Render function. Now the differences, we have a static AreaControl, much like Entities have a static control. This will allow us to manipulate an Area from any class by using this object. Next, we have the AreaSize which is the number of maps. We are going to assume that areas will always be square, so an AreaSize of 3, would be a 3x3 area. If you wanted to, and I don't think it's necessary, you would have AreaWidth and AreaHeight. Next, we have a surface for the Tileset. You probably noticed that within the CMap class we never actually loaded a tileset. That's because the Area is going to do it for us, and then pass that pointer to the CMap class. That way each map doesn't load a tileset for itself, but effectively all maps share the same tileset. Again, if you wanted each Map to have its own tileset you can easily modify the class to do so.

Okay, now open up the CArea.cpp class:
#include "CArea.h"

CArea CArea::AreaControl;

CArea::CArea() {
    AreaSize = 0;
}

bool CArea::OnLoad(char* File) {
    MapList.clear();

    FILE* FileHandle = fopen(File, "r");

    if(FileHandle == NULL) {
        return false;
    }

    char TilesetFile[255];

    fscanf(FileHandle, "%s\n", TilesetFile);

    if((Surf_Tileset = CSurface::OnLoad(TilesetFile)) == false) {
        fclose(FileHandle);

        return false;
    }

    fscanf(FileHandle, "%d\n", &AreaSize);

    for(int X = 0;X < AreaSize;X++) {
        for(int Y = 0;Y < AreaSize;Y++) {
            char MapFile[255];

            fscanf(FileHandle, "%s ", MapFile);

            CMap tempMap;
            if(tempMap.OnLoad(MapFile) == false) {
                fclose(FileHandle);

                return false;
            }

            tempMap.Surf_Tileset = Surf_Tileset;

            MapList.push_back(tempMap);
        }
        fscanf(FileHandle, "\n");
    }

    fclose(FileHandle);

    return true;
}

void CArea::OnRender(SDL_Surface* Surf_Display, int CameraX, int CameraY) {
    int MapWidth  = MAP_WIDTH * TILE_SIZE;
    int MapHeight = MAP_HEIGHT * TILE_SIZE;

    int FirstID = -CameraX / MapWidth;
        FirstID = FirstID + ((-CameraY / MapHeight) * AreaSize);

    for(int i = 0;i < 4;i++) {
        int ID = FirstID + ((i / 2) * AreaSize) + (i % 2);

        if(ID < 0 || ID >= MapList.size()) continue;

        int X = ((ID % AreaSize) * MapWidth) + CameraX;
        int Y = ((ID / AreaSize) * MapHeight) + CameraY;

        MapList[ID].OnRender(Surf_Display, X, Y);
    }
}

void CArea::OnCleanup() {
    if(Surf_Tileset) {
        SDL_FreeSurface(Surf_Tileset);
    }

    MapList.clear();
}
Quickly, going from the top, we declare the static object, and then set the AreaSize to 0. Then we have our Load function. It works just like the Load function within CMap, except there are a few differences. We have to load tileset first. We try to load this tileset into the surface, and if it fails return false. We then load the size of the Area. Okay, after that we have two loops, just like maps, that will go through and grab each map filename. It will then create a map object, load the map, set the tileset to be used, and then push it into the list. Pretty simply and straight forward.

Next, we have the Render function. We calculate the actual Map width and height, in pixels, first. This will let us find the first map to render. Some explanation first of what I am trying to do. Since an area can be of any size, like 100x100 maps, we don't want to go through all the trouble and render every single map. We only want to render the maps that are visible in the screen area. For our size of a screen, 640x480, only a possible 4 maps can be visible at one time (like standing at the corner of 4 maps). So what we have to do is calculate the first Map ID to grab. This is going to the first map to render. From the first ID, we know the next 3 maps to render. The one to the right of the first map, the one at the bottom of the first map, and the one at the bottom right of the first map. How is that accomplished? Look at the image below:

We have a guy, the circle, in the very center of the screen, represented by the red square. The other squares are each Map, and we have a Camera position of -700, -700. Why negative? Think about it, the screen itself never really moves, everything else does. So, for something to move up, it must increase its Y in the negative direction. Same with the X coordinate. So, to get to the 4th map, we have to move the area in the negative direction. Now, notice the grayed out maps, these are maps not visible within the players view, so they shouldn't be rendered. To figure out the First ID, which is 4 in this case, we need to use the specified Camera coordinates. We translate those Camera coordinates into Map coordinates. So, taking -(-700) / 640 (which is 40 * 16, remember the MapWidth calculation done above) we get 1 (dropping the decimal because its an integer operation). This is the X coordinate in Maps, but we aren't done. We then calculate the Y coordinate in Maps the same way -(-700) / 640, but we multiply that by the AreaSize. That's because we are grabbing the ID. So, it would become 1 * 3, which is 3, and adding that to the first calculation of 1, we get 4. And what do you know, the First ID on the map!

Okay, so we go through each of the four maps now. Since I did a regular loop of i < 4, we need to figure out how to add that to the First ID, to actually figure out each of the four Map IDs. This is done by taking the First ID as an offset first. We then take i, our position in the loop, divide by two and multiply by the area size. What does this do? Much like the Map class, it creates a pattern, 0, 0, 1, 1. Same with the last operation i % 2, it creates a pattern of 0, 1, 0, 1. This then gives us the correct pattern of, 4, 5, 7, 8. Which are the correct map to render.

We do a little check to make sure the ID is good, since the ID may not exist. And now the last calculation (yes, I know, a lot of complex calculations). It works just like the way we calculated which tile to grab on the tileset. It's turning an ID into actual pixel coordinates, and then offsetting those coordinates by the camera (making it seem like it moved). We then finally render the map, passing the coordinates of where to draw it.

Lastly, we have our cleanup function that frees the surface and clears the maps.

What a load! I hope all of the various calculations don't get you confused. There are really only two basic things we are trying to do, turn pixel coordinates into grid based coordinates. Such as with tiles, and with the maps. The other thing was turning those grid based coordinates back and forth into IDs.

Okay, we are not quite done yet, still a little bit to do (now you see why this tutorial took so long to complete!). You may have noticed that I called the arguments on the Render function on the CArea class CameraX and CameraY. We are going to need to make a camera class that defines the viewing area. This is what we are going to manipulate to move around on a map!

So create two new files, CCamera.cpp and CCamera.h. Open up the header file first:
#ifndef _CCAMERA_H_
#define _CCAMERA_H_
#include <SDL.h>
#include "Define.h"

enum {
    TARGET_MODE_NORMAL = 0,
    TARGET_MODE_CENTER
};

class CCamera {
    public:
        static CCamera CameraControl;

    private:
        int X;
        int Y;

        int* TargetX;
        int* TargetY;

    public:
        int TargetMode;

    public:
        CCamera();

    public:
        void OnMove(int MoveX, int MoveY);

    public:
        int GetX();
        int GetY();

    public:
        void SetPos(int X, int Y);

        void SetTarget(int* X, int* Y);
};

#endif
First, we have a control member object, just like area. Then we have the coordinates of where the camera is. An extra thing I threw in, was the ability to target something. For example, in a Megaman game the camera would target Megaman himself. That way, when Megaman moves the camera would automatically update. So we have two pointers for X and Y coordinates. If these are null, the camera will automatically revert to the cameras position. Next, we have a target mode, which, for now, is either normal (the camera will position to the top left of the target) or center (will center the camera to the target). Pretty simply I think.

We then have a few functions, the first is the OnMove, which will increase the X and Y of the camera by MoveX and MoveY. So OnMove(-1, 0) would move the camera to the left one pixel. We then have Get functions for the coordinates, and the ability to set the coordinates and set a target.

Okay, now open up CCamera.cpp:
#include "CCamera.h"

CCamera CCamera::CameraControl;

CCamera::CCamera() {
    X = Y = 0;

    TargetX = TargetY = NULL;

    TargetMode = TARGET_MODE_NORMAL;
}

void CCamera::OnMove(int MoveX, int MoveY) {
    X += MoveX;
    Y += MoveY;
}

int CCamera::GetX() {
    if(TargetX != NULL) {
        if(TargetMode == TARGET_MODE_CENTER) {
            return *TargetX - (WWIDTH / 2);
        }

        return *TargetX;
    }

    return X;
}

int CCamera::GetY() {
    if(TargetY != NULL) {
        if(TargetMode == TARGET_MODE_CENTER) {
            return *TargetY - (WHEIGHT / 2);
        }

        return *TargetY;
    }

    return Y;
}

void CCamera::SetPos(int X, int Y) {
    this->X = X;
    this->Y = Y;
}

void CCamera::SetTarget(int* X, int* Y) {
    TargetX = X;
    TargetY = Y;
}
From top to bottom, like usual, we have are static member first. We then have the constructor defaulting some variables. The OnMove function will increase the X and Y, like I said before. We then have GetX. It will check to see if we have a valid target, and if so return the targets coordinates as the camera coordinates, otherwise return the camera x. For centering mode, we take the screen width and divide by two to find the center of the screen, and then subtract it from the target coordinates. This will center the camera to the target. We then have the SetPos and SetTarget functions, which are self-explanatory.

So lets put all this junk together! Open up CApp.h and modify it to include the new header files:
#include "Define.h"
#include "CArea.h"
#include "CCamera.h"
Also, add a new function prototype:
void OnKeyDown(SDLKey sym, SDLMod mod, Uint16 unicode);
We are going to checking that event for move our map around with the keyboard. So, open up CApp_OnEvent.cpp and add the function:
void CApp::OnKeyDown(SDLKey sym, SDLMod mod, Uint16 unicode) {
    switch(sym) {
        case SDLK_UP:      CCamera::CameraControl.OnMove( 0,  5); break;
        case SDLK_DOWN:  CCamera::CameraControl.OnMove( 0, -5); break;
        case SDLK_LEFT:  CCamera::CameraControl.OnMove( 5,  0); break;
        case SDLK_RIGHT: CCamera::CameraControl.OnMove(-5,  0); break;

        default: {
        }
    }
}
We check the SDL keys, and according to the key move the camera around. Next, open up CApp_OnCleanup.cpp and add the cleanup function for the Area:
CArea::AreaControl.OnCleanup();
Open up CApp_OnRender.cpp and add the Render call too:
CArea::AreaControl.OnRender(Surf_Display, CCamera::CameraControl.GetX(), CCamera::CameraControl.GetY());
Notice that we are passing the Camera coordinates to the OnRender. And lastly, open up CApp_OnInit.cpp:
if(CArea::AreaControl.OnLoad("./maps/1.area") == false) {
    return false;
}

SDL_EnableKeyRepeat(1, SDL_DEFAULT_REPEAT_INTERVAL / 3);
SDL_EnableKeyRepeat call sets the keyboard repeat count. So if we hold down a key it will keep calling the event above. And we are done! Compile the code and try it out. I hope it all works as expected, this was a very lengthy tutorial. Please check out the tutorial files below if you are having a bit of trouble. And please let me know if for some reason I left some important code out.


Tutoriel SDL

SDL Collision
If you are still trying to catch your breath from the last tutorial, I recommend you sit down, because this next tutorial won't be any nicer. In this tutorial we will be extending our entity class a bit further with collisions, and movement. So hold on to your pants, and get ready.

The first order of business we'll tackle is movement. A seemingly simple idea that can be a bit tricky. Now, I am going to introduce a method that has worked for me for quiet some time, though I am not sure if it can be readily optimized, changed, or whatever. So save the criticism. The reason I like it so much is that it's pixel perfect now matter what system you are on, and it's already fast for its purpose.

But before we get into that, we're going to need a standard for movement. You see, when we play our games on old Pentium 2 as opposed to a quad core, there are apparent differences in speed. So, if we did a simple X++; on the older machine, the entity would move slower than the newer machine. So the idea is simple, figure out the frame rate of the machine, and base our movements off of the frame rate. I call that the speed factor. Now, in order for this to work, we have to have a base frame rate. I usually pick 32, as that is a good number. So if our machine had a frame rate of 32, our speed factor would be 1. If our frame rate was 64, our speed factor would be 0.5. We move twice as slower on the faster machine, that way speeds are consistent across different machines.

So, lets create this class. Create two new files called CFPS.h and CFPS.cpp. Open up the header file first:
#ifndef _CFPS_H_
    #define _CFPS_H_

#include <SDL.h>

class CFPS {
    public:
        static CFPS FPSControl;

    private:
        int     OldTime;
        int     LastTime;

        float     SpeedFactor;

        int    NumFrames;
        int     Frames;

    public:
        CFPS();

        void    OnLoop();

    public:
        int     GetFPS();

        float   GetSpeedFactor();
};

#endif
First, we have a single control for accessing our speed facor, and frame rate. Again, a singleton object that we should be used to using by now. Next, we have OldTime, which is used to calculate the Frames per second. Then we have LastTime, which is used to calculate the speed factor. SpeedFactor is the current speed factor of the game (of course), NumFrames is the games current FPS, and Frames is the frame count when calculating the frames per second (explanation for all these below). Notice that all of them are private members, so to access our FPS and SpeedFactor we have two get functions. We don't need anyone messing with those variables directly. Then, we have a simple constructor, and an OnLoop function to calculate everything.

Now, open up CFPS.cpp to add the actual code:
#include "CFPS.h"

CFPS CFPS::FPSControl;

CFPS::CFPS() {
    OldTime     = 0;
    LastTime    = 0;

    SpeedFactor    = 0;

    Frames      = 0;
    NumFrames   = 0;
}

void CFPS::OnLoop() {
    if(OldTime + 1000 < SDL_GetTicks()) {
        OldTime = SDL_GetTicks();

        NumFrames = Frames;

        Frames = 0;
    }

    SpeedFactor = ((SDL_GetTicks() - LastTime) / 1000.0f) * 32.0f;

    LastTime = SDL_GetTicks();

    Frames++;
}

int CFPS::GetFPS() {
    return NumFrames;
}

float CFPS::GetSpeedFactor() {
    return SpeedFactor;
}
I thought I'd drop a note in here, since I have had some people miss this; whenever you make a variable of a class static, you also need to declare it elsewhere. Notice I have CFPS::FPSControl at the top. If you fail to do this, you'll get an undeclared error.

Okay, we have a basic constructor, setting everything to 0. Next, we have our OnLoop function where all the magic happens. The first calculation we do, is finding the Frames per Second of the game. This is where SDL_GetTicks comes in. SDL_GetTicks returns the number of milliseconds that have passed since we have called SDL_Init. So, effectively, 1000 milliseconds equals 1 second. Now, notice the condition I am checking. I am checking if the OldTime (which is currently set to 0) + 1000, is less than the current tick count. Think about it, if OldTime is 0 + 1000, and 1001 milliseconds have passed, that means at least 1 second has passed. Why is that important? That tells use to restart the FPS counter.

Notice inside the if statement we then set OldTime to SDL_GetTicks, set NumFrames to the Frames count, and reset our Frames counter. So, going with the example above, OldTime now becomes 1001. Now, 1001 + 1000, is the next calculation, so another second must pass. Concurrent with this, is the Frames++ at the bottom of that function. This continuelly counts up, until the OldTime resets it. I hope that is easy to understand.

Next, we have the actual calculation for the SpeedFactor. Like I mentioned earlier, the SpeedFactor determines how fast to move objects. Now, for a better explanation. Say we are on a really slow computer that gets 1 FPS per second. That's a really bad computer by the way. We have the base movement rate set at 32 (our made up number). So, ideally, any computer will move objects 32 pixels per second. So this 1 FPS computer will move the object 32 pixels every second. Now, jump to a 10 FPS computer. We still want to move 32 pixels per second. So, that's 1/10 of 32 right? So, for each calculation we move 3.2 pixels, and after 1 second, we'll have moved 32 pixels.

To calculate this, we base it on how fast the computer is currently going. This is what LastTime is for. LastTime holds the time it took for the last loop in the game. So, in the constructor it's currently 0. Now, say we have an FPS of 10. That means we have 10 loops in the game every second. How many milliseconds is that? That's 100 milliseconds. So, 100 / 1000 (for 1 second), is 1/10. The same number we got above. Now multiply this by our desired movement rate, and we have our Speed Factor! I'll be showing you how to use this later on, just try to understand how we calculated it.

And lastly, we have our two get functions that return NumFrames (FPS), and the SpeedFactor.

Okay, so much already and we still have a long way to go!

From the last tutorial we loaded an area, 1.area, we'll be leaving this, as well as everything else from that code. But, we're going to take one thing out right now. Open up CApp_OnEvent.cpp and remove the following:
case SDLK_UP:      CCamera::CameraControl.OnMove( 0,  5); break;
case SDLK_DOWN:  CCamera::CameraControl.OnMove( 0, -5); break;
case SDLK_LEFT:  CCamera::CameraControl.OnMove( 5,  0); break;
case SDLK_RIGHT: CCamera::CameraControl.OnMove(-5,  0); break;
We aren't going to be controlling the camera anymore, but we'll be controlling our main Player who will dictate where the camera goes. Note: In the last tutorial I had some values backwards for the camera! Please read the notes in the last tutorial for the fix. This new tutorial will have the fixes already in place, but without any explanation.

Now, lets go ahead and add this new Player to the game. But first, some explanation. Remember, anything that moves in the game or that can be interacted with (besides the Map), is an Entity. So, our main Player will be an entity. But, Player is distinct, he likes certain things other Entities do not. So, instead of simply doing CEntity Player; we need to create a class just for Player. But! Before we can even to get to making the Player class, we're going to need to extend the Entity class. Things like this include the ability to Move an Entity, Collision (what we've all been waiting for), Animation states, and Entity Flags. Jumping and a few other things will have to wait until the next lesson (sorry).

So open up CEntity.h and change it to the following:
#ifndef _CENTITY_H_
#define _CENTITY_H_
#include <vector>
#include "CArea.h"
#include "CAnimation.h"
#include "CCamera.h"
#include "CFPS.h"
#include "CSurface.h"

enum {
    ENTITY_TYPE_GENERIC = 0,

    ENTITY_TYPE_PLAYER
};

enum {
    ENTITY_FLAG_NONE     = 0,

    ENTITY_FLAG_GRAVITY    = 0x00000001,
    ENTITY_FLAG_GHOST    = 0x00000002,
    ENTITY_FLAG_MAPONLY    = 0x00000004
};

class CEntity {
    public:
        static std::vector<CEntity*>    EntityList;

    protected:
        CAnimation      Anim_Control;

        SDL_Surface*    Surf_Entity;

    public:
        float           X;
        float           Y;

        int             Width;
        int             Height;

        bool        MoveLeft;
        bool        MoveRight;

    public:
        int        Type;

        bool        Dead;
        int        Flags;

    protected:
        float        SpeedX;
        float        SpeedY;

        float        AccelX;
        float        AccelY;

    public:
        float        MaxSpeedX;
        float        MaxSpeedY;

    protected:
        int             CurrentFrameCol;
        int             CurrentFrameRow;

    protected:
        int        Col_X;
        int        Col_Y;
        int        Col_Width;
        int        Col_Height;

    public:
        CEntity();

        virtual ~CEntity();

    public:
        virtual bool OnLoad(char* File, int Width, int Height, int MaxFrames);

        virtual void OnLoop();

        virtual void OnRender(SDL_Surface* Surf_Display);

        virtual void OnCleanup();

        virtual void OnAnimate();

        virtual void OnCollision(CEntity* Entity);

    public:
        void    OnMove(float MoveX, float MoveY);

        void     StopMove();

    public:
        bool    Collides(int oX, int oY, int oW, int oH);

    private:
        bool     PosValid(int NewX, int NewY);

        bool     PosValidTile(CTile* Tile);

        bool     PosValidEntity(CEntity* Entity, int NewX, int NewY);
};

class CEntityCol {
    public:
        static std::vector<CEntityCol>    EntityColList;

    public:
        CEntity* EntityA;
        CEntity* EntityB;

    public:
        CEntityCol();
};

#endif
Oh geez, what happened, and where did all this code come from?! Sorry, for all of you who just fainted, maybe you'll understand a little bit more now why this tutorial is so long and took so long. It's a lot to handle, let alone trying to teach to everyone. One step at a time.

First, we have the basic things still there, OnLoad, OnLoop, OnRender, and OnCleanup. The two new virtual functions are OnAnimate and OnCollision. Now, OnAnimate is used to define Animation States, like I mentioned earlier. This is useful for selecting the right animation when the Player moves left or right. The second, OnCollision, is called when an Entity collides with another Entity.

So, for example, if EntityA and EntityB collide, what happens? Well, first, neither can pass through each other; that's the easy part. The other part, is that EntityA needs to know EntityB collided with him. Such as, Mario falling on a spike. The spike notifies Mario, "Hey, I killed you! Change your animation and die." Well, the thing is, we cannot notify the other entity when the collision happens. One important reason is that, if the Spike kills Mario, Mario may not have been able to notify the Goomba that he killed him. So, what we do instead is store everything in a queue. This is the new vector called CEntityColList. This stores any collision that have happened when all the entities have moved. And once all movement has taken place, that is when we'll notify all the Entities of collisions, and call OnCollision.

Whew. Also, in correlation with this, is Collides function, which is used to determine the actual collision (more on this later). Also with this, is four Col_ variables. These variables are used to offset a collision for an entity. Back in previous tutorials we had a Yoshi image we were using. If you notice, it would make little sense to use the width of the image for collision purposes. Yoshi doesn't actually come to the edges of the images; so, we have to offset it a little, so when Yoshi runs into a wall it actually looks like his nose is hitting the wall, and not being a few pixels away. And since these variables are used to collision purposes only, I have prefixed them with Col_.

Now, for some movement explanation. Whenever an Entity wants to move, we do not move it to where it wants to go immediately. What we do instead, is move it in the direction we want it to go at a certain speed. Remember the whole CFPS class? That's why time based movement is so important. So, in the new OnMove function, we pass it how fast we want our Entity to go. If we want the Entity to move 1 pixel a second to the right, we would do OnMove(1, 0). Now, I wish it was this simple, but it's not. In real life we don't move in steps. We move according to speed and acceleration. Meaning, it takes us a little time to start moving at first (inertia). It looks unnatural to have entities jumping around.

So, to get past this, we have six variables SpeedX, SpeedY, AccelX, AccelY, MaxSpeedX, and MaxSpeedY. Using these is simple. First, the MaxSpeed variables cap how fast the entity can go; if we didn't cap it, the Entities would continue to go faster and faster into infinity. The SpeedX and SpeedY are the current speed of the entity. If these were 1, again, that would be 1 pixel per second. Now, the AccelX and AccelY, determine how fast we increase or decrease our speed. If AccelX and AccelY are 0, then we aren't moving at all. If AccelX is 1, then our SpeedX is increasing over time. Now, this is where it gets a little confusing with the Speed factor.

I want my player to increase in speed 1 pixel per second. This would be:
AccelX = 1;

SpeedX += AccelX * CFPS::FPSControl.GetSpeedFactor();
Notice we multiply the AccelX by the Speed Factor. That's because we want to increase the speed 1 pixel every second. If I wanted to increase the speed 1 pixel every loop, I would leave the speed factor out of it. Now, when OnMove receives these Speed variables, it also has to figure in the speed factor:
MoveX *= CFPS::FPSControl.GetSpeedFactor();
MoveY *= CFPS::FPSControl.GetSpeedFactor();
(MoveX and MoveY are the arguments passed to OnMove). Now, the SpeedX and SpeedY only determine how fast we want to move per second. So we have to multiply it by the speed factor. A little hint for you people out there, the entire speed of the game is determined by the Speed Factor! If we changed the Speed Factor of the game, it would determine how fast anything in the games moves. Slow motion deaths anyone?

Now, whenever an Entity moves, there are some things to consider. Say in one loop we are supposed to move the Entity 4 pixels (slow computer perhaps, or really fast entity). Now, lets say 2 pixels away there is a wall. Now, if we checked 4 pixels in front of use, we wouldn't let our guy move. But, we still want him to move as close as he can. So, to move this 2 pixel distance, we have to check every pixel along the way. Now, you might be thinking this will be slow. Well, not really. Someone with a 100 FPS who wants to move 5 pixels a second (a reasonable speed), will move less than one pixel every loop (0.05 to be exact). So, every loop, this entity moves 0.05 pixels over. That means we have 1 or 2 checks when checking for collisions for that entity. More explanation on this later when we actually implement moving.

Before an entity is allowed to move, we have to check a few things. The first is if the entity collides with the map, and the second is if the entity collides with another entity. This is where the three PosValid functions come in. The first, PosValid(), is the main function that handles the other two. It checks to see if the tile the entity is trying to move to is valid, and also checks the entities. The other two PosValid functions determine what a valid tile is, and what a valid entity is. Again, more explanation later.

Now, last for movement, we have MoveLeft, MoveRight, Flags, and StopMove. Now, MoveLeft and MoveRight are simply flags, true or false, that determine if the entity is moving left or right. If true, the AccelX will be set a value, causing the entity to move. Now, StopMove simply sets the AccelX in the opposite direction until we reach 0. Again, we don't stop suddenly in real life, we gradually stop. The last item, Flags, is used to determine a few things. In the second enum, you'll notice several flags.
ENTITY_FLAG_NONE
ENTITY_FLAG_GRAVITY
ENTITY_FLAG_GHOST
ENTITY_FLAG_MAPONLY
These are used to determine certain properties about an entity. For example, an entity that is affected by gravity will have the ENTITY_FLAG_GRAVITY turned on. An Entity that can go through walls, will have ENTITY_FLAG_GHOST turned on. An entity that only collides with the map, and not other entities, will have ENTITY_FLAG_MAPONLY turned on. Now, notice how we have them numbered in hex. This is because we're treating the Flags in binary. This way, we can activate multiple flags at once:
Flags = ENTITY_FLAG_GRAVITY | ENTITY_FLAG_GHOST;

To check for a single flag is simple:

if(Flags & ENTITY_FLAG_MAPONLY) {
    //Map only
}
Wow, what a load, but still more in the class to explain. Next, we have some Animation variables. There are only two of them: CurrentFrameCol, and CurrentFrameRow. These two determine where on our image we start to find individual frames of an animation. A CurrentFrameCol and CurrentFrameRow would be the top left of the image. Moving over one column would change things. Think of it like tilesets. Look at the example image below:

(Save this Image for this Tutorial)

Last, but not least, we have variables to indicate if a variable is dead, and the type of entity. You'll notice we have another enum, like the Flags, to specify the type of entity. This is important when dealing with collision events. So, back to the Mario and Spike example; lets say a Goomba happens to step in a spike. Now, in Mario world, the Goomba would not die. So, the Spike needs to know that entities type. Simple enough I think.

Okay, lets get into the core of the class now. Open up CEntity.cpp and lets have some more fun. Since we have had so many drastic changes, I am going to go through this line by line with the basics, and then move on to specific areas of the class. So, starting at the top we have our basic constructor, and statics:
std::vector<CEntity*>     CEntity::EntityList;

std::vector<CEntityCol> CEntityCol::EntityColList;

CEntityCol::CEntityCol() {
    EntityA = NULL;
    EntityB = NULL;
}

CEntity::CEntity() {
    Surf_Entity = NULL;

    X = 0;
    Y = 0;

    Width     = 0;
    Height     = 0;

    MoveLeft  = false;
    MoveRight = false;

    Type =     ENTITY_TYPE_GENERIC;

    Dead = false;
    Flags = ENTITY_FLAG_GRAVITY;

    SpeedX = 0;
    SpeedY = 0;

    AccelX = 0;
    AccelY = 0;

    MaxSpeedX = 5;
    MaxSpeedY = 5;

    CurrentFrameCol = 0;
    CurrentFrameRow = 0;

    Col_X = 0;
    Col_Y = 0;

    Col_Width  = 0;
    Col_Height = 0;
}
Everything is simply set to 0, NULL, or some respective value. One thing to notice is the MaxSpeed variables. Set these to whatever you like. Usually this will depend on the particular entity that is in the game. Such as Mario will be able to move faster than a Goomba. Notice that I have grouped two classes into one file; that is okay to do sometimes! OnLoad is the exact same, so skip over that, and then move on to OnLoop:
void CEntity::OnLoop() {
    //We're not Moving
    if(MoveLeft == false && MoveRight == false) {
        StopMove();
    }

    if(MoveLeft) {
        AccelX = -0.5;
    }else

    if(MoveRight) {
        AccelX = 0.5;
    }

    if(Flags & ENTITY_FLAG_GRAVITY) {
        AccelY = 0.75f;
    }

    SpeedX += AccelX * CFPS::FPSControl.GetSpeedFactor();
    SpeedY += AccelY * CFPS::FPSControl.GetSpeedFactor();

    if(SpeedX > MaxSpeedX)  SpeedX =  MaxSpeedX;
    if(SpeedX < -MaxSpeedX) SpeedX = -MaxSpeedX;
    if(SpeedY > MaxSpeedY)  SpeedY =  MaxSpeedY;
    if(SpeedY < -MaxSpeedY) SpeedY = -MaxSpeedY;

    OnAnimate();
    OnMove(SpeedX, SpeedY);
}
Lots of new things here. First, remember MoveLeft and MoveRight are used to indicate if the entity is moving left or right (duh). If both are these are false, we call StopMove, which will handle settings Acceleration in the reverse direction so we come to a gradual stop. Below that, if MoveLeft or MoveRight are true, we give AccelX a value. 0.5 is an arbitrary number, you probably want to mess with this until you find a value that you like. Just below that, we have a condition checking if gravity is turned on for that particular entity. If it is, we set AccelY. This will cause our entity to fall down. Again, this is whatever value you want. The larger the number, the faster you will fall. Next, below that we have our SpeedX and SpeedY. Again, we increase the Speed of the entity over time in correlation to our Speed Factor. Then, below that, we check against our MaxSpeed variables to make sure we only go a certain speed. Finally, we call OnAnimate for our entity to animate, and then OnMove, which will actually move our entity.

Moving on, we have a few changes to OnRender:
void CEntity::OnRender(SDL_Surface* Surf_Display) {
    if(Surf_Entity == NULL || Surf_Display == NULL) return;

    CSurface::OnDraw(Surf_Display, Surf_Entity, X - CCamera::CameraControl.GetX(), Y - CCamera::CameraControl.GetY(), CurrentFrameCol * Width, (CurrentFrameRow + Anim_Control.GetCurrentFrame()) * Height, Width, Height);
}
First, we have added the Camera X and Y to the rendering coordinates for the entity. This makes the entities render in connection to where the camera is. This way, if the camera moves, all the entities will in respect to the camera position. Next, we have added CurrentFrameCol and CurrentFrameRow to the X2 and Y2 of OnDraw. This makes it so we can set different animations.

The first column has Yoshi facing the left, while the second column has Yoshi facing to the right. When we move to the left, we will want to set CurrentFrameCol = 0, so that Yoshi appears to be walking left. And when we move to the right, we will set CurrentFrameCol = 1, so that Yoshi appears to be walking to the right.

Skip over OnCleanup, nothing new there. We then have OnAnimate:
void CEntity::OnAnimate() {
    if(MoveLeft) {
        CurrentFrameCol = 0;
    }else

    if(MoveRight) {
        CurrentFrameCol = 1;
    }

    Anim_Control.OnAnimate();
}
This is fairly straight forward. Like I said above, depending on which direction we are moving, we set the animation. Next, we have OnCollision:
void CEntity::OnCollision(CEntity* Entity) {
}
Yay, an empty function. This function will be called whenever two entities hit each other. This entire time we've been creating an class for basic entities. In order to take advantage of this function, you will need to create our own class (like CPlayer, which we'll do later on in this tutorial) and override this function. This is because the action taken by an entity depends upon it's type. Like my earlier examples, a Goomba will not react the same way to a collision with a spike as Mario would.

Next, we our OnMove function.
void CEntity::OnMove(float MoveX, float MoveY) {
    if(MoveX == 0 && MoveY == 0) return;

    double NewX = 0;
    double NewY = 0;

    MoveX *= CFPS::FPSControl.GetSpeedFactor();
    MoveY *= CFPS::FPSControl.GetSpeedFactor();

    if(MoveX != 0) {
        if(MoveX >= 0)     NewX =  CFPS::FPSControl.GetSpeedFactor();
        else             NewX = -CFPS::FPSControl.GetSpeedFactor();
    }

    if(MoveY != 0) {
        if(MoveY >= 0)     NewY =  CFPS::FPSControl.GetSpeedFactor();
        else             NewY = -CFPS::FPSControl.GetSpeedFactor();
    }

    while(true) {
        if(Flags & ENTITY_FLAG_GHOST) {
            PosValid((int)(X + NewX), (int)(Y + NewY)); //We don't care about collisions, but we need to send events to other entities

            X += NewX;
            Y += NewY;
        }else{
            if(PosValid((int)(X + NewX), (int)(Y))) {
                X += NewX;
            }else{
                SpeedX = 0;
            }

            if(PosValid((int)(X), (int)(Y + NewY))) {
                Y += NewY;
            }else{
                SpeedY = 0;
            }
        }

        MoveX += -NewX;
        MoveY += -NewY;

        if(NewX > 0 && MoveX <= 0) NewX = 0;
        if(NewX < 0 && MoveX >= 0) NewX = 0;

        if(NewY > 0 && MoveY <= 0) NewY = 0;
        if(NewY < 0 && MoveY >= 0) NewY = 0;

        if(MoveX == 0) NewX = 0;
        if(MoveY == 0) NewY = 0;

        if(MoveX == 0 && MoveY     == 0)     break;
        if(NewX  == 0 && NewY     == 0)     break;
    }
}
This function is quite a bit to take in. Lets see if we can straighten it all out. First, MoveX and MoveY are the where we want to move our entity. Remember up above, we call this passing our SpeedX and SpeedY. So, calling OnMove(1, 0) we would want to move 1 pixel to the right every second. At the top, we check our Speed, and if 0, we don't do anything (duh). Below that, we do something I explained earlier. We multiply MoveX and MoveY by the speed factor. This gives us the correct movement per second. Just below that, we figure out the values for two new variables, NewX and NewY. These will be our X and Y increments. Remember, I mentioned above that this method is pixel perfect, so we need to inch closer and closer to our desired position. Now, we set our NewX and NewY to our speed factor because this is how much we should use to inch closer and closer to our desired position.

Below that, we have a seemingly infinite loop. This is where the movement magic happens and collision. First, we have a condition to check if the entity is a ghost, if it is, we increase the X and Y regardless if a collision has happened. We still call PosValid though, because this will notify entities of any collision that have taken place. Notice in PosValid we pass the current X and Y, along with adding NewX and NewY. This is where we want to go, so we check if that spot is empty.

If the entity is not a ghost, we do normal collision. We check X and Y separatly because we may be able to move up but not forward (think jumping into a wall). If the position is valid (empty), we increase the X or Y, if not, we set SpeedX or SpeedY to 0 which causes you to immediately stop (this is as close to reality as we need, if you do run into a wall you usually stop immediately). Now, below that, we change MoveX and MoveY depending upon our NewX. Say our MoveX was 2, and our NewX was 0.5. We would slowly decrease MoveX until it reaches 0. Once it does, we have come to our desired position and we are done. The 8 if statements below check if MoveX and MoveY have reached 0, and break out of the loop.

Moving on, we have StopMove:
void CEntity::StopMove() {
    if(SpeedX > 0) {
        AccelX = -1;
    }

    if(SpeedX < 0) {
        AccelX =  1;
    }

    if(SpeedX < 2.0f && SpeedX > -2.0f) {
        AccelX = 0;
        SpeedX = 0;
    }
}
This one is fairly straight forward. Depending on our Speed, we set the Acceleration in the reverse direction. So, if we are moving right, we set the acceleration to the left. The last if statement is simply a boundary to how slow we need to be going until we stop. If we didn't do that, it's possible that we could be stuck in a loop of moving left to right.

Next, we have a function that determines collision of two boxes. I will say right now that this function was borrowed from Cone3D (), and then modified. If it works, why fix it?

Anyway, lets take a look at it to see if I can help explain. At the top we define 8 variables, to define 8 sides. 4 sides are for the first object, and 4 sides are for the second object. Basically, we are doing bounding box collision:

So, in a very basic sense, we want to check if two boxes have overlapped. The easiest method, is to determine if we have not collided on all 4 sides. So first, we determine the locations of all the sides:
int left1, left2;
int right1, right2;
int top1, top2;
int bottom1, bottom2;

int tX = (int)X + Col_X;
int tY = (int)Y + Col_Y;

left1 = tX;
left2 = oX;

right1 = left1 + Width - 1 - Col_Width;
right2 = oX + oW - 1;

top1 = tY;
top2 = oY;

bottom1 = top1 + Height - 1 - Col_Height;
bottom2 = oY + oH - 1;
Look at the figure below to get an idea:

The top and left, are the X,Y of the object, and the right and bottom, are the X+Width-1, Y+Height-1. We subtract one in order to get the true coordinate of the side.

Next, we finally do the checking. What we do here is to check if one of the 4 sides of the first object are out of range of the second object. If one is, then we cannot possibly be colliding. But, if all 4 checks fail, we are colliding. So, if bottom1 is less than top2, that means the Bottom side of the first object is higher up on the screen then the Top of the second object:
if (bottom1 < top2) return false;
if (top1 > bottom2) return false;

if (right1 < left2) return false;
if (left1 > right2) return false;
Finally, if all checks pass, then we return true for a collision. One note, remember that the Col_ variables are used to simply offset the size of the entity. If the entity is 32 width, but we offset by 2, that means when we check collision we use the Width of 30. Here's how our final Collides method looks:
bool CEntity::Collides(int oX, int oY, int oW, int oH) {
    int left1, left2;
    int right1, right2;
    int top1, top2;
    int bottom1, bottom2;

    int tX = (int)X + Col_X;
    int tY = (int)Y + Col_Y;

    left1 = tX;
    left2 = oX;

    right1 = left1 + Width - 1 - Col_Width;
    right2 = oX + oW - 1;

    top1 = tY;
    top2 = oY;

    bottom1 = top1 + Height - 1 - Col_Height;
    bottom2 = oY + oH - 1;

    if (bottom1 < top2) return false;
    if (top1 > bottom2) return false;

    if (right1 < left2) return false;
    if (left1 > right2) return false;

    return true;
}
Moving on we find some more fun. We have our 3 last functions, the 3 PosValid functions. Overall, this functions basically determine if a collision has taken place. Remember, earlier OnMove will call PosValid to determine if a requested position is open to move to. So, keep in mind that NewX, and NewY in these functions is where the Entity wants to move to, not where it currently is.

Lets first look at PosValid. This function will determine any type of collision that has taken place, and return true or false. Now, this function basically has only two type of collision, Entity to Entity, and Map to Entity. That's what PosValidTile and PosValidEntity are used for.
bool CEntity::PosValid(int NewX, int NewY) {
    bool Return = true;

    int StartX     = (NewX + Col_X) / TILE_SIZE;
    int StartY     = (NewY + Col_Y) / TILE_SIZE;

    int EndX    = ((NewX + Col_X) + Width - Col_Width - 1)         / TILE_SIZE;
    int EndY    = ((NewY + Col_Y) + Height - Col_Height - 1)    / TILE_SIZE;

    for(int iY = StartY;iY <= EndY;iY++) {
        for(int iX = StartX;iX <= EndX;iX++) {
            CTile* Tile = CArea::AreaControl.GetTile(iX * TILE_SIZE, iY * TILE_SIZE);

            if(PosValidTile(Tile) == false) {
                Return = false;
            }
        }
    }

    if(Flags & ENTITY_FLAG_MAPONLY) {
    }else{
        for(int i = 0;i < EntityList.size();i++) {
            if(PosValidEntity(EntityList[i], NewX, NewY) == false) {
                Return = false;
            }
        }
    }

    return Return;
}
Now, the first collision we check for is a Map Collision. This is done by first grabbing 4 variables about our Entity. Basically, we want to know all the TileIDs that this entity is over. This is done by grabbing the X (and the Col_ offset) and dividing by the TILE_SIZE. This gives us our TileID for our X. The same goes for the Y variable. These two calculations determine our StartX and StartY. Now, EndX and EndY are determined in a similiar fashion, but we want to also add the Width and Height (just like we determined sides in the Collision function above). This will give us an area of tiles that this entity is over:
int StartX     = (NewX + Col_X) / TILE_SIZE;
int StartY     = (NewY + Col_Y) / TILE_SIZE;

int EndX    = ((NewX + Col_X) + Width - Col_Width - 1)     / TILE_SIZE;
int EndY    = ((NewY + Col_Y) + Height - Col_Height - 1)    / TILE_SIZE;
Now that we have the Start and End of tiles, we simply loop through them:
for(int iY = StartY;iY <= EndY;iY++) {
    for(int iX = StartX;iX <= EndX;iX++) {
    }
}
Inside of this loop, this is where we check each tile to see if it's a valid tile to move into. It the tile happens to be a wall, we would set the Return variable to false, to indicate that this requested position (hence the name PosValid) is not good:
CTile* Tile = CArea::AreaControl.GetTile(iX * TILE_SIZE, iY * TILE_SIZE);

if(PosValidTile(Tile) == false) {
    Return = false;
}
Notice the new function GetTile, we'll define this later on in this tutorial. But to explain it here, GetTile will return the CTile of any X and Y coordinate. After we get the tile, we pass it on to PosValidTile for it to figure out if the tile can be moved over or not. So, for a moment, jump on over to PosValidTile:
bool CEntity::PosValidTile(CTile* Tile) {
    if(Tile == NULL) {
        return true;
    }

    if(Tile->TypeID == TILE_TYPE_BLOCK) {
        return false;
    }

    return true;
}
Remember the enum in CTile.h at the top? Those enums were a list of types that you could give a tile. They are used here now, to determine if a tile can be moved on or not. So, that's basically what we do here. First, if the Tile is NULL (which you should always check your pointers), we return true (meaning, yes, they can walk on this tile). Next, we check if the tile is a TILE_TYPE_BLOCK, if so, then no, the entity cannot walk on this tile. And finally, for all other types, yes, they can walk on this tile. Simple enough.

Now, jump back to the PosValid function. You will then find another loop:
if(Flags & ENTITY_FLAG_MAPONLY) {
}else{
    for(int i = 0;i < EntityList.size();i++) {
        if(PosValidEntity(EntityList[i], NewX, NewY) == false) {
            Return = false;
        }
    }
}
First, we do a simple check with our Flags again, this will check if we have the ENTITY_FLAG_MAPONLY flag turned on. If so, this means we're only checking collisions against the map, so we don't worry about Entity collisions. If we are checking collisions, then we jump to the loop. The loop is fairly simply, we go through the EntityList and pass each entity to the PosValidEntity, along with NewX and NewY. So, Jump on over to PosValidEntity:
bool CEntity::PosValidEntity(CEntity* Entity, int NewX, int NewY) {
    if(this != Entity && Entity != NULL && Entity->Dead == false &&
        Entity->Flags ^ ENTITY_FLAG_MAPONLY &&
        Entity->Collides(NewX + Col_X, NewY + Col_Y, Width - Col_Width - 1, Height - Col_Height - 1) == true) {

        CEntityCol EntityCol;

        EntityCol.EntityA = this;
        EntityCol.EntityB = Entity;

        CEntityCol::EntityColList.push_back(EntityCol);

        return false;
    }

    return true;
}
Okay, this function is actually quite simple. We do some checks first on the Entity (the Entity we are wanting to collide with) to determine that this Entity is not the same Entity we're moving (Mario cannot collide with Mario). That's the "this != Entity." It simply checks to make sure the Entities are not the same. We then check to make sure the Entity is not NULL, not Dead, and doesn't have the ENTITY_FLAG_MAPONLY flag turned on. So far, so good. If all these checks pass, then we finally pass it to the Collides function of that Entity, passing along our Entities coordinates and Width/Height (with Col_ offsets). Remember, if any part of an if statement fails, like this != Entity, none of the other code in the if statement will be executed.

So, if the Entities do collide, finally, we use our new CEntityCol class. This will be a queue of collisions that have taken place. So, we push our Entity into EntityA, and the other Entity into EntityB, and then push it into the list. Finally, we return false, to indicate that this position is not valid. Wow, we are almost done!

Don't rush out to compile, it won't work. Why? Because, remember that GetTile function? :)

So, let's add this into our code. Open CArea.h and add two functions at the bottom of the class:
public:
    CMap*    GetMap(int X, int Y);

    CTile*    GetTile(int X, int Y);
Then, open CArea.cpp and add the functions:
CMap* CArea::GetMap(int X, int Y) {
    int MapWidth  = MAP_WIDTH * TILE_SIZE;
    int MapHeight = MAP_HEIGHT * TILE_SIZE;

    int ID = X / MapWidth;
        ID = ID + ((Y / MapHeight) * AreaSize);

    if(ID < 0 || ID >= MapList.size()) {
        return NULL;
    }

    return &MapList[ID];
}

CTile* CArea::GetTile(int X, int Y) {
    int MapWidth  = MAP_WIDTH * TILE_SIZE;
    int MapHeight = MAP_HEIGHT * TILE_SIZE;

    CMap* Map = GetMap(X, Y);

    if(Map == NULL) return NULL;

    X = X % MapWidth;
    Y = Y % MapHeight;

    return Map->GetTile(X, Y);
}
I know, I know... they are more complicated than they look. First, look at GetMap. GetMap simply returns a CMap pointer at the given coordinates. It's just like GetTile, but for Maps. Now, how it works is first by determining the size of our maps. We then translate our X,Y into an ID, and then return the CMap pointer. I won't go into how translating X,Y into an ID works, because I believe this is explained in the last tutorial.

Now, go to GetTile. We determine the MapWidth and MapHeight, again (and I suppose this can be optimized), and then grab the Map with GetMap. We do a simple check for a NULL pointer (hint, hint), and then turn our X and Y into Tile coordinates. So, if our TILE_SIZE was 16, and our X was at 128, our Tile coordinate for X would be 8. Meaning, we are 8 tiles over. We finally then send this over to the Map GetTile. (Oh yes, one other function). This is why we have a GetMap function, if you are wondering. Rememember, CArea is a bunch of maps making one big area. So, we will have to determine which map is needed when grabbing tiles.

So, jump over to CMap.h now, and add this at the bottom:
public:
    CTile*    GetTile(int X, int Y);
Then, open CMap.cpp and add this at the bottom:
CTile* CMap::GetTile(int X, int Y) {
    int ID = 0;

    ID = X / TILE_SIZE;
    ID = ID + (MAP_WIDTH * (Y / TILE_SIZE));

    if(ID < 0 || ID >= TileList.size()) {
        return NULL;
    }

    return &TileList[ID];
}
This is basically just like GetMap, but uses the TileList instead.

We're almost done!

Just a few more changes to go. Create two new files called CPlayer.h and CPlayer.cpp. This class will be our main player and will extend our new CEntity class. So, open up CPlayer.h:
#ifndef _CPLAYER_H_
#define _CPLAYER_H_
#include "CEntity.h"

class CPlayer : public CEntity {
    public:
        CPlayer();

        bool OnLoad(char* File, int Width, int Height, int MaxFrames);

        void OnLoop();

        void OnRender(SDL_Surface* Surf_Display);

        void OnCleanup();

        void OnAnimate();

        void OnCollision(CEntity* Entity);
};

#endif
I won't explain any of this other than it's overriding all the virtual functions from CEntity.

Now, open up CPlayer.cpp:
#include "CPlayer.h"

CPlayer::CPlayer() {
}

bool CPlayer::OnLoad(char* File, int Width, int Height, int MaxFrames) {
    if(CEntity::OnLoad(File, Width, Height, MaxFrames) == false) {
        return false;
    }

    return true;
}

void CPlayer::OnLoop() {
    CEntity::OnLoop();
}

void CPlayer::OnRender(SDL_Surface* Surf_Display) {
    CEntity::OnRender(Surf_Display);
}

void CPlayer::OnCleanup() {
    CEntity::OnCleanup();
}

void CPlayer::OnAnimate() {
    if(SpeedX != 0) {
        Anim_Control.MaxFrames = 8;
    }else{
        Anim_Control.MaxFrames = 0;
    }

    CEntity::OnAnimate();
}

void CPlayer::OnCollision(CEntity* Entity) {
}
Again, just like CEntity, but simply overriding the functions. One minor note, look at OnAnimate. In there we check our SpeedX to make a modification to our MaxFrames. Basically, if SpeedX != 0, (which means we are moving) we set our MaxFrames to 8. If we aren't moving we set it to 0. Now, remember our Yoshi animation above, it has 8 frames for walking. So, if our Speed is not 0, we make Yoshi walk, otherwise we make it look like he's standing still. This is the area where you would define other animations, like Idle, Walking, Running, Jumping, and so on.

Now, open up CApp.h and add the header file at the top:
#include "CPlayer.h"
Then, add two Players in your class:
CPlayer    Player;
CPlayer Player2;
Then, open up CApp_OnInit.cpp and initialize them:
if(Player.OnLoad("yoshi.png", 64, 64, 8) == false) {
    return false;
}

if(Player2.OnLoad("yoshi.png", 64, 64, 8) == false) {
    return false;
}

Player2.X = 100;

CEntity::EntityList.push_back(&Player);
CEntity::EntityList.push_back(&Player2);
Simple enough. We set PlayerX, the other Yoshi, over, so that we're not on top of each other initially. Now, just below this add this extra code:
CCamera::CameraControl.TargetMode = TARGET_MODE_CENTER;
CCamera::CameraControl.SetTarget(&Player.X, &Player.Y);
Remember I said earlier, we're going to make the camera follow our Yoshi. Now, open up CApp_OnRender and change this:
CArea::AreaControl.OnRender(Surf_Display, CCamera::CameraControl.GetX(), CCamera::CameraControl.GetY());
To this:
CArea::AreaControl.OnRender(Surf_Display, -CCamera::CameraControl.GetX(), -CCamera::CameraControl.GetY());
This change is also related to the Camera bug I mentioned above earlier. Open up CCamera.h and change the TargetX and TargetY from ints to floats. Also change the SetTarget ints to floats.

One last change now! Open up CApp.h and add this to your code (you should already have OnKeyDown):
void OnKeyUp(SDLKey sym, SDLMod mod, Uint16 unicode);
Then, open up CApp_OnEvent.cpp:
void CApp::OnKeyDown(SDLKey sym, SDLMod mod, Uint16 unicode) {
    switch(sym) {
        case SDLK_LEFT: {
            Player.MoveLeft = true;
            break;
        }

        case SDLK_RIGHT: {
            Player.MoveRight = true;
            break;
        }

        default: {
        }
    }
}

void CApp::OnKeyUp(SDLKey sym, SDLMod mod, Uint16 unicode) {
    switch(sym) {
        case SDLK_LEFT: {
            Player.MoveLeft = false;
            break;
        }

        case SDLK_RIGHT: {
            Player.MoveRight = false;
            break;
        }

        default: {
        }
    }
}
To go over this quickly, we basically check if Left or Right is pressed, and set MoveLeft or MoveRight on our Entity to true. If the key is depressed, we set it to false.

Now, we're done! Remember to grab the Yoshi picture above to make sure he shows up right. Also, I changed the 1.map to have TILE_TYPE_BLOCKs in it. You can grab an updated one here: Example Map. Since this tutorial is so lengthy, and took me a long time (sorry), please be sure to let me know of any bugs or something I forgot to address. Thanks.

P.S.
In the next tutorial, we'll see how to react to collisions that happen. In this tutorial, though we pushed collisions into a queue, we didn't do anything with the queue yet.


Tutoriel SDL

SDL Collision Events
Finally! The next tutorial. This is the 2nd part to the SDL Collision tutorial. We'll be looking at collision events, the part we left unfinished in the last tutorial. To refresh your memory, a collision event is an event that is stored in queue after a collision has taken place. Take our recent shooter contest for an example. When the player fires a bullet, the bullet flies through the air, and then hits the enemy. The moment the bullet and the enemy collide, an event is triggered and stored in the queue. After all movement has taken place, the queue is iterated through, and events can appropriately respond.

So lets get started. Open up your project from the last tutorial. We're first going to make a few prelimary changes. Make a new file called CEntityCol.cpp. Inside that file, put the following:
#include "CEntity.h"

std::vector<CEntityCol> CEntityCol::EntityColList;

CEntityCol::CEntityCol() {
    this->EntityA = NULL;
    this->EntityB = NULL;
}
Now, get rid of that code from CEntity.cpp. I am basically moving the code to its own file so that everything is much neater. The second thing. Open up CEntity.h and change:
virtual void OnCollision(CEntity* Entity);
To this:
virtual bool OnCollision(CEntity* Entity);
Also, change the void to bool in CEntity.cpp, CPlayer.h, and CPlayer.cpp. Why the change? Well, you'll see in a minute. ;)

We're already storing the collision events in a queue, now we need to do something with them. Open up CApp_OnLoop.cpp. You'll notice we already have a loop going for entities and we're calling OnLoop. We're basically going to do the same thing again, but after our first loop:
for(int i = 0;i < CEntity::EntityList.size();i++) {
    if(!CEntity::EntityList[i]) continue;

    CEntity::EntityList[i]->OnLoop();
}

//Collision Events
for(int i = 0;i < CEntityCol::EntityColList.size();i++) {
    CEntity* EntityA = CEntityCol::EntityColList[i].EntityA;
    CEntity* EntityB = CEntityCol::EntityColList[i].EntityB;

    if(EntityA == NULL || EntityB == NULL) continue;

    if(EntityA->OnCollision(EntityB)) {
        EntityB->OnCollision(EntityA);
    }
}

CEntityCol::EntityColList.clear();
Not much going on here. We're first iterating through every event in the queue. We then grab the two entities involved (EntityA, and EntityB), and we make sure they are valid pointers. If so, we first tell EntityA that it has collided with EntityB by calling OnCollision. Now, this is where that void to bool change comes in. EntityA can choose to prevent EntityB from knowing it has collided with him. If you remember, only Entities that are moving check for collision. This would mean EntityA is definitly a moving entity, and EntityB may or may not be moving.

Here's an example to explain why you wouldn't want to waste time calling EntityB's OnCollision. Say you fire a bullet, and it hits a rock entity. Now, lets say the rock has a flag stating it's indestructable. Our rock doesn't care its been hit. So, when the OnCollision is called for the bullet, the bullet can see the entity it has hit is indestructable. And because of that, it can create a little "bullet being destroyed" animation, and then we can ignore telling the rock about it.

This is about the extent of entity collisions. So, now that we have this base, lets do something semi-useful with it.

Open up CEntity.h and add this new function, and variable:
protected:
    bool    CanJump;

public:
    bool     Jump();
Then, open up CEntity.cpp, and add the Jump function. Also make CanJump = false in the constructor:
CEntity::CEntity() {
    CanJump = false;

    // ... Other code ...
}

bool CEntity::Jump() {
    if(CanJump == false) return false;

    SpeedY = -MaxSpeedY;

    return true;
}
Finally, go to the OnMove function. We're going to figure out when our entity is allowed to jump. An entity is allowed to jump whenever it is touching the ground - or, in other words, whenever the entity is no longer moving on the positive Y-axis. So, at the top, we are first always going to reset CanJump to false:
CanJump = false;
Now, go down a bit, and modify the PosValid for the Y-axis to the following:
if(PosValid((int)(X), (int)(Y + NewY))) {
    Y += NewY;
}else{
    if(MoveY > 0) {
        CanJump = true;
    }

    SpeedY = 0;
}
Now, to run through everything we did. First, we have a flag that states if an entity can jump. When set to true, we are allowed to call the Jump function. This CanJump variable is always false, unless our feet are on the ground. Whenever we actually do want to jump, we simply set our SpeedY in the negative Y-axis direction (up). Remember, gravity is always pushing us down, but at a slow speed. So, if we have a SpeedY of -5, and a AccelY of 0.75, we'll move up for a time, gravity will kick in, and then we will eventually be pushed back down. Thus, giving the effect of a real jump. The trick here is to get the Speed and Acceleration variables just right.

Also, as a side note, the reason we made Jump return true/false is simply for informational purposes. Meaning, when a key is pressed by the player to jump, they won't always be allowed to jump. And, if you wanted, you could do something based upon what Jump() returns.

So a few more tweaks before we try this out. In the constructor of CEntity, change the MaxSpeedX and MaxSpeedY to 10. Then, open up CApp_OnEvent.cpp and add the following to OnKeyDown:
case SDLK_SPACE: {
    Player.Jump();
    break;
}
And one last thing. Open up CPlayer.cpp, and change the OnCollision function to this:
bool CPlayer::OnCollision(CEntity* Entity) {
    Jump();

    return true;
}
That's it! Compile, run, and have fun with Yoshi jumping all over the place. You'll notice the simple collision event we added is that when Yoshi 1 collides with Yoshi 2, it causes Yoshi 1 to jump. Kind of lame, but you should get the idea.


Tutoriel SDL: Introduction et graphisme de base | gnurou.org

Skip navigation.
gnurou.org
Just your average bearded geek
  • Home
  • Research page

About

  • About me
  • Contact me
  • Blog

Code

  • Github
  • Linux on NVIDIA SHIELD and Tegra Note
  • Kernel hacking with KDevelop
  • Tagaini Jisho
  • Karmascrobbler
  • Sharp Zaurus ports
  • Romenux
  • PmWiki Bibtex module
  • Adonthell

Writing

  • Linux Magazine France
    • Tutoriel SDL
      • Partie 1
      • Partie 2
      • Partie 3
      • Partie 4
      • Partie 5
      • Partie 6
  • Linux Pratique
  • De la bonne manière de poser les questions

Japan

  • Tagaini Jisho
  • Internet and Linux resources for learning Japanese
  • How to locate a Japanese address

Sponsors

Tekhattan
Home » Programmation ludique avec SDL

Tutoriel SDL: Introduction et graphisme de base

Paru dans Linux Mag 65, octobre 2004

Cette nouvelle série consacrée à SDL prend la suite des épisodes parus dans les numéros 48, 49 et 51 de Linux Mag. À travers ces articles, nous découvrirons comment utiliser les différentes fonctionnalités de SDL. Nous irons également au delà de l'utilisation d'une bibliothèque graphique pour aborder des éléments liés à la programmation des effets graphiques et des jeux 2D en général. Dans ces cas-là, SDL nous servira de support.

La bibliothèque Simple Directmedia Layer

SDL est une bibliothèque multi plate-formes qui offre une abstraction sur des éléments de programmation généralement considérés comme non-portables: vidéo, audio, entrées, évènements mais également threads, accès aux fichiers, et gestion du temps. Elle offre aussi des types de base standards quelle que soit l'architecture, des informations sur le système sur lequel tournent les applications, et un accès à OpenGL pour la programmation 3D. Ainsi, tout programme se servant correctement de SDL devrait compiler et se lancer sur tous les systèmes supportés par la bibliothèque: GNU/Linux bien sûr, mais aussi MS-Windows, MacOS, Free/OpenBSD, BeOS, etc... La plupart du temps, SDL utilise elle-même d'autres bibliothèques spécifiques aux systèmes sur lesquels elle tourne. Ainsi, sous GNU/Linux, on pourra choisir entre en rendu graphique par X-Window, DirectFB, aalib, ou d'autres. Cette portabilité ne se fait pas au détriment de la flexibilité puisque, comme nous allons le voir, SDL reste une bibliothèque d'assez bas niveau.

Non contente d'être portable entre les systèmes, SDL laisse également un choix impressionnant de langages pour l'utiliser. Écrite entièrement en C et compatible avec C++, elle dispose de bindings pour de nombreux langages, entre autres Perl, Python, Ruby, Ada et Java.

Enfin, SDL est dotée d'une importante collection de bibliothèques périphériques qui fournissent des fonctions d'un peu plus haut niveau: SDL_image, pour charger et sauvegarder des images dans un nombre impressionnant de formats, SDL_mixer pour facilement jouer et mixer sons et musiques, SDL_net qui offre des sockets réseau portables, etc. Vous pouvez retrouver la liste de ces bibliothèques sur le site officiel de SDL, http://www.libsdl.org.

À propos de ces articles...

Le but avoué de cette série est de donner les bases nécessaires pour commencer dans le développement de jeux. Cependant, pour ne pas tomber dans le piège de ne parler que d'un type de jeu en particulier, nous ne nous baserons pas sur un exemple que l'on construirait petit à petit à chaque nouvel article. Nous allons plutôt nous concentrer à chaque étape sur un aspect spécifique et indépendant, libre à vous ensuite d'assembler les éléments que nous aurons développés pour réaliser votre propre jeu (qui sera de toute façon bien plus intéressant que tout ce que nous aurions pu faire ici...).

Nous utiliserons le langage C car il permet d'appréhender au plus près SDL et sera accessible pour plus de lecteurs. Cette série ne donnera aucune notion de C ni d'algorithmique de base - elle s'adresse aux lecteurs familiarisés avec le C et les structures de données élémentaires comme les listes chaînées. Vous pouvez vous reporter à la série des « briques de bases en C » si vous manquez de connaissances en la matière. Nous tenterons d'être le plus modulaire possible. Chaque article sera dédié à la réalisation d'un ou de plusieurs « modules » qui pourra être connecté avec les autres pour réaliser un moteur de jeu. Pour commencer, nous parlerons un peu de quelques techniques d'affichage et de leur exploitation sous SDL.
  • Home
  • Research page


Tutoriel SDL gnurou

Nous utiliserons le langage C car il permet d'appréhender au plus près SDL et sera accessible pour plus de lecteurs. Cette série ne donnera aucune notion de C ni d'algorithmique de base - elle s'adresse aux lecteurs familiarisés avec le C et les structures de données élémentaires comme les listes chaînées. Vous pouvez vous reporter à la série des « briques de bases en C » si vous manquez de connaissances en la matière. Nous tenterons d'être le plus modulaire possible. Chaque article sera dédié à la réalisation d'un ou de plusieurs « modules » qui pourra être connecté avec les autres pour réaliser un moteur de jeu. Pour commencer, nous parlerons un peu de quelques techniques d'affichage et de leur exploitation sous SDL.

Surfaces et formats d'image - un peu de théorie...

La structure C qui représente les éléments graphiques dans SDL s'appelle SDL_Surface. Cette structure est utilisée pour tout élément graphique, de l'écran aux images que nous aurons chargées. En effet, du point de vue de SDL, l'écran n'est qu'une surface presque comme une autre.

Une surface est définie entre autres par sa taille (longueur et hauteur en nombre de pixels), son format et ses données graphiques. Le format d'image est un élément important à prendre en compte. Il existe une multitude de manières de coder des images, que ce soit sur écran ou dans un fichier: on peut utiliser une palette et donner à chaque pixel une valeur qui référencera une des couleurs de cette palette (format palettisé type GIF) ; on peut aussi découper chaque pixel selon différents masques pour coder les valeurs de ses composantes rouge, verte et bleue (format en « couleurs vraies », comme le JPEG). Les formats palettisés conviennent aux images de peu de couleurs. En pratique de nos jours, bien peu de jeux utilisent encore ces formats qui étaient très populaires autrefois en raison du peu de mémoire vidéo qu'ils nécessitaient (1 octet par pixel pour un ensemble de 256 couleurs affichables simultanément). Nous travaillerons pour notre part en true color.

Il existe une multitude de formats true color. La plupart sont nommés en fonction du nombre de bits utilisés pour coder un pixel: Le mode 24 bits utilise 1 octet pour chacune des composantes rouge, verte et bleue d'un pixel, tandis que le mode 16 bits alloue 5 bits pour coder le rouge, 6 pour le vert et 5 pour le bleu.

Fig. 1: Utilisation des bits pour le codage des couleurs en formats 16 et 24 bits.
Fig. 1: Utilisation des bits pour le codage des couleurs en formats 16 et 24 bits.

Cette générosité pour les nuances de vert en mode 16 bits s'explique par le fait qu'elles sont les plus évidentes à capter pour l'oeil humain. Les couleurs sont obtenues par mélange des trois composantes basiques (comme un peintre mélangerait ses couleurs pour en obtenir de nouvelles) et s'expriment en hexadécimal. Voici quelques couleurs et leurs expressions en 24 et 16 bits:

blanc
respectivement 0xffffff et 0xffff (tous les bits à 1),
noir
0x000000 et 0x0000 (tous les bits à 0),
bleu intense
0x0000ff et 0x001f (tous les bits de la composante bleue à 1, les autres à 0).

Le format « standard » pour exprimer les couleurs est le triplet hexadécimal 24 bits. Vous pouvez mélanger des couleurs et observer le triplet hexa correspondant avec l'outil palette de Gimp, par exemple.

Dessiner sur l'écran

La mémoire d'écran est une zone continue dans laquelle les pixels se suivent ligne par ligne. Ainsi pour une résolution de 640 * 480, le pixel (1, 0) se trouve juste après le (0, 0) et le pixel (0, 1) après le (639, 0). Connaissant le format utilisé par l'écran, modifier la couleur d'un pixel consiste donc à écrire dans la zone mémoire correspondante pour le mettre à la couleur désirée. La formule permettant de connaître l'offset d'un pixel (x, y) par rapport au début de l'écran est

(y x longueurEcran + x) x octetsParPixel

Ainsi, si screen est un pointeur de type Uint8 * (pointant des élément de 8 bits non-signés) vers le début d'une zone écran de dimensions 640 * 480 et utilisant un format de 16 bits par pixel, on pourra mettre le pixel aux coordonnées (50, 30) à la couleur blanche de la manière suivante:

*((Uint16 *)(screen + (30 * 640 + 50) * 2)) = 0xffff;

Dessiner une image complète sur l'écran consiste alors à copier chacun de ses pixels vers la mémoire d'écran. En général, le matériel fournit un support permettant de faire des copies rapides de blocs de mémoire, et c'est ce mécanisme qui est utilisé pour permettre de faire des affichages rapides. Cette opération de copie d'image d'une source vers une destination est appelée un blit. Mais bien évidemment, cette copie rapide ne peut se faire que si source et destination ont le même format d'image. Si l'on copiait directement une image 24 bits vers une mémoire d'écran de 16 bits, l'affichage serait totalement erroné. Pour avoir un affichage correct, il faudrait convertir un à un les pixels de la source vers du 16 bits avant de les copier. Vous l'aurez donc deviné, il est très lent de dessiner une image vers une surface qui ne partage pas le même format.

Lorsque l'on écrit dans la mémoire vidéo, l'affichage est immédiat, c'est à dire que les pixels modifiés apparaissent sous leur nouvelle couleur au prochain balayage de l'écran. Cela pose un gros problème dans le cas d'un jeu: imaginons qu'un rafraîchissement d'écran intervienne en plein milieu de l'affichage d'un sprite: durant un court instant, seule la moitié du sprite sera affichée!

La technique du double buffering permet de contourner ce problème.

Le double buffering

Cette technique consiste à avoir non pas une, mais deux zones d'écran: l'une représente l'écran affiché actuellement et l'autre le prochain écran en préparation. Dès que le nouvel écran est terminé, le pointeur de mémoire vidéo est modifié pour pointer sur celui-ci, et lors du prochain balayage vidéo le nouvel écran apparaît d'un coup. L'ancienne zone affichée devient alors la zone de travail dans laquelle est préparé le prochain écran à afficher, sur lequel re-pointera le pointeur de mémoire vidéo lorsqu'il sera terminé, et ainsi de suite.

Fig. 2: Utilisation du double buffering pour réaliser une animation: l&#39;un des buffers est affiché tandis que l&#39;image suivante est préparée dans l&#39;autre.
Fig. 2: Utilisation du double buffering pour réaliser une animation: l'un des buffers est affiché tandis que l'image suivante est préparée dans l'autre.

En plus de permettre un affichage sans faille, cette technique n'est absolument pas pénalisante (on ne change qu'un pointeur pour afficher la nouvelle image au moment où l'écran effectue son retour de balayage vertical) en dehors du fait qu'elle nécessite plus de mémoire vidéo. Cependant le double buffering n'est vraiment applicable que pour un mode plein écran. Si l'application tourne dans une fenêtre, on ne peut modifier le pointeur de mémoire vidéo étant donné que notre application n'occupe qu'une partie de l'écran physique. Dans ce cas, le double buffering est simulé en copiant purement et simplement la zone de préparation vers la mémoire d'écran pour l'afficher, ce qui est un peu plus brutal et surtout plus lent.

Fig. 3: Simulation du double buffering par copie directe dans la mémoire d&#39;écran.
Fig. 3: Simulation du double buffering par copie directe dans la mémoire d'écran.

Le graphisme avec SDL

Voyons un peu comment tout cela se rapporte à SDL. Commençons par l'initialisation de l'écran, qui se fait par la fonction SDL_SetVideoMode.

SDL_Surface *SDL_SetVideoMode(int width, int height, int bpp, Uint32 flags);

Elle prend en paramètres les largeurs et hauteurs d'écran désirées en pixels, le nombre de bits par pixels voulu ainsi qu'un ensemble de flags, et nous retourne un pointeur vers la surface d'écran. Les flags les plus courants de cette fonction sont les suivants:

  • SDL_HWSURFACE demande la création de la surface dans la mémoire de la carte vidéo, pour profiter de l'accélération matérielle. Cette demande n'est satisfaite que si possible: par exemple, dans une fenêtre X, SDL ne peut pas créer de surfaces en mémoire vidéo.
  • SDL_DOUBLEBUF demande l'utilisation du double buffering (applicable uniquement si SDL_HWSURFACE est satisfait).
  • SDL_FULLSCREEN demande un mode plein écran. Par défaut, sous X, une fenêtre est créée.
  • SDL_ANYFORMAT informe que nous sommes prêts à accepter un autre format d'image que celui que nous avons demandé, si cela permet d'être plus rapide. Ainsi, sous X, nous aurons toujours le même mode que celui du serveur X si ce flag est activé.

Une fois que l'écran est initialisé, il est possible de dessiner dessus et d'y copier d'autres surfaces.

Dessiner directement sur une surface

Pour dessiner directement sur une surface, il suffit d'affecter les bonnes valeurs aux bons endroits de la zone mémoire contenant ses pixels, en utilisant la formule décrite plus haut. En premier lieu, il faut donc obtenir un pointeur vers le début de l'image de la surface. Justement, le membre pixels de la structure SDL_Surface remplit ce rôle.

Toutefois, nous devons nous assurer que cette zone est modifiable en l'état. Pour cela, nous demanderons un verrou avant de toucher à quoi que ce soit avec la fonction SDL_LockSurface. Nous pourrons le libérer une fois nos manipulations terminées avec SDL_UnlockSurface. Durant la période de verrouillage, nous pouvons modifier comme bon nous semble le contenu de la mémoire pointée par pixels, en prêtant particulièrement attention au format d'image. La fonction putPixel de l'exemple ci-après se charge d'afficher un pixel en prenant soin du format de la surface de destination et de l'ordre des octets de la machine. Son seul impératif est que la couleur du pixel à afficher soit déjà au bon format. Comme nous manipulons par convention les couleurs au format 24 bits, il faudra convertir la couleur à afficher avant d'appeler cette fonction. Cette opération sera remplie par la fonction SDL_MapRGB qui prend en paramètre le format vers lequel convertir un triplet de couleurs (le format d'une surface est accessible via son membre format) ainsi que les valeurs des composantes rouge, verte et bleue de la couleur à convertir. Elle retourne un entier qui correspond à la couleur convertie.

En pratique, il est plutôt rare d'accéder directement aux pixels d'une surface. C'est indispensable pour appliquer des effets graphiques (nous aurons l'occasion d'en découvrir quelques uns par la suite), mais totalement inefficace pour les opérations courantes telles que copier une image vers l'écran. Au lieu de cela, nous utiliserons les fonctions fournies par SDL qui tireront avantage du matériel chaque fois que ce sera possible. Voyons donc maintenant comment afficher une surface à l'écran.

Affichage d'une surface sur une autre: le blit

Pour créer nos surfaces, nous nous contenterons pour le moment d'utiliser la fonction SDL_LoadBMP qui charge une image BMP et retourne la surface correspondante. Cependant, il n'est absolument pas garanti que cette surface retournée aura le même format que notre écran. Pour pouvoir profiter d'un blit rapide, il faut la convertir au format de notre surface d'écran, de la même manière que nous convertirions la couleur d'un pixel avec SDL_MapRGB. SDL_DisplayFormat prend en paramètre une surface et en renvoie une autre qui est sa copie convertie au format de l'écran. C'est donc cette dernière surface que nous allons utiliser.

Nous pourrons libérer la mémoire utilisée par les surfaces que nous aurons allouées (à l'exception de la surface d'écran) avec SDL_FreeSurface.

Enfin, le plus important: le blit! C'est comme son nom l'indique le rôle de SDL_BlitSurface.

SDL_BlitSurface(SDL_Surface * src, SDL_Rect * srcrect, SDL_Surface * dst, SDL_Rect dstrect)

Celle-ci prend en paramètres la surface source, l'adresse d'un SDL_Rect permettant de ne sélectionner qu'une partie de la surface source à dessiner, la surface destination et l'adresse d'un autre SDL_Rect qui indique la position à partir de laquelle copier l'image source.

Fig. 4: Rôle des paramètres de la fonction SDL_BlitSurface.
Fig. 4: Rôle des paramètres de la fonction SDL_BlitSurface.

La structure d'un SDL_Rect est on ne peut plus simple: il s'agit d'un bête rectangle avec ses coordonnées x et y, sa longueur w et sa hauteur h.

typedef struct {
 Sint16 x, y;
 Uint16 w, h;
} SDL_Rect;

Si le paramètre srcrect est à NULL, l'image source est alors copiée en entier. Si le paramètre dstrect est à NULL, l'image source est copiée aux coordonnées (0, 0) de la destination. Dans tous les cas, les problèmes de clipping (le fait qu'une image source « dépasse » des dimensions de la surface destination) sont pris en charge par cette fonction.

Certains paramètres de l'image peuvent influencer le blit. Ainsi, une image peut être masquée, c'est à dire qu'une de ses couleurs ne sera pas copiée lors du blit. Cette couleur est appelée le masque et est choisie par la fonction SDL_SetColorKey. Une couleur de choix pour remplir cette tâche ingrate est la couleur 0xff00ff, qui correspond à une espèce de mauve infâme qu'aucun graphiste sain n'oserait utiliser dans ses compositions. :)

Une image peut également se voir appliquer un paramètre d'alpha, c'est à dire de transparence. Lors du blit, l'image apparaîtra translucide au dessus de la zone qu'elle aurait dû recouvrir. Ce paramètre est appliqué par la fonction SDL_SetAlpha. Il est dans ce cas global à toute l'image. Une autre solution pour obtenir de la transparence consiste à donner un canal alpha à chaque pixel de l'image, au même titre que les composantes rouge, verte et bleue. On parle alors de format d'image 32 bits (8 bits pour chacune des 3 composantes de base et 8 autres pour la composante alpha).

Le masque et l'alpha ont un coût sur l'opération de blit, comme vous devez vous en douter, mais bien souvent le matériel offre une accélération à ces opérations... quand il est possible de l'utiliser. Faute de quoi, SDL fait tout de manière logicielle.

D'autres fonctions ont également une importance capitale: SDL_Flip échange les buffers vidéos dans les cas où le double-buffering est utilisé, ou copie la surface tampon vers la mémoire d'écran dans les autres cas. C'est la fonction à appeler lorsque le double-buffering est utilisé ou que l'écran a changé dans sa totalité.

Une autre fonction de mise à jour de l'écran est SDL_UpdateRect:

void SDL_UpdateRect(SDL_Surface *screen, Sint32 x, Sint32 y, Sint32 w, Sint32 h);

Elle permet de ne mettre à jour qu'une partie de l'écran. Si seule une petite zone a été modifiée par rapport à l'écran précédente, cette fonction est bien plus rapide que SDL_Flip si le double-buffering n'est pas utilisé. Ses paramètres sont la surface d'écran, la position de la zone à mettre à jour ainsi que ses longueur et hauteur. Elle a une petite soeur nommée SDL_UpdateRects qui met, elle, une liste de SDL_Rects à jour:

void SDL_UpdateRects(SDL_Surface *screen, int numrects, SDL_Rect *rects);

Enfin, une autre fonction bonne à connaître est SDL_FillRect qui remplit une zone d'une surface avec une certaine couleur:

int SDL_FillRect(SDL_Surface *dst, SDL_Rect *dstrect, Uint32 color);

Les arguments sont utilisés de la même manière que SDL_BlitSurface, et la couleur doit être au format de la surface destination.

Un premier exemple

Les bases étant posées, terminons cet aperçu rapide par un exemple reprenant ce que nous avons vu. Inaugurons un test.c par les habituels en-têtes auxquels nous ajouterons celui de SDL:

#include <stdio.h>
#include <unistd.h>
#include "SDL.h"

Voici la définition de la fonction putPixel décrite plus haut, qui allume un pixel donné d'une surface:

void putPixel(SDL_Surface * surface, Uint16 x, Uint16 y, Uint32 color)
{
    /* Nombre de bits par pixels de la surface d'écran */
    Uint8 bpp = surface->format->BytesPerPixel;
    /* Pointeur vers le pixel à remplacer (pitch correspond à la taille
       d'une ligne d'écran, c'est à dire (longueur * bitsParPixel)
       pour la plupart des cas) */
    Uint8 * p = ((Uint8 *)surface->pixels) + y * surface->pitch + x * bpp;
     switch(bpp)
    {
      case 1:
        *p = (Uint8) color;
        break;
        case 2:
            *(Uint16 *)p = (Uint16) color;
            break;
        case 3:
            if (SDL_BYTEORDER == SDL_BIG_ENDIAN)
            {
                *(Uint16 *)p = ((color >> 8) & 0xff00) | ((color >> 8) & 0xff);
                *(p + 2) = color & 0xff;
            }
            else
            {
                *(Uint16 *)p = color & 0xffff;
                *(p + 2) = ((color >> 16) & 0xff) ;
            }
            break;
        case 4:
            *(Uint32 *)p = color;
            break;
    }
}

Remarquez l'utilisation du membre pitch de la structure SDL_Surface qui représente la taille exacte, en octets, d'une ligne de la surface.

Vient ensuite la fonction principale du programme. La première chose à faire dans tout programme utilisant SDL est bien entendu de l'initialiser:

int main(int argc, char * argv[])
{
    SDL_Surface * screen;
    SDL_Surface * image, * tmp;
    SDL_Rect blitrect = {0, 0, 0, 0};
    int i, j;
     if (SDL_Init(SDL_INIT_VIDEO) == -1)
    {
        printf("Erreur lors de l'initialisation de SDL: %s\n", SDL_GetError());
        return 1;
    }

SDL_init prend en paramètre la liste des modules de SDL à initialiser (nous nous contentons de la vidéo pour le moment) et retourne -1 en cas d'erreur, dont nous pouvons récupérer le message détaillé avec SDL_GetError.

Ensuite, sélectionnons le mode vidéo en utilisant SDL_SetVideoMode:

#define SDL_VIDEO_FLAGS (SDL_HWSURFACE | SDL_DOUBLEBUF | SDL_ANYFORMAT)

    screen = SDL_SetVideoMode(640, 480, 24,
                              SDL_VIDEO_FLAGS);

    printf("Mode vidéo: dx%d\n", screen->w, screen->h,
           screen->format->BitsPerPixel);

La surface d'écran retournée sera pointée par la variable screen. Elle sera de dimensions (640, 480), si possible dans la mémoire de la carte vidéo, utilisera le double buffering et un format de 24 bits par pixel, sauf si SDL juge qu'un autre format true color sera plus approprié (flag SDL_ANYFORMAT). Ce sera notamment le cas si cet exemple est lancé à partir d'un serveur X configuré en 16 bits. Pour éviter d'avoir à faire des conversions coûteuses à chaque affichage d'écran, SDL choisira le même format que celui du serveur. L'inconvénient du flag SDL_ANYFORMAT est que nous aurons en revanche un format d'image différent selon les machines sur lesquelles tournera le programme: il faudra en tenir compte!

Si la surface a été allouée correctement (valeur de retour différente de NULL), nous pouvons dessiner dessus! Commençons par la remplir de bleu, en utilisant SDL_FillRect:

SDL_FillRect(screen, NULL, SDL_MapRGB(screen->format, 0x00, 0x00, 0xff));

Notez d'une part que le paramètre dstrect vaut NULL (utilisé pour demander un remplissage complet de la surface) et d'autre part l'utilisation de SDL_MapRGB pour convertir notre triplet hexa au format de l'écran (ici les composantes rouges et vertes sont à zéro, la bleue est au maximum: nous aurons un bleu intense).

Ensuite, la surface est verrouillée puis une boucle simple est utilisée pour afficher un pixel sur deux de l'écran en blanc. Cela créera une sorte d'effet de grille similaire à une matrice de calibrage. Ce n'est pas particulièrement beau mais se montrera utile pour remarquer les effets de blit - et puis, nous aurons toujours l'occasion de faire des choses plus esthétiques par la suite!

SDL_LockSurface(screen);
    for (j = 0; j < screen->h; j++)
        for (i = j % 2; i < screen->w; i+=2)
            putPixel(screen, i, j, SDL_MapRGB(screen->format, 0xff, 0xff, 0xff));
    SDL_UnlockSurface(screen);

Une image est ensuite chargée, convertie au format de la surface d'écran, puis dessinée de 4 manières: brute, masquée, avec un paramètre d'alpha puis avec l'alpha et le masque.

tmp = SDL_LoadBMP("image.bmp");
    image = SDL_DisplayFormat(tmp);
    SDL_FreeSurface(tmp);

    /* Affichage sans masque ni alpha */
    blitrect.x = (screen->w - (image->w * 4)) / 2;
    blitrect.y = (screen->h - image->h) / 2;
    SDL_BlitSurface(image, NULL, screen, &blitrect);

    blitrect.x += image->w;
    /* Sélection de la couleur 0xff00ff comme masque */
    SDL_SetColorKey(image, SDL_SRCCOLORKEY, SDL_MapRGB(image->format, 0xff, 0x00, 0xff));
    SDL_BlitSurface(image, NULL, screen, &blitrect);

    blitrect.x += image->w;
    /* Suppression du masque de l'image */
    SDL_SetColorKey(image, 0, 0);
    /* Alpha mis à 128 (soit 50% de transparence) */
    SDL_SetAlpha(image, SDL_SRCALPHA, 128);
    SDL_BlitSurface(image, NULL, screen, &blitrect);

    blitrect.x += image->w;
    /* Sélection de la couleur 0xff00ff comme masque */
    SDL_SetColorKey(image, SDL_SRCCOLORKEY, SDL_MapRGB(image->format, 0xff, 0x00, 0xff));
    SDL_BlitSurface(image, NULL, screen, &blitrect);

L'écran est enfin affiché en utilisant SDL_Flip. Le programme attend alors 5 secondes, puis se termine. L'appel à SDL_Quit libère la surface d'écran et finalise l'utilisation de SDL dans notre programme.

SDL_Flip(screen);
    sleep(5);
    SDL_Quit();
    return 0;
}

Vous pouvez recopier ce programme et le compiler en suivant les instructions qui vont suivre. Les sources ainsi qu'une image de test sont disponibles sur le CD-ROM ou sur http://www.gnurou.org/documents/linuxmag/SDL/SDL-1.tar.gz.

Compiler pour SDL

SDL offre un script facilitant grandement la compilation des programmes qui l'utilisent. Ce script, nommé sdl-config, est créé lors de la compilation de SDL et permet de fournir tous les paramètres nécessaires au compilateur pour compiler ou lier des programmes utilisant SDL. Il s'appelle avec les arguments --cflags pour avoir les arguments de compilation, et --libs pour la liaison.

Si votre project utilise autoconf/automake, la macro AM_PATH_SDL vous permettra de détecter la présence de la bibliothèque et de vérifier que la version installée est suffisamment récente. Elle affectera également aux macros SDL_CFLAGS et SDL_LIBS les paramètres supplémentaires à passer lors de la compilation et de l'édition des liens.

Notre petit exemple se compilera pour sa part de la manière suivante:

gcc `sdl-config --cflags --libs` test.c -o test

En le lançant, vous constaterez les effets du masque et de l'alpha: le masque permet de donner des contours souples à une image, l'alpha permet de voir au travers de l'image ce qui se trouve derrière. Vous pouvez remplacer le fichier image par un autre de votre choix, au format BMP.

La prochaine fois...

Les bases étant posées, nous passerons à des choses plus dynamiques pour les prochains épisodes: nous verrons une manière de faire des sprites animés avec SDL, de lire les entrées du clavier ainsi qu'un moyen de gérer le temps pour obtenir une vitesse de jeu cohérente sur toute machine.


Tutoriel SDL gnurou

Notre premier article constituait une introduction encore très incomplète à SDL. Ce mois-ci, nous allons la compléter en étudiant la gestion des entrées qui nous permettra de réaliser des programmes plus réactifs.

Le plus gros défaut de notre programme précédent est qu'il est strictement linéaire: il dessine quelques bricoles, attend 5 secondes, et se termine. Pour donner un peu plus de goût à nos futurs programmes ludiques, il est nécessaire de disposer d'une bonne interaction avec l'utilisateur.

Les entrées avec SDL

La gestion des entrées dans SDL est, comme le reste, d'assez bas niveau. Contrairement à des APIs comme GTK ou Qt, les entrées ne sont pas gérées par un mécanisme de callback où une fonction est automatiquement appelée lorsqu'un événement survient. Ici, c'est au programmeur de prendre l'initiative de regarder si quelque chose s'est passé et de réagir en conséquence. Cela se fera pour notre cas par un appel régulier à une fonction de gestion des entrées dans notre boucle de jeu.

L'utilisation des entrées nécessite qu'un mode vidéo soit activé. Il est donc inutile de s'en préoccuper tant que SDL_SetVideoMode n'a pas été appelé.

SDL stocke les évènements arrivants dans une file d'attente. La fonction SDL_PollEvent permet de retirer l'élément en tête de file:

int SDL_PollEvent(SDL_Event * event);

Elle retourne 1 si il y avait des évènements en attente, 0 si la file d'attente était vide. Dans notre fonction de gestion des entrées, nous allons l'appeler tant qu'elle n'est pas vide pour traiter les évènements qui sont survenus depuis notre dernier passage. Il existe plusieurs autres fonctions liées à la gestion de la file des évènements, notamment pour la regarder sans la vider ou ajouter arbitrairement un événement. N'étant pas primordiales dans le cadre de la programmation ludique, nous les ignorerons lors de nos articles.

Dans le cas où SDL_PollEvent retire un événement, ce dernier est placé dans la structure SDL_Event donnée en argument. SDL_PollEvent va retourner absolument tous les évènements susceptibles d'arriver: appui d'une touche clavier, mouvement de la souris, mais aussi redimensionnement de la fenêtre SDL ou réception du focus. Tous ces évènements sont stockables dans la structure SDL_Event qui n'est rien d'autre qu'une union de tous les évènements gérés par SDL. Pour savoir à quel type d'évènement on a affaire, on regardera son champ type. Voici quelques-unes de ses valeurs qui nous intéressent dans un premier temps:

SDL_KEYDOWN
appui d'une touche clavier
SDL_KEYUP
relâchement d'une touche clavier
SDL_MOUSEMOTION
mouvement de la souris
SDL_MOUSEBUTTONDOWN
appui d'un bouton de la souris
SDL_MOUSEBUTTONUP
relâchement d'un bouton de la souris

En fonction du type d'évènement, on accèdera au membre du SDL_Event correspondant au membre d'union qui correspond. Pour un évènement clavier, ce sera key, pour un mouvement de souris motion, ou pour un click de souris button.

Chacun de ces membres a lui-même sa propre structure, que nous allons découvrir superficiellement. Pour plus de détails, et pour connaître les évènements gérés par SDL mais non-liés aux entrées, vous pouvez vous reporter à sa documentation.

Évènements clavier

Comme dit précédemment, les informations associées à un événement clavier sont accessibles via le membre key du SDL_Event correspondant. Il est lui-même d'un type structure (SDL_KeyboardEvent) et son seul membre intéressant (outre type) est keysym. C'est en effet lui qui va nous renseigner sur quelle touche a été actionnée ou relâchée. Il s'agit encore d'une structure (SDL_Keysym) dont voici les membres les plus utiles:

sym
nous donne le code de la touche concernée. Il peut être testé avec les macros de type SDLK_NOMDETOUCHE pour connaître précisément quelle touche a été enfoncée ou relâchée. La liste complète des noms de touche est disponible dans le fichier SDL_keysym.h du répertoire d'include de SDL, ou dans la documentation.
mod
nous informe sur l'état des modifieurs (touches CTRL, ALT, etc.). Son intérêt étant limité dans notre cadre, nous ne ferons que le mentionner.
unicode
fournit une information d'un peu plus haut niveau et nous donne, si le mode unicode de SDL a été activé, le caractère unicode qui a été émis par l'appui de cette touche. L'information est de plus haut niveau car le caractère retourné tient compte de l'état des modifieurs (majuscule si SHIFT est enfoncé, par exemple) et de la localisation du clavier. Il peut être utile si vous avez à lire du texte via le clavier. Pour activer le mode unicode (désactivé par défaut à cause du traitement supplémentaire qu'il induit), il suffit d'appeler SDL_EnableUNICODE(1).

Gestion de la souris

Les évènements souris sont gérés par les structures SDL_MouseMotionEvent (nom de membre: motion) pour les déplacements et SDL_MouseButtonEvent (nom de membre: button) pour les clics de souris. Concernant SDL_MouseMotionEvent, ses membres intéressants sont:

x et y
qui donnent les coordonnées absolues de la souris dans le repère graphique correspondant à la zone d'écran.
xrel et yrel
qui donnent le déplacement du curseur de souris, par rapport à sa position initiale.

Pour la gestion des boutons de la souris et la structure SDL_MouseButtonEvent:

Le membre button indique quel bouton a été cliqué ou relâché. Les macros SDL_BUTTON_LEFT, SDL_BUTTON_MIDDLE, SDL_BUTTON_RIGHT, SDL_BUTTON_WHEELUP et SDL_BUTTON_WHEELDOWN) peuvent être utilisées pour tester les trois principaux boutons ainsi que la molette. Les éventuels boutons additionnels portent des numéros supérieurs à 5.

On retrouve aussi les membres x et y qui jouent un rôle similaire à ceux de SDL_MouseMotionEvent et permettent de connaître quelle zone d'écran a été cliquée.

Comme nous venons de le voir, la gestion des deux périphériques d'entrée principaux est relativement aisée. Cependant, nombreux sont les types de jeux dont le gameplay est multiplié s'ils sont joués avec un joystick ou un joypad. Comme il se doit, SDL nous offre une gestion très complète de ces périphériques.

Gestion du joystick

La gestion du joystick, incontournable pour beaucoup de jeux, est plus complexe. En effet, si les claviers et souris sont relativement standardisés, les joysticks sont eux très variés. Les différences incluent notamment le nombre et le type des axes, le nombre de boutons, la présence ou non d'un POV ou d'un trackball, etc.

Commençons par standardiser le langage: un axe sera un élément de type manche à balai (ou axes à potentiomètre), un chapeau pourra être soit le « hat » que l'on trouve en haut d'un joystick et qui sert à gérer le point de vue dans les simulateurs de vol, soit le pad d'un joypad (ou tout type d'axe à 3 positions: gauche, centré ou droite). Les boutons comprennent tout ce qui est pressable, et les trackballs sont les petites boules orientables.

Pour activer la gestion des joysticks dans SDL, il faudra ajouter le flag SDL_INIT_JOYSTICK lors de notre appel à SDL_Init, récupérer le nombre de joysticks connectés au système avec SDL_NumJoysticks et les ouvrir avec SDL_JoystickOpen (faute de quoi ils n'émettront pas d'évènements). Cette dernière fonction prend l'index du joystick à ouvrir (qui doit être inférieur à la valeur retournée par SDL_NumJoysticks) et retourne un pointeur vers un SDL_Joystick qui pourra être utilisé pour obtenir plus d'informations sur les capacités du périphérique. Ces informations s'obtiennent grâce à une série de fonctions dédiées:

SDL_JoystickName
prend un index de joystick et retourne sa chaîne d'identifiant,
SDL_JoystickNumAxes, SDL_JoystickNumButtons, SDL_JoyStickNumBalls et SDL_JoyStickNumHats
prennent quant à elles le pointeur retourné par SDL_JoystickOpen et retournent respectivement le nombre d'axes, de boutons, de trackballs et de chapeaux disponibles sur le joystick.

Une dernière fonction d'intérêt est SDL_JoystickClose qui permet de faire « taire » un joystick.

Une fois nos joysticks ouverts, ils enverront des évènements au même titre que les autres périphériques d'entrée. Un joystick est capable de générer quatre types d'évènements, selon que l'on a touché à ses axes, ses chapeaux, ses boutons ou ses trackballs.

Un mouvement d'axe sera représenté par un événement de type SDL_JOYAXISMOTION, accessible via le membre jaxis de SDL_Event. Ses membres sont:

which
le numéro du joystick qui a émis l'évènement (ce membre se retrouvera dans tous les évènements liés aux joysticks).
axis
le numéro de l'axe ayant bougé.
value
la position de l'axe, comprise entre -32767 (complètement à gauche/en haut) et 32767 (complètement à droite/en bas).

Un chapeau émettra quant à lui un évèmenent de type SDL_JOYHATMOTION, accessible par le membre jhat. Outre le membre which indiquant le joystick émetteur, on trouvera:

hat
qui indique le numéro du chapeau concerné par l'évènement.
value
qui comme pour les évènements liés aux axes donnera la position du chapeau. Le nombre de valeurs possibles étant limité, les valeurs se testeront avec les macros SDL_HAT_CENTERED, SDL_HAT_UP, SDL_HAT_RIGHT, SDL_HAT_DOWN, SDL_HAT_LEFT, ces macros étant composables par ou logique pour en obtenir d'autres: SDL_HAT_RIGHTUP, SDL_HAT_RIGHTDOWN, SDL_HAT_LEFTUP, SDL_HAT_LEFTDOWN. En toute logique, SDL_HAT_RIGHTUP sera égal à (SDL_HAT_RIGHT | SDL_HAT_UP).

Les trackballs émettront des évènements SDL_JOYBALLMOTION lisibles via jball, très proches d'un mouvement de souris:

ball
indique le numéro du trackball émetteur.
xrel et yrel
donnent les mouvements du trackball par rapport à leur dernière position. Notez que, contrairement aux évènements souris, on ne dispose pas de la position absolue.

Enfin, les évènements boutons se gèrent pratiquement comme des appuis sur des touches clavier. Deux types d'évènements leur sont associés, SDL_JOYBUTTONDOWN pour un appui et SDL_JOYBUTTONUP pour un relâchement, et les informations utiles sont accessibles via le membre jbutton de SDL_Event:

which
toujours présent.
button
indique le numéro du bouton appuyé ou relâché.

Le programme de ce mois-ci

Notre programme d'exemple met toutes ces notions en application. Il analyse tous les évènements d'entrée venant du clavier, de la souris ou des joysticks. Pour chacun de ces évènements, il fait un rapport détaillé sur la sortie standard. De plus, les touches directionnelles du clavier, les mouvements de la souris, ainsi que le déplacement des axes, chapeaux et trackballs des joysticks connectés permettent de diriger le petit sprite affiché à l'écran. La touche Escape permet de quitter le programme. Il se compile et se lance de la même manière que l'exemple du mois dernier. Remarquez les différentes façons de mettre à jour la position de notre sprite (soit par sa vélocité, soit directement par sa position absolue selon le type d'évènement). Comme d'habitude, le source complet ainsi que l'image utilisée sont disponibles sur le CD-ROM du magazine ou à l'adresse http://www.gnurou.org/documents/linuxmag/SDL/SDL-2.tar.gz

#include <stdio.h>
#include "SDL.h"

/* Variable permettant de quitter la boucle de jeu */
unsigned char letsexit = 0;
/* Vélocité du sprite */
Uint16 xvel = 0, yvel = 0;
/* Position du sprite */
SDL_Rect spritepos;

/* Gestion des évènements d'entrée */
void process_events()
{
    SDL_Event event;
    /* Un évènement attend d'être traité? */
    while (SDL_PollEvent(&event))
    {
        /* Si oui, quel type? */
    switch (event.type)
    {
          /* Appui sur une touche */
      case SDL_KEYDOWN:
           printf("Touche %d enfoncée (caractère produit: %c)\n",
              event.key.keysym.sym,
              event.key.keysym.unicode);
           switch (event.key.keysym.sym)
           {
           case SDLK_ESCAPE:
            letsexit = 1;
            break;
           case SDLK_LEFT:
            xvel = -1;
            break;
           case SDLK_RIGHT:
            xvel = 1;
            break;
           case SDLK_UP:
            yvel = -1;
            break;
           case SDLK_DOWN:
            yvel = 1;
            break;
           default:
            break;
           }
           break;
          /* Relâchement d'une touche */
      case SDL_KEYUP:
           printf("Touche %d relâchée\n", event.key.keysym.sym);
           switch (event.key.keysym.sym)
           {
           case SDLK_LEFT:
           case SDLK_RIGHT:
            xvel =0;
            break;
           case SDLK_UP:
           case SDLK_DOWN:
            yvel = 0;
            break;
           default:
            break;
           }
           break;
          /* Déplacement souris */
      case SDL_MOUSEMOTION:
           spritepos.x += event.motion.xrel;
           spritepos.y += event.motion.yrel;
           printf("Position souris: (d)\n", event.motion.x,
              event.motion.y);
           break;
          /* Enfoncement bouton souris */
      case SDL_MOUSEBUTTONDOWN:
           printf("Click bouton souris %d\n", event.button.button);
           break;
          /* Relâchement bouton souris */
      case SDL_MOUSEBUTTONUP:
           printf("Relâchement bouton souris %d\n", event.button.button);
           break;
          /* Déplacement axe joystick */
      case SDL_JOYAXISMOTION:
           printf("Axe d positionné à %d\n",
              event.jaxis.axis, event.jaxis.which, event.jaxis.value);
           /* Les axes pairs sont verticaux, les impairs horizontaux.
          Les valeurs de jaxis.value étant très grandes, on compense
          en faisant un décalage à droite (division par 2^13) */
           if (event.jaxis.axis % 2)
            yvel = event.jaxis.value >> 13;
           else
            xvel = event.jaxis.value >> 13;
           break;
          /* Déplacement chapeau joystick */
      case SDL_JOYHATMOTION:
           printf("Chapeau d en position ",
              event.jhat.hat, event.jhat.which);
           switch (event.jhat.value)
           {
           case SDL_HAT_CENTERED:
            xvel = 0; yvel = 0;
            printf("centrée\n");
            break;
           case SDL_HAT_UP:
            xvel = 0; yvel = -1;
            printf("haute\n");
            break;
           case SDL_HAT_RIGHT:
            xvel = 1; yvel = 0;
            printf("droite\n");
            break;
           case SDL_HAT_DOWN:
            xvel = 0; yvel = 1;
            printf("basse\n");
            break;
           case SDL_HAT_LEFT:
            xvel = -1; yvel = 0;
            printf("gauche\n");
            break;
           case SDL_HAT_RIGHTUP:
            xvel = 1; yvel = -1;
            printf("haute/droite\n");
            break;
           case SDL_HAT_RIGHTDOWN:
            xvel = 1; yvel = 1;
            printf("basse/droite\n");
            break;
           case SDL_HAT_LEFTUP:
            xvel = -1; yvel = -1;
            printf("haute/gauche\n");
            break;
           case SDL_HAT_LEFTDOWN:
            xvel = -1; yvel = 1;
            printf("basse/gauche\n");
            break;
           }
           break;
          /* Déplacement trackball joystick */
      case SDL_JOYBALLMOTION:
           spritepos.x += event.jball.xrel;
           spritepos.y += event.jball.yrel;
           printf("Trackball d a bougé de (d)\n",
              event.jball.ball, event.jball.which,
              event.jball.xrel, event.jball.yrel);
           break;
          /* Appui bouton joystick */
      case SDL_JOYBUTTONDOWN:
           printf("Bouton d enfoncé\n",
              event.jbutton.button, event.jbutton.which);
           break;
          /* Relâchement bouton joystick */
      case SDL_JOYBUTTONUP:
           printf("Bouton d relâché\n",
              event.jbutton.button, event.jbutton.which);
           break;
      default:
           break;
      }
     }
}

#define SDL_VIDEO_FLAGS (SDL_HWSURFACE | SDL_DOUBLEBUF | SDL_ANYFORMAT)

int main(int argc, char * argv[])
{
     SDL_Surface * screen;
     SDL_Surface * sprite, * tmp;
     int nbjoysticks;
     int i;

     if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK) == -1)
     {
      fprintf(stderr, "Erreur lors de l'initialisation de SDL: %s\n",
          SDL_GetError());
      return 1;
     }

     screen = SDL_SetVideoMode(640, 480, 24, SDL_VIDEO_FLAGS);
     printf("Mode vidéo: dx%d\n", screen->w, screen->h,
        screen->format->BitsPerPixel);

     /* Activation du support UNICODE */
     SDL_EnableUNICODE(1);

     nbjoysticks = SDL_NumJoysticks();
     printf("Nombres de joysticks attachés: %d\n\n", nbjoysticks);

     for (i = 0; i < nbjoysticks; i++)
     {
      SDL_Joystick * joy = SDL_JoystickOpen(i);
      printf("Joystick s\n", i, SDL_JoystickName(i));
      printf("Axes: %d\n", SDL_JoystickNumAxes(joy));
      printf("Boutons: %d\n", SDL_JoystickNumButtons(joy));
      printf("Trackballs: %d\n", SDL_JoystickNumBalls(joy));
      printf("Chapeaux: %d\n\n", SDL_JoystickNumHats(joy));
     }

     /* Chargement du sprite */
     tmp = SDL_LoadBMP("image.bmp");
     sprite = SDL_DisplayFormat(tmp);
     SDL_FreeSurface(tmp);
     SDL_SetColorKey(sprite, SDL_SRCCOLORKEY, SDL_MapRGB(sprite->format, 0xff, 0x00, 0xff));

     /* Position initiale du sprite */
     spritepos.x = (screen->w - sprite->w) / 2;
     spritepos.y = (screen->h - sprite->h) / 2;
     spritepos.w = 0;
     spritepos.h = 0;

     /* Boucle de jeu */
     while (!letsexit)
     {
      /* Mise à jour de l'état du jeu à partir des
         évènements */
      process_events();

      /* Mise à jour de la position du sprite à partir
         de sa vélocité */
      spritepos.x += xvel;
      spritepos.y += yvel;
      /* Test de débordement d'écran */
      if (spritepos.x < 0) spritepos.x = 0;
      if (spritepos.x > screen->w - sprite->w) spritepos.x = screen->w - sprite->w;
      if (spritepos.y < 0) spritepos.y = 0;
      if (spritepos.y > screen->h - sprite->h) spritepos.y = screen->h - sprite->h;

      /* Mise à jour de l'écran */
      /* Remplissage de l'écran en noir */
      SDL_FillRect(screen, NULL, 0);
      /* Affichage du sprite puis mise à jour du
         buffer d'affichage */
      SDL_BlitSurface(sprite, NULL, screen, &spritepos);
      SDL_Flip(screen);
     }

     SDL_Quit();
     return 0;
}

La prochaine fois...

Ceci clôturera ce tour d'horizon de la gestion des entrées de SDL. Il semblait important de le faire tôt pour pouvoir profiter de l'interaction avec l'utilisateur, base de tout programme ludique. Vous l'aurez constaté, le panel d'entrées gérées par SDL est très large et extrêmement complet, en particulier pour les joysticks. Seul absente, la gestion du retour de force, très pauvre et anarchique sous Linux et totalement ignorée par SDL. Espérons que ce manque soit comblé un jour.

Le prochain épisode sera consacré aux animations et à la gestion du temps, du moins si l'ampleur des sujets n'oblige pas à revoir encore une fois la portée des articles à la baisse!

Références

  • Le SDL documentation project à voir pour des informations plus complètes: http://sdldoc.csn.ul.ie/
  • Les includes de SDL, très documentés, que vous trouverez probablement dans /usr/include/SDL! :)


Tutoriel SDL gnurou

Maintenant que nous savons dessiner sur l'écran et réagir aux entrées, nous pouvons nous lancer dans un peu de technique. Au programme cette fois-ci: l'animation de sprites.

Nous n'entendons pas ici animation au sens déplacement d'un élément graphique sur l'écran, mais plutôt comme une succession d'images fixes à un rythme donné. Chaque élément d'un jeu (vaisseau, projectile, ...) est susceptible de se déplacer à n'importe quel moment. Pour paraître crédible, le mouvement sur l'écran doit s'accompagner d'une animation de l'élément graphique représentant notre objet. Même les objets immobiles gagnent à être animés. Quoi de plus triste qu'un sprite désespérément figé dans l'attente d'un événement... Nous allons mettre au point quelques structures rudimentaires qui nous permettront de gérer facilement et de manière transparente (presque aussi simplement que les éléments non-animés) les sprites animés.

Le module d'animation

Le module que nous allons réaliser sera facilement réutilisable et incorporable dans un projet existant. Il se composera d'un fichier de déclaration (animation.h) et d'un fichier d'implémentation (animation.c) définissant des structures et les fonctions permettant de les manipuler. Ces fonctions peuvent être vues comme des « méthodes » qui s'appliquent à la « classe » dont le nom compose leur préfixe. Le principe des animations est le suivant: des images qui sont affichées les unes après les autres, chaque image restant affichée durant un certain délai. Fixons-nous tout d'abord certaines limites. Toutes les images d'une animation auront la même taille, pour simplifier leur gestion. Les animations seront par défaut cycliques. Enfin, toutes les images d'une animation seront masquées par la couleur 0xff00ff, traditionnellement utilisée pour les masques de couleurs. Commençons par l'écriture du fichier animation.h, qui contiendra les déclarations des structures et fonctions de notre module d'animation. L'en-tête inclura les headers de SDL et définira une macro qui permettra d'éviter les inclusions multiples:

 #ifndef ANIMATION_H__
 #define ANIMATION_H__

 #include "SDL.h"

L'élément de base d'une animation sera la frame, qui comprend la référence de la surface à afficher et le délai à attendre avant d'afficher la frame suivante:

 typedef struct
 {
   SDL_Surface * image;
   Uint16 delay;
 } animation_frame;

Les frames pourront subir 3 opérations: initialisation, nettoyage (libération de la mémoire allouée), et blit sur une autre surface.

 void animation_frame_init(animation_frame * frame, SDL_Surface * image, Uint16 delay);
 void animation_frame_cleanup(animation_frame * frame);
 void animation_frame_draw(animation_frame * frame, SDL_Surface * dst, SDL_Rect * pos);

Bien que déclarées publiques, ces opérations ne seront vraisemblablement utilisées que de manière interne au module.

Regroupées dans un tableau, les frames forment une animation :

 typedef struct
 {
   Uint16 nbr_of_frames;
   animation_frame * frames;
 }animation;

La lecture d'une animation se fait en affichant la frame 0 pendant son délai, puis en passant à la 1, et ainsi de suite. Une fois la dernière frame affichée, on boucle sur la première. Une frame restera indéfiniment affichée si son délai est égal à 0, ce qui nous permettra de créer des animations non-cycliques. La figure 1 illustre les deux structures animation et animation_frame et leur interaction.

Fig. 1: Illustration des structures de l'animation utilisée dans l'exemple de ce mois-ci.
Fig. 1: Illustration des structures de l'animation utilisée dans l'exemple de ce mois-ci.

De même que pour les frames, une animation aura des fonctions d'initialisation et de nettoyage. Le nombre de frames dont dispose une animation se décide lors de son initialisation. La troisième fonction, animation_setframe, permet d'affecter une surface et un délai à une frame donnée.

 void animation_init(animation * anim, Uint16 nbr_of_frames);
 void animation_cleanup(animation * anim);
 void animation_setframe(animation * anim, Uint16 pos, SDL_Surface * surface, Uint16 delay);

Les structures animation et animation_frame ne sont rien de plus qu'une description de l'animation: nombre d'images, surface à afficher pour chacune, délai à attendre avant de passer à l'image suivante. On peut comparer une animation à une cassette vidéo ou un DVD: seule, elle est incapable d'afficher quoi que ce soit. Il faut un moyen de lecture, qui fera avancer l'animation et gardera une trace de la position courante.

C'est le rôle de la structure animator. Cette structure est liée à une animation et permet de la jouer. Elle comprend un certain nombre de variables d'état permettant notamment de connaître la position courante dans l'animation:

 typedef struct
 {
   const animation * anim;
   enum { STOP, PLAY } status;
   Uint16 current_frame;
   Uint16 counter;
 }animator;

anim est la référence vers l'animation qui est jouée. status permet de mettre une animation en pause (STOP) ou au contraire de la laisser se jouer (PLAY). current_frame et counter sont respectivement l'index de la frame courante dans l'animation qui est jouée, et le temps écoulé par rapport au délai durant lequel cette frame doit être affichée. La structure animator permet en fait un scénario irréaliste avec notre métaphore de la cassette vidéo: plusieurs animators peuvent être associés à une animation, ce qui serait équivalent à une cassette vidéo qui serait jouée simultanément par plusieurs magnétoscopes, chacun à une position différente. L'avantage pour nous est clair: si une zone de jeu comprend 50 fois le même élément animé, nous n'aurons pas besoin de charger 50 fois en mémoire sa représentation graphique: elle sera présente une fois en mémoire, mais utilisée par autant d'animators qu'il y aura de présences à l'écran, comme l'illustre la figure 2.

Fig. 2
Fig. 2: Les animators permettent de séparer l'état d'une animation de ses données, et ainsi de jouer plusieurs fois la même animation simultanément.

N'allouant pas de mémoire, animator dispose d'une fonction d'initialisation qui prend en argument l'animation à jouer, mais pas de fonction de nettoyage.

 void animator_init(animator * ator, animation * anim);

Trois autres fonctions permettent de manipuler l'état de lecture de l'animation, pour la jouer, l'arrêter et la remettre dans son état initial:

 void animator_play(animator * ator);
 void animator_stop(animator * ator);
 void animator_rewind(animator * ator);
 void animator_nextframe(animator * ator);

La fonction nextframe, elle, passe directement à la frame suivante quel que soit l'état de l'animator. Elle est utilisée en interne mais permet aussi de faire des animations dont le déroulement est géré par évènements: pour cela, on mettra les délais de toutes les frames à 0 et on appellera nextframe lorsque l'on souhaite passer à la frame suivante.

Enfin, les deux dernières fonctions sont également les principales : update incrémente le compteur interne de l'animator et met à jour la frame à afficher si nécessaire. draw dessine la frame courante sur la surface et à la position données.

 void animator_update(animator * ator);
 void animator_draw(animator * ator, SDL_Surface * dest, SDL_Rect * pos);

 #endif

La fonction animator_update doit être appelée régulièrement pour obtenir des animations cohérentes. Le délai que nous définissons n'a en effet aucune valeur temporelle. Si une frame a un délai de 15, il faudra appeler 15 fois animator_update pour passer à la frame suivante.

L'implémentation de ces fonctions est réalisée dans le fichier animation.c, dont voici le listing, assez trivial si l'on a compris le fonctionnement des animations:

 #include "animation.h"
 #include <stdlib.h>

 void animation_frame_init(animation_frame * frame, SDL_Surface * image, Uint16 delay) {
   frame->image = image;
   frame->delay = delay;
 }

 void animation_frame_cleanup(animation_frame * frame) {
   if (frame->image) SDL_FreeSurface(frame->image);
 }

 void animation_frame_draw(animation_frame * frame, SDL_Surface * dest, SDL_Rect * pos) {
   SDL_BlitSurface(frame->image, (SDL_Rect *)NULL, dest, pos);
 }

 void animation_init(animation * anim, Uint16 nbr_of_frames) {
   anim->frames = (animation_frame *) calloc(nbr_of_frames, sizeof(animation_frame));
   anim->nbr_of_frames = nbr_of_frames;
 }

 void animation_setframe(animation * anim, Uint16 pos, SDL_Surface * surface, Uint16 delay) {
   animation_frame_cleanup(&anim->frames[pos]);
   animation_frame_init(&anim->frames[pos], surface, delay);
 }

 void animation_cleanup(animation * anim) {
   int i;
   for (i = 0; i < anim->nbr_of_frames; i++)
   animation_frame_cleanup(&anim->frames[i]);

   free(anim->frames);
 }

 void animator_init(animator * ator, animation * anim) {
   ator->anim = anim;
   animator_rewind(ator);
 }

 void animator_play(animator * ator) {
   ator->status = PLAY;
 }

 void animator_stop(animator * ator) {
   ator->status = STOP;
 }

 void animator_rewind(animator * ator) {
   ator->current_frame = 0;
   ator->counter = 0;
   animator_stop(ator);
 }

 void animator_nextframe(animator * ator) {
   /* Retour à la frame 0 si nous sommes à la dernière */
   if (++ator->current_frame == ator->anim->nbr_of_frames) ator->current_frame = 0;
   ator->counter = 0;
 }

 void animator_update(animator * ator) {
   const animation_frame * frame;

   /* Ne mettre à jour l'animation que si elle est jouée */
   if (ator->status != PLAY) return;

   frame = &ator->anim->frames[ator->current_frame];
   if (frame->delay == 0) return;

   /* Passage à la frame suivante? */
   if (++ator->counter == frame->delay) animator_nextframe(ator);
 }

 void animator_draw(animator * ator, SDL_Surface * dest, SDL_Rect * pos) {
   animation_frame_draw(&ator->anim->frames[ator->current_frame], dest, pos);
 }

Gestion du temps

Nous allons également nous préoccuper un peu du caractère temporel de nos programmes. Ils seront amenés à s'exécuter sur des machines de puissance et de configuration différentes, et ne fonctionneront donc pas exactement de la même manière. SDL offre une abstraction permettant de ne pas avoir à prendre en compte les différences d'architectures et de matériel, il nous reste cependant à gérer le cas où une machine fait tourner notre programme tellement vite que cela en devient injouable ou, plus fréquent, n'est pas assez puissante l'exécuter de manière optimale.

Il s'agit d'une situation connue de tous les joueurs: un jeu se met à « ramer ». Deux attitudes de sa part sont alors possibles: soit la cadence du jeu se ralentit, et le jeu devient plus « lent », soit elle reste la même mais des « saccades » apparaissent alors.

Bien qu'aucune de ces situations ne soit vraiment agréable, il est en général préférable de tomber dans la seconde que dans la première. Le premier comportement est ce qui arrive si aucune prise en charge du temps n'a été prévue. Nous allons voir comment implémenter le second.

Il existe plusieurs manières de procéder, mais la suivante est à la fois simple et efficace. Tout d'abord, nous allons fixer un « rythme » pour notre programme, exprimé en cycles par secondes. Un cycle correspondant à la mise à jour de son l'état interne (position des sprites, mise à jour des animations, etc). Ce rythme, quoi qu'il en coûte, devra toujours être respecté. Si notre programme détecte qu'il a pris du retard, il lui faudra trouver un moyen de faire moins de calculs pour rattraper ce retard. Qu'allons-nous alors sacrifier? Le feedback à l'utilisateur, c'est à dire l'affichage. En général, la mise à jour de l'écran prend un temps très condérable, souvent la plus grosse partie du temps d'exécution d'un jeu, en particulier si la cible n'est pas accélérée. Nous allons donc avoir un deuxième « rythme », le nombre d'images par secondes, qui idéalement sera égal au nombre de cycles de jeu par secondes mais pourra être inférieur si besoin est. Autrement dit, si la machine sur laquelle tourne le programme est trop lente pour honorer ses délais, nous « sauterons » le nombre d'images nécessaires pour que l'état interne du programme puisse au moins être mis à jour en temps et en heure.

Afin de pouvoir en arriver là, il est nécessaire de bien séparer l'affichage du jeu en lui-même. C'est d'ailleurs une bonne pratique en programmation et le reflet d'un bon design. Un jeu n'a besoin de l'affichage que pour montrer son état interne au joueur. Il peut et doit être capable de fonctionner sans prendre ce dernier en compte.

Notre petit module d'animation prend déjà cette séparation en compte. La structure animator contient l'état d'une animation, la structure animation contient ses éléments graphiques. animation dépend de SDL dont elle utilise des éléments, animator en est indépendante (en dehors des types primitifs comme Uint16, mais ceux-ci peuvent être redéfinis). On pourrait ainsi remplacer notre implémentation de animation utilisant SDL par une autre utilisant une autre bibliothèque graphique et continuer à l'utiliser avec animator sans changer cette dernière. C'est un de effets de bord bénéfiques d'un bon design et donc d'une bonne séparation des préoccupations.

Mais revenons-en à notre gestion du temps. Nous allons l'implémenter dans time.h et time.c, de la même manière que nous avons fait pour les animations. SDL offre une fonction qui permet de connaître le nombre de millisecondes qui se sont écoulées depuis l'initialisation de la bibliothèque:

 Uint32 SDL_GetTicks(void);

Nous pouvons nous servir de cette fonction pour mesurer le temps qui s'est écoulé entre deux cycles. Pour cela, il nous faudra une fonction qui soit appelée à chaque cycle de jeu pour battre la cadence. C'est le rôle de notre fonction de synchronisation, time_update, qui a pour vocation d'être appelée dans la boucle de jeu et va réaliser les tâches suivantes:

  • Calculer combien de temps s'est écoulé entre le dernier appel à la fonction et l'appel courant,
  • Si le temps écoulé est inférieur au temps d'un cycle, attendre que le temps requis soit écoulé,
  • Une fois que le temps d'un cycle est passé, calculer combien de cycles de retard le jeu a pris (s'il en a pris), et mettre à jour une variable globale permettant à la boucle principale de connaître de combien de cycles avancer avant d'afficher l'état du jeu à l'écran.

Deux autres fonctions font partie de notre module de gestion du temps, time_init qui l'initialise aux valeurs par défaut, et time_set_game_speed qui permet de changer la vitesse du jeu, en nombre de cycles par secondes.

Voici les listings de time.h et time.c:

time.h:

 #ifndef TIME_H__
 #define TIME_H__

 #include "SDL.h"

 extern Uint16 cycles_to_calculate;
 extern Uint16 game_speed;
 extern Uint16 cycle_length;

 void time_init();
 void time_set_game_speed(Uint16 speed);
 void time_update();

 #endif

time.c:

 #include "time.h"

 static Uint32 timer1, timer2;

 /* Nombre maximum de cycles à passer - au delà, le jeu
    sera ralenti */
 #define MAX_SKIPPED_FRAMES 20
 /* Variable globale indiquand le nombre de cycles à
    calculer avant d'afficher une image */
 Uint16 cycles_to_calculate = 0;

 /* Vitesse par défaut du jeu, en cycles par seconde */
 #define DEFAULT_GAME_SPEED 70
 /* Vitesse courante du jeu */
 Uint16 game_speed;
 /* Durée d'un cycle en millisecondes */
 Uint16 cycle_length;

 /* Initialise le système temporel */
 void time_init() {
   time_set_game_speed(DEFAULT_GAME_SPEED);
 }

 /* Affecte la vitesse du jeu, en cycles par seconde */
 void time_set_game_speed(Uint16 speed) {
   if (speed == 0) speed = 1;
   game_speed = speed;
   cycle_length = 1000 / game_speed;
   timer1 = SDL_GetTicks();
 }

 /* Mise à jour du temps et des variables associées */
 void time_update() {
   /* S'assurer que l'on ne va pas trop vite... */
   while (1) {
     timer2 = SDL_GetTicks() - timer1;
     if (timer2 >= cycle_length) break;
     else SDL_Delay(3);
   }

   /* Mise à jour de l'état des timers et des variables
      globales - timer1 va contenir le "surplus" de
      timer2 par rapport au temps que l'on aurait exactement
      dû passer, cycles_to_calculate le nombre de cycles à
      faire jouer avant d'afficher une nouvelle image */
   timer1 = SDL_GetTicks() - (timer2 % cycle_length);
   cycles_to_calculate = timer2 / cycle_length;
   /* Vérifier la limite des cycles à sauter */
   if (cycles_to_calculate > MAX_SKIPPED_FRAMES) cycles_to_calculate = MAX_SKIPPED_FRAMES;
 }

Le programme du mois

Comme il se doit, notre programme d'exemple va utiliser ces deux modules pour créer un petit effet graphique. Il crée une animation correspondant à l'explosion illustrée en figure 1 et l'afficher plusieurs fois à l'écran. Le nombre d'explosions simultanées est contrôlé par deux tableaux, l'un d'animators, l'autre de SDL_Rects. Chaque couple correspond à une lecture de l'animation et à sa position à l'écran. Les touches + et -du pavé numérique permettent d'augmenter ou de diminuer le nombre d'explosions simultanées. Le module de gestion du temps est utilisé pour donner le rythme du jeu. Par défaut, le jeu tourne à 70 cycles par secondes. En utilisant les touches PageUp et PageDown, il est possible d'augmenter ou de diminuer de rythme par palliers de 5 cycles par seconde. Pour permettre de mesurer les performances de la machine, le nombres d'images par secondes est affiché toutes les secondes sur la console. Idéalement, il sera à peu près égal au nombre de cycles par secondes (une légère imprécision peut donner des chiffres légèrement supérieurs ou inférieurs), mais si le rythme est trop soutenu, ou que la machine est chargée par d'autres programmes, vous allez voir qu'il va rapidement tomber. Le jeu, en revanche, gardera la même vitesse. Voici le listing de ce programme, à mettre dans main.c:

 #include <stdio.h>
 #include <stdlib.h>
 #include <unistd.h>
 #include "SDL.h"

 #include "animation.h"
 #include "time.h"

 char letsexit = 0;

 /* Nombre d'explosions simultanées et valeur
    par défaut */
 #define DEFAULT_EXPLO 10
 static int nb_explo;

 /* L'animation et les tableaux contenant les
    animators ainsi que leurs positions */
 static animation anim;
 static animator * player = NULL;
 static SDL_Rect * pos = NULL;

 SDL_Surface * screen;

 /* Réalloue la taille des tableaux pour
    n explosions */
 void allocate_explosions(int n) {
   nb_explo = n;
   player = (animator *) realloc(player, sizeof(animator) * nb_explo);
   pos = (SDL_Rect *) realloc(pos, sizeof(SDL_Rect) * nb_explo);
 }

 /* Initialise une animation nouvellement allouée */
 void init_explosion(int i) {
   /* Position aléatoire sur l'écran */
   pos[i].x = rand() % (screen->w - player[i].anim->frames[0].image->w);
   pos[i].y = rand() % (screen->h - player[i].anim->frames[0].image->h);
   /* Position aléatoire dans l'animation */
   player[i].current_frame = rand() % player[i].anim->nbr_of_frames;
   player[i].counter = rand() % player[i].anim->frames[player[i].current_frame].delay;
 }

 /* Gestion des entrées clavier */
 void input_update() {
   SDL_Event event;
   while (SDL_PollEvent(&event)) {
   switch (event.type) {
     case SDL_KEYDOWN:
       switch (event.key.keysym.sym) {
         /* Quitter */
        case SDLK_ESCAPE:
          letsexit = 1;
          break;
        /* Plus d'explosions */
        case SDLK_KP_PLUS:
          allocate_explosions(nb_explo + 1);
          animator_init(&player[nb_explo - 1], &anim);
          animator_play(&player[nb_explo - 1]);
          init_explosion(nb_explo - 1);
          printf("Nombre d'explosions: %d\n", nb_explo);
          break;
        /* Moins d'explosions */
        case SDLK_KP_MINUS:
          if (nb_explo == 0) break;
          allocate_explosions(nb_explo - 1);
          printf("Nombre d'explosions: %d\n", nb_explo);
          break;
        /* Plus rapide */
        case SDLK_PAGEUP:
          time_set_game_speed(game_speed + 5);
          printf("Vitesse de jeu: %d cycles/seconde\n", game_speed);
          break;
        /* Moins rapide */
        case SDLK_PAGEDOWN:
          if (game_speed < 5) time_set_game_speed(1);
          else time_set_game_speed(game_speed - 5);
          printf("Vitesse de jeu: %d cycles/seconde\n", game_speed);
          break;
        default:
          break;
      }
      break;
    default:
      break;
   }
   }
 }

 int main(int argc, char * argv[]) {
   SDL_Surface * img;
   int i;
   if (SDL_Init(SDL_INIT_VIDEO) == -1) {
     printf("Erreur lors de l'initialisation de SDL!\n");
     return 1;
   }
   screen = SDL_SetVideoMode(640, 480, 24,
                             SDL_HWSURFACE | SDL_DOUBLEBUF | SDL_ANYFORMAT);

   /* Création de l'animation */
   animation_init(&anim, 3);
   /* Frame 0, délai 10 cycles */
   img = SDL_LoadBMP("explo1.bmp");
   SDL_SetColorKey(img, SDL_SRCCOLORKEY, SDL_MapRGB(img->format, 0xff, 0x00, 0xff));
   animation_setframe(&anim, 0, img, 10);
   /* Frame 1, délai 15 cycles */
   img = SDL_LoadBMP("explo2.bmp");
   SDL_SetColorKey(img, SDL_SRCCOLORKEY, SDL_MapRGB(img->format, 0xff, 0x00, 0xff));
   animation_setframe(&anim, 1, img, 15);
   /* Frame 2, délai 30 cycles */
   img = SDL_LoadBMP("explo3.bmp");
   SDL_SetColorKey(img, SDL_SRCCOLORKEY, SDL_MapRGB(img->format, 0xff, 0x00, 0xff));
   animation_setframe(&anim, 2, img, 30);
   /* Allocation de initialisation des explosions et de leur position */
   allocate_explosions(DEFAULT_EXPLO);
   for (i = 0; i < nb_explo; i++) {
     animator_init(&player[i], &anim);
     animator_play(&player[i]);
     init_explosion(i);
   }
   /* Initialisation du temps avant la boucle principale */
   time_init();
   /* Ces variables nous permettront de calculer les FPS
      (frames per second, images par seconde) */
   Uint32 timer1 = SDL_GetTicks(), timer2;
   int frames_per_second = 0;
   /* Boucle de jeu */
   while (!letsexit) {
     int k;
     /* Mise à jour des entrées et du temps */
     input_update();
     time_update();
     /* Remplissage de l'écran en bleu */
     SDL_FillRect(screen, NULL, SDL_MapRGB(screen->format, 0x00, 0x00, 0xff));
     /* Mise à jour de l'état interne du jeu. cycles_to_calculate nous indique
        de combien de cycles avancer pour garder une vitesse cohérente */
     for (k = 0; k < cycles_to_calculate; k++) {
       /* Mise à jour des explosions */
       for (i = 0; i < nb_explo; i++) {
         animator_update(&player[i]);

         /* Si l'animation vient de boucler... */
         if (player[i].current_frame == 0 && player[i].counter == 0) {
           /* ... on lui change sa position aléatoirement */
           pos[i].x = rand() % (screen->w - img->w);
           pos[i].y = rand() % (screen->h - img->h);
         }
       }
     }
     /* Dessiner les explosions sur l'écran */
     for (i = 0; i < nb_explo; i++)
       animator_draw(&player[i], screen, &pos[i]);
     /* Afficher l'écran que l'on vient de dessiner */
     SDL_Flip(screen);
     /* Calculer les fps */
     frames_per_second++;
     timer2 = SDL_GetTicks();
     /* Une seconde s'est écoulée? Afficher le nombre d'écrans
        dessinés et réinitialiser */
     if (timer2 - timer1 > 1000) {
       printf("FPS: %d\n", frames_per_second);
       timer1 = timer2;
       frames_per_second = 0;
     }
   }
   /* Libération des ressources allouées */
   free(player);
   free(pos);
   animation_cleanup(&anim);
   SDL_Quit();
   return 0;
 }

Comme d'habitude, vous retrouverez le listing complet ainsi que les images utilisées se trouvent sur le CD-ROM, ou sur http://www.gnurou.org/documents/linuxmag/SDL/SDL-3.tar.gz. La compilation se fait de la manière suivante (ou mieux, en utilisant un Makefile):

 $ gcc `sdl-config --cflags --libs` animation.c time.c main.c -o test

Fig. 3: Le programme du mois
Fig. 3: Le programme du mois.

La prochaine fois

Nous voilà maintenant avec les bases nécessaires pour créer des animations plus évoluées. La prochaine fois, nous changerons un peu de registre en étudiant des effets graphiques calculés, comme l'effet de feu ou le champ d'étoiles. Ces effets sont étonnants à deux titres: leur simplicité d'implémentation et le résultat visuel. D'ici là, codez bien!


Tutoriel SDL gnurou

Ce mois-ci, nous allons nous plonger de manière plus approfondie dans la manipulation des surfaces de SDL en implémentant un effet de feu. Cet effet, l'un des plus vieux effets graphiques qui existent, est très simple à implémenter et la multitude de variantes qui existent autour de lui le rendent particulièrement intéressant pour le débutant en programmation graphique qui peut l'expérimenter avec de multiples paramètres.

Principe de l'effet

L'effet de feu est algorithmiquement très simple, pourtant il donne un résultat visuel très convainquant (voir figure 1). Mais s'il est facile à implémenter, il n'en reste pas moins qu'il coûte assez cher en temps de calcul, pour la simple raison que chaque pixel de la zone sur laquelle s'applique l'effet doit être recalculé en fonction de 4 de ses voisins. Cela implique un nombre très conséquent d'accès mémoire et le réservera en général à des zones assez petites.

Fig. 1
Fig. 1: L'exemple du mois.

L'algorithme calcule l'état de la nouvelle zone sur laquelle s'applique l'effet en fonction de son état précédent. L'effet de base parcourt la zone ligne par ligne, du haut vers le bas, et affecte à chaque pixel la moyenne des pixels inférieur gauche, inférieur, inférieur droit ainsi que du pixel situé deux lignes en dessous, comme l'illustre la figure 2. C'est tout!

Fig. 2: Principe de l'effet de feu. La nouvelle couleur du pixel gris, en haut, sera la moyenne des couleurs des 4 autres pixels.
Fig. 2: Principe de l'effet de feu. La nouvelle couleur du pixel gris, en haut, sera la moyenne des couleurs des 4 autres pixels.

Comme il n'y a pas beaucoup de sens à additionner directement des pixels, ce sont les composantes rouge, verte et bleue des quatre pixels adjacents qui seront moyennées pour obtenir les nouvelles valeurs des composantes du pixel en cours de calcul. Il faudra donc à chaque fois les décomposer.

Bien évidemment, puisque l'aspect du feu dépend de l'état précédent de la zone, il va falloir donner à cette dernière un état initial. En fait, comme pour de vraies flammes, il va nous falloir un « départ » du feu, ou un foyer. Celui-ci sera représenté par les deux lignes en dessous de notre effet de feu et aura une mise à jour totalement différente.

Le foyer, comme son nom l'indique, constitue le point de départ de nos flammes. L'état de ces deux lignes va totalement déterminer le contour des flammes et, sans elles, l'effet de feu ne ferait rien d'autre que calculer des pixels noirs. Le foyer ainsi que la manière dont il est mis à jour est donc tout aussi important que l'effet de feu en lui-même.

La manière de le gérer restera cependant elle aussi d'une simplicité enfantine: il suffira d'allumer et d'éteindre au hasard les pixels de ces deux lignes. Cela sera suffisant pour démarrer l'effet et conditionnera totalement l'aspect des flammes. Pour garder un ensemble homogène, les pixels allumés le seront avec une couleur dont les trois composantes sont uniformes (un gris plus ou moins clair). Bien évidemment, plus le gris du pixel sera clair, plus la flamme qui l'exploitera montera haut. De même, plus il y aura de pixels adjacents allumés, plus les flammes seront denses. On distinguera trois paramètres de l'évolution du foyer qui nous permettront d'expérimenter plusieurs variantes de flammes:

  • La vivacité : il s'agit du nombre de pixels qui seront allumés lors de la mise à jour du foyer.
  • L'intensité : pour chaque pixel allumé, il s'agit de la valeur minimale de ses composantes. Plus cette valeur est élevée, plus le pixel sera clair.
  • La résistance enfin, est l'inverse de la vivacité. Elle indique le nombre de pixels du foyer qui seront éteints lors de la mise à jour.

La figure 3 illustre un état du foyer. Sur l'exemple montré, les flammes monteront très haut, car beaucoup de pixels sont allumés avec une intensité suffisante.

Fig. 3: Un exemple de foyer de 22 pixels de long sur deux lignes. À chaque mise à jour, des pixels sont allumés ou éteints aléatoirement.
Fig. 3: Un exemple de foyer de 22 pixels de long sur deux lignes. À chaque mise à jour, des pixels sont allumés ou éteints aléatoirement.

Implémentation avec SDL

L'implantation de cet effet avec SDL nous donne l'occasion de découvrir les surfaces un peu plus en détail. Lors du premier épisode, nous avions écrit des fonctions permettant de lire ou d'écrire un pixel d'une surface, et nous avions utilisé les fonctions que fournit SDL pour obtenir la valeur d'un pixel pour un format de surface donné en fonction de ses trois composantes. Cette fois-ci, nous allons écrire nos propres macros nous permettant de faire ces opérations, ce qui sera autrement plus performant que d'appeler une fonction à chaque manipulation de pixel, en plus d'être instructif. En contrepartie, notre implantation aura quelques limites :

  • Tout d'abord, l'effet de feu s'appliquera uniquement aux surfaces 24 bits. Il est possible d'en faire une version pour d'autres formats de surface, mais cela introduit des problèmes supplémentaires, notamment si les composantes de couleur ne sont pas codées avec le même nombre de bits. En mode 16 bits par exemple, le fait que le vert n'évolue pas de la même manière que le rouge et le bleu finit par déséquilibrer le rapport entre les différentes composantes. Un format 24 bits a au moins le bon goût d'être équitable entre les composantes.
  • Ensuite, dans l'exemple du mois, nous utiliserons directement la surface d'écran pour réaliser l'effet. Dans la pratique, l'effet se fera plus probablement sur une autre surface, mais faire ainsi nous permettra de garder le code concis. Le fait que l'effet se déroule directement sur la surface d'écran nous interdit également l'utilisation du double-buffering, afin de pouvoir retrouver l'état précédent de nos flammes.
  • Enfin, nous n'utiliserons pas de gestion de temps. Le coût de notre exemple, qui applique l'effet sur un écran de 320x240, en terme d'occupation CPU est suffisamment important pour occuper même une machine puissante, et il sera de toute façon impossible de rattraper les éventuelles frames de retard que nous pourrions avoir, puisque la mise à jour de l'effet est directement liée à son affichage.

Les surfaces en profondeur

Jusqu'à présent, notre utilisation des surfaces se limitait à les passer en paramètres de fonctions définies par SDL et, parfois, à accéder directement aux pixels via le champ homonyme. Il est cependant tout aussi important, en particulier pour l'implantation d'effets graphiques, de connaître précisément le format de stockage des pixels. On peut facilement savoir si une surface utilise 16 ou 24 bits par pixels, mais cela ne veut pas dire grand chose (bien que dans 99% des cas on retrouve la même structure). Par exemple, en format 24 bits, quel est l'ordre dans lequel sont stockées les composantes? Nous sommes habitués au format RGB (Red, Green, Blue) mais rien n'empêche le matériel d'utiliser autre chose. C'est le membre format de SDL_Surface qui va nous renseigner sur les subtilités du format d'une surface. Il est du type SDL_PixelFormat et fournit notamment les champs suivants :

  • BitsPerPixel et BytesPerPixel que nous avons déjà rencontrés et qui fournissent respectivement le nombre de bits et de bytes par pixel,
  • Rmask, Gmask et Bmask sont les masques des composantes rouge, verte et bleue, respectivement. Étant donné un pixel « brut », dans le format concerné, le masquer avec l'un de ces membres nous permettra d'isoler une des composantes de couleur, comme le montre la figure 4 pour un format de 16 bits par pixel pour lequel les bits sont répartis de la sorte que les 5 bits de poids faibles soient alloués au bleu, les 6 bits suivants au vert et les 5 derniers au rouge.
  • Rshift, Gshift et Bshift fournissent le décalage à appliquer à la composante isolée par le membre *mask correspondant pour obtenir sa valeur réelle. Par exemple, avec le même format de couleur que sur la figure 4, on aura Bshift == 0, Gshift == 5 et Rshift == 11.

  • Enfin, Rloss, Gloss et Bloss nous indiquent la « perte » en bits d'une composante de couleur par rapport au format original où chaque composante est représentée par 8 bits. Ainsi, avec le même format 16 bits que précedemment, on aura Rloss == 3, Gloss == 2 et Bloss == 3. Pour un format 24 bits, ces trois membres vaudront en toute logique 0.

Fig. 4: Utilisation des membres Rmask et Rshift pour isoler la composante rouge d'un pixel.
Fig. 4: Utilisation des membres Rmask et Rshift pour isoler la composante rouge d'un pixel.

Ces informations nous permettront d'extraire les composantes des pixels et de construire des pixels à partir de ces composantes de manière plus efficace que si l'on appelait directement SDL_GetRGB et SDL_MapRGB.

Implémentation de l'effet

Comme vous l'aurez peut-être déjà deviné, l'effet de feu sera en fait constitué de deux effets : la montée des flammes et leur génération, c'est-à-dire le foyer. Chacun de ces effets sera géré par une fonction qui prendra en paramètres la surface sur laquelle appliquer l'effet ainsi qu'un SDL_Rect pour indiquer sa zone de délimitation. Nous allons implémenter nos effets dans deux fichiers fire.h et fire.c. Voici le contenu de fire.h, qui déclare nos deux fonctions:

#ifndef FIRE_H__
#define FIRE_H__

#include "SDL.h"

/**
 * Génère un foyer gris sur la surface donnée, dans les limites de
 * rect, en fonction des paramètres intensity, vivacity et résistance.
 * Attention: la surface doit être verrouillée!
 */
void fireRoot(SDL_Surface * surface, SDL_Rect * rect, Uint8 intensity, Uint8 vivacity,
        Uint8 resistance);

/**
 * Applique l'effet de feu sur la surface donnée, dans les limites de
 * rect.  Attention: la surface doit être verrouillée!
 */
void fire(SDL_Surface * surface, SDL_Rect * rect);

#endif

fire.c quant à lui commencera par les déclarations des macros nous permettant de manipuler les pixels. REDVALUE, GREENVALUE et BLUEVALUE isolent les composantes de couleurs d'un pixel comme décrit précédemment et donnent leur valeur comprise entre 0 et 255.

#include <stdlib.h>
#include <stdlib.h>
#include "fire.h"

/* Retourne les valeurs sur 8 bits des composantes rouges, vertes et
   bleues par rapport au format donné */
#define REDVALUE(format, pixel) (Uint8) (((pixel & format->Rmask) << format->Rloss) \
        >> format->Rshift)
#define GREENVALUE(format, pixel) (Uint8) (((pixel & format->Gmask) << format->Gloss) \
        >> format->Gshift)
#define BLUEVALUE(format, pixel) (Uint8) (((pixel & format->Bmask) << format->Bloss) \
        >> format->Bshift)

Les opérations inverses, obtenir le masque correspondant à une composante de couleur à partir de sa valeur sur 8 bits, sont également fournies, ainsi qu'une autre macro pour obtenir un pixel directement utilisable à partir des composantes:

/* Crée les masques correspondants pour les composantes de couleur
   données sur 8 bits */
#define REDMASK(format, red) (Uint32) (((red << format->Rshift) \
        >> format->Rloss) & format->Rmask)
#define GREENMASK(format, green) (Uint32) (((green << format->Gshift) \
        >> format->Gloss) & format->Gmask)
#define BLUEMASK(format, blue) (Uint32) (((blue << format->Bshift) \
        >> format->Bloss) & format->Bmask)

/* Retourne la valeur d'un pixel selon ses 3 composantes de couleur,
   données sur 8 bits */
#define MAKEPIXEL(format, red, green, blue) (Uint32) (REDMASK(format, red) \
        | GREENMASK(format, green) | BLUEMASK(format, blue))

Enfin, deux dernières macros s'occupent d'écrire et de lire correctement un pixel codé sur 24 bits à partir d'un pointeur donné en paramètre:

/* Lit et affecte un pixel à partir d'un pointeur */
#define READPIXEL(pointer, pixel) { pixel = *(Uint16 *)(pointer) + \
        (*(Uint8 *)((pointer) + 2) << 16); }
#define STOREPIXEL(pointer, pixel) { *(Uint16 *)(pointer) = (Uint16)(pixel); \
        *(Uint8 *)((pointer) + 2) = (Uint8)((pixel) >> 16) & 0xff; }

Ces macros étant définies, nous pouvons attaquer les effets proprement dits. Le foyer ne posera pas de problèmes particuliers. Il s'agira juste, dans un premier temps, d'allumer des pixels à des positions et des intensités aléatoires en fonction des paramètres vivacity et intensity, puis, dans un deuxième temps, d'en éteindre d'autres en fonction du paramètre resistance.

/* Effet de foyer */
void fireRoot(SDL_Surface * surface, SDL_Rect * rect, Uint8 intensity, Uint8 vivacity, \
        Uint8 resistance) {
  Uint32 i, j;

  /* Pour chaque ligne du foyer, la vivacité détermine combien de
     pixels mettre à jour */
  for (j = rect->y; j < rect->y + rect->h; j++)
    for (i = 0; i < vivacity; i++) {
      /* Déterminer la valeur de chaque composante en fonction de
         l'intensité - elles auront les mêmes valeurs, ce qui nous
         fournira un gris */
      Uint8 color = (intensity + (rand() % (256 - intensity)));
      /* Choisir une position au hasard */
      Uint32 position = (rand() % rect->w) + rect->x;

      /* Et y affecter le pixel */
      STOREPIXEL(surface->pixels + j * surface->pitch + position *
                 surface->format->BytesPerPixel,
                 MAKEPIXEL(surface->format, color, color, color));
    }

  /* Même chose avec la résistance - on éteint aléatoirement des
     pixels */
  for (j = rect->y; j < rect->y + rect->h; j++)
    for (i = 0; i < resistance; i++) {
      Uint32 position = (rand() % rect->w) + rect->x;

      STOREPIXEL(surface->pixels + j * surface->pitch + (position *
              surface->format->BytesPerPixel), 0);
    }
}

Enfin, l'effet de feu. Il sera implémenté fidèlement à notre description. Chaque pixel de la zone parcourue est re-calculé en faisant la moyenne de ses quatre voisins.

/* Effet de feu */
void fire(SDL_Surface * surface, SDL_Rect * rect) {
  Uint32 i, j;

  /* Chaque pixel de la zone doit être mis à jour */
  for (j = rect->y; j < rect->y + rect->h; j++)
    for (i = rect->x; i < rect->x + rect->w; i++) {
      /* Pointeur vers le pixel courant */
      void * curpixel;
      /* Nouvelle valeur du pixel courant */
      Uint32 pixvalue = 0;
      /* Addition des valeurs des composantes rouges, vertes et bleues
        des pixels adjacents au pixel courant */
      Uint16 red = 0, green = 0, blue = 0;
      /* Le pixel à mettre à jour */
      curpixel = surface->pixels + (surface->pitch * j) +
              (i * surface->format->BytesPerPixel);
      /* Addition des composantes des 4 pixels adjacents */
      /* Inférieur gauche */
      if (i > 0) {
        READPIXEL(curpixel + surface->pitch - surface->format->BytesPerPixel,
                pixvalue);
        red += REDVALUE(surface->format, pixvalue);
        green += GREENVALUE(surface->format, pixvalue);
        blue += BLUEVALUE(surface->format, pixvalue);
      }

      /* Inférieur */
      READPIXEL(curpixel + surface->pitch, pixvalue);
      red += REDVALUE(surface->format, pixvalue);
      green += GREENVALUE(surface->format, pixvalue);
      blue += BLUEVALUE(surface->format, pixvalue);

      /* Deux lignes en dessous */
      READPIXEL(curpixel + surface->pitch * 2, pixvalue);
      red += REDVALUE(surface->format, pixvalue);
      green += GREENVALUE(surface->format, pixvalue);
      blue += BLUEVALUE(surface->format, pixvalue);

      /* Inférieur droit */
      if (i < surface->w - 1) {
        READPIXEL(curpixel + surface->pitch + surface->format->BytesPerPixel,
                pixvalue);
        red += REDVALUE(surface->format, pixvalue);
        green += GREENVALUE(surface->format, pixvalue);
        blue += BLUEVALUE(surface->format, pixvalue);
      }

      /* Altération des composantes afin d'obtenir la teinte de la
        flamme */
      if (red > 2) red -= 2; else red = 0;
      if (green > 5) green -= 5; else green = 0;
      if (blue > 150) blue -= 150; else blue = 0;

      /* Division par quatre de l'addition des composantes */
      red /= 4; green /= 4; blue /= 4;

      /* Affectation de la nouvelle valeur du pixel */
      STOREPIXEL(curpixel, MAKEPIXEL(surface->format, red, green, blue));
    }
}

Vous aurez remarqué que, avant la division de la somme des quatre pixels adjacents, il y a une phase d'altération. Le bleu est sévèrement reduit, le vert un peu moins, et le rouge encore moins. C'est ce qui donnera la teinte de couleur à nos flammes et les fera tourner très vite au jaune, puis au rouge en fin de course. Sans cette étape, les flammes resteront grises et l'effet ressemblera alors plus à de la fumée. Vous pouvez expérimenter en supprimant ou modifiant cette étape.

Enfin, comme de coutume, voici le programme principal utilisant le module que nous avons implémenté. Il offre plusieurs voies d'interaction: le paramètre d'intensité de foyer peut être modifié à l'aide des touches haut et bas, la résistance avec les touches gauche et droite et la vivacité avec pageup/pagedown. La touche entrée fera s'afficher sur l'écran l'image du logo SDL qui subira automatiquement l'effet de feu et se « vaporisera » en flammes. Le programme en lui-même n'a rien de compliqué et ne fait que reprendre des éléments déjà vus précedemment. Voici son listing, à mettre dans main.c:

#include <stdio.h>
#include <stdlib.h>
#include "SDL.h"

#include "fire.h"

/* Variables d'intensités, de vivacité et de résistance */
#define DEFAULT_INTENSITY 150
#define DEFAULT_VIVACITY 128
#define DEFAULT_RESISTANCE 75

static Uint8 intensity = DEFAULT_INTENSITY;
static Uint8 vivacity = DEFAULT_VIVACITY;
static Uint8 resistance = DEFAULT_RESISTANCE;

static Sint8 intensity_delta = 0;
static Sint8 vivacity_delta = 0;
static Sint8 resistance_delta = 0;

/* Mise à 1 s'il faut quitter */
Uint8 letsexit = 0;

SDL_Surface * screen, * image;

/* Gestion des entrées clavier */
void input_update() {
  SDL_Event event;
  while (SDL_PollEvent(&event)) {
    switch (event.type) {
    case SDL_KEYDOWN:
      switch (event.key.keysym.sym) {
        /* Quitter */
      case SDLK_ESCAPE:
        letsexit = 1;
        break;
        /* Enflammement du logo SDL - il suffit de le dessiner
         et de laisser l'effet de feu faire le reste */
      case SDLK_RETURN:
        {
          SDL_Rect blitRect = { 0, 0, 0, 0 };
          blitRect.x = (rand() % (screen->w - image->w));
          blitRect.y = (rand() % (screen->h - image->h));

          SDL_BlitSurface(image, NULL, screen, &blitRect);
          break;
        }
        /* Augmenter intensité */
      case SDLK_UP:
        intensity_delta = 1;
        break;
        /* Diminuer intensité */
      case SDLK_DOWN:
        intensity_delta = -1;
        break;
        /* Augmenter résistance */
      case SDLK_RIGHT:
        resistance_delta = 1;
        break;
        /* Diminuer résistance */
      case SDLK_LEFT:
        resistance_delta = -1;
        break;
        /* Augmenter vivacité */
      case SDLK_PAGEUP:
        vivacity_delta = 1;
        break;
        /* Diminuer vivacité */
      case SDLK_PAGEDOWN:
        vivacity_delta = -1;
        break;
      default:
        break;
      }
      break;
      /* Remise à zéro du delta des variables de paramètre du foyer si
       une touche leur correspondant est relâchée */
    case SDL_KEYUP:
      switch (event.key.keysym.sym) {
      case SDLK_UP:
      case SDLK_DOWN:
        intensity_delta = 0;
        break;
      case SDLK_RIGHT:
      case SDLK_LEFT:
        resistance_delta = 0;
        break;
      case SDLK_PAGEUP:
      case SDLK_PAGEDOWN:
        vivacity_delta = 0;
        break;
      default:
        break;
      }
      break;
    default:
      break;
    }
  }
}

int main(int argc, char * argv[]) {
  /* Rectangles dans lesquels vont s'appliquer le foyer et l'effet de
     feu */
  SDL_Rect rootRect, fireRect;
  /* Surface temporaire pour charger l'image */
  SDL_Surface * tmp;

  if (SDL_Init(SDL_INIT_VIDEO) == -1) {
    printf("Erreur lors de l'initialisation de SDL!\n");
    return 1;
  }

  screen = SDL_SetVideoMode(320, 240, 24,
                            SDL_SWSURFACE);

  /* Le foyer est appliqué aux deux dernières lignes */
  rootRect.x = 0;
  rootRect.y = screen->h - 2;
  rootRect.w = screen->w;
  rootRect.h = 2;

  /* L'effet de feu s'applique sur tout l'écran, à l'exception
     des deux dernières */
  fireRect.x = 0;
  fireRect.y = 0;
  fireRect.w = screen->w;
  fireRect.h = screen->h - 2;

  /* Création de l'image du logo SDL */
  tmp = SDL_LoadBMP("image.bmp");
  image = SDL_DisplayFormat(tmp);
  SDL_FreeSurface(tmp);
  SDL_SetColorKey(image, SDL_SRCCOLORKEY, SDL_MapRGB(image->format, 0xff, 0x00, 0xff));

  /* Boucle principale */
  while (!letsexit) {
    /* Mise à jour des entrées */
    input_update();

    /* Mise à jour des paramètre du foyer */
    if ((intensity + intensity_delta) < 0) intensity = 0;
    else if ((intensity + intensity_delta) > 255) intensity = 255;
    else intensity += intensity_delta;
    if (intensity_delta != 0) printf("Intensité: %d\n", intensity);

    if ((vivacity + vivacity_delta) < 0) vivacity = 0;
    else if ((vivacity + vivacity_delta) > 255) vivacity = 255;
    else vivacity += vivacity_delta;
    if (vivacity_delta != 0) printf("Vivacité: %d\n", vivacity);

    if ((resistance + resistance_delta) < 0) resistance = 0;
    else if ((resistance + resistance_delta) > 255) resistance = 255;
    else resistance += resistance_delta;
    if (resistance_delta != 0) printf("Résistance: %d\n", resistance);

    /* Ne pas oublier de verrouiller la surface que l'on va directement
       modifier! */
    SDL_LockSurface(screen);
    fireRoot(screen, &rootRect, intensity, vivacity, resistance);
    fire(screen, &fireRect);
    SDL_UnlockSurface(screen);

    /* Mise à jour de l'écran */
    SDL_Flip(screen);
  }

  SDL_Quit();
  return 0;
}

Le Makefile suivant permet de compiler tout cela vers l'exécutable fire:

CFLAGS = `sdl-config --cflags` -Wall -ansi -g -O3
LIBS = `sdl-config --libs`

OBJECTS = main.o fire.o

all: $(OBJECTS)
        $(CC) $(OBJECTS) -o fire $(LIBS)

clean:
        rm -f *.o fire

Comme d'habitude, toutes les sources se trouvent sur le CD qui accompagnent le magazine, ou à l'adresse http://www.gnurou.org/documents/linuxmag/SDL/SDL-4.tar.gz.

Variantes

Il existe une multitude de variantes à l'effet de feu. Vous pouvez jouer avec la variation de teinte des flammes, les paramètres du foyer, mais aussi avec le nombre de pixels adjacents pris en compte (3, 6, ...). En partant de la logique qui consiste à calculer le nouvel état de chaque pixel en fonction de l'état précédent de ses voisins, il y a beaucoup d'autres effets à découvrir. N'hésitez pas à expérimenter!


Tutoriel SDL gnurou

Durant les prochains mois, nous allons découvrir une facette de SDL encore inconnue pour nous: la gestion de l'audio. L'article de ce mois-ci posera les bases qui nous permettront d'aller plus loin. Commençons donc par comprendre comment nos machines produisent de si jolis sons...

Théorie sur l'audio numérique

L'élément de base du son numérique est l'échantillon. Il correspond à une prise de mesure de la pression sur l'air générée par la source sonore à un instant donné. Cette dépression ou surpression sera restituée lors de la lecture par une vibration de la membrane des enceintes. L'échantillon est au son ce que le pixel est à l'image: comme pour un pixel, un échantillon possède un format, et comme pour un pixel celui-ci conditionne le nombre de nuances qu'il est possible d'obtenir. SDL gère les formats sonores 8 et 16 bits, les formats supérieurs étant réservés aux cartes sonores haut de gamme. Pour plus d'informations sur le son numérique: http://www.commentcamarche.net/audio/son.php3

Tout comme une image est le résultat de l'assemblage de nombreux pixels, un son se compose d'un certain nombre d'échantillons qui sont joués à une certaine fréquence. Cette fréquence s'exprime en hertz, une fréquence de 11025 Hz correspondant à 11025 échantillons joués en une seconde. La fréquence sonore est en quelque sorte l'équivalent de la résolution d'une image: plus une image comporte de pixels, plus elle est nette. De la même manière, une fréquence sonore importante garantit la pureté du son. Dans les deux cas, une résolution ou une fréquence plus élevée est également synonyme de consommation mémoire plus importante pour le stockage des médias.

Un autre paramètre est à prendre en compte: le nombre de canaux, autrement dit le nombre d'échantillons pris en même temps à partir de sources différentes. Un son mono ne dispose que d'un canal, le son stéréo offre un canal par oreille, le son 5.1 6 canaux. En imagerie, l'équivalent du son sur deux canaux correspondrait aux lunettes se branchant sur la sortie moniteur d'une carte vidéo. Ces périphériques d'affichage coûteux, aujourd'hui peu utilisées en dehors de la recherche en imagerie, offrent une image différente à chaque oeil pour donner une impression de relief.

À titre d'information, un CD audio a une fréquence d'échantillonage de 44100 Hz, un format d'échantillonnage de 16 bits et deux canaux. Le terme « qualité radio » s'applique quant à lui souvent à une fréquence de 22050 Hz.

Vous le voyez donc, nous allons vite retrouver les marques que nous avons acquises lorsque nous avons appris à manipuler des images.

Jouer un son avec SDL

Ce mois-ci, nous allons simplement étudier l'architecture sonore de SDL et la tester par un programme très basique. Comme pour la vidéo, SDL propose des fonctions de très bas niveau, et il nous faudra les maîtriser avant d'envisager de réaliser des programmes vraiment utiles...

L'utilisation des fonctions audio de SDL passe, comme à l'habitude, par le passage du flag correspondant (ici, SDL_INIT_AUDIO) à la fonction SDL_Init. Puis nous allons définir les paramètres du périphérique audio avec la fonction SDL_OpenAudio:

 int SDL_OpenAudio(SDL_AudioSpec *desired, SDL_AudioSpec *obtained);

Elle prend en paramètre deux pointeurs: l'un pointant vers la description du format sonore désiré, le deuxième étant utilisé pour renseigner le programmeur sur le format sonore effectivement obtenu, pour le cas où le matériel ne supporterait pas le format demandé. Si ce pointeur vaut NULL, et que le format demandé n'est pas supporté par le matériel, SDL met alors en place un mécanisme d'émulation. Le format visible pour le programmeur sera bien le format qu'il a demandé, mais SDL fera une conversion transparente vers le format effectivement supporté avant de l'envoyer à la carte son. La valeur de retour sera 0 en cas de succès, ou -1 en cas d'échec.

La structure SDL_AudioSpec comporte les champs nécessaires à la description du format sonore tel que nous l'avons décrit plus haut:

  • format décrit le format des échantillons. Les valeurs possibles prennent en compte non seulement le nombre de bits, mais aussi leur ordre et leur signe. Les valeurs les plus courantes sont AUDIO_S8 et AUDIO_U8 pour des échantillons de 8 bits signés ou non-signés, et AUDIO_S16SYS et AUDIO_U16SYS pour des échantillons de 16 bits dont l'ordre des bits correspond au système sur lequel le programme tourne. D'autres valeurs sont disponibles, vous pouvez les retrouver dans la documentation.
  • freq désigne la fréquence, en herts, du son. Les valeurs usuelles sont 11025, 22050 ou 44100, en fonction de la qualité de son désirée.
  • channels renseigne sur le nombre de canaux, et vaut 1 pour du mono, 2 pour de la stéréo.

D'autres champs, non moins utiles, sont disponibles:

  • samples indique la taille du tampon audio, en nombre d'échantillons. Nous y reviendrons tout de suite.
  • callback est un pointeur vers la fonction de rappel utilisée pour copier les données sonores dans le tampon audio.
  • userdata, enfin, peut pointer vers des données qui seront passées en paramètre à la fonction de rappel.

Les trois derniers champs méritent quelques explications supplémentaires. Contrairement à l'affichage, pour lequel nous avions un contrôle total sur la fréquence des images affichées (grâce à SDL_Flip, notamment), l'audio requiert de la régularité dans l'envoi des données sonores au matériel. De plus, la fréquence du média sonore est bien plus élevée que pour le média visuel (jusqu'à 44100 Hz pour l'audio, en général 70 Hz pour l'affichage). Il est donc difficilement envisageable d'envoyer manuellement chaque échantillon vers le matériel. À la place, nous allons remplir un tampon audio dans lequel le matériel ira continuellement lire les données sonores. Il est par conséquent très important de remplir ce tampon de manière régulière: s'il n'est plus rempli, le son va « boucler » jusqu'à ce que de nouvelles données y soient placées. Vous avez probablement déjà remarqué ce comportement dans certains jeux, notamment lors de plantages.

La régularité du remplissage du tampon est assurée par la fonction de rappel. Celle-ci est appelée automatiquement par SDL, via un thread dédié, lorsque cela devient nécessaire. Elle reçoit en paramètres le pointeur userdata, un pointeur vers le buffer audio, et le nombre d'octets à y placer. Son rôle, bien souvent, est de mixer les différentes source sonores et de copier le résultat dans le tampon audio.

La fréquence d'appel de la fonction de rappel est directement liée à la taille du tampon audio. Il y a un compromis à trouver ici: plus le tampon audio est petit, plus la fonction de rappel devra être appelée souvent, et plus il y aura de chances de remplir le tampon audio en retard et donc d'obtenir un son haché. Au contraire, un grand tampon garantit un son mixé en temps et en heure – par contre, la latence sera plus importante. Une trop grande latence se traduira par un décalage perceptible entre une action faite par l'utilisateur (par exemple, l'appui d'une touche au clavier) et l'effet sonore qui lui est associé. Cet effet, très désagréable, est intolérable dans un jeu.

Fort heureusement, des tailles de tampon faibles restent exploitables en pratique, et il est ainsi très rare de rater une échéance. Nous pourrons utiliser un taille de tampon de 512 échantillons sans problèmes.

La taille réelle du tampon sonore sera égale au champ samples multiplié par la taille d'un échantillon, fois le nombre de canaux.

Ces précisions devraient nous permettre de remplir correctement la structure SDL_AudioSpec passée à SDL_OpenAudio. Une fois le périphérique audio ouvert, la fonction de rappel n'est pas immédiatement appelée. Le son est mis en « pause » par défaut pour permettre au programmeur d'initialiser ses propres structures sonores. Un appel à SDL_PauseAudio avec un paramètre de 0 suffira pour commencer le mixage sonore.

Le chargement d'un son, via un fichier .wav, est assez similaire à l'ouverture d'une image .bmp. Dans un premier temps, le son est chargé par la fonction SDL_LoadWAV:

 SDL_AudioSpec *SDL_LoadWAV(const char *file, SDL_AudioSpec *spec,
                                                    Uint8 **audio_buf, Uint32 *audio_len);

Le premier paramètre contient le nom du fichier .wav à ouvrir. Le second est un pointeur vers un SDL_AudioSpec qui sera rempli avec le format du fichier chargé. audio_buf est un pointeur qui sera alloué par SDL_LoadWAV et rempli avec les données audio. Enfin, l'entier indiqué par le pointeur audio_len contiendra, à l'issue de l'appel, la taille de audio_buf en octets.

Si le fichier a pu être chargé, SDL_LoadWAV retourne le même pointeur que spec, ou NULL si le chargement a échoué. Les sons ainsi chargés peuvent être libérés par SDL_FreeWAV.

Le format des sons chargés ne sera vraisemblablement pas le même que celui du périphérique audio. Comme pour une image, il faudra passer par une phase de conversion avant d'envoyer nos données au matériel. C'est le rôle de la fonction SDL_ConvertAudio:

 int SDL_ConvertAudio(SDL_AudioCVT *cvt);

La conversion sonore demandant un grand nombre de paramètres, SDL passe par une structure dédiée pour cette opération. La structure SDL_AudioCVT doit être préalablement remplie par un appel à SDL_BuildAudioCvt:

 int SDL_BuildAudioCVT(SDL_AudioCVT *cvt, Uint16 src_format, Uint8 src_channels, int src_rate,
                                     Uint16 dst_format, Uint8 dst_channels, int dst_rate);

Les paramètres à passer sont la structure à initialiser, suivie des formats, nombre de canaux et fréquences de la source et de la destination. Une fois cette opération réalisée, il reste à copier les données audio à convertir dans le champ buf de SDL_AudioCVT, qu'il faudra au préalable allouer nous-mêmes. La taille à allouer sera égale à la taille du tampon source en octets, multiplié par le champ len_mult de la structure SDL_AudioCVT remplie. Ce facteur multiplicatif permet d'assurer suffisamment de place à SDL pour effectuer la conversion.

Une fois toutes ces étapes accomplies, l'appel à SDL_ConvertAudio transformera les données contenues dans buf du format source vers le format destination. La longueur du nouveau buffer sera égale au champ len_cvt de SDL_AudioCVT.

À présent que notre son est dans le bon format, comment le jouer? Tout simplement en attendant que notre fonction de rappel soit appelée, et en y copiant les données dans le tampon audio. Si notre son est plus long que le tampon audio (ce qui sera vraisemblablement le cas), il faudra procéder en plusieurs fois, chaque appel de la fonction de rappel correspondant à la copie d'un bout du son de la même taille que le tampon audio.

Le programme du mois

Le programme du mois illustre tous ces principes, en jouant un fichier sonore .wav avant de quitter:

 #include <stdio.h>
 #include <stdlib.h>
 #include <unistd.h>
 #include <string.h>
 #include "SDL.h"

 /* Les données du fichier son chargé */
 Uint8 * sounddata;
 /* La taille du fichier son chargé, en octets */
 Uint32 soundlength;
 /* Position courante de lecture dans le fichier son */
 Uint32 soundpos;

 /* Fonction de rappel qui copie les données sonores dans le tampon audio */
 void mixaudio(void * userdata, Uint8 * stream, int len)
 {
   /* Attention à ne pas déborder lors de la copie */
   Uint32 tocopy = soundlength - soundpos > len ? len : soundlength - soundpos;

   /* Copie des données sonores dans le tampon audio... */
   memcpy(stream, sounddata + soundpos, tocopy);

   /* Mise à jour de la position de lecture */
   soundpos += tocopy;
 }

 int main(int argc, char * argv[])
 {
   SDL_AudioSpec desired, obtained, soundfile;
   SDL_AudioCVT cvt;

   if (SDL_Init(SDL_INIT_AUDIO) == -1) {
     printf("Erreur lors de l'initialisation de SDL!\n");
     return 1;
   }

   /* Son 16 bits stéréo à 44100 Hz */
   desired.freq = 44100;
   desired.format = AUDIO_U16SYS;
   desired.channels = 2;

   /* Le tampon audio contiendra 512 échantillons */
   desired.samples = 512;

   /* Mise en place de la fonction de rappel et des données utilisateur */
   desired.callback = &mixaudio;
   desired.userdata = NULL;

   if (SDL_OpenAudio(&desired, &obtained) != 0) {
     printf("Erreur lors de l'ouverture du périphérique audio: %s\n", SDL_GetError());
     return 1;
   }

   printf("Paramètres audio obtenus: d canaux, fréquence d échantillons.\n",
      obtained.format & 0xff, obtained.channels, obtained.freq, obtained.samples);

   /* Chargement du fichier .wav */
   if (SDL_LoadWAV("KDE_Startup_new.wav", &soundfile, &sounddata, &soundlength) == NULL) {
     printf("Erreur lors du chargement du fichier son: %s\n", SDL_GetError());
     return 1;
   }

   printf("Propriétés du fichier audio: d canaux, fréquence d octets.\n",
      soundfile.format & 0xff, soundfile.channels, soundfile.freq, soundlength);

   /* Conversion vers le format du tampon audio */
   if (SDL_BuildAudioCVT(&cvt, soundfile.format, soundfile.channels, soundfile.freq,
             obtained.format, obtained.channels, obtained.freq) < 0) {
     printf("Impossible de construire le convertisseur audio!\n");
     return 1;
   }

   /* Création du tampon utilisé pour la conversion */
   cvt.buf = malloc(soundlength * cvt.len_mult);
   cvt.len = soundlength;
   memcpy(cvt.buf, sounddata, soundlength);

   /* Conversion... */
   if (SDL_ConvertAudio(&cvt) != 0) {
     printf("Erreur lors de la conversion du fichier audio: %s\n", SDL_GetError());
     return 1;
   }

   /* Libération de l'ancien tampon, création du nouveau,
      copie des données converties, effacement du tampon de conversion */
   SDL_FreeWAV(sounddata);
   sounddata = malloc(cvt.len_cvt);
   memcpy(sounddata, cvt.buf, cvt.len_cvt);
   free(cvt.buf);

   soundlength = cvt.len_cvt;
   printf("Taille du son converti: %d octets\n", soundlength);
   soundpos = 0;

   /* La fonction de rappel commence à être appelée à partir de maintenant. */
   printf("Démarrage de la lecture...\n");
   SDL_PauseAudio(0);

   /* On attend que l'autre thread ait fini la lecture du son... */
   while (soundpos < soundlength);

   /* On cesse d'appeler la fonction de rappel */
   SDL_PauseAudio(1);

   /* Fermer le périphérique audio */
   SDL_CloseAudio();

   SDL_Quit();
   return 0;
 }

Comme d'habitude, vous pourrez trouver de programme sur le CD du magazine ou sur http://www.gnurou.org/documents/linuxmag/SDL/SDL-5.tar.gz.

Attention: pour vos tests, il est recommandé de désactiver les démons sonores tels que arts ou esd. SDL sait les utiliser, et ainsi mixer ses sa sortie sonore avec d'autres programmes, mais les tailles de tampon utilisées sont en général très grandes augmentent ainsi grandement la latence, faussant le résultat.

Vous pouvez essayer de changer les valeurs de la structure desired et observer les effets qu'ont la fréquence, la nombre de canaux et le format sur la qualité du son joué et sur la taille des échantillons sonores. Vous ne pourrez pas vous permettre n'importe quelle valeur, notamment pour la fréquence ; mais les classiques 44100, 22050 et 11025 Hz devraient fonctionner correctement.

Ce qui nous attend par la suite: mixage de différentes sources sonores, moteur de son plus sophistiqué et surtout plus simple à utiliser, et effets sur les sons. D'ici là, joyeux code!


Tutoriel SDL gnurou

La dernière fois, nous avions évalué les fonctions audio mises à notre disposition par SDL. Force est de constater, qu'en s'y limitant strictement on ne parvient pas à des résultats très sophistiqués. Nous allons cette fois-ci construire un véritable moteur sonore à partir des fonctions de base offertes par SDL.

Il ne fallait de toute façon pas s'attendre à des miracles – tout comme pour le graphisme et la gestion des évènements, SDL nous démontre qu'elle est une bibliothèque de bas niveau. C'est au programmeur d'en faire ce qu'il veut – au moins n'est-il pas enfermé dans un carcan prédéfini par les développeurs de la bibliothèque. En ce qui nous concerne, réaliser nous même notre moteur sonore a un intérêt avant tout didactique.

Un moteur de son plus évolué

Notre dernier programme d'exemple se contentait de charger un son, de le convertir au format audio du périphérique, et de le lui envoyer progressivement au travers d'une fonction de rappel appelée par SDL lorsque nécessaire. Nous avions retenu trois paramètres (qui se sont traduits par trois variables globales) pour décrire le son :

 /* Les données du fichier son chargé */
 Uint8 * sounddata;
 /* La taille du fichier son chargé, en octets */
 Uint32 soundlength;
 /* Position courante de lecture dans le fichier son */
 Uint32 soundpos;

Si nous faisons une analogie avec l'article sur les animations, nous pouvons dire que les variables sounddata et soundlength correspondent aux données du son, tandis que soundpos est un paramètre de lecture. Nous pouvons donc en déduire deux structures comparables à celles que nous avions mises en place pour gérer les animations. L'une, sound, contiendra les données sonores ainsi que la longueur du son. L'autre, soundplayer, pointera vers un son joué et contiendra la position courante du lecteur dans le son. Voilà pour les structures de base, nous pourrons leur associer les fonctions permettant de charger un son déjà codées le mois dernier. Les déclarations se feront dans audio.h :

 #ifndef _AUDIO_H__
 #define _AUDIO_H__

 #include "SDL.h"

 /* Contient un son chargé */
 typedef struct
 {
   /* Données sonores */
   Uint8 * data;
   /* Longueur du son */
   Uint32 length;
 } sound;

 /* Chargement d'un son à partir d'un fichier .wav. Le son est converti au format
    donné par la variable hw_spec, qui doit être le format utilisé par le matériel.
    Le son chargé est retourné dans loaded_sound. La fonction retourne 0 en cas de
    succès, une valeur négative en cas d'erreur. */
 int sound_load(sound * loaded_sound, char * filename, SDL_AudioSpec * hw_spec);
 /* Libère un son précédemment chargé par sound_load */
 void sound_free(sound * loaded_sound);

 /* Informations sur la lecture d'un son */
 typedef struct
 {
   /* Le son en train d'être joué */
   sound * played_sound;
   /* Position du lecteur dans le son */
   Uint32 pos;
 } soundplayer;

 /* Affecte un son à jouer à un soundplayer */
 void soundplayer_play(soundplayer * player, sound * toplay);
 /* Cesse la lecture du son */
 void soundplayer_stop(soundplayer * player);
 /* Mise à jour de l'état du player en fonction de sa position */
 void soundplayer_update(soundplayer * player);
 /* Retourne non-zéro si le lecteur est en train de jouer un son */
 Uint8 soundplayer_isplaying(soundplayer * player);

 #endif

Et les définitions dans audio.c :

#include <stdlib.h>
#include <string.h>
#include "audio.h"
#include "sound_effects.h"
int sound_load(sound * loaded_sound, char * filename, SDL_AudioSpec * hw_spec) {
   SDL_AudioSpec filespec;
   SDL_AudioCVT cvt;

   Uint8 * data;
   Uint32 length;

   /* Chargement du fichier .wav */
   if (SDL_LoadWAV(filename, &filespec, &data, &length) == NULL) {
     fprintf(stderr, "Erreur lors du chargement de s\n", filename, SDL_GetError());
     return -1;
   }

   /* Conversion vers le format supporté par le matériel */
   if (SDL_BuildAudioCVT(&cvt, filespec.format, filespec.channels, filespec.freq,
             hw_spec->format, hw_spec->channels, hw_spec->freq) < 0) {
     fprintf(stderr, "Impossible de construire le convertisseur audio!\n");
     SDL_FreeWAV(data);
     return -1;
   }

   /* Création du tampon utilisé pour la conversion */
   cvt.buf = malloc(length * cvt.len_mult);
   cvt.len = length;
   memcpy(cvt.buf, data, length);

   /* Conversion... */
   if (SDL_ConvertAudio(&cvt) != 0) {
     fprintf(stderr, "Erreur lors de la conversion du fichier audio: %s\n", SDL_GetError());
     SDL_FreeWAV(data);
     return -1;
   }

   /* Libération de l'ancien tampon, création du nouveau,
      copie des données converties, effacement du tampon de conversion */
   SDL_FreeWAV(data);
   loaded_sound->data = malloc(cvt.len_cvt);
   memcpy(loaded_sound->data, cvt.buf, cvt.len_cvt);
   free(cvt.buf);

   loaded_sound->length = cvt.len_cvt;
   return 0;
 }

 void sound_free(sound * loaded_sound) {
   free(loaded_sound->data);
   loaded_sound->data = NULL;
   loaded_sound->length = 0;
 }

 void soundplayer_play(soundplayer * player, sound * toplay) {
   player->played_sound = toplay;
   player->pos = 0;
 }

 void soundplayer_stop(soundplayer * player) {
   player->played_sound = NULL;
   player->pos = 0;
 }

 void soundplayer_update(soundplayer * player) {
   if (player->pos >= player->played_sound->length + (echo_delay * echo_repeat))
     soundplayer_stop(player);
 }

 Uint8 soundplayer_isplaying(soundplayer * player) {
   return (player->played_sound != NULL);
 }

Vous remarquerez que nous faisons référence au fichier sound_effects.h ainsi qu'à deux de ses variables, echo_delay et echo_repeat. Nous les décrirons un peu plus loin – il faut juste savoir pour le moment que ces variables nous permettent de prendre en compte de temps de lecture ajouté par un éventuel effet d'écho pour savoir si le lecteur doit s'arrêter de jouer ou pas.

Somme toute, cette partie du programme ressemblait à un étrange mélange entre l'exemple du mois dernier et notre implémentation des animations. C'est dans le moteur sonore en lui-même que se trouvera l'innovation. Nous voulons être capable de jouer un son à l'aide d'un soundplayer arbitrairement, à n'importe quel moment. Si un autre son est déjà en train d'être joué, l'on doit pouvoir entendre les deux en même temps. Mais le matériel n'étant capable de jouer qu'un son simultanément (du moins au niveau de la vue que SDL nous en donne), il faudra mixer les deux sons afin qu'ils ne fassent plus qu'un, qui sera envoyé au matériel. Et tant que nous y sommes, il serait utile de pouvoir contrôler le volume sonore global, ainsi que d'appliquer des effets aux sons en train d'être joués. Nous allons étudier ces différentes fonctions dans les sections qui suivent au travers d'un programme d'exemple réagissant à l'appui de touches du clavier par l'émission d'un son préalablement chargé. Il supportera également les variations de volume ainsi que l'application d'effets en temps réel. Sa limitation: il ne sera capable de travailler que dans un format sonore bien défini, 16 bits signé stéréo à 44100Hz.

Commençons donc par étudier quelques effets de base utiles pour notre programme : le mixage, le changement de volume et l'écho.

Le mixage

Cette fonction est primordiale pour n'importe quelle application multimédia un tant soit peu évoluée (jeux bien évidemment, mais aussi logiciels de composition musicale, lecteurs audio permettant le cross-fading, etc.). Le principe est extrêmement simple: il suffit d'additioner, un à un, chacun des échantillons des sons à mixer pour obtenir le son correspondant à leur lecture simultanée. Il faudra cependant faire attention aux débordements de capacité: en effet, l'addition des deux échantillons peut dépasser la capacité de nos entiers sur 16 bits. Pour garder un résultat correct, l'opération se fera sur 32 bits et sera ramenée aux bornes codables sur 16 bits signés si le résultat venait à les dépasser. Cette erreur de précision, également appelée saturation, est illustrée par les figures 1 et 2 ; elle sera malheureusement audible et se traduira par un craquement si elle venait à se répéter trop fréquemment. Pour l'éviter, une précaution (que nous ne prendrons pas pour notre exemple): s'assurer que le volume des sons à mixer n'est pas trop élevé.

Fig. 1: Représentation graphique d'un son non-saturé (capture d'écran d'Audacity)
Fig. 1: Représentation graphique d'un son non-saturé (capture d'écran d'Audacity).

Fig. 2: Le même son horriblement saturé après une augmentation de volume trop importante
Fig. 2: Le même son horriblement saturé après une augmentation de volume trop importante.

Notre fichier sound_effects.c commencera donc par la définition de la fonction de base permettant de mixer deux échantillons :

#include "sound_effects.h"
Sint16 sample_mix(Sint16 src1, Sint16 src2)
 {
   Sint32 mix = src1 + src2;

   /* Dépassement de la capacité de l'échantillon sur 16 bits? */
   if (mix > 32767) mix = 32767;
   if (mix < - 32768) mix = -32768;

   return (Sint16)mix;
 }

La variation de volume

De même que pour le mixage, cet effet est on ne peut plus classique. Changer le volume d'un son consiste simplement à augmenter ou baisser la valeur de ses échantillons de manière uniforme. Bien évidemment, les mêmes limitations que pour le mixage existent, et si l'on augmente trop le volume d'un son il risque de se produire le désagréable effet que tous ses échantillons soient à l'une des bornes codables dans son format (voir encore les figures 1 et 2). Et là, gare aux oreilles! Dans notre exemple, nous nous contenterons de baisser le volume. Nous pouvons donc compléter sound_effects.c avec la fonction règlant le volume d'un échantillon selon une échelle allant de 0 à 255, 0 rendant notre son totalement muet et 255 le laissant inchangé :

 Sint16 sample_volume(Sint16 sample, Uint8 volume)
 {
   return (sample * volume) / 255;
 }

L'écho

Nous entrons ici dans une famille d'effets un peu plus sophistiqués. Les effets que nous avons étudié jusqu'à présent consistent à appliquer à tous les échantillons d'un son la même opération qui ne dépend pas de la valeur de ses autres échantillons. Pour pouvoir appliquer l'effet d'écho correctement, la fonction responsable devra disposer du son dans son intégralité.

Formalisons ce qu'est un écho : l'écho est la répétition, un certain nombre de fois, du son avec un décalage temporel. À chaque répétition, le volume du son sera plus faible que lors de la répétition précédente. Les paramètres de l'écho sont donc:

  • Le délai entre chaque répétition,
  • Le nombre de répétitions à effecter,
  • Le taux de diminution du volume à chaque répétition.

Cet effet se base donc sur les deux autres effets que nous connaissons déjà : diminuer le volume d'un échantillon, et mixer deux échantillons entre eux (pour mixer le son original avec son écho à un instant donné).

La procédure complète de l'effet d'écho pour un échantillon sera donc d'identifier les échantillons antérieurs qui devront être entendus simultanément, de diminuer leur volume en fonction de leur distance avec l'échantillon courant et de les mixer avec ce dernier. La figure 3 démontre visuellement ce principe. Notre implémentation utilisera des variables globales entières pour régler les paramètres de l'effet d'écho. Voici donc le code de sound_effects.c qui, étant donné un soundplayer, retourne l'échantillon correspondant à l'effet d'écho de l'échantillon à la position courante :

Fig. 3
Fig. 3

 Uint8 echo_volume_dec; /* Décrémentation du volume */
 Uint8 echo_repeat; /* Nombre de répétitions */
 Uint32 echo_delay; /* Délai entre chaque répétition */

 Sint16 soundplayer_echo(soundplayer * player)
 {
   Uint8 k;
   Sint16 res = 0;

   for (k = 1; k <= echo_repeat; k++) {
     Sint16 newvolume = global_volume - (echo_volume_dec * k);
     /* Si le volume a atteint l'inaudible, inutile de continuer... */
     if (newvolume <= 0) break;
     /* Si la position de l'échantillon répété sort des bornes du son,
        inutile de continuer... */
     if (!(player->pos >= echo_delay * k &&
       player->pos - (echo_delay * k) < player->played_sound->length))
       break;
     /* Sinon, alors on le mixe! */
     res = sample_mix(res, sample_volume((*(Sint16 *)(player->played_sound->data +
                                  player->pos - echo_delay * k)), newvolume));
   }

   return res;
 }

Et voilà pour les effets de base. Ci-dessous, les déclarations de toutes nos variables et fonctions qui doivent se trouver dans sound_effects.h :

 #ifndef _SOUND_EFFECTS_H__
 #define _SOUND_EFFECTS_H__

 #include "audio.h"

 /* Les paramètres d'echo */
 Uint8 echo_volume_dec = 50; /* Décrémentation du volume */
 Uint8 echo_repeat = 5; /* Nombre de répétitions */
 Uint32 echo_delay = 35000; /* Délai entre chaque répétition */

 /* Change le volume sur le sample passé en paramètre.
    Le volume retourné est égal au paramètre volume / 255. */
 Sint16 sample_volume(Sint16 sample, Uint8 volume);
 /* Retourne le mixage des deux échantillons src1 et src2 */
 Sint16 sample_mix(Sint16 src1, Sint16 src2);

 /* Retourne l'échantillon correspondant à l'écho du player pour sa
    position courante, en fonction des paramètres d'écho globaux */
 Sint16 soundplayer_echo(soundplayer * player, Uint8 initial_volume);

 #endif

Voyons maintenant comment orchestrer tout cela dans notre moteur de son.

Le moteur sonore

Dans notre exemple, les sons pourront être joués à n'importe quel moment, et dans n'importe quel ordre. N'ayant aucune information sur la manière dont ils vont être joués, nous devrons effectuer le mixage juste avant d'émettre le son sur la carte, c'est à dire dans la fonction de rappel appelée par SDL. Encore faut-il savoir quoi mixer. Nous allons utiliser une table globale de soundplayer qui contiendra les sons en train d'être joués avec leur position courante, et qui sera parcourue par notre fonction de rappel pour les mixer. Le nombre de sons jouables simultanément sera donc déterminé par la taille de cette table. Une fonction permettant de jouer un son sur cette « table de mixage » sera chargée de trouver un élément libre dans cette table et d'en initialiser la structure afin qu'il pointe vers le son à jouer. Sa lecture commencera ainsi dès le prochain passage dans la fonction de rappel.

Nous déclarerons toutes ces choses dans le fichier mixer.h :

 #ifndef _MIXER_H__
 #define _MIXER_H__

 #include "audio.h"

 /* Nombre de sons pouvant être joués simultanément */
 #define NBR_OF_AUDIO_SLOTS 8
 /* "Table de mixage" de taille NBR_OF_SLOTS */
 soundplayer audio_slots[NBR_OF_AUDIO_SLOTS];

 /* Initialise le mixeur audio */
 void audio_init();
 /*Cherche un emplacement libre pour jouer le son donné en
   paramètre. S'il est trouvé, il sera utilisé pour jouer le son.
   Retourne 0 en cas de succès, -1 si aucun slot n'est libre. */
 int audio_play(sound * toplay);

 #endif

L'implémentation, relativement simple, se trouve dans mixer.c :

 #include "mixer.h"

 void audio_init() {
   int i;

   for (i = 0; i < NBR_OF_AUDIO_SLOTS; i++) {
     soundplayer_stop(&audio_slots[i]);
   }
 }

 int audio_play(sound * toplay) {
   int i;

   /* Tout d'abord, chercher un slot de libre pour jouer le son */
   for (i = 0; i < NBR_OF_AUDIO_SLOTS; i++) {
     if (!soundplayer_isplaying(&audio_slots[i])) break;
   }

   /* Tous les slots sont utilisés? On retourne l'erreur */
   if (i == NBR_OF_AUDIO_SLOTS) return -1;

   /* Sinon on affecte le son à jouer au slot correspondant */
   soundplayer_play(&audio_slots[i], toplay);

   return 0;
 }

La structure est en place, il reste maintenant à créer la fonction de rappel de SDL responsable de mixer tout cela. La procédure de base est de parcourir tous les éléments de la table audio_slots, de mixer les sons qui y sont présents pour la durée qu'on nous demande de fournir, et de mettre à jour les états des soundplayer (position et statut pour ceux qui sont arrivée à échéance).

Notre fonction de mixage ira plus loin, puisqu'elle contrôlera également un volume global, et un effet d'écho. Ces paramètres ainsi que la fonction de rappel pour SDL seront définis dans notre main.c :

 #include <stdio.h>
 #include <unistd.h>
 #include <string.h>
 #include "SDL.h"

 #include "mixer.h"
 #include "sound_effects.h"

 /* Le volume global */
 static Uint8 global_volume = 255;

 /* Variable de contrôle de l'écho */
 static Uint8 echo_on = 0;

 /* Fonction de rappel qui copie les données sonores dans le tampon audio */
 void mixaudio(void * userdata, Uint8 * stream, int len)
 {
   int i, j;

   /* Parcours de tous les slots audio, à la recherche de ceux qui sont utilisés */
   for (i = 0; i < NBR_OF_AUDIO_SLOTS; i++) {
     if (soundplayer_isplaying(&audio_slots[i])) {
       soundplayer * player = &audio_slots[i];
       sound * toplay = player->played_sound;

       /* Le nombre de samples que nous allons produire */
       Uint32 nbSamples = len / sizeof(Sint16);

       for (j = 0; j < nbSamples; j++) {
         Sint16 mix = ((Sint16 *)stream)[j];
     /* Récupération de l'échantillon courant, ou de la valeur 0 si nous
        avons fini de jouer le son */
         Sint16 sample = (player->pos < toplay->length) ?
                                    (*(Sint16 *)(toplay->data + player->pos)) : 0;

         /* Application du volume global */
         sample = sample_volume(sample, global_volume);

         if (echo_on) {
           /* On applique ensuite l'effet d'echo */
           sample = sample_mix(sample, soundplayer_echo(player, global_volume));
         }

         /* L'échantillon est enfin additionné au buffer */
         sample = sample_mix(sample, mix);

         /* Ecriture du nouveau son mixé */
         ((Sint16 *)stream)[j] = sample;

         player->pos += sizeof(Sint16);
       }

       /* Mise à jour du lecteur */
       soundplayer_update(player);
     }
   }
 }

Ensuite, la fonction de gestion des entrées se chargera de jouer les sons et d'activer les effets en fonction des entrées clavier :

 /* Gestion des entrées clavier */
Uint8 letsexit = 0;
/* Les sons jouables */
sound KDE_startup;
sound KDE_logout;
sound K3B_error;
sound KDE_dialog;
/* Pas de modification du volume par le clavier */
#define VOLUME_STEP 10
 void input_update() {
   SDL_Event event;
   while (SDL_PollEvent(&event)) {
     switch (event.type) {
     case SDL_KEYDOWN:
       switch (event.key.keysym.sym) {
     /* Quitter */
       case SDLK_ESCAPE:
     letsexit = 1;
     break;
       case SDLK_TAB:
     printf("Joue: KDE_Startup\n");
     audio_play(&KDE_startup);
     break;
       case SDLK_a:
     printf("Joue: KDE_Logout\n");
     audio_play(&KDE_logout);
     break;
       case SDLK_z:
     printf("Joue: K3B_Error\n");
     audio_play(&K3B_error);
     break;
       case SDLK_e:
     printf("Joue: KDE_Dialog\n");
     audio_play(&KDE_dialog);
     break;
     /* Gestion du volume */
       case SDLK_KP_PLUS:
     if (global_volume < 255 - VOLUME_STEP) global_volume += VOLUME_STEP;
     else global_volume = 255;
     printf("Volume: %d\n", global_volume);
     break;
       case SDLK_KP_MINUS:
     if (global_volume > 0 + VOLUME_STEP) global_volume -= VOLUME_STEP;
     else global_volume = 0;
     printf("Volume: %d\n", global_volume);
     break;
         /* Echo on/off */
       case SDLK_RETURN:
     if (echo_on) { echo_on = 0; printf("Echo desactivé\n"); }
     else { echo_on = 1; printf("Echo activé\n"); }
     break;
       default:
     break;
       }
       break;
      default:
       break;
     }
   }
 }

Les touches TAB, A, Z et E déclenchent chacune la lecture d'un son différent, qui sera ajouté à la table de mixage pour être entendu avec les autres sons éventuellement en cours de lecture. Les touches + et – du pavé numérique contrôlent le volume global, tandis que la touche Entrée active ou désactive l'effet d'écho. Et enfin, la fonction principale! Elle ressemblera beaucoup à celle de la dernière fois, avec sa phase d'initialisation, le chargement des sons, le paramétrage de la fonction de rappel et la boucle d'attente.

 int main(int argc, char * argv[])
 {
   SDL_AudioSpec desired, obtained;
   SDL_Surface * screen;

   if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) == -1) {
     printf("Erreur lors de l'initialisation de SDL!\n");
     return 1;
   }

   /* Création d'une fenêtre pour récupérer les évènements clavier */
   screen = SDL_SetVideoMode(640, 480, 24,
                 SDL_SWSURFACE);

   /* Son 16 bits stéréo à 44100 Hz */
   desired.freq = 44100;
   desired.format = AUDIO_S16SYS;
   desired.channels = 2;

   /* Le tampon audio contiendra 512 échantillons */
   desired.samples = 512;

   /* Mise en place de la fonction de rappel et des données utilisateur */
   desired.callback = &mixaudio;
   desired.userdata = NULL;

   if (SDL_OpenAudio(&desired, &obtained) != 0) {
     printf("Erreur lors de l'ouverture du périphérique audio: %s\n", SDL_GetError());
     return 1;
   }
   printf("Paramètres audio obtenus: d canaux, fréquence d échantillons.\n",
      obtained.format & 0xff, obtained.channels, obtained.freq, obtained.samples);

   audio_init();

   /* Chargement de quelques fichiers son... */
   if (sound_load(&KDE_startup, "KDE_Startup_new.wav", &obtained) ||
       sound_load(&KDE_logout, "KDE_Logout_new.wav", &obtained) ||
       sound_load(&K3B_error, "k3b_error1.wav", &obtained) ||
       sound_load(&KDE_dialog, "KDE_Dialog_Appear.wav", &obtained)) {
     fprintf(stderr, "Sons non chargés - arrêt\n");
     SDL_CloseAudio();
     SDL_Quit();
     return 1;
   }

   /* La fonction de rappel commence à être appelée à partir de maintenant. */
   SDL_PauseAudio(0);

   while(!letsexit) {
     input_update();
     /* Attente d'1/10 de seconde */
     SDL_Delay(100);
   }

   /* On cesse d'appeler la fonction de rappel */
   SDL_PauseAudio(1);

   sound_free(&KDE_dialog);
   sound_free(&K3B_error);
   sound_free(&KDE_logout);
   sound_free(&KDE_startup);

   /* Fermer le périphérique audio */
   SDL_CloseAudio();

   SDL_Quit();
   return 0;
 }

Le Makefile suivant vous permettra de compiler le tout aisément:

 CFLAGS = `sdl-config --cflags` -Wall -ansi -g
 LIBS = `sdl-config --libs`

 OBJECTS = audio.o mixer.o sound_effects.o main.o

 all: $(OBJECTS)
     $(CC) $(OBJECTS) -o audiodemo $(LIBS)

 clean:
     rm -f *.o audiodemo

Vous pouvez bien évidemment améliorer ce petit programme. Notamment en gérant d'autres touches du clavier pour changer, en temps réel, les paramètres de l'effet d'écho, ou en appliquant les effets de manière séparée pour chaque emplacement de la table de mixage.

Bilan

Nous venons de clôturer le tour d'horizon des fonctions sonores offertes par SDL. Vraiment? En fait, une fonction a volontairement été omise : SDL_MixAudio.

 void SDL_MixAudio(Uint8 *dst, Uint8 *src, Uint32 len, int volume);

Vous l'avez deviné, cette fonction ne fait rien d'autre que mixer deux sons, de manière très similaire à la fonction de mixage que nous avons écrite. Elle peut de plus faire un ajustement du volume. Mais il était plus intéressant d'écrire notre propre fonction, n'est-ce pas?


Tutoriel SDL

Programmation graphique portable en C avec la SDL - Partie 1: les bases

Table des matières

  • 1. Introduction
  • 2. Installer la bibliothèque SDL
    • 2.1. Linux & Unix
    • 2.2. Windows
  • 3. Notre premier programme SDL
    • 3.1. Code source complet
    • 3.2. Analyse des fonctions d'initialisation
      • 3.3. Les surfaces
        • 3.3.1. Les surfaces d'affichage logicielles
        • 3.3.2. Les surfaces d'affichage matérielles
      • 3.4. La gestion des événements
  • 4. Afficher un pixel
    • 4.1. La fonction setPixel()
    • 4.2. Les couleurs
    • 4.3. Programme "Ciel étoilé"
  • 5. Conclusion

Ce premier article présente la création d'une application graphique SDL minimale, ainsi que la fonction de base du graphisme : afficher un pixel.

1. Introduction

La SDL, Simple DirectMedia Layer, est une bibliothèque destinée à permettre l'accès au matériel graphique pour faire, par exemple, des jeux en plein écran (ou en fenêtre), et ça de manière portable. La SDL peut notamment fonctionner sous Linux (où elle utilise X11), comme sous Windows (où elle utilise DirectX).

La SDL est simple d'emploi, mais particulièrement bas niveau. Vous pouvez initialiser facilement le mode graphique, mais pour dessiner, vous n'avez rien. Même pas une fonction pour afficher un pixel ! Cet article est là pour vous aider à utiliser cette bibliothèque et à écrire les fonctions de base.

La SDL est disponible en licence libre LGPL. Cela veut dire que vous pouvez donc librement utiliser cette bibliothèque dans votre application, qu'elle soit libre ou commerciale. La seule obligation est que la bibliothèque SDL doit rester en liaison dynamique.

2. Installer la bibliothèque SDL

Je vous présente ici la manière d'installer SDL sur votre système d'exploitation ainsi que la manière dont vous devrez compiler vos applications.

2.1. Linux & Unix

Heureux possesseurs d'un système Unix, vous apprendrez avec plaisir que c'est un bonheur de travailler avec la SDL sous cet environnement.

Vous devez disposer simplement du compilateur gcc, ainsi que les paquets SDL et développement SDL (sdl-devel pour la majorité des distributions Linux). La SDL est livrée en standard pour la quasi-totalité des distributions Linux.

Pour compiler votre programme, vous ajouterez `sdl-config --cflags` aux options de compilation (attention, ce sont réellement des anti-quotes). Pour lier votre programme, vous ajouterez `sdl-config --libs` aux options de liaison.

Exemple : compiler un fichier source et le lier

Sélectionnez
gcc `sdl-config --cflags` -Wall -c TestSDL01.c
gcc `sdl-config --libs` -o TestSDL01 TestSDL01.o

Techniquement, sdl-config est un petit programme qui affiche à l'écran les options de gcc nécessaires pour la compilation ou la liaison selon les paramètres. La mise entre anti-quotes fait que cet affichage est récupéré et utilisé directement comme paramètres de gcc. Ce mécanisme, appelé substitution de commande, est géré directement par le shell.

2.2. Windows

Vous pouvez télécharger la SDL ici. Il y a la version de développement, nécessaire pour compiler votre programme. Il en existe deux versions : une pour MingW (gcc ; cela concerne aussi DevC++), et une pour Visual C++ (version 5 et supérieures). La version de développement inclut aussi le runtime, vous n'avez donc pas besoin de télécharger ce dernier.

Nous allons détailler l'installation de la bibliothèque SDL sous Dev-C++.

Installer SDL sous Dev-C++
  1. Téléchargez (lien donné ci-dessus) le fichier SDL-devel-1.2.9-mingw32.tar.gz
  2. Décompressez cette archive dans le dossier de DevC++ (par défaut: C:\Dev-Cpp)
  3. Lancez DevC++, allez dans "Outils", "Options du Compilateur", onglet "Répertoire"
  4. Pour "Répertoire bibliothèques", ajoutez le chemin des fichiers .lib de SDL (par défaut: C:\Dev-Cpp\SDL-1.2.9\lib).
  5. Pour "Répertoire C .h", ajoutez le chemin des fichiers .h de SDL (par défaut: C:\Dev-Cpp\SDL-1.2.9\include).
  6. Pour "Répertoire C++ .h", ajoutez la même chose qu'à l'étape précédente (par défaut: C:\Dev-Cpp\SDL-1.2.9\include).
  7. Validez le tout en cliquant sur OK : c'est installé.
Créer un projet SDL sous Dev-C++
  1. Créez normalement un projet vide ("Fichier", "Nouveau", "Projet", "Empty Project"), donnez-lui un nom et sauvegardez-le dans son propre répertoire.
  2. Allez dans les options du projet ("Projet", "Options du Projet")
  3. Dans l'onglet "Général", choisissez "Win32 GUI".
  4. Dans l'onglet "Paramètres", zone "Editeur de liens", ajoutez "-lmingw32 -lSDLmain -lSDL"
  5. Validez le tout en cliquant sur OK.
  6. Copiez le runtime SDL (SDL.DLL, que l'on trouve dans le dossier bin de la SDL, par défaut: C:\Dev-Cpp\SDL-1.2.9\bin) dans le dossier de votre projet.
  7. C'est prêt !

3. Notre premier programme SDL

Pour commencer, nous allons créer un programme minimal qui initialise une fenêtre, et qui attend une touche pour quitter.

3.1. Code source complet

Voici le code source complet de ce premier programme, nous l'analyserons après :

Sélectionnez
#include <stdlib.h>
#include <stdio.h>
#include "SDL.h"

SDL_Surface* affichage;

void initSDL(void);
void attendreTouche(void);

int main(int argc, char** argv)
{
  initSDL();
  attendreTouche();
  return EXIT_SUCCESS;
}

void initSDL(void)
{
  if (SDL_Init(SDL_INIT_VIDEO) < 0) {
    fprintf(stderr, "Erreur à l'initialisation de la SDL : %s\n", SDL_GetError());
    exit(EXIT_FAILURE);
  }

  atexit(SDL_Quit);
  affichage = SDL_SetVideoMode(800, 600, 32, SDL_SWSURFACE);

  if (affichage == NULL) {
    fprintf(stderr, "Impossible d'activer le mode graphique : %s\n", SDL_GetError());
    exit(EXIT_FAILURE);
  }

  SDL_WM_SetCaption("Mon premier programme SDL", NULL);
}

void attendreTouche(void)
{
  SDL_Event event;

  do
    SDL_WaitEvent(&event);
  while (event.type != SDL_QUIT && event.type != SDL_KEYDOWN);
}

Compilez ce programme et exécutez-le. Il affiche une fenêtre noire, de taille 800x600. Si vous enfoncez une touche, la fenêtre disparaît et le programme s'arrête. Remarquez que si vous cliquez sur la case de fermeture, il se passe la même chose.

3.2. Analyse des fonctions d'initialisation

Maintenant, examinons le programme ligne par ligne.

Sélectionnez
#include "SDL.h"

Ceci inclut tous les fichiers headers de la SDL.

Sélectionnez
SDL_Surface* affichage;

Cette variable globale sert à stocker la surface concernant l'affichage. Nous détaillerons un peu plus loin ce que représente une surface, mais pour le moment, il suffit de savoir que lorsque nous voudrons dessiner directement à l'écran, nous utiliserons cette variable.

Sélectionnez
SDL_Init(SDL_INIT_VIDEO);

Cette fonction initialise la bibliothèque SDL. Elle doit être appelée avant toute autre fonction de la bibliothèque. Le paramètre correspond aux sous-systèmes qu'il faut activer. En effet, il faut savoir que la SDL est constituée de huit sous-systèmes : Audio, CD-ROM, Gestion des événements, Gestion de fichiers, Gestion du joystick, Processus légers (threads), Timers (fonctions de délais ultra-précises) et Vidéo.

SDL_Init() active automatiquement les sous-systèmes Gestion des événements, Gestion de fichiers et les Processus légers ; si on a besoin d'autres sous-systèmes, on le précise en paramètre de cette fonction. Ici, comme on va faire du graphisme, on initialise en plus le sous-système Vidéo.

Sélectionnez
atexit(SDL_Quit);

Quand l'application a initialisé la SDL avec SDL_Init(), elle ne doit surtout pas oublier de "l'éteindre" avant que le programme se termine, afin que les sous-systèmes soient correctement fermés : notamment, si vous avez changé le mode vidéo, il faut restaurer le mode normal.

Pour "éteindre" la SDL, il faut appeler la fonction SDL_Quit(). Mais il est encore mieux de s'assurer que cette fonction sera appelée automatiquement quand le programme s'arrête, quelqu'en soit la raison. C'est la raison d'être de la fonction atexit() (de la bibliothèque C standard), qui enregistre les fonctions qu'il faudra appeler automatiquement à la fin du programme.

Nota: Sous Unix, si le programme est tué par un signal SIGKILL (avec kill -9 par exemple), la fonction SDL_Quit() ne sera malheureusement pas appelée.

Sélectionnez
affichage = SDL_SetVideoMode(800, 600, 32, SDL_SWSURFACE);

Cette fonction active le mode vidéo demandé. Remarquez que ceci ne signifie pas forcément changer de mode graphique : on peut très bien seulement créer une fenêtre dans lequel on travaillera.

On lui passe les dimensions (je suppose ici que vous travaillez en 1024x768 afin que la fenêtre puisse s'afficher entièrement), ainsi que le nombre de bits par pixel (32). Remarquez, dans ce cas précis, qu'il n'est pas nécessaire que ce nombre de bits par pixel soit identique à la véritable résolution de l'écran. Si cela ne correspond pas, la SDL se chargera de convertir les accès quand ça sera nécessaire. Nous avons choisi 32 bits par pixel parce qu'il s'agit de la résolution la plus courante.

Le dernier argument indique le type de surface (nous y reviendrons). Ici, nous avons demandé une surface logicielle, sans demander le plein écran : SDL_SetVideoMode() va donc créer une fenêtre. Si nous voulions que l'application tourne en plein écran, nous aurions mis : SDL_SWSURFACE | SDL_FULLSCREEN.

Sélectionnez
SDL_WM_SetCaption("Mon premier programme SDL", NULL);

Sans surprise, cette fonction permet de donner un titre à cette fenêtre. Le premier argument est une chaîne contenant le titre, le deuxième argument une chaîne contenant l'icône à attribuer.

3.3. Les surfaces

Qu'est-ce qu'une surface ? Hé bien c'est une zone de mémoire qui contient des pixels. Une surface, ça peut très bien être une image bitmap chargée en mémoire centrale, ou une zone directement en mémoire vidéo.

Quand on initialise le mode vidéo avec SetVideoMode(), on obtient en retour une surface. Cette surface permet de dessiner à l'intérieur de la fenêtre (ou sur l'écran si vous avez choisi le mode plein écran).

Les surfaces destinées à l'affichage peuvent être de deux types : logicielle, ou matérielle.

3.3.1. Les surfaces d'affichage logicielles

Les surfaces logicielles sont stockées en mémoire centrale. On peut alors librement accéder à cette surface pour y écrire des pixels ou ce que l'on souhaite. Comme cette zone est en mémoire centrale, l'accès est très rapide, mais ce que l'on dessine n'apparaît pas à l'écran. En effet, lorsque le moment opportun sera arrivé, on pourra alors demander à SDL de mettre à jour l'affichage ; c'est-à-dire de copier le contenu de la surface en mémoire vidéo. Cette opération n'est pas très rapide, et peut même s'avérer très lente si le format de la surface n'est pas le même que la résolution réelle (bits par pixel) de l'écran.

Ceci dit, il n'est pas nécessaire de mettre à jour tout l'écran systématiquement. Si on n'a modifié que quelques portions, il suffit de demander la mise à jour du plus petit rectangle contenant ces modifications avec SDL_UpdateRect() ou SDL_UpdateRects() s'il s'agit de plusieurs rectangles, ce qui accélère fortement la vitesse de mise à jour. Cependant, s'il y a de très nombreux rectangles à mettre à jour, il reste plus simple et plus rapide de demander la mise à jour de l'écran entier avec SDL_UpdateRect(affichage, 0, 0, 0, 0).

Pour résumer, les avantages :
  • Accès très rapide
  • N'importe quelle résolution (bits par pixel) peut être utilisée
  • Double tampon : l'affichage n'est mis à jour que sur demande
  • Fonctionne en mode fenêtré ou en plein écran
  • Fonctionne sur tous les systèmes
Les inconvénients :
  • Ne profite pas des fonctions accélérées de la carte graphique
  • L'opération de mise à jour de l'affichage est lente

3.3.2. Les surfaces d'affichage matérielles

Au contraire des surfaces logicielles, les surfaces matérielles sont stockées directement en mémoire vidéo. Ce qui signifie que, normalement, lorsqu'on écrit un pixel dans cette surface, l'affichage est mis à jour instantannément ! Si cela pose un problème, il suffit d'activer le double tampon matériel : on dispose alors de deux pages écran : la page affichée et la page cachée, qui se suivent dans la mémoire vidéo.

Lorsqu'on écrit dans la surface, on écrit dans la page cachée ; les modifications ne sont donc pas visibles parce que l'écran affiche toujours le contenu de la page affichée. Lorsqu'on désire mettre à jour l'affichage, il suffit d'appeler SDL_Flip() qui consiste à tout simplement inverser les deux pages, ce qui met à jour l'affichage. Cette opération est extrêmement rapide puisqu'il n'y a strictement aucune copie : elle ne fait que programmer quelques registres de la carte vidéo.

Remarquons que ce mode double tampon n'est pas équivalent au double tampon qu'offre les surfaces logicielles : en effet, après une mise à jour de l'affichage d'une surface logicielle, la surface de dessin contient exactement ce que contient l'affichage. Sur un double tampon d'une surface matérielle, en revanche, après un SDL_Flip(), la surface de dessin contient le précédent contenu affiché.

Néanmoins, l'accès direct à la mémoire vidéo peut poser problème dans un environnement multi-tâches. Le programme ne peut pas faire ce qu'il veut ! C'est pour ça que, avant tout accès à la surface, il doit verrouiller la surface, ce qui interdit à toute autre application de tenter d'accéder à la mémoire vidéo. Une fois le travail de dessin terminé, il doit toujours déverrouiller la surface.

Pour résumer, les avantages :
  • Peut fonctionner en affichage direct, sans nécessité de mise à jour
  • Mode double tampon (optionnel) avec mise à jour ultra-rapide
  • Peut profiter des fonctions accélérées de la carte graphique
Les inconvénients :
  • Ne fonctionne qu'en plein écran
  • Ne fonctionne pas sur tous les systèmes (notamment il faut des droits sous Unix)
  • Nécessite de contrôler strictement l'accès à la surface

Comme notre programme n'a pas besoin de grandes performances, nous avons choisi d'utiliser une surface logicielle, pour son universalité et sa facilité d'utilisation.

3.4. La gestion des événements

Maintenant, voyons quelque chose d'un peu plus technique. Un programme SDL ressemble à un programme Win32 normal : il est piloté par les événements. Un événement, ça peut être un clic souris, une touche clavier enfoncée, une demande de fermeture de la fenêtre principale, etc. Les événements, lorsqu'ils sont reçus, sont placés dans une file d'attente, où ils seront lus par le programme au moment voulu.

Pour lire les événements, on utilise une boucle. Dans cette boucle, on appelle SDL_WaitEvent(). Cette fonction fait attendre le programme tant qu'il n'y a pas d'événement à lire (il s'agit d'une attente passive : le cpu n'est pas du tout utilisé, les autres applications ne sont donc absolument pas pénalisées).

Lorsqu'un événement arrive, la fonction le lit, le retire de la file d'attente et le retourne au programme (en remplissant la structure SDL_Event passée en paramètre).

Dans notre premier programme, je me contente de lire (et retirer de la file) les événements jusqu'à ce que je reçoive un événement SDL_QUIT (clic sur la case de fermeture) ou SDL_KEYDOWN (touche enfoncée).

Nota: Si on oublie de gérer l'événement SDL_QUIT, la case de fermeture de la fenêtre n'aura pas d'effet. Sous Unix, remarquez qu'on ne peut pas arrêter un programme SDL avec Ctrl-C à la console ; en revanche, Ctrl - \ fonctionne (et désactive correctement la SDL).

4. Afficher un pixel

4.1. La fonction setPixel()

Aussi bizarre que ça puisse paraître au premier abord, la SDL ne propose aucun moyen d'écrire un pixel. Il nous faut écrire la fonction nous-même.

Mais en y regardant de plus près, cela s'explique aisément : écrire un pixel dépend énormément de la résolution (en bits par pixel). Si on veut gérer tous les cas, on perd du temps en exécution. De plus, un appel de fonction peut être trop lent dans certains cas. Enfin, on peut vouloir ou ne pas vouloir que la fonction teste si ses paramètres sont corrects (encore du temps d'exécution perdu).

Nous allons écrire une fonction simple, conçue pour fonctionner dans le mode 32 bits par pixel uniquement. Cette fonction ne teste pas la validité des coordonnées pour gagner en temps d'exécution. Si vous utilisez gcc (ou le C++), il sera une très bonne idée d'en faire une fonction "inline" afin de l'accélérer.

Sélectionnez
void setPixel(int x, int y, Uint32 coul)
{
  *((Uint32*)(affichage->pixels) + x + y * affichage->w) = coul;
}

Pour comprendre grossièrement comment cette fonction est conçue, il faut connaître le format d'affichage. Quand la SDL fonctionne en mode 32 bits, la surface d'affichage est toujours conçue de la même manière : il faut 4 octets par pixel (le code couleur), et les points sont répartis linéairement, ligne par ligne.

Donc pour afficher un pixel, on prend l'adresse de début de la surface ; on décale le pointeur vers la bonne position (y lignes de pixels, soit y multiplié par la largeur de la surface, plus x colonnes). Comme le pointeur est transtypé en pointeur vers entier 32 bits, le décalage se fait automatiquement par sauts de 4 octets. Enfin, on déréférence le pointeur et on y stocke la couleur.

4.2. Les couleurs

Contrairement à l'affichage de pixels, la SDL fournit une fonction qui permet de faire la conversion d'un code couleur RVB en code couleur. L'avantage, c'est qu'on n'a pas besoin de se préoccuper du format actuel des pixels.

Sélectionnez
Uint32 SDL_MapRGB(SDL_PixelFormat *fmt, Uint8 r, Uint8 g, Uint8 b);

Le premier paramètre est le format des pixels, pour ça on devra lui passer affichage->format.

Nous ne sommes pas tous des experts en mélange de couleurs, alors j'ai préparé ici une fonction qui charge une liste des couleurs les plus utilisées.

Sélectionnez
enum {
  C_NOIR, C_BLEU_FONCE, C_VERT_FONCE, C_CYAN_FONCE, C_ROUGE_FONCE,
  C_MAGENTA_FONCE, C_OCRE, C_GRIS_CLAIR, C_GRIS, C_BLEU, C_VERT,
  C_CYAN, C_ROUGE, C_MAGENTA, C_JAUNE, C_BLANC,

  NB_COULEURS
};

Uint32 couleurs[NB_COULEURS];

void initCouleurs(void)
{
  couleurs[C_NOIR]          = SDL_MapRGB(affichage->format, 0x00, 0x00, 0x00);
  couleurs[C_BLEU_FONCE]    = SDL_MapRGB(affichage->format, 0x00, 0x00, 0x80);
  couleurs[C_VERT_FONCE]    = SDL_MapRGB(affichage->format, 0x00, 0x80, 0x00);
  couleurs[C_CYAN_FONCE]    = SDL_MapRGB(affichage->format, 0x00, 0x80, 0x80);
  couleurs[C_ROUGE_FONCE]   = SDL_MapRGB(affichage->format, 0x80, 0x00, 0x00);
  couleurs[C_MAGENTA_FONCE] = SDL_MapRGB(affichage->format, 0x80, 0x00, 0x80);
  couleurs[C_OCRE]          = SDL_MapRGB(affichage->format, 0x80, 0x80, 0x00);
  couleurs[C_GRIS_CLAIR]    = SDL_MapRGB(affichage->format, 0xC0, 0xC0, 0xC0);
  couleurs[C_GRIS]          = SDL_MapRGB(affichage->format, 0x80, 0x80, 0x80);
  couleurs[C_BLEU]          = SDL_MapRGB(affichage->format, 0x00, 0x00, 0xFF);
  couleurs[C_VERT]          = SDL_MapRGB(affichage->format, 0x00, 0xFF, 0x00);
  couleurs[C_CYAN]          = SDL_MapRGB(affichage->format, 0x00, 0xFF, 0xFF);
  couleurs[C_ROUGE]         = SDL_MapRGB(affichage->format, 0xFF, 0x00, 0x00);
  couleurs[C_MAGENTA]       = SDL_MapRGB(affichage->format, 0xFF, 0x00, 0xFF);
  couleurs[C_JAUNE]         = SDL_MapRGB(affichage->format, 0xFF, 0xFF, 0x00);
  couleurs[C_BLANC]         = SDL_MapRGB(affichage->format, 0xFF, 0xFF, 0xFF);
}

Après avoir appelé initCouleurs() en début de programme (après initSDL() bien entendu) pour initialiser le tableau, si vous souhaitez par exemple utiliser du jaune, il vous suffira de faire couleurs[C_JAUNE].

Le jeu de couleurs n'est pas tout à fait choisi par hasard : il s'agit des 16 couleurs classiques que l'on obtient dans un mode texte ou graphique 16 couleurs sur PC.

4.3. Programme "Ciel étoilé"

Pour mettre en pratique l'affichage de pixels et les couleurs, nous allons écrire un petit programme qui affiche un ciel étoilé aléatoire (en couleurs).

Sélectionnez
#include <stdlib.h>
#include <stdio.h>
#include "SDL.h"

SDL_Surface* affichage;

void initSDL(void);
void attendreTouche(void);
void setPixel(int x, int y, Uint32 coul);
void actualiser(void);
void dessinerEtoiles(void);

int main(int argc, char** argv)
{
  initSDL();
  dessinerEtoiles();
  actualiser();
  attendreTouche();
  return EXIT_SUCCESS;
}

void initSDL(void)
{
  if (SDL_Init(SDL_INIT_VIDEO) < 0) {
    fprintf(stderr, "Erreur à l'initialisation de la SDL : %s\n", SDL_GetError());
    exit(EXIT_FAILURE);
  }

  atexit(SDL_Quit);
  affichage = SDL_SetVideoMode(800, 600, 32, SDL_SWSURFACE);

  if (affichage == NULL) {
    fprintf(stderr, "Impossible d'activer le mode graphique : %s\n", SDL_GetError());
    exit(EXIT_FAILURE);
  }

  SDL_WM_SetCaption("Ciel étoilé", NULL);
}

void attendreTouche(void)
{
  SDL_Event event;

  do
    SDL_WaitEvent(&event);
  while (event.type != SDL_QUIT && event.type != SDL_KEYDOWN);
}

void setPixel(int x, int y, Uint32 coul)
{
  *((Uint32*)(affichage->pixels) + x + y * affichage->w) = coul;
}

void actualiser(void)
{
  SDL_UpdateRect(affichage, 0, 0, 0, 0);
}

void dessinerEtoiles(void)
{
  int i;

  for (i = 0; i < 100; i++)
    setPixel(rand() % 800, rand() % 600,
             SDL_MapRGB(affichage->format,
             rand() % 128 + 128, rand() % 128 + 128, rand() % 128 + 128));
}


Tutoriel SDL

Programmation graphique portable en C avec la SDL - Partie 2: les figures géométriques

Table des matières

  • 1. Introduction
  • 2. Figures géométriques simples
    • 2.1. Pixels "sécurisés"
    • 2.2. Barres (rectangles pleins)
      • 2.3. Lignes horizontales et verticales
      • 2.4. Rectangles
  • 3. Lignes quelconques
    • 3.1. Principe
    • 3.2. Implémentation
  • 4. Cercles et disques
    • 4.1. Principe
    • 4.2. Implémentation
  • 5. Conclusion

Ce second article présente les algorithmes utilisés pour tracer les figures géométriques simples.

1. Introduction

La SDL ne contient que très peu de fonctions, presque tout est à faire soi-même. Il existe bien entendu des bibliothèques diverses qui ajoutent diverses fonctions, mais elles sont rarement installées sur les systèmes Linux et la plupart sont sous licence GPL, ce qui oblige les programmes qui en dépendent d'être aussi en GPL.

Nous allons donc tout simplement écrire nos propres routines en n'utilisant seulement que la bibliothèque SDL de base. Ce n'est pas bien difficile et c'est très instructif. Si vous n'êtes pas intéressés par l'aspect théorique des algorithmes de tracés, vous pouvez sauter sans problème les parties "Principes" de cet article.

Cette partie sera très peu dépendante de la SDL ; vous pourrez réutiliser ces routines pour n'importe quel autre système graphique, pour peu que vous disposiez d'une instruction pour poser un pixel.

2. Figures géométriques simples

2.1. Pixels "sécurisés"

Dans la première partie de ce tutoriel, nous avons écrit une fonction setPixel() très rapide, mais qui ne vérifiait pas ses coordonnées. Si vous tentez d'écrire en-dehors de l'écran, vous aurez au mieux un pixel mal placé, au pire un plantage du programme.

Voici une version de setPixel() qui contrôle ses arguments et qui n'affiche pas le pixel en cas de coordonnées hors-limites :

Sélectionnez

void setPixelVerif(int x, int y, Uint32 coul)
{
  if (x >= 0 && x < affichage->w &&
      y >= 0 && y < affichage->h)
    setPixel(x, y, coul);
}

Vous vous demandez peut-être pourquoi je n'ai pas intégré le code de vérification directement dans setPixel() plutôt que de créer une nouvelle fonction setPixelVerif() qui appelle la première si les coordonnées sont correctes. La raison est simple : contrôler les paramètres coûte du temps de calcul. Si par exemple, on appelle 100000 fois setPixel() dans une fonction, il sera bien plus efficace de contrôler les coordonnées une fois dans la grande fonction afin de calculer les intervalles à afficher et d'appeler uniquement setPixel() pour les coordonnées valides que de faire 100000 vérifications.

2.2. Barres (rectangles pleins)

La SDL nous fournit une fonction toute prête pour tracer des barres : la fonction SDL_FillRect(). Bien qu'on pourrait très bien utiliser setPixel() pour tracer la barre, cette fonction est particulièrement optimisée, et peut tirer parti de l'accélération 2D de votre matériel (sur une surface matérielle bien entendu).

Cependant, cette fonction exige une structure en paramètre, c'est pourquoi je préfère écrire une fonction pour faciliter son utilisation.

Sélectionnez

void barre(int x, int y, int w, int h, Uint32 coul)
{
  SDL_Rect r;

  r.x = x;
  r.y = y;
  r.w = w;
  r.h = h;

  SDL_FillRect(affichage, &r, coul);
}

barre() prend donc 5 paramètres : les coordonnées du point supérieur gauche, la largeur, la hauteur et le code couleur.

SDL_FillRect() vérifie les coordonnées et supporte parfaitement de dessiner une barre partiellement en-dehors de l'écran ; il n'y a donc aucun souci à se faire à ce niveau.

Notez que si on passe NULL en guise de rectangle à cette fonction, SDL_FillRect() remplit l'entièreté de l'écran. Ceci nous donne alors immédiatement une fonction toute prête pour effacer l'écran.

Sélectionnez

void effacerEcran(void)
{
  SDL_FillRect(affichage, NULL, couleurs[C_NOIR]);
}

2.3. Lignes horizontales et verticales

Nous verrons plus loin comment tracer des lignes d'inclinaison quelconque, mais prévoir des fonctions spécifiques pour les lignes horizontales et verticales sont utiles, car elles seront bien plus rapides et nous permettront de remplir rapidement des surfaces.

Nous allons simplement utiliser la même fonction SDL_FillRect() avec une des deux dimensions à 1 pour obtenir la ligne concernée.

Sélectionnez

void ligneHorizontale(int x, int y, int w, Uint32 coul)
{
  SDL_Rect r;

  r.x = x;
  r.y = y;
  r.w = w;
  r.h = 1;

  SDL_FillRect(affichage, &r, coul);
}

void ligneVerticale(int x, int y, int h, Uint32 coul)
{
  SDL_Rect r;

  r.x = x;
  r.y = y;
  r.w = 1;
  r.h = h;

  SDL_FillRect(affichage, &r, coul);
}

Ces deux fonctions prennent 4 paramètres : les coordonnées, la largeur (pour la ligne horizontale) ou la hauteur (pour la ligne verticale), et le code couleur.

Le tracé de ligne horizontale sera toujours plus rapide que celui de ligne verticale (parce que les pixels se suivent en mémoire vidéo). C'est pourquoi nous utiliserons de préférence le tracé de ligne horizontale pour faire du remplissage.

2.4. Rectangles

Rien de sorcier là-dedans, nous allons nous contenter de tracer quatre lignes avec les deux fonctions précédentes.

Sélectionnez

void rectangle(int x, int y, int w, int h, Uint32 coul)
{
  ligneHorizontale(x, y, w, coul);
  ligneHorizontale(x, y + h - 1, w, coul);
  ligneVerticale(x, y + 1, h - 2, coul);
  ligneVerticale(x + w - 1, y + 1, h - 2, coul);
}

Le format d'appel est le même que celui de la fonction barre() : les coordonnées, les dimensions et le code couleur.

3. Lignes quelconques

3.1. Principe

Tracer une ligne d'inclinaison quelconque n'a en revanche rien d'évident, il va donc nous falloir nous y pencher un peu plus sérieusement. Si vous n'êtes pas intéressés par le fonctionnement général de l'algorithme, vous pouvez sans problèmes vous reporter directement à l'implémentation au point 3.2.

Prenons le cas d'une ligne dont l'angle d'inclinaison est comprise entre 0 et 45°. Voici l'image zoomée d'une ligne avec une telle inclinaison :

Image non disponible

Comme vous pouvez le constater sur l'illustration, il y a, pour une abcisse X donnée, un et un seul point correspondant. Dessiner la ligne reviendra donc à faire une boucle qui parcourt l'axe des X et qui calcule l'ordonnée Y du point.

Ce n'est pas tout. Quand on a un point de la ligne, le point suivant est toujours soit à la même ordonnée que le précédent, soit un point en-dessous. Une solution simple serait donc de disposer d'un compteur flottant auquel on ajoute, à chaque nouveau point, le rapport (x2 - x1) / (y2 - y1). Tant que ce compteur est inférieur à 0.5, on ajoute les points à la même ordonnée que le point précédent. Si le compteur atteint ou dépasse ce seuil, on place le point en-dessous et on retire 1 au compteur.

La limitation d'inclinaison peut facilement être levée. Pour un angle d'inclinaison de 45° à 90°, la règle "pour une abcisse X donnée, il n'y a qu'un seul point correspondant" n'est plus valable, mais elle s'applique en revanche aux ordonnées. Il suffit donc de parcourir par les ordonnées au lieu des abcisses. Pour les autres angles, il suffit d'inverser le sens de parcours et/ou décrémenter la coordonnée calculée au lieu de l'incrémenter.

L'algorithme que je vais vous présenter est cependant une variante optimisée de ce principe général : il est connu sous le nom d'algorithme de Bresenham modifié. Le grand avantage de cet algorithme est de n'utiliser que de l'arithmétique entière, et de se limiter à des additions, soustractions et à des multiplications par 2. Même si les opérations flottantes ne sont plus aussi lentes qu'elles l'étaient il y a une quinzaine d'années, les additions/soustractions entière restent toujours bien plus rapides, et les multiplications par 2 peuvent être implémentées par un simple décalage de bits. Cet algorithme est donc très performant.

3.2. Implémentation

Sélectionnez

void echangerEntiers(int* x, int* y)
{
  int t = *x;
  *x = *y;
  *y = t;
}

void ligne(int x1, int y1, int x2, int y2, Uint32 coul)
{
  int d, dx, dy, aincr, bincr, xincr, yincr, x, y;

  if (abs(x2 - x1) < abs(y2 - y1)) {
    /* parcours par l'axe vertical */

    if (y1 > y2) {
      echangerEntiers(&x1, &x2);
      echangerEntiers(&y1, &y2);
    }

    xincr = x2 > x1 ? 1 : -1;
    dy = y2 - y1;
    dx = abs(x2 - x1);
    d = 2 * dx - dy;
    aincr = 2 * (dx - dy);
    bincr = 2 * dx;
    x = x1;
    y = y1;

    setPixelVerif(x, y, coul);

    for (y = y1+1; y <= y2; ++y) {
      if (d >= 0) {
    x += xincr;
    d += aincr;
      } else
    d += bincr;

      setPixelVerif(x, y, coul);
    }

  } else {
    /* parcours par l'axe horizontal */

    if (x1 > x2) {
      echangerEntiers(&x1, &x2);
      echangerEntiers(&y1, &y2);
    }

    yincr = y2 > y1 ? 1 : -1;
    dx = x2 - x1;
    dy = abs(y2 - y1);
    d = 2 * dy - dx;
    aincr = 2 * (dy - dx);
    bincr = 2 * dy;
    x = x1;
    y = y1;

    setPixelVerif(x, y, coul);

    for (x = x1+1; x <= x2; ++x) {
      if (d >= 0) {
    y += yincr;
    d += aincr;
      } else
    d += bincr;

      setPixelVerif(x, y, coul);
    }
  }
}

La fonction ligne() comporte 5 paramètres, les coordonnées du premier point et les coordonnées du deuxième point, ainsi que le code couleur. Les deux points sont interchangeables. Il aurait été fastidieux de tester les limites d'écran dans la fonction elle-même (calculs nécessaires), c'est pourquoi la fonction utilise setPixelVerif(). Enfin, la fonction echangerEntier(), comme son nom l'indique, échange les entiers dont les adresses sont fournies en paramètres ; cela soulage le code de la fonction de tracé.

4. Cercles et disques

4.1. Principe

A l'instar du tracé de ligne quelconque, tracer un cercle d'aspect parfait et sans exiger de calculs complexes se révèle particulièrement délicat. Si vous n'êtes pas intéressés par le fonctionnement général de l'algorithme, vous pouvez directement passer à l'implémentation au point 4.2.

Il existe deux méthodes principales de définir un cercle. La première est la méthode trigonométrique : chaque point du cercle peut être projeté sur l'axe des ordonnées ou des abcisses, la distance entre le centre et ces points projetés s'exprimant alors en fonction du sinus ou du cosinus de l'angle. La deuxième est la méthode polynômiale : elle tire parti du fait que si on projette le point du cercle sur l'axe des abcisses, on obtient un triangle rectangle et on peut donc exprimer les coordonnées à l'aide du théorème de Pythagore. Voici un schéma explicatif :

Image non disponible

Cependant, vous vous doutez bien qu'utiliser des racines carrées ou des fonctions trigonométriques n'est pas particulièrement efficace. De plus, il ne s'agit pas seulement de placer des points, mais il faut qu'ils soient reliés entre eux de manière harmonique afin de dessiner un cercle qui ait un aspect régulier.

C'est là que revient notre ami Bresenham, déjà auteur de l'algorithme de tracé de ligne; qui nous présente un algorithme de tracé élégant et surtout très efficace, n'utilisant que de l'arithmétique entière.

Pour commencer, il n'est pas nécessaire de calculer les coordonnées des points du cercle en entier. Il suffit de calculer qu'un simple huitième de ce cercle, par exemple l'arc compris entre 0 et 45 °. Les autres morceaux peuvent être dessinés par symétrie. Ensuite, si on regarde de près la forme de l'arc dans ce cas, on constate une grande similitude avec le tracé de ligne.

Image non disponible

Comme vous pouvez le constater sur l'illustration, il y a, pour une ordonnée Y donnée, un et un seul point correspondant. Dessiner la ligne reviendra donc à faire une boucle qui parcourt l'axe des Y et qui calcule l'abcisse X du point. De plus, quand on a un point du cercle, le prochain point aura soit la même abcisse, soit un point à gauche du précédent. C'est donc très proche du tracé de ligne.

4.2. Implémentation

Sélectionnez

void cercle(int cx, int cy, int rayon, int coul)
{
  int d, y, x;

  d = 3 - (2 * rayon);
  x = 0;
  y = rayon;

  while (y >= x) {
    setPixelVerif(cx + x, cy + y, coul);
    setPixelVerif(cx + y, cy + x, coul);
    setPixelVerif(cx - x, cy + y, coul);
    setPixelVerif(cx - y, cy + x, coul);
    setPixelVerif(cx + x, cy - y, coul);
    setPixelVerif(cx + y, cy - x, coul);
    setPixelVerif(cx - x, cy - y, coul);
    setPixelVerif(cx - y, cy - x, coul);

    if (d < 0)
      d = d + (4 * x) + 6;
    else {
      d = d + 4 * (x - y) + 10;
      y--;
    }

    x++;
  }
}

Voilà pour le cercle. Pour dessiner un disque (cercle rempli), on va simplement tracer des lignes horizontales entre chaque couple de points situé sur une même ligne. C'est pour cette raison que nous avons implémenté une version "rapide" du tracé de ligne horizontale parce que ça nous permet de remplir rapidement des formes.

Sélectionnez

void disque(int cx, int cy, int rayon, int coul)
{
  int d, y, x;

  d = 3 - (2 * rayon);
  x = 0;
  y = rayon;

  while (y >= x) {
    ligneHorizontale(cx - x, cy - y, 2 * x + 1, coul);
    ligneHorizontale(cx - x, cy + y, 2 * x + 1, coul);
    ligneHorizontale(cx - y, cy - x, 2 * y + 1, coul);
    ligneHorizontale(cx - y, cy + x, 2 * y + 1, coul);

    if (d < 0)
      d = d + (4 * x) + 6;
    else {
      d = d + 4 * (x - y) + 10;
      y--;
    }

    x++;
  }
}

Les deux fonctions prennent 4 paramètres : les coordonnées du centre, le rayon et le code couleur.

5. Conclusion

Cet article vous a présenté la manière de tracer les figures géométriques élémentaires à l'aide de la seule fonction de posage de points. Il est facile de réutiliser ces fonctions pour une autre bibliothèque en utilisant une autre implémentation de setPixel().

Voici un programme contenant toutes les fonctions présentées ainsi qu'une fonction générale de test qui poussent les fonctions de tracés à leurs limites (il faut enfoncer une touche pour passer des lignes aux cercles).


Tutoriel SDL Morpion

Programmation de Jeux 2D: Un morpion en SDL

Cette première série de tutoriels traitera de la programmation 2D. Nous présenterons comment faire facilement un jeu simple: un morpion! Nous verrons comment ouvrir une fenêtre SDL, comment gérer l'affichage, les clics de la souris, un menu et une intelligence artificielle.

Bonne lecture!

I. Morpion

Bienvenue à ce groupe de tutoriels sur le morpion. En espérant qu'ils répondront à vos attentes concernant la programmation de ce petit jeu, je vous laisse et vous souhaite une bonne lecture.

Tutoriels :
  • Ouvrir une fenêtre SDL;
  • Lier la souris à la fenêtre et afficher des ronds à l'endroit cliqué;
  • Ajouter les règles du jeu et le finaliser;
  • Ajouter les classes Objet et Moteur pour rendre le jeu plus souple;
  • Ajouter un menu dans un jeu;
  • Ajouter une Intelligence Artificielle (Min-Max);
  • Ajouter une Intelligence Artificielle (Alpha-Beta); Image non disponible

Pour toute remarque, suggestion, question ou correction, vous pouvez me contacter par message privé.


Tutoriel CSS3

Programmation de Jeux 2D: Un morpion en SDL, Première partie

  • 1. Introduction
    • 1.1. SDL
    • 1.2. Motivation
    • 1.3. Plan
  • 2. Ouvrir une fenêtre SDL
    • 2.1. Initialisation
    • 2.2. Boucle globale
    • 2.3. Compilation
  • 3. Conclusion
  • 4. Téléchargements

Beaucoup de débutants tentent de programmer des jeux mais ne savent pas trop par où commencer. En effet, les choses à gérer sont nombreuses et il est souvent facile de se perdre et donc de laisser tomber.
Cet article et les suivants aideront, je l'espère, à montrer les fondements de l'élaboration d'un petit moteur 2D utilisant la bibliothèque SDL.

1. Introduction

Bienvenue aux tutoriels sur la programmation de jeux en 2D et en utilisant la bibliothèque SDL. Beaucoup de personnes commencent par faire de la 2D avant de passer au 3D. Certes, la troisième dimension permet de faire beaucoup de choses qui risquent de surprendre vos amis et votre famille, mais si vous n'êtes pas encore capables de faire un petit morpion ou un pong, il sera difficile de faire un remake de Half-Life...

C'est donc dans cette optique que je vais présenter comment faire des jeux en 2D, comment gérer l'affichage, le clavier, la souris et le son. Nous allons commencer par des présentations générales pour ensuite travailler sur des sujets de plus en plus spécifiques.

1.1. SDL

Ces tutoriels utiliseront la bibliothèque portable SDL.

La SDL, Simple DirectMedia Layer, est une bibliothèque destinée à permettre l'accès au matériel graphique pour faire, par exemple, des jeux en plein écran (ou en fenêtre), et ça de manière portable. La SDL peut notamment fonctionner sous Linux (où elle utilise X11), comme sous Windows (où elle utilise DirectX).

Il existe beaucoup d'extensions pour permettre d'inclure du son, faire des transformations de base sur des surfaces, charger des images PNG, TGA... Nous allons voir ici comment créer un moteur 2D avec cette bibliothèque.

La SDL est disponible en licence libre LGPL. Cela veut dire que vous pouvez donc librement utiliser cette bibliothèque dans votre application, qu'elle soit libre ou commerciale. La seule obligation est que la bibliothèque SDL doit rester en liaison dynamique.

Où télécharger la bibliothèque SDL: http://www.libsdl.org/index.php

Comment compiler avec la bibliothèque SDL?

Sous Linux/Unix, on utilise l'outil sdl-config pour compiler un programme SDL.

Par exemple:

Compilation d'un programme SDL
Sélectionnez

g++ -o main  main.cpp `sdl-config --cflags --libs`

Pour une présentation plus en détail de la bibliothèque SDL ou pour l'installation sous windows, le tutoriel d'Anomaly explique comment installer SDL avec Dev-C++ (voir Programmation graphique portable en C avec la SDL)

1.2. Motivation

Souvent lorsqu'on veut faire un jeu, on se jette sur la programmation et on se perd tellement rapidement que le projet tombe à l'eau. Bien que certains projets arrivent à décoller avec cette technique appelée l'Extreme Programming, pour un débutant, c'est presque cause perdue. La motivation de ces tutoriels est donc de présenter comment créer un moteur 2D pas à pas qui permet de faire tourner un jeu sans perdre ses cheveux.

Lorsqu'on commence à faire un projet, il ne faut jamais tenter l'impossible. Il est inutile de tenter de faire un moteur 3D si on ne sait pas faire un morpion en 15 minutes. Ces tutoriels vont servir comme préparation à la création d'un tel moteur. Nous allons donc expliquer comment faire ce morpion :-). Mais aussi comment créer une fenêtre, puis comment créer un petit morpion. Une fois que nous avons réussi à faire cela, nous allons compliquer le moteur pour qu'il puisse gérer un pong. Finalement, nous allons montrer comment faire tourner un casse-brique.

Une chose importante à savoir, c'est que ces petits jeux ne sont pas vraiment le but ultime de ces tutoriels. Arriver à faire un morpion n'est pas en soit une chose compliqué (ni vraiment un pong) mais arriver à créer un code robuste, réutilisable est une autre paire de manches. Donc pour chaque jeu, je vais tenter de proposer des extensions qui compliqueront le jeu de base mais ajouteront le petit plus qu'on cherche tous.

1.3. Plan

Ce groupe de tutoriels va donc montrer les techniques de base que j'utilise pour créer des petits jeux en 2 dimensions en utilisant la bibliothèque SDL. Je vais commencer par un des jeux les plus faciles à implémenter: le morpion.

Le but de cette première partie est de se familiariser avec cette bibliothèque. Nous verrons dans les parties suivantes que, finalement, l'implémentation du morpion n'est qu'un détail par rapport au but recherché: programmer un jeu. Ces tutoriels supposeront une petite connaissance en C++ (de toute façon, je suis plutôt un programmeur C, donc la partie C++ restera relativement simple).
Ce premier tutoriel sera divisé en huit parties:

Plan des tutoriels
  1. Ouvrir une fenêtre SDL;
  2. Lier la souris à la fenêtre et afficher des ronds à l'endroit cliqué;
  3. Ajouter les règles du jeu et le finaliser
  4. Ajouter une classe Objet pour rendre le jeu plus souple
  5. Ajouter un menu dans un jeu
  6. Ajouter une Intelligence Artificielle
  7. Ajouter du son
  8. Extensions

Attention, cet article ne parlera pas tout le temps d'optimisation de code ou de technique pour rendre l'affichage le plus rapide possible. Nous commencerons d'abord par faire un code qui marche et, seulement après, nous nous intéresserons aux optimisations possibles.

2. Ouvrir une fenêtre SDL

2.1. Initialisation

Puisque le code de cette première partie tient sur un seul fichier, je vais simplement expliquer chaque partie du code de façon linéaire... Lorsque le code commencera à être plus compliqué, ne vous inquiétez pas, les explications seront plus thématiques. Pour l'instant, intéressons-nous à la création d'une fenêtre SDL. Le programme commence par de simples inclusions de fichiers d'entête.

Inclusions
Sélectionnez

#include <SDL.h>
#include <iostream>

Ensuite, bonne ou mauvaise habitude, lorsque je crée une fenêtre, j'utilise des constantes pour définir la taille de la fenêtre. Pourquoi? Parce que si le code est en fonction de ces constantes, il sera plus facile de passer à une version qui a une taille de fenêtre modifiable. Mais nous n'en sommes pas encore là.

Définitions de constantes
Sélectionnez

const int WIDTH=640;
const int HEIGHT=480;

Finalement, nous définissons la fonction main, elle contiendra l'initialisation SDL, la création de la fenêtre et la boucle événementielle.

Déclarations des variables locales
Sélectionnez

int main(int argc, char **argv)
{
    //Déclaration des variables
    SDL_Event event;
    SDL_Surface *screen;
    int done = 0;

L'initialisation de SDL se fait avec la fonction SDL_Init. Puisque nous voulons ouvrir une fenêtre, nous passons le flag SDL_INIT_VIDEO. Comme tout bon programmeur, on teste le retour de SDL_Init. Il faut toujours tester le retour des fonctions SDL (et les autres d'ailleurs).

Initialisation de SDL
Sélectionnez

    //Initialisation de SDL
    if(SDL_Init(SDL_INIT_VIDEO)!=0) {
        std::cerr << "Problème pour initialiser SDL\n" << SDL_GetError() << std::endl;
        return 1;
    }

Nous pouvons si nous le désirons, donner un titre à la fenêtre obtenue:

Mise en place d'un titre pour la fenêtre
Sélectionnez

    //Mettre un titre à la fenêtre
    SDL_WM_SetCaption("Programme SDL de base", NULL);

Je mets cet appel avant la création du programme parce que, si vous voulez aller plus loin, vous pouvez appeler la fonction SDL_WM_SetIcon qui elle doit être appelée avant le premier appel de la fonction qui crée une surface de fenêtre: SDL_SetVideoMode. La fonction SDL_WM_SetIcon permet de définir quelle icône sera utilisée par la fenêtre. Ensuite on récupère une surface pour la fenêtre avec la fonction SDL_SetVideoMode.

Création de la fenêtre
Sélectionnez

    //Ouvrerture d'une surface pour l'affichage de la fenêtre
    screen = SDL_SetVideoMode(WIDTH,HEIGHT, 32,
         SDL_DOUBLEBUF | SDL_HWSURFACE);
    if(screen==NULL)
         done = 1;

Les paramètres de cette fonction sont:

Paramètres de la fonction SDL_SetVideoMode
  1. La largeur de la fenêtre;
  2. La hauteur de la fenêtre;
  3. Le nombre de bits par pixel (32 veut donc dire 3 couleurs et un octet pour l'alpha);
  4. Quel genre de fenêtre... Nous demandons un double buffer et on demande de le mettre dans la mémoire video (Voir la FAQ pour plus de détails).

2.2. Boucle globale

Finalement, nous avons une boucle infinie (du moins tant que le programme doit continuer). Le programme sortira de la boucle lorsque le programme n'a plus besoin de la fenêtre. Cela se traduit par: lorsque done==1, nous sortirons de la boucle et le programme se terminera.

Boucle générale
Sélectionnez

    //Boucle générale
    while(!done)
    {

Dans cette boucle englobante, se trouve une boucle événementielle, nous allons d'abord regarder les événements à gérer. Comme beaucoup de bibliothèques du genre, SDL fonctionne avec une pile d'événements (comme l'API Windows par exemple). Chaque événement (clic souris, frappe de clavier, changement de l'état de la fenêtre, etc...) provoque un appel à la fonction

Prototype de fonction
Sélectionnez

int SDL_PushEvent(SDL_Event *event);

Logiquement, il faut vider cette pile et nous le faisons avec une deuxième boucle. Pour récupérer ce qui se trouve dans la pile, nous utilisons la fonction

Prototype de fonction
Sélectionnez

int SDL_PollEvent(SDL_Event *event);

La boucle interne la plus simple (avec aucune gestion des événements) ressemble donc à:

Boucle événementielle générale
Sélectionnez

    //Traiter les événements
    while(SDL_PollEvent(&event))
        {
        //Pour chaque événement, on regarde d'abord le type
        switch(event.type)
            {
            //Gestion de l'événement en fonction de son type

            //Si on ne veut pas le gérer, on ne fait rien
            default:
                break;
            }
        }

Donc dans notre cas, nous avons:

Notre Boucle événementielle
Sélectionnez

    //Traiter les évènements
    while(SDL_PollEvent(&event))
    {
         switch(event.type)
         {
            //Si on veut quitter, done=1 suffira
            case SDL_QUIT:
                   done=1;
                   break;
            //Si on vient de relâcher une touche de clavier
            case SDL_KEYUP:
                //Et si c'est la lettre q
                   if(event.key.keysym.sym==SDLK_q)
                //On met done a 1 pour quitter
            done=1;
                    break;
            //Sinon on ne fait rien
            default:
                break;
         }
    }

Les commentaires pourraient suffire pour la compréhension de cette boucle mais je vais prendre quelques minutes pour mieux expliquer. Pour chaque événement, on peut affecter un comportement particulier. Pour distinguer le comportement, on utilise donc un switch avec la valeur de event.type.

Ainsi, si l'événement est de type SDL_QUIT, cela veut dire que quelque part dans le code, on a demandé de quitter le programme, il semble naturel de terminer cette boucle. En positionnant done à 1, on assure que la boucle englobante ne fera pas une autre itération mais on terminera quand même les événements restantes sur la pile. Cela semble tout de même plus propre.

Le deuxième type d'événement est SDL_KEYUP qui est lancé lorsque l'utilisateur a appuyé sur une touche et la relâche. Lorsque nous recevons ce type d'événement, il semble également logique de vérifier quelle touche a été relâchée. Si c'est le 'q', on va demander de quitter (en positionnant done à 1) (1) Si nous voulions faire les choses dans les règles, une solution élégante serait de poser sur la pile un événement SDL_QUIT pour n'avoir le code de fin de programme à un seul endroit. Ce sera une amélioration pour la suite.

Après notre boucle externe, on quitte avec SDL_Quit.

Nettoyage mémoire
Sélectionnez

SDL_Quit();

2.3. Compilation

Finalement, pour compiler le programme SDL, il suffit de faire ceci:

Compilation
Sélectionnez

g++ -o main Main.cpp `sdl-config --cflags --libs`

3. Conclusion

Voilà pour cette première partie, nous avons fait une introduction de l'utilisation de la bibliothèque SDL et ce n'est sûrement pas inutile.

Par la suite, nous verrons comment lier la souris à l'application et comment associer les clics de souris à des zones de la fenêtre.

Jc

Liens
  • Sommaire du tutoriel;
  • Ouvrir une fenêtre SDL;
  • Lier la souris à la fenêtre et afficher des ronds à l'endroit cliqué;
  • Ajouter les règles du jeu et le finaliser;
  • Ajouter les classes Objet et Moteur pour rendre le jeu plus souple;
  • Ajouter un menu dans un jeu;
  • Ajouter une Intelligence Artificielle (Min-Max);
  • Ajouter une Intelligence Artificielle (Alpha-Beta);

4. Téléchargements

Voici le code source pour ce premier tutoriel: (3 Ko).

Voici la version pdf de cet article: (37 Ko).

Si vous avez des suggestions, remarques, critiques, si vous avez remarqué une erreur, ou bien si vous souhaitez des informations complémentaires, n'hésitez pas à me contacter !


Tutoriel SDL Morpion

Programmation de Jeux 2D: Un morpion en SDL, Deuxième partie

  • I. Introduction
    • I-A. Plan
  • II. Le programme
    • II-A. Define.h
    • II-B. Les fichiers Jeu
      • II-B-1. Ajout de Jeu.h
      • II-B-2. Ajout de Jeu.cpp
        • II-B-2-a. Constructeur
        • II-B-2-b. Destructeur
        • II-B-2-c. Fonction init
        • II-B-2-d. Fonction d'affichage
        • II-B-2-e. Fonction clic
    • II-C. Les fichiers Main
      • II-C-1. Ajout de Main.h
      • II-C-2. Modification de Main.cpp
  • III. Fichiers de données
  • IV. Conclusion
  • V. Téléchargements

Dans cette partie, nous allons voir comment ajouter le début du code qui s'occupera du morpion. On verra les concepts de bases pour la gestion de l'affichage et de la souris.

I. Introduction

Bienvenue à la deuxième partie de ce tutoriel. Nous allons voir comment intégrer au code de la première partie, du code qui permettra d'afficher un plateau de morpion et gérer le clic de la souris.

Nous pourrions faire ce programme en un seul fichier. Et cela montrerait aussi comment faire un morpion, mais si vous essayez de faire des programmes plus compliqués, très rapidement vous allez vous perdre. Nous allons donc présenter comment séparer correctement (enfin, comment je le fais) le code des programmes.

Nous allons prendre le code de la première partie (Voici le lien : .cpp (3 Ko)) et nous allons le compléter pour qu'il affiche correctement le jeu et gère le clic de la souris.

I-A. Plan

Le nouveau code est réparti en 5 fichiers. Cette page sera décomposée par rapport aux nouveaux fichiers. Voici une présentation rapide :

Fichiers du projet

  1. Define.h : Définition des constantes
  2. Main.h et Main.cpp : Presque identique à la première version
  3. Jeu.h et Jeu.cpp : L'affichage et gestion du plateau de jeu

II. Le programme

II-A. Define.h

Tout d'abord, nous allons centraliser les inclusions et les constantes du programme dans un fichier Define.h. Tous les fichiers du projet incluront donc ce fichier s'ils ont besoin des constantes générales du programme.

Centralisation des constantes
Sélectionnez

#ifndef H_DEFINE
#define H_DEFINE

#include <SDL.h>
#include <iostream>

const int WIDTH=600;
const int HEIGHT=600;

#endif

II-B. Les fichiers Jeu

Pour rendre le programme le plus réutilisable possible, nous allons séparer le plus possible le code qui s'occupe du morpion du reste du code. La classe Jeu présentée ici sera donc le point d'entrée du code qui gère la souris et s'occupe de l'affichage du plateau de jeu. L'avantage de faire cette séparation est dans un esprit de portabilité. En effet, tout ce qui n'est pas en rapport avec le morpion peut être réutilisé pour un autre jeu.

En effet, dans la première partie, nous avons montré comment est utilisée la boucle générale SDL. Par exemple, DirectX utilise la même solution mais, bien sûr, les structures et les fonctions ne sont pas les mêmes. En séparant donc notre gestion du jeu de cette boucle, notre programme pourra facilement passer d'un système à l'autre.

Il possède donc certaines fonctions pour afficher/gérer les clics souris ou le clavier de façon indépendante à la représentation ou technique utilisée par SDL/DirectX/Glut/GTK/etc.

II-B-1. Ajout de Jeu.h

Maintenant, à la place d'avoir le code du jeu dans le main, nous allons le mettre dans un fichier Jeu.cpp. Pour pouvoir utiliser cette classe dans le fichier Main.cpp, nous avons besoin d'un fichier d'en-tête. Nous commencerons donc par présenter le fichier Jeu.h.

Définition de la classe Jeu
Sélectionnez

#ifndef H_JEU
#define H_JEU

#include "Define.h"

//Enumération des différentes possibilités d'une case du jeu
enum Case {
    Vide=0,
    Rond,
    Croix
};

//Classe du jeu
class Jeu
{
    private:
        //Le plateau jeu
        Case plateau[3][3];

        //Surfaces d'un rond, d'une croix et d'un fond
        SDL_Surface *o, *x, *bg;

        //Variable pour un tour
        Case tour;

    public:
        //Créateur/Destructeur
        Jeu();
        ~Jeu();

        //Fonction d'initialisation (chargement des surfaces)
        bool init();

        //Gestion du jeu lors d'un clic
        void clic(int , int);

        //Fonction d'affichage
        void aff(SDL_Surface *screen);
};
#endif

Commençons par expliquer les variables privées de la classe. Nous avons trois groupes de variables pour cette classe :

  1. Le plateau de jeu pour le morpion est bien sûr un tableau de dimension [3][3]. Le type d'une case est donné par une énumération Case. Chaque case peut être de type Vide, Rond ou Croix;
  2. Les surfaces d'un rond, d'une croix et de l'image de fond. Avec la bibliothèque SDL, les images sont définies par des surfaces;
  3. Et finalement, pour savoir qui doit jouer, nous utiliserons la variable tour. Cette variable pourra donc prendre les valeurs Rond ou Croix.

II-B-2. Ajout de Jeu.cpp

Présentons, à présent, les fonctions membres de la classe Jeu.

II-B-2-a. Constructeur

Constructeur de Jeu
Sélectionnez

Jeu::Jeu()
{
    int i,j;

    //On met toutes les cases à Vide
    for(i=0;i<3;i++)
        for(j=0;j<3;j++)
            plateau[i][j] = Vide;

    //Valeur par défaut pour les surfaces
    o=NULL;
    x=NULL;
    bg=NULL;
}

En quelques lignes, nous avons initialisé le tableau du jeu a Vide. Ensuite, nous mettons les pointeurs vers les surfaces à NULL. Ce sera le travail de la fonction init de charger les images.

II-B-2-b. Destructeur

Bien sûr, dans le destructeur, nous allons libérer les surfaces allouées (remarquons que, comme pour free/delete, nous pouvons passer NULL à ces fonctions) :

Destructeur
Sélectionnez

Jeu::~Jeu()
{
    //On libére les surfaces
    SDL_FreeSurface(o);
    SDL_FreeSurface(bg);
    SDL_FreeSurface(x);
}

II-B-2-c. Fonction init

La fonction init permettra de charger les images dans les surfaces de la classe. Si jamais il y a un problème, la fonction rendra false.

Fonction init
Sélectionnez

bool Jeu::init()
{
    //Vérification de l'allocation des surfaces
    if(o!=NULL)
        {
            SDL_FreeSurface(o), o = NULL;
        }
    if(x!=NULL)
        {
            SDL_FreeSurface(x), x = NULL;
        }
    if(bg!=NULL)
        {
            SDL_FreeSurface(bg), bg = NULL;
        }

    //On charge toutes les images dans les surfaces associées
    o = SDL_LoadBMP("o.bmp");
    x = SDL_LoadBMP("x.bmp");
    bg = SDL_LoadBMP("bg.bmp");

    //On teste le retour du chargement
    if( (o==NULL) || (x==NULL) || (bg==NULL))
        {
        cout << "Probleme de chargement du O, du X ou de l'image de fond" << endl;
        return false;
        }

    //On initialise le premier tour: ce sera Rond qui commencera
    tour = Rond;

    //Mis en place de la transparence
    if(SDL_SetColorKey(o,SDL_SRCCOLORKEY,0)==-1)
        cout << "Erreur avec la transparence du rond" << endl;

    if(SDL_SetColorKey(x,SDL_SRCCOLORKEY,0)==-1)
        cout << "Erreur avec la transparence de la croix" << endl;

    return true;
}

Avant de commencer le chargement des images, nous vérifions d'abord que les images n'ont pas déjà été chargées. Si c'est le cas, nous libérons la mémoire. Je tiens à souligner la technique suivante :

Une bonne habitude
Sélectionnez

    if(o!=NULL)
        {
            SDL_FreeSurface(o), o = NULL;
        }

Cela permet d'être sûr que personne ne s'amuse à insérer du code entre la libération et la mise à NULL du pointeur. Ce genre d'erreur est souvent évité en mettant les deux instructions sur la même ligne, separées d'une virgule. C'est d'ailleurs la seule fois où vous me verrez mettre deux instructions sur une même ligne ou utiliser une virgule.

Nous utilisons la bibliothèque SDL pour charger les images du fond d'écran (la grille du morpion), l'image d'un rond et l'image d'une croix. Comme vous le voyez, le chargement d'un fichier BMP se fait simplement. Par contre, il faudra utiliser l'extension de SDL_image pour charger d'autres formats d'image

Remarque, en principe, il est bon d'utiliser la fonction SDL_DisplayFormat pour avoir le même format pour toutes les surfaces. C'est une optimisation facile à mettre en place et importante. Nous ne le faisons pas ici parce que nous voulons d'abord un programme qui fonctionne et ensuite nous procéderons à des optimisations.

Après le chargement, nous vérifions que le chargement s'est bien passé. Ensuite, nous initialisons tour à Rond pour que le premier joueur joue avec les ronds. Comme beaucoup d'images que nous chargerons pour l'affichage à l'écran, nous avons besoin d'une couleur transparente. Généralement, nous utilisons la couleur magenta (puisqu'elle est rarement utilisée dans les images) mais ici nous utilisons le noir. Donc, pour définir la couleur noire comme couleur transparente, nous utilisons la fonction :

Prototype de SDL_SetColorKey
Sélectionnez

int SDL_SetColorKey(SDL_Surface *surface, Uint32 flag, Uint32 key);

Cette fonction retourne -1 en cas d'erreur. Comme vous pouvez le remarquer, je ne fais qu'un simple affichage dans le cas d'une erreur. SDL possède un comportement différent dépendant du Système d'Exploitation pour l'affichage texte (via cout sous C++ et via printf/fprintf sous C). Sous Windows, il crée un fichier stdout.txt et stderr.txt et, sous linux, il laisse le comportement natif (donc via le terminal qui a lancé le programme). Pour les problèmes mineurs, j'affiche simplement qu'il y a eu une erreur mais je ne vais pas sortir du programme pour autant.

Revenons à la fonction SDL_SetColorKey, la variable flag peut prendre ces masques :

  • SDL_SRCCOLORKEY : pour définir la couleur transparente;
  • SDL_RLEACCEL : pour définir l'accélération RLE si possible. En résumé, cette option permet d'accélérer l'affichage pour des images ayant beaucoup de pixels transparents.

Le dernier paramètre de cette fonction permet de donner la couleur qui deviendra transparente lorsque que nous copierons la surface. Sans rentrer trop dans les détails, il existe trois canaux de couleurs en informatique : le rouge, le vert et le bleu. Mais, nous avons aussi un canal alpha qui servira pour faire du blending (par exemple). Par contre, SDL_SetColorKey demande à ce que ce dernier paramètre soit donné sous la forme d'un entier à 32 bits. Puisqu'il est difficile de calculer directement la correspondance (r,g,b,a) vers ce format à 32 bits, SDL fournit donc une fonction qui permet de le faire :

Prototype de SDL_MapRGB
Sélectionnez

Uint32 SDL_MapRGB(SDL_PixelFormat* fmt, Uint8 r, Uint8 g, Uint8 b)

et si nous voulons spécifier le canal alpha :

Prototype de SDL_MapRGBA
Sélectionnez

Uint32 SDL_MapRGBA(SDL_PixelFormat* fmt, Uint8 r, Uint8 g, Uint8 b, Uint8 a)

Le premier paramètre est fourni par la surface qui nous intéresse. Donc si nous prenons notre code précédent, nous avions :

Solution proposée
Sélectionnez

    if(SDL_SetColorKey(x,SDL_SRCCOLORKEY,0)==-1)
        cout << "Erreur avec la transparence de la croix" << endl;

Nous aurions pu aussi écrire :

Solution alternative
Sélectionnez

    if(SDL_SetColorKey(x,SDL_SRCCOLORKEY,SDL_MapRGB(x->format,0,0,0))==-1)
        cout << "Erreur avec la transparence de la croix" << endl;

II-B-2-d. Fonction d'affichage

Présentons maintenant la fonction d'affichage. Cette fonction est assez simple, elle affichera l'image de fond, puis parcourt le tableau et affiche chaque case non vide. Puisque nous affichons un jeu de morpion, nous allons diviser la zone d'affichage de la fenêtre en trois colonnes et trois lignes.

Rappelons d'abord son prototype :

Prototype de la fonction d'affichage
Sélectionnez

void affichage(SDL_Surface *screen);

Cette fonction prend donc en paramètre la surface sur laquelle nous voulons dessiner l'image courante. Généralement, nous passerons la surface qui a été rendue par la fonction SDL_SetVideoMode.

Pour afficher une surface, nous déclarons un rectangle avec la structure SDL_Rect. En effet, pour afficher une surface on utilise la fonction (1).

Prototype de SDL_BlitSurface
Sélectionnez

int SDL_BlitSurface(SDL_Surface *src, SDL_Rect *srcrect, SDL_Surface *dst, SDL_Rect *dstrect);

Le premier argument est l'image que l'on veut copier. Le deuxième argument est la partie de la surface source que nous voulons copier (si nous mettons NULL, toute la surface source est utilisée). Le troisième argument est la surface destination et le quatrième argument est la position que va prendre la copie de src (s'il est égal à NULL, alors la position est (0,0)).

Je dis bien "position". En effet, la version actuelle de SDL n'utilise pas la taille du rectangle pour faire l'affichage. Il n'y a donc pas de zoom possible avec la bibliothèque SDL de base. Il faudra utiliser l'extension SDL_gfx pour faire des zooms/rotations ou alors faire les transformations à la main.

Donc la fonction d'affichage commence par le calcul de la largeur et hauteur d'une case et l'initialisation de la structure r qui sera la position où copier les ronds et les croix.

La fonction d'affichage
Sélectionnez

void Jeu::aff(SDL_Surface *screen)
{
    //Le couple (w,h) représentera les dimensions d'une case du plateau
    int w = WIDTH/3, h=HEIGHT/3,i,j,k,l;
    SDL_Rect r = { 0 };

    //Dessiner le fond d'ecran
    SDL_BlitSurface(bg,NULL,screen,&r);

    //On parcourt les cases du tableau, r sera le SDL_Rect qui représentera la position de la case courante
    for(k=0,i=0;i<HEIGHT;i+=h,k++)
        {
        r.y = i;
        for(j=0,l=0;j<WIDTH;j+=w,l++)
            {
            r.x = j;

            //On dessine en fonction du type de la case
            if(plateau[k][l]==Croix)
                {
                SDL_BlitSurface(x, NULL, screen,&r);
                }
            else if(plateau[k][l]==Rond)
            {
                SDL_BlitSurface(o, NULL, screen,&r);
                }
            }
        }
}

Dans cette fonction d'affichage, nous commençons par la copie de l'image de fond. En principe, on met d'abord une couleur de fond (généralement le noir) sur toute la surface de la fenêtre, mais ici, notre image de fond occupera toute la fenêtre, donc cette mise à zéro est inutile.

Ensuite, nous avons un nid de boucles qui permet de parcourir le plateau du jeu. Nous allons donc pouvoir dessiner les ronds et les croix. Nous allons avoir deux variables par dimensions : le couple (k,l) parcourrait le tableau jeu de la classe et le couple (i,j) représentera la position courante de la case à afficher.

Puis, le corps de la boucle met à jour la position de la variable r (la position destination de la surface copiée). Finalement, un simple test vérifie si la case courante est une croix ou un rond et affiche la surface associée.

Pourquoi avoir deux couples ? Pour une petite raison d'optimisation et simplification du code. Nous pouvons facilement remarquer, qu'à tout moment de cette boucle, nous avons l'équivalence :

Rapport entre (i,j) et (k,l)
Sélectionnez

    i == k*h
    j == l*w

Donc nous pourrions écrire la mise à jour de la position de r comme ceci :

Solution alternative
Sélectionnez

    r.x = l*w;
    r.y = k*h;

Mais, nous échangeons donc une paire de sommes par deux multiplications. Une multiplication prenant plus de temps qu'une somme, je préfère limiter leur nombre. Par contre, il est déconseillé de trop tenter d'optimiser lors d'un début de projet. Les déclarations de boucles for avec plusieurs itérateurs sont généralement des mauvaises idées. Je me le permets ici parce ce que cela ne complique pas vraiment le code ou sa lecture.

II-B-2-e. Fonction clic

La fonction clic gère l'évènement souris. Rappelons d'abord son prototype :

Prototype de la fonction clic
Sélectionnez

void clic(int x, int y);

Elle prend comme argument les coordonnées du clic souris. Comme pour l'affichage, nous allons commencer par calculer la largeur et hauteur d'une case.

Calcul de la dimension d'une case
Sélectionnez

    //On récupère la largeur et l'hauteur d'une case
    w = WIDTH/3;
    h = HEIGHT/3;

Ensuite, nous calculons la case associée à la position de la souris lors du clic avec le calcul suivant :

Transformation (x,y) -> (i,j)
Sélectionnez
    //Calcul de la case associée
    i = y/h;
    j = x/w;

Chaque case du morpion fait w pixels de large et h pixels de haut. Logiquement, si on prend la division entière (y/h, x/w) nous aurons la case associée. Remarquez l'inversion des coordonnées. Ceci arrive souvent lorsque nous passons de l'affichage à l'interprétation des données du jeu. En effet, par convention en C/C++ et d'autres langages, la 1ère dimension d'un tableau à 2 dimensions est considérée comme les lignes du tableau. Donc (2, 3) représente la case qui se trouve à la 3ème ligne (on commence à zéro) et la 4ème colonne. Or, à l'affichage, (53, 123) représente souvent le 54ème pixel (on commence aussi à zéro!) de la 124ème ligne! Il faut donc faire attention et programmer en connaissance de cause.

Enfin, nous allons mettre à jour la valeur de la case si la case est de type Vide. Ahhh, après tant de discussions, nous arrivons à la première ligne de code qui est intimement liée au fait que nous programmons un morpion. En effet, le fait que le clic n'est pris en compte que si la case est vide est déjà un bon point. Remarquons qu'après la mise à jour de la case (si elle est effectuée), nous mettons également à jour la variable tour.

Mise a jour du plateau de jeu
Sélectionnez

    //Si la case est vide, on met à jour son type et la variable tour
    if(plateau[i][j]==Vide)
        {
        plateau[i][j] = tour;
        tour = (tour==Rond)?Croix:Rond;
        }

II-C. Les fichiers Main

Nous arrivons au fichier Main.cpp. D'abord, nous allons ajouter un fichier Main.h. J'ai l'habitude d'avoir une seule inclusion dans les fichiers sources vers un fichier d'en-tête du même nom. Donc, bien que le fichier Main.cpp ne nécessite que l'inclusion de Define.h, je crée quand même le fichier Main.h (c'est aussi pour simplifier mon makefile...).

II-C-1. Ajout de Main.h

Le fichier Main.h
Sélectionnez

#ifndef H_MAIN
#define H_MAIN

#include "Define.h"

#endif

II-C-2. Modification de Main.cpp

Le Main.cpp commence par l'inclusion des fichiers d'entête Main.h et Jeu.h. Ensuite, nous déclarons la variable globale jeu. Personnellement, je ne suis pas contre l'utilisation des variables globales tant que nous les réduisons au minimum.

Début du fichier Main.cpp
Sélectionnez

#include "Main.h"
#include "Jeu.h"
Jeu jeu;

Ensuite, l'initialisation SDL ne change pas par rapport à la première version. On ajoute tout de même un appel vers la fonction init de l'instance jeu. Si jamais l'initialisation de la fonction init retourne false, nous sortons de la fonction main, mettons une fin au programme.

Initialisation du jeu
Sélectionnez

    if(!jeu.init())
        return 1;

Puis, dans la boucle générale, deux lignes sont ajoutées à la fin de la boucle :

Ajout de l'appel d'affichage et SDL_Flip
Sélectionnez

    jeu.aff(screen);
    SDL_Flip(screen);

Nous demandons au jeu de dessiner l'état du morpion à l'écran. Une fois que le l'instance jeu dessine le plateau de jeu, nous appelons la fonction SDL_Flip. Je vais maintenant prendre un peu plus de détails sur le double buffering.

Le double buffering est une technique pour améliorer l'affichage d'un jeu. Ce qu'il faut savoir, c'est que la carte video affiche l'information contenue dans la mémoire video et, qu'en même temps, à travers l'appel jeu.aff(screen), nous calculons les couleurs des pixels de la fenêtre.

Forcément, si nous modifions des pixels pendant que la carte video les affiche, il peut y avoir un problème de synchronisation, ce qui peut se traduire par un scintillement. La solution est donc d'afficher une image pendant qu'on travaille sur une autre image. Lorsque le calcul de la prochaine image est finie, on "flip" (retourne en français) les images.

L'utilisation du mot flip se traduit par l'analogie qu'on vous montre le verso d'une feuille pendant que le programme dessine sur le recto. Lorsque le dessin est fini, on retourne la feuille et on recommence à travailler sur le recto pendant que vous regardez le "nouveau" verso.

Le dernier changement dans la fonction main se trouve dans la boucle événementielle. Nous y ajoutons la gestion du clic souris. Lorsqu'un tel évènement se produit, nous appelons la fonction membre clic de la classe Jeu en lui transmettant les coordonnées de la souris.

Ajout de l'événement clic de souris
Sélectionnez

    case SDL_MOUSEBUTTONUP:
        jeu.clic(event.button.x, event.button.y);
        break;

III. Fichiers de données

L'utilisation de fichiers bitmap pour l'affichage ajoute donc à ce projet des fichiers de données. Nous verrons par la suite d'autres exemples de fichiers de données mais sachez que leur utilisation permet une grande souplesse dans le projet. En effet, il est possible de modifier les fichiers de données pour modifier le comportement du programme de base. Tout cela, sans devoir recompiler le programme!

C'est une technique souvent utilisée. Dans notre cas, cela nous permet de changer à volonté les fichiers o.bmp, x.bmp et bg.bmp à volonté sans devoir tout recompiler.

Et enfin, le moment tant espérer, voilà la première image du morpion en action. C'est une image provisoire car il y aura encore beaucoup de changements (Les images de la grille, des ronds et des croix ont été créées par Gimp)!

Image non disponible

IV. Conclusion

Ceci termine donc cette deuxième partie de l'élaboration du morpion. Nous avons maintenant un programme qui affiche l'état du jeu et gère les clics. En effet, lorsque nous cliquons sur une case vide, nous mettons la case à jour et l'affichage se fait en conséquence. Par contre, si la case est déjà occupée, le clic est ignoré. Dans la prochaine partie, nous verrons comment ajouter les règles du morpion. Cela permettra de vérifier si le jeu est terminé, de décider qui a gagné et de recommencer la partie.

Jc

Liens

  • Sommaire du tutoriel;
  • Ouvrir une fenêtre SDL;
  • Lier la souris à la fenêtre et afficher des ronds à l'endroit cliqué;
  • Ajouter les règles du jeu et le finaliser;
  • Ajouter les classes Objet et Moteur pour rendre le jeu plus souple;
  • Ajouter un menu dans un jeu;
  • Ajouter une Intelligence Artificielle (Min-Max);
  • Ajouter une Intelligence Artificielle (Alpha-Beta);

V. Téléchargements

Voici le code source de ce tutoriel : zip (108 Ko)

Voici la version pdf de cet article : (120 Ko).


Tutoriel SDL Morpion

Programmation de Jeux 2D: Un morpion en SDL, Troisième partie

  • I. Introduction
    • I-A. Présentation de cette partie
    • I-B. Qu'est-ce que la logique de jeu?
  • II. Le programme
    • II-A. Modification du fichier Jeu.h
      • II-A-1. Changement de l'énumération Case
      • II-A-2. Modification des membres de la classe Jeu
      • II-A-3. Ajout de fonctions membres dans la classe Jeu
        • II-A-3-a. La fonction videJeu
        • II-A-3-b. La fonction verifFini
      • II-A-4. Modification des fonctions membres de Jeu
        • II-A-4-a. Le constructeur et destructeur
        • II-A-4-b. La fonction clic
        • II-A-4-c. La fonction aff
  • III. Conclusion
  • IV. Téléchargements

Dans cette partie du tutoriel, nous allons voir comment ajouter les règles du morpion et comment recommencer une partie sans devoir relancer le programme. A la fin de cette partie, nous aurons une première version d'un morpion qui fonctionne correctement et nous prévient si quelqu'un a gagné.

I. Introduction

Bienvenue dans la troisième partie du tutoriel sur le jeu du morpion. Nous allons voir comment ajouter un peu de logique de jeu et comment intégrer une remise à zéro d'un jeu (sans devoir relancer le programme!).

A la fin de la deuxième partie de ce tutoriel, nous avions un programme qui affichait un quadrillage de morpion (image de fond). Lorsque nous cliquons dans la fenêtre, le programme est capable d'afficher les ronds et les croix des deux joueurs.

Voyons maintenant comment ajouter un peu de logique de jeu et rendre ce morpion un peu plus correct.

I-A. Présentation de cette partie

Dans cette troisième partie, nous allons ajouter les règles associées au morpion. Il faudra donc pouvoir vérifier si le jeu est fini et pouvoir déterminer qui a gagné (ou s'il y a un match nul).

Nous allons compléter le code du deuxième tutoriel. Voici le lien vers ce code: zip (105 Ko).

I-B. Qu'est-ce que la logique de jeu?

J'appelle "logique de jeu" toutes les règles du jeu. Par exemple, pour un morpion, si trois ronds forment une ligne alors les ronds gagnent. Ceci fait partie de la logique du jeu. Donc dans la logique du jeu, nous devons pouvoir savoir si le jeu est fini, qui a gagné (ou déterminer qu'il y a un match nul).

Graphiquement, nous traduirons cela par l'affichage d'une autre surface pour montrer à l'utilisateur que quelqu'un a gagné.

II. Le programme

Aucun fichier ne sera ajouté par rapport à la deuxième version du programme. De plus, le fait d'avoir séparé correctement le lancement du programme et la boucle générale de la gestion du morpion, aucun changement ne sera apporté aux fichiers Main.cpp, Main.h et Define.h. Regardons donc les changements des fichiers Jeu.h et Jeu.cpp.

II-A. Modification du fichier Jeu.h

Nous allons modifier l'énumération Case pour ajouter un type Gagne et ajouter quelques fonctions dans la classe Jeu pour gérer les règles du morpion. Ce qui suit est une présentation plus détaillée.

II-A-1. Changement de l'énumération Case

Dans le fichier Jeu.h, nous avons ajouté un élément dans l'énumération des types de cases pour avoir le type Gagne. Ce nouveau type servira pour pouvoir afficher une surface différente pour les cases qui forment une ligne.

Ajout de l'énumération Gagne
Sélectionnez

enum Case {
        Vide=0,
        Rond,
        Croix,
        Gagne
};

II-A-2. Modification des membres de la classe Jeu

Ensuite, nous ajoutons deux surfaces pour pouvoir dessiner les cases Gagne pour les ronds et les croix. Nous ajoutons aussi une variable typegagne de type Case pour nous donner la personne qui a gagné (s'il y en a une). Finalement, nous avons un booléen fini qui nous dit si le jeu est fini.

Membres de la classe Jeu
Sélectionnez

//Surfaces d'un rond, d'une croix, d'un fond et les surfaces gagnées
SDL_Surface *o, *x, *bg, *gagneo, *gagnex;

//Variable pour un tour et pour savoir qui a gagné
Case tour, typegagne;

//Booléen pour savoir si la partie est terminée
bool fini;

N'est-ce pas redondant d'avoir un booléen fini et une variable typegagne?

On pourrait par exemple dire: Si typegagne != Vide alors quelqu'un a gagné donc le jeu est fini. Mais alors comment définir le match nul?
On pourrait répondre que si typegagne == Gagne, alors c'est une égalité...

Je n'aime pas ce genre de chose. Il est bon de séparer (tant qu'on a le luxe d'avoir assez de mémoire) les choses correctement. Nous avons donc un booléen qui nous informe si le jeu est fini et une autre variable pour savoir qui a gagné (ou match nul).

Alors la convention prise pour ce jeu est de dire:

  1. Rond a gagné si (fini==true) && (typegagne==Rond);
  2. Croix a gagné si (fini==true) && (typegagne==Croix);
  3. Egalité si (fini==true) && (typegagne==Vide);
  4. Pas encore fini si (fini==false).

II-A-3. Ajout de fonctions membres dans la classe Jeu

Ensuite nous ajoutons deux fonctions à la classe:

Ajout de fonctions de la classe Jeu
Sélectionnez

//Fonction pour vider le plateau de jeu
void videJeu();

//Fonction pour vérifier si la partie est terminée
void verifFini();

II-A-3-a. La fonction videJeu

La fonction videJeu remet toutes les valeurs à zéro. Elle sera appelée dans le constructeur pour éviter une redondance de code. En fait, elle n'a rien de nouveau, ce n'est qu'une migration de code qui se trouvait dans le constructeur (on y ajoute tout de même la mise à false de la variable fini). Cette fonction est plus importante que ce qu'on pourrait croire. Elle permettra de recommencer le jeu sans quitter le programme. Plus le jeu est compliqué, plus cette fonction sera difficile à écrire mais sera toujours aussi importante.

Fonction videJeu
Sélectionnez

void Jeu::videJeu()
{
    int i,j;
    //On vide le plateau
    for(i=0;i<3;i++)
        for(j=0;j<3;j++)
            plateau[i][j] = Vide;

    //On commence par rond
    tour = Rond;

    //On met fini à false
    fini = false;
}

II-A-3-b. La fonction verifFini

La deuxième fonction la plus importante pour un jeu est une fonction de type verifFini qui permet de savoir si le jeu est terminé. De plus, si la fonction est capable de savoir qui a gagné, alors c'est la cerise sur le gâteau. Nous n'allons pas montrer tout le code de cette fonction, seulement le début puisque c'est toujours la même chose:

Nous vérifions les lignes, les colonnes puis les diagonales. Si la première case est non vide, alors nous regardons le reste de la ligne/colonne/diagonale. S'ils sont tous du même type, alors nous savons que quelqu'un a gagné. Nous mettons donc les cases en question à Gagne et nous mettons typegagne et fini à jour.

Test d'alignement par ligne dans verifFini
Sélectionnez

for(i=0;i<3;i++)
{
    if( plateau[i][0]!=Vide )
        {
        //Tester la ligne
        if((plateau[i][0]==plateau[i][1]) && (plateau[i][0]==plateau[i][2]))
            {
            fini = true;
            typegagne = plateau[i][0];
            plateau[i][0] = Gagne;
            plateau[i][1] = Gagne;
            plateau[i][2] = Gagne;
            }
        }
    else
        {
        casevide = true;
        }
}

Rappelons que le jeu se termine aussi s'il n'y a plus de cases vides. Dans cette fonction, nous avons donc un booléen casevide qui est initialisé à faux et qu'on met à vrai dès qu'on voit une case vide. Vu qu'on teste que la première case des lignes, des colonnes et des diagonales pour les cases vides, nous devons aussi regarder les cases qui restent. Ceci est fait à la fin de la fonction, avant de mettre à jour la variable fini.

Fin du test pour le booléen casevide dans verifFini
Sélectionnez

    //Dernières cases
    if( (plateau[1][1] == Vide)||(plateau[1][2] == Vide)||
        (plateau[2][1] == Vide)||(plateau[2][2] == Vide))
        casevide = true;

    fini = !casevide || fini;

Le test booléen dit bien: le jeu est terminé si aucune case n'est vide ou si fini a déjà été mis à vrai (cela veut dire qu'on a eu un alignement).

II-A-4. Modification des fonctions membres de Jeu

Finalement, le fait d'ajouter ces informations dans le programme a affecté toutes les fonctions de la classe Jeu. Nous avons de la chance puisque cette classe est encore relativement petite. Regardons ensemble les différences apportées.

II-A-4-a. Le constructeur et destructeur

Les seules deux modifications du constructeur de la class Jeu est la mise à NULL des variables gagnex et gagneo et la migration du code vers la fonction videJeu.

Modification du constructeur de la classe Jeu
Sélectionnez

Jeu::Jeu()
{
    //Vider le Jeu
    videJeu();

    //Valeur par défaut pour les surfaces
    o=NULL;
    x=NULL;
    gagneo=NULL;
    gagnex=NULL;
    bg=NULL;
}

Du côté destructeur, nous ajoutons simplement la désallocation des nouvelles surfaces.

Modification du destructeur de la classe Jeu
Sélectionnez

Jeu::~Jeu()
{
    //On libére les surfaces
    SDL_FreeSurface(o);
    SDL_FreeSurface(bg);
    SDL_FreeSurface(x);
    SDL_FreeSurface(gagneo);
    SDL_FreeSurface(gagnex);
}

II-A-4-b. La fonction clic

Très peu de changements sont effectués à la fonction clic. On ajoute un test dans le cas où fini est mis à vrai. La décision pour le moment est de simplement remettre le jeu à zéro.

Remise à zéro
Sélectionnez

    if(fini)
        {
        videJeu();
        }

Sinon, on gère le clic de la même façon que dans le dernier tutoriel mais on ajoute un appel à la fonction verifFini. Donc, à chaque clic, on vérifie si la partie est terminée.

II-A-4-c. La fonction aff

On ajoute dans cette fonction la gestion des cases de type "Gagne". Ceci se fait en ajoutant deux if-else ou alors un switch:

Gestion de l'affichage des cases du morpion
Sélectionnez

//On dessine en fonction du type de la case
switch(plateau[k][l])
    {
    case Croix:
        SDL_BlitSurface(x, NULL, screen,&r);
        break;
    case Rond:
        SDL_BlitSurface(o, NULL, screen,&r);
        break;
    case Gagne:
        if(typegagne==Rond)
            SDL_BlitSurface(gagneo, NULL, screen,&r);
        else
            SDL_BlitSurface(gagnex, NULL, screen,&r);
        break;
    default:
        break;
}

On aurait pu ajouter deux éléments dans l'énumération de la Case: GagneX et GagneO à la place d'un type Gagne. C'est une solution acceptable, et vous pouvez, si vous voulez, tenter de le faire comme exercice!

III. Conclusion

En conclusion, on remarque que finalement les changements ne sont pas extraordinaires pour ajouter les règles du jeu du morpion et les intégrer dans le programme. En effet, avec quelques ajustements, on peut les gérer de façon intéressante et claire. Dans la prochaine partie, nous montrerons comment abstraire le moteur du jeu par rapport à tout ce qui se trouve en-dessous, afin de bien séparer les différentes parties du programme.

Jc

Liens

  • Sommaire du tutoriel;
  • Ouvrir une fenêtre SDL;
  • Lier la souris à la fenêtre et afficher des ronds à l'endroit cliqué;
  • Ajouter les règles du jeu et le finaliser;
  • Ajouter les classes Objet et Moteur pour rendre le jeu plus souple;
  • Ajouter un menu dans un jeu;
  • Ajouter une Intelligence Artificielle (Min-Max);
  • Ajouter une Intelligence Artificielle (Alpha-Beta);

IV. Téléchargements

Voici le code source de ce tutoriel: zip (127 Ko)

Voici la version pdf: pdf (40 Ko)


Tutoriel SDL Morpion

Programmation de Jeux 2D: Un morpion en SDL, Quatrième partie

  • I. Introduction
  • II. Le programme
    • II-A. Modification de la classe Jeu
      • II-A-1. Fonction Init
      • II-A-2. La fonction videJeu
      • II-A-3. La fonction verifFini
      • II-A-4. La fonction clic
      • II-A-5. La fonction aff
      • II-A-6. La fonction getFini
    • II-B. Modification du fichier Main.cpp
    • II-C. Ajout de la classe Objet
      • II-C-1. Le fichier Objet.h
      • II-C-2. Le ficher Objet.cpp
        • II-C-2-a. Constructeur
        • II-C-2-b. La fonction init
        • II-C-2-c. Les fonctions setImage et setPos
        • II-C-2-d. Les fonctions affiche et estDedans
        • II-C-2-e. La fonction estVide
        • II-C-2-f. Les fonctions getType, setType et memeType
    • II-D. Ajout de la classe Moteur
      • II-D-1. Le ficher Moteur.h
      • II-D-2. Le ficher Moteur.cpp
        • II-D-2-a. Le Constructeur et le Destructeur
        • II-D-2-b. La fonction clic et la fonction aff
        • II-D-2-c. Remarques sur la classe Moteur
  • III. Conclusion
  • IV. Téléchargement

Dans ce tutoriel nous allons montrer à quoi sert une classe Objet et une classe Moteur. La classe Objet permettra d'internaliser le code qui gère l'affichage et le clic d'une case. La classe Moteur sera le lien entre la fonction main et la classe Jeu. Ce tutoriel sera plus long que les précédents parce que nous allons intégrer et modifier beaucoup de code.

I. Introduction

Bienvenue à la quatrième partie du tutoriel sur le jeu du morpion. Pour le moment, notre code fonctionne et il fait ce que nous voulons. Mais, bien sûr, il ne fait pas exactement ce que nous voulons puisque nous avons une nouvelle partie (et encore d'autres sont prévues!).

Dans cette partie, nous allons enfin créer une classe Moteur et une classe Objet. La classe Moteur sera le lien entre la fonction main et la classe Jeu. La classe Objet sera là pour internaliser une bonne partie du code qui se trouve en ce moment dans la classe Jeu.

Nous allons ajouter une fonctionnalité au morpion : il ne sera plus nécessaire d'avoir le plateau du morpion sur toute la fenêtre! Nous allons pouvoir séparer les cases du morpion et gérer de façon intelligente le clic de la souris.

II. Le programme

Nous allons d'abord présenter les modifications du code et, seulement ensuite, nous présenterons les nouvelles classes. Cela peut sembler étonnant, mais je pense qu'il est plus facile de comprendre le fonctionnement d'une classe en regardant comment elle est utilisée et non comment elle est définie.

II-A. Modification de la classe Jeu

Nous allons commencer par montrer les différences dans la classe Jeu. Finalement, si nous regardons les membres et les fonctions membres, il n'y a pas beaucoup de changements. La seule modification est le passage de :

Ancienne déclaration du plateau
Sélectionnez

//Le plateau jeu
Case plateau[3][3];

à

Nouvelle déclaration du plateau
Sélectionnez

//Le plateau jeu
Objet plateau[3][3];

Mais, la majorité du code de cette classe a changé. Lorsque nous allons étudier de plus près les changements, nous verrons qu'en fait, à la place d'accéder directement au type d'une case, on utilise les fonctions de la classe Jeu. Maintenant, étudions les fonctions modifiées une à une.

II-A-1. Fonction Init

Dans la fonction init, il y aura seulement un ajout à la fin de la fonction : la mise en place des positions des cases du plateau du morpion et, ensuite, la classe Jeu passera les surfaces qu'utiliseront les instances de la classe Objet. En fait, ceci est une nouveauté dans le code.

A la place d'avoir l'écran divise en 9 cases, on va avoir les cases parsemées dans la fenêtre. Voici une image du jeu de la version 4 (histoire de vous donner envie!) :

Image non disponible
Le nouveau morpion

Sans montrer tout le code de la fonction init (il serait un peu trop long), nous montrerons qu'une petite partie.

Initialisation des cases du plateau
Sélectionnez

    SDL_Rect r;

    /* Début du code d'initialisation */
    /* Mettre en place la largeur et l'hauteur d'une case */
    r.w = 150;
    r.h = 150;

    /* Mise en place de la position de la première case */
    r.x = 74;
    r.y = 51;
    plateau[0][0].setPos(&r);

    /*
    Positionnement des 8 autres cases
    */

    //Mise en place des surfaces des cases
    for(i=0;i<3;i++)
        {
        for(j=0;j<3;j++)
            {
            plateau[i][j].setImage(vide,o,x,gagneo,gagnex);
            }
        }

Dans cette partie, nous utilisons une structure SDL_Rect pour initialiser la position des cases. Une fois que chacune des cases est initialisée, on parcourt les cases et on initialise les pointeurs des surfaces des cases. Vous remarquerez que j'ai ajouté une surface nommée vide. C'est le carré semi transparent que vous avez vu dans l'image.

La classe Jeu possède et gère les surfaces du programme. Lorsque je programme un jeu, une classe va gérer les surfaces et les autres possèdent seulement des pointeurs. Cela permet de centraliser l'information et limiter les redondances. Pour le moment, c'est la classe Jeu qui contiendra les surfaces du jeu.

Vous remarquerez aussi que l'affichage de la case vide est transparent. Ceci est fait grâce au canal alpha. La valeur du canal alpha donne le pourcentage de transparence des pixels. Pour définir la valeur de ce canal, on utilise la fonction SDL_SetAlpha. D'un point de vue graphique, lorsqu'on dessine un pixel à l'écran, on veut calculer la couleur de ce pixel. Pour cela, on utilise la couleur de l'ancien pixel (celle qui se trouve déjà à l'écran), la couleur du nouveau pixel (celle de la surface qu'on va copier à l'écran) et la valeur du canal alpha pour calculer la nouvelle couleur du pixel.

Prototype de la fonction SDL_SetAlpha
Sélectionnez

int SDL_SetAlpha(SDL_Surface *surface, Uint32 flag, Uint8 alpha);

Cette fonction prend une surface en paramètre. Le deuxième paramètre de cette fonction ressemble beaucoup au paramètre similaire passé à la fonction SDL_SetColorKey. Voici les valeurs possibles :

  1. Si flag vaut 0, alors l'alpha-blending sera désactivé;
  2. Si flag vaut SDL_SRCALPHA, alors l'alpha-blending sera activé;
  3. Si on ajoute l'option SDL_RLEACCEL, l'accélération RLE sera activé.

Voici, comment on l'appelle :

Utilisation de SDL_SetAlpha
Sélectionnez

//Mise en place de la transparence
if(SDL_SetAlpha(vide,SDL_SRCALPHA,180)==-1)
    cout << "Erreur avec la transparence de la case vide" << endl;

Et voici comment on ajoute l'accélération SDL_RLEACCEL :

Utilisation de SDL_SetAlpha
Sélectionnez

//Mise en place de la transparence
if(SDL_SetAlpha(vide,SDL_SRCALPHA|SDL_RLEACCEL,180)==-1)
    cout << "Erreur avec la transparence de la case vide" << endl;

Et le dernier paramètre est le pourcentage de transparence. Si on met 0, alors l'image sera entièrement transparente. Si on met 255, l'image sera totalement opaque. Pour revenir à des choses que vous connaissez, une implémentation possible pour la fonction SDL_SetColorKey serait de mettre à zéro le canal alpha de tous les pixels de la couleur que vous passez en paramètre.

Le calcul de la couleur du pixel se fait à partir de l'ancienne couleur, la nouvelle couleur et la valeur du canal alpha. Voici le calcul de la nouvelle couleur :

Calcul de la couleur du nouveau pixel
Sélectionnez

On veut mettre le pixel de l'image A sur le pixel de l'image B.
Soit dest la couleur du pixel de l'image B.
Soit orig la couleur du pixel de l'image A.
Soit alpha le canal alpha du pixel de l'image A.
Soit res la couleur du pixel résultat.

res = (dest * (255-alpha)/255)) + ((orig * alpha)/255)

II-A-2. La fonction videJeu

Le code de la fonction videJeu est presque le même que dans la partie 3. Le seul changement est l'appel à la fonction init pour chaque case. En effet, ce sera maintenant à chaque case de se remettre à zéro.

Nouvelle version de videJeu
Sélectionnez

void Jeu::videJeu()
{
    int i,j;

    //On met toutes les cases à Vide
    for(i=0;i<3;i++)
        for(j=0;j<3;j++)
            plateau[i][j].init();

    //On commence par Rond
    tour = Rond;

    //On met fini à false
    fini = false;
}

II-A-3. La fonction verifFini

Comme pour la fonction videJeu, la fonction verifFini ressemble énormément à la version de la troisième partie. La seule différence est l'utilisation des fonctions pour les comparaisons et pour les mises à jour. Voici le code qui vérifie l'alignement par ligne :

Nouvelle version de verifFini
Sélectionnez

for(i=0;i<3;i++)
    if(!plateau[i][0].estVide())
        {
        if((plateau[i][0].memeType(plateau[i][1]))&&(plateau[i][0].memeType(plateau[i][2])))
            {
            fini = true;
            typegagne = plateau[i][0].getType();
            plateau[i][0].setType(Gagne);
            plateau[i][1].setType(Gagne);
            plateau[i][2].setType(Gagne);
            }
        }
    else
        casevide = true;

Et la dernière partie de la fonction devient donc :

Nouvelle version de la fin de verifFini
Sélectionnez

//Dernieres cases à vérifier pour le booléen casevide
if( (plateau[1][1].estVide())||(plateau[1][2].estVide())||
    (plateau[2][1].estVide())||(plateau[2][2].estVide()))
        casevide = true;

Finalement, cette fonction montre bien le changement entre une version avec énumération et une version objet. Toutes les comparaisons et toutes les mises à jour passent par des fonctions, mais le fond est le même.

II-A-4. La fonction clic

La fonction clic est devenue nettement plus compliquée. En effet, nous n'avons plus la facilité de pouvoir calculer la case par rapport au couple (x,y). En fait, il va falloir parcourir chaque case du tableau plateau.

La fonction clic
Sélectionnez

void Jeu::clic(int x, int y)
{
    int i,j;
    bool trouve=false;

    //Si le jeu n'est pas fini
    if(!fini)
        {

        //On parcourt tout le plateau pour trouver la case associée
        for(i=0;(i<3)&&(!trouve);i++)
            for(j=0;(j<3)&&(!trouve);j++)
                {

                //Si le clic a touché cette case
                if(plateau[i][j].estDedans(x,y))
                    {

                    //Si la case est vide
                    if((plateau[i][j].estVide()))
                        {
                        //On met le type à jour
                        plateau[i][j].setType(tour);

                        //On met à jour le tour
                        tour = (tour==Rond)?Croix:Rond;
                        }

                    //On dit qu'on a trouvé la case associé
                    trouve = true;
                    }
                }
        }

    //Si le clic a touché une case, il faudra vérifier si la partie est terminée
    if(trouve)
        verifFini();
}

On ajoute un test au début du parcours pour vérifier que la partie n'est pas finie. Si ce n'est pas le cas, on va tester chaque case. Si le clic correspond à la case, alors on regarde si la case est vide. Enfin, on met à jour le type de la case si toutes ces conditions sont remplies.

Donc pour savoir si nous sommes dans la case, la classe Objet possède une méthode qui rendra un booléen pour savoir si la position (x,y) est dans la zone de la case. Finalement, on a l'impression que le code est plus compliqué, mais il est plus lisible. En utilisant cette nouvelle classe Objet, je trouve personnellement que la fonction clic est plus facile à comprendre.

En effet, ce code dit bien : "Pour chaque case, on vérifie si le clic est dans la case courante. Si c'est le cas, on teste si la case est vide et enfin on met à jour le type de la case si nécessaire".

Vous vous demandez sûrement l'avantage d'une telle séparation du code et d'une complication de la fonction clic. Premièrement, je rappellerais l'image que j'ai donné au début du tutoriel. Est-ce que vous voudriez faire cela à la main ? Il faudrait vérifier chaque case séparément, le code serait long et difficile à modifier. En plus, qui nous oblige d'avoir des rectangles comme forme d'une case ? Il suffit de définir un objet qui permet d'avoir une case en forme d'ellipse/de triangle/etc. Le calcul étant internalisé, la classe Jeu n'a pas besoin de s'en soucier. Tout ce qu'elle sait, c'est que chaque objet est capable de dire si (x,y) est incluse dedans. C'est là, tout l'intérêt de la classe Objet.

Mais nous pourrions aller encore plus loin : qu'est-ce qui oblige les cases d'être à un endroit fixe ? Nous pourrions mettre en place une version qui fait bouger les cases. Le code de la classe Objet serait à compléter mais, de nouveau, la classe Jeu ne changerait pas.

Enfin, remarquez l'utilisation du booléen trouve pour ne pas rester inutilement dans la boucle.

Finalement, la fonction se termine avec un test qui nous dit si l'état du jeu change. Si c'est le cas, on vérifie si la partie est terminée (mettant donc le booléen fini à jour).

II-A-5. La fonction aff

La fonction aff est devenue plus simple puisque nous avons migré le code d'affichage à l'objet sous-jacent. En effet, nous supposons maintenant qu'un objet correctement initialisé saura s'occuper de l'affichage correctement.

La fonction d'affichage
Sélectionnez

void Jeu::aff(SDL_Surface *screen)
{
    int k,l;

    //Dessiner le fond d'ecran
    SDL_BlitSurface(bg,NULL,screen,NULL);

    //Dessiner chaque case
    for(k=0;k<3;k++)
        for(l=0;l<3;l++)
            {
            plateau[k][l].affiche(screen);
            }
}

II-A-6. La fonction getFini

Enfin, la dernière fonction qu'on présentera de la classe Jeu est la fonction getFini. Cette fonction permettra au moteur de savoir si le jeu est fini. Voici le code étonnant qui fait ceci :

La fonction getFini
Sélectionnez

bool Jeu::getFini()
{return fini;}

On peut considérer, généralement, deux solutions pour l'implémentation de cette fonction :

  1. La première solution est celle que nous avons proposée. Nous avons gardé le booléen fini de la partie 3 et l'avons migré dans la classe Jeu. Mais ce sera à la classe Jeu de mettre à jour ce booléen lorsqu'il le faut. Dans le cas du morpion, cela est fait à chaque clic qui provoque un changement de l'état du jeu. C'est ce qu'on voit dans la fonction clic;
  2. La deuxième solution est de ne jamais calculer si le jeu est fini sauf si le moteur le demande. Ceci veut donc dire que la fonction getFini appellera verifFini. Cette deuxième solution est pratique lorsque le moteur n'a pas besoin de savoir tout de suite (ou tout le temps) après un changement d'état du jeu si la partie est finie. Il n'est donc pas nécessaire de faire exécuter le code de verifFini à chaque clic qui provoque un changement.

II-B. Modification du fichier Main.cpp

Les seules modifications qui sont faites dans ce fichier sont le passage d'une instance jeu à une instance moteur. Sinon, toutes les fonctions sont les mêmes.

II-C. Ajout de la classe Objet

Nous avons ajouté une classe Objet à ce projet puisque cela permet de gérer de façon plus complète les clics de souris et plus transparente l'affichage. Voyons comment nous avons implémente cette classe.

II-C-1. Le fichier Objet.h

Nous allons présenter dans cette section les membres de cet objet.

Les membres de la classe Objet
Sélectionnez

                //Les surfaces des cases
                std::vector<SDL_Surface *> images;

                //L'image à afficher
                int curimage;

                //Type de la case
                Case type;

                //Position de la case
                SDL_Rect pos;

Chaque objet possédera un vecteur d'images. La classe Jeu initialisera les surfaces de chaque objet.

Ensuite, nous avons un indice pour l'image qui sera affichée par l'objet et on aura une autre variable pour le type de la case. De nouveau, cette séparation des données est faite pour ne pas mélanger le type de l'image et l'image qu'on utilise pour faire le dessin.

Enfin, la variable pos donne la position et taille de chaque case.

II-C-2. Le ficher Objet.cpp

Dans cette section, nous présenterons les fonctions de la classe Objet.

II-C-2-a. Constructeur

Le constructeur initialise simplement les membres de la classe. On initialise la taille du vecteur images et on met tous les autres membres à zéro.

Le constructeur
Sélectionnez

Objet::Objet()
{
    int i;

    images.resize(5);
    for(i=0;i<5;i++)
        images[i] = NULL;

    curimage = 0;
    pos.x = 0;
    pos.y = 0;
    pos.w = 0;
    pos.h = 0;
}

Le destructeur étant vide, nous ne montrerons pas le code ici.

II-C-2-b. La fonction init

La fonction init est très facile à comprendre. Elle met curimage à zéro et le type de la case est Vide.

La fonction init
Sélectionnez

//Fonction d'initialisation
void Objet::init()
{
    //On met l'image à zéro et le type est Vide
    curimage = 0;
    type = Vide;
}

Finalement, cette fonction permettra de mettre la case à vide mais nous ajoutons la remise à zéro de la variable curimage.

Je vais maintenant bien insister sur la raison derrière l'existence des variables curimage et type. En effet, on aurait pu garder la même variable et l'utiliser à la fois pour le type de la case et s'en servir pour l'affichage. Mais, encore une fois, la séparation est faite pour bien dissocier les choses. En effet, le type de la case et ce qu'on affiche ne seront pas toujours associés, c'est pour cela que je dissocie tout de suite ces deux caractéristiques des cases.

II-C-2-c. Les fonctions setImage et setPos

La fonction setPos est très facile à comprendre. Avec l'opérateur d'affectation, on met les valeurs de la structure pos à jour.

La fonction setPos
Sélectionnez

//Mise en place de la position de l'objet
void Objet::setPos(SDL_Rect *p)
{pos=*p;}

La fonction setImage initialise simplement le vecteur images. Voici son code :

La fonction setImage
Sélectionnez

 //Mise en place des surfaces
void Objet::setImage(SDL_Surface *vide,SDL_Surface *o, SDL_Surface* x,
             SDL_Surface *gagneo, SDL_Surface *gagnex)
{
        images[0] = vide;
        images[1] = o;
        images[2] = x;
        images[3] = gagneo;
        images[4] = gagnex;
}

II-C-2-d. Les fonctions affiche et estDedans

La fonction affiche est devenue très simple. En effet, en utilisant l'indice curimage et le vecteur de surfaces images, c'est très facile de faire une copie de l'image sur la surface passée en argument.

La fonction affiche
Sélectionnez

//Fonction d'affichage
void Objet::affiche(SDL_Surface *dest)
{
    if(images[curimage])
        SDL_BlitSurface(images[curimage],NULL,dest,&pos);
}

La fonction estDedans est une simple fonction d'inclusion dans un point et un rectangle. Il suffit de voir si les abscisses et les ordonnées sont incluses dans les bornes associées à la case du jeu.

La fonction estDedans
Sélectionnez

//Est-ce que (x,y) est dans la case?
bool Objet::estDedans(int x, int y)
{
    return ( (x>pos.x)&&(x<pos.x+pos.w)&&(y>pos.y)&&(y<pos.y+pos.h));
}

II-C-2-e. La fonction estVide

Pour tester si une case est vide ou non, il suffit de tester la valeur de la variable type. Cela est montré dans la fonction suivante :

La fonction estVide
Sélectionnez

//Est-ce que la case est vide?
bool Objet::estVide()
{return type==Vide;}

II-C-2-f. Les fonctions getType, setType et memeType

La particularité de la fonction setType est la gestion interne de curimage. En effet, il y a deux intérêts de cette variable :

  1. Nous avons une seule énumération Gagne mais nous voulons pouvoir distinguer une case gagnée de rond et de croix.
  2. Dans la version 3, si jamais nous avions deux alignements en même temps (par exemple, si vous avez une ligne horizontale et une ligne verticale qui se complète en même temps) le programme en affiche un seul (une seule sera considérée comme la ligne gagnante). Dans cette nouvelle version, nous pouvons afficher les deux alignements (voir l'image ci-dessous).
Image non disponible
Rond a gagné avec un double alignement

Finalement, l'ordre du stockage des surfaces que nous utilisons nous permet d'écrire cette fonction. En effet, si la case est vide, est un rond ou est une croix alors nous pouvons juste utiliser la valeur de l'énumération pour avoir l'indice de l'image.

Mais si la case est de type Gagne alors il faut ruser un peu. Nous savons que les surfaces sont stockées dans l'ordre Vide, Rond, Croix, Rond gagne et Croix gagne. Donc si la case est de type Gagne, l'indice de la surface est égale au type de la case (Rond ou Croix) plus 2.

La fonction setType
Sélectionnez

//On met en place le type de la case
void Objet::setType(Case t)
{
    switch(t)
    {
        //Dans ces cas, on a directement le type et curimage
        case Vide:
        case Rond:
        case Croix:
            type=t;
            curimage = t;
            break;
    //Dans le cas Gagne, on met seulement à jour curimage
    //curimage = type+2 pour avoir l'indice de la surface Gagne X ou Gagne O
        case Gagne:
            curimage = type+2;
            break;
    }
}

La dernière fonction que nous présenterons pour cette classe est la fonction memeType qui retourne vrai si les objets sont de même type. Ne confondez pas même objet avec même type.

Certaines personnes demanderaient pourquoi ne pas utiliser/redéfinir l'operateur de comparaison == ?

La réponse est assez simple : je trouve qu'il est mauvais usage de redéfinir cet opérateur. Surtout que nous pourrions avoir besoin de vérifier les valeurs des adresses des objets. Donc le code de la fonction memeType est :

La fonction memeType
Sélectionnez

//Est-ce que l'objet est du même type?
bool Objet::memeType(Objet &obj)
{
    return obj.type == type;
}

II-D. Ajout de la classe Moteur

Nous arrivons au dernier grand changement de cette partie. Nous allons ajouter une classe Moteur. Cette classe va encore être vide à la fin de ce tutoriel mais on va la remplir assez rapidement et elle deviendra vite le centre d'intérêt du projet.

II-D-1. Le ficher Moteur.h

Pour le moment, le seul membre de cette classe est le plateau de jeu. Les seules fonctions membres de cette classe seront les fonctions clic et aff qui feront le lien entre la fonction main et la classe Jeu. Voici sa déclaration :

La classe Moteur
Sélectionnez

class Moteur
{
        private:
                //Le plateau de jeu
                Jeu jeu;

        public:
                //Constructeur/Destructeur
                Moteur();
                ~Moteur();

                //La fonction clic
                void clic(int , int);
                //La fonction d'affichage
                void aff(SDL_Surface *screen);

                //La fonction d'initialisation
                bool init();
};

II-D-2. Le ficher Moteur.cpp

Présentons maintenant l'implémentation de la classe Moteur.

II-D-2-a. Le Constructeur et le Destructeur

Le constructeur et le destructeur de cette classe étant encore vides, on ne va pas les montrer ici.

II-D-2-b. La fonction clic et la fonction aff

Les deux fonctions qui sont intéressantes à voir sont la fonction clic et la fonction aff. Voici la fonction clic :

La fonction clic
Sélectionnez

void Moteur::clic(int x, int y)
{
    if(jeu.getFini())
        {
        jeu.videJeu();
        }
    else
        jeu.clic(x,y);
}

Cette fonction teste donc si le jeu est terminé. Si c'est le cas, on vide le jeu. Sinon, on passe les coordonnées à l'instance du jeu.

La deuxième fonction est donc la fonction d'affichage. Voici son code :

La fonction aff
Sélectionnez

void Moteur::aff(SDL_Surface *screen)
{
        jeu.aff(screen);
}

Comme vous le voyez, la fonction d'affichage du moteur est simplement un appel à la fonction d'affichage du membre jeu.

II-D-2-c. Remarques sur la classe Moteur

Vous devez maintenant vous demander pourquoi avoir créé cette classe qui est entièrement (enfin presque) vide! Quel intérêt de l'avoir et quel est le futur de cette classe ? Pour mieux le comprendre, je vais présenter ce que va faire cette classe à la fin de cette série de tutoriels.

A la fin de ces tutoriels, nous allons avoir un morpion qui possède un menu, peut être joué à un ou deux joueurs, ce qui implique une présence d'intelligence artificielle et, en plus, possède un module son. Ce sera au moteur du jeu d'initialiser tous ces modules, de les coordonner et de les faire fonctionner en même temps.

En conclusion, il est vrai que cette classe est un peu vide pour l'instant, mais elle va se remplir très rapidement et va pouvoir faire nettement plus de choses.

III. Conclusion

Ok, je l'avoue. Cette partie a été la plus longue depuis le début et je m'en excuse. Beaucoup de choses ont été faites pour mettre en place la classe Objet et la classe Moteur. Et je trouvais important de le faire dans le même tutoriel. Nous avons pris une version d'un morpion assez simpliste et nous l'avons complétée pour le rendre bien plus performant et bien plus intéressant.

La classe Objet a été mise en place pour permettre une gestion plus transparente des clics de la souris et de l'affichage des cases du morpion. Sans cette classe, l'état actuel du morpion aurait été difficile à mettre en place.

La classe Moteur a été mise en place pour devenir le lien entre le jeu et la fonction main. Pour l'instant, il n'y a pas grand chose dedans mais je pense avoir bien expliqué l'importance de cette classe. Nous verrons rapidement son intérêt et comment s'en servir.

Enfin, dans la prochaine partie, nous allons voir comment initialiser les positions des cases à partir d'un fichier. Mais nous verrons aussi comment mettre en place un menu de jeu et le séparer du jeu sous-jacent.

Jc

Liens

  • Sommaire du tutoriel;
  • Ouvrir une fenêtre SDL;
  • Lier la souris à la fenêtre et afficher des ronds à l'endroit cliqué;
  • Ajouter les règles du jeu et le finaliser;
  • Ajouter les classes Objet et Moteur pour rendre le jeu plus souple;
  • Ajouter un menu dans un jeu;
  • Ajouter une Intelligence Artificielle (Min-Max);
  • Ajouter une Intelligence Artificielle (Alpha-Beta);

IV. Téléchargement

Voici le code source de ce tutoriel: zip (948 Ko)

Voici la version pdf: pdf (137 Ko)


Tutoriel SDL Morpion

Programmation de Jeux 2D: Un morpion en SDL, Cinquième partie

  • 1. Introduction
  • 2. La classe Jeu
    • 2.1. Le fichier Jeu.h
    • 2.2. Le fichier Jeu.cpp
  • 3. Le menu
    • 3.1. Présentation du fichier d'entrée
    • 3.2. Le fichier Menu.h
    • 3.3. Le fichier Menu.cpp
      • 3.3.1. Constructeur et Destructeur
      • 3.3.2. La fonction init
      • 3.3.3. La fonction clic
      • 3.3.4. La fonction aff
  • 4. La classe Moteur
    • 4.1. Le fichier Moteur.h
    • 4.2. Le fichier Moteur.cpp
      • 4.2.1. Le constructeur et destructeur
      • 4.2.2. La fonction init
      • 4.2.3. La fonction clic
      • 4.2.4. La fonction aff
      • 4.2.5. Les fonctions pour la gestion des modules
      • 4.2.6. La fonction fin
  • 5. La fonction Main
  • 6. Conclusion
  • 7. Téléchargements

Dans ce tutoriel nous allons montrer comment créer un fichier de données pour paramétrer le menu mais aussi le plateau de jeu. Oui, vous avez bien lu, dans cette partie, nous allons introduire un menu! Notre jeu commence à ressembler à quelque chose.

1. Introduction

Dans cette cinquième partie de ce tutoriel, nous marquons le début de notre éloignement du jeu du morpion. En effet, les concepts présentés n'ont plus vraiment un rapport avec le jeu du morpion. Bien sûr, nous continuons de nous intéresser au cas du morpion, mais les concepts s'appliquent à n'importe quel jeu.

Nous allons, dans cette partie, mettre en place des fichiers de configuration et mettre plus en valeur la classe Moteur. A la fin de cette partie, nous aurons donc un morpion avec un menu et le tout sera paramétré avec des fichiers textes. Ceci permettra de modifier certains aspects du jeu sans devoir tout recompiler.

2. La classe Jeu

Il y a une seule modification dans la classe Jeu. On va utiliser un fichier pour paramétrer le plateau de jeu. L'utilisation de ce fichier va donc changer la fonction init. Ce sera le seul changement de cette classe. Enumérons les choses qui sont paramétrables :

  1. Nom des fichiers images qui seront à charger.
  2. La taille d'une case.
  3. La position des 9 cases du jeu.

Donc le fichier commence par les noms des différents fichiers images nécessaires pour l'affichage. Ensuite, vu que chaque case aura les mêmes dimensions, on définit la taille d'une case. Enfin, les neuf prochaines lignes contiennent les positions des neuf cases du morpion.

Fichier d'entrée pour le plateau
Sélectionnez

data/bg.bmp
data/vide.bmp
data/o.bmp
data/x.bmp
data/gagneo.bmp
data/gagnex.bmp
150 150
93 25
368 35
628 42
53 225
328 235
588 242
93 425
368 435
628 442

Forcément, ce fichier transforme une partie de la fonction init. Cette section présente donc les changements de cette fonction.

2.1. Le fichier Jeu.h

Si on regarde la version précédente de l'implémentation de la fonction init, les noms des fichiers images sont codés en dur, donc on pourrait aussi coder en dur le nom du fichier d'entrée. Mais, pour faire encore mieux, on va passer le nom du fichier en paramètre.

Changement du prototype de la fonction init
Sélectionnez

    //Fonction d'initialisation (chargement des surfaces)
    bool init(const std::string &file);

2.2. Le fichier Jeu.cpp

La seule chose qui change dans le fichier Jeu.cpp est donc la fonction init. Mais, si nous regardons bien, on passe simplement d'une version codée en dur à une version avec fichier de configuration.

La fonction commence donc par l'ouverture du fichier et la vérification que l'ouverture s'est bien passée.

Ouverture du fichier de configuration
Sélectionnez

    ifstream inputfile(file.c_str());

    //Test sur l'ouverture de la fonction
    if(inputfile==NULL)
        {
        std::cerr << "Erreur dans l'ouverture de " << file << std::endl;
        return false;
        }

Une fois que nous avons ouvert le fichier, nous allons pouvoir passer à la phase de chargement des surfaces SDL. Un problème potentiel est le passage des string au type char * qui est nécessaire pour utiliser la fonction SDL_LoadBMP. On utilisera donc la fonction c_str de la classe string pour pouvoir utiliser la fonction SDL.

Alors, vous me demanderez pourquoi ne pas passer directement par un char*?

Puisque ce tutoriel se fait en C++/SDL, je tente de garder le code au maximum dans le C++. L'utilisation du type char* est presque considérée comme une hérésie en C++, c'est pour cela que je préfère passer par la classe string. Hérésie parce qu'en C++, la STL fournit une classe permettant de faire beaucoup plus de choses en transparence que ce qu'on peut faire en C. Sans rentrer dans le débat "Qu'est-ce qui est mieux: les strings ou les char*?", je dirais qu'une bonne habitude est d'utiliser les char* en C et les string en C++. Dans de rares circonstances, il sera probablement préférable d'utiliser des char* mais, pour la plupart des programmeurs (moi y compris!), ces occasions ne se présenteront jamais.

Je prendrai un paragraphe de plus pour bien expliquer qu'il y a beaucoup d'avantages à l'utilisation de la classe string. Premièrement, toutes les fonctions C++ de la STL vont utiliser la classe string à la place du type char*. Ensuite, beaucoup de vérifications sont faites sans que le programmeur soit obligé de s'y intéresser. Finalement, bien que le type char* existe en C++, je pense (et je suis à la base un programmeur C, alors si je le dis, c'est que j'en suis convaincu...) que la classe string servira plus au programmeur dans 99% des cas. Oubliez donc l'utilisation du char* lorsque vous programmez en C++.

Voir cette question de la FAQ C++ pour plus d'informations.

Chargements des surfaces SDL
Sélectionnez

    string s;

    //On charge toutes les images dans les surfaces associées
    inputfile >> s;
    bg = SDL_LoadBMP(s.c_str());
    inputfile >> s;
    vide = SDL_LoadBMP(s.c_str());
    inputfile >> s;
    o = SDL_LoadBMP(s.c_str());
    inputfile >> s;
    x = SDL_LoadBMP(s.c_str());
    inputfile >> s;
    gagneo = SDL_LoadBMP(s.c_str());
    inputfile >> s;
    gagnex = SDL_LoadBMP(s.c_str());

Une fois que les images sont correctement chargées (je ne remettrais pas la vérification du chargement mais, ne vous inquiétez pas, elle se trouve encore après le chargement), on va initialiser les positions et les surfaces des cases.

Pour le faire, on utilise les fonctions setPos et setImage présentées dans les parties précédentes. La seule différence est l'obtention des positions par le fichier à la place d'avoir les positions codées en dur.

Chargement des positions et des surfaces
Sélectionnez

    //Initialisation de chaque case
    for(i=0;i<3;i++)
        for(j=0;j<3;j++)
        {
        inputfile >> r.x >> r.y;

        //On initialise les positions de chaque case
        plateau[i][j].setPos(&r);
        //On initialise les images de chaque case
        plateau[i][j].setImage(vide,o,x,gagneo,gagnex);
        }

En conclusion pour les changements de la classe Jeu, on voit qu'on a simplement changé la mise en place des paramètres codés en dur par le passage par un fichier de configuration. Rien de vraiment spécial mais je pense que c'est important de voir cela une fois en détail.

3. Le menu

Nous allons continuer cette partie par la présentation de la classe menu. Nous proposons ici une solution parmi tant d'autres. Celle-ci a l'avantage d'être facile à mettre en place et à gérer.

Comme toujours, cette facilité vient avec un inconvénient : le menu sera assez limité et simpliste. Voici une image du menu :

Image non disponible
Le menu

Une classe Menu n'est pas toujours une chose évidente à mettre en place. N'oublions pas comment sont séparées les données du jeu. En effet, nous avons la classe Jeu et la classe Moteur. Il existe plusieurs possibilités dans le positionnement de cette nouvelle classe par rapport aux autres.

L'image suivante montre les trois techniques d'intégration de cette nouvelle classe Menu. Les noeuds représentent les classes, les flèches représentent le fait que la classe peut appeler directement des fonctions de l'autre classe. Je fais exprès de faire l'amalgame entre classe et instance dans la dernière phrase puisque dans l'implémentation de ce morpion, on remarque que le programme est tellement simpliste que toutes les classes sont finalement ce qu'on appelle des singletons.

Je vais donc prendre ce paragraphe pour présenter les singletons de façon théorique. Un singleton est une technique qui permet de restreindre le nombre d'instances de la classe dans le programme à un. Bien que je comprends parfaitement les raisons de cette technique, je ne la mets que très rarement en place. La plus grande raison : je veux garder mon code simple, lisible et je ne veux pas perdre trop de temps sur des détails d'implémentation, j'aime voir le programme fonctionner et, seulement lorsqu'il commence à tourner, je prends le temps d'optimiser le code. Je vous conseille de connaître l'existence de cette technique singleton, de savoir quand il est judicieux de s'en servir.

Sans aller jusqu'à faire un singleton, on peut réduire le risque d'avoir des copies d'une instance en surchangeant le constructeur par copie et l'opérateur d'affectation. En effet, en les déclarant comme des fonctions privées (et ne les implémentant pas), on enlève l'occasion d'utiliser ce constructeur et de faire une copie du pointeur.

Sélectionnez

//Fonctions privées:
Menu(const Menu&);            //Constructeur par copie
Menu& operator=(const Menu&);

Revenons donc à notre intégration de la classe Menu, voici une image qui résume les trois grandes possibilités :

Image non disponible
Intégration de la classe Menu
  1. La classe Menu se place entre le moteur et le jeu. Cette solution permet de rendre le menu plus dépendant du jeu et le moteur peut rester indépendant de toutes les interactions possibles entre les classes Menu et Jeu. Cette solution sera rapidement écartée puisque le moteur ne possède plus un lien direct avec le jeu, cela rendra la gestion du jeu beaucoup moins habile.
  2. La classe Menu se place au même niveau que la classe Jeu sans avoir un lien direct vers le jeu. Donc toutes les interactions entre les deux se feront à travers la classe Moteur. Cette solution permet de garder les 3 classes relativement indépendantes. Par contre, l'inconvénient est le nombre de fonctions de gestion que le moteur va devoir intégrer.
  3. La classe Menu se place au même niveau que la classe Jeu mais aura un lien direct vers le jeu. Cette dernière solution possède l'avantage que le moteur peut rester relativement simple et le menu peut gérer entièrement le jeu sans devoir passer par le moteur. L'inconvénient est le nombre d'interaction entre les classes. En effet, par rapport à la deuxième solution, on doit gérer parfaitement bien toutes les interactions entre les 3 classes, ce qui n'est pas forcément évident.

Puisque nous voulons garder le programme assez simple et le plus réutilisable possible, la deuxième solution semble la plus appropriée. Le menu passera donc par le moteur pour interagir avec le jeu.

Ce menu va donc être la première chose que le joueur va voir. Voici ce que nous voulons que le jeu puisse faire avec le menu :

  • Quitter le jeu à partir du menu mais aussi lancer le jeu.
  • Retourner dans le menu à partir du jeu. Lorsqu'on y revient, il faut pouvoir retrouver le jeu dans l'état où on l'a quitté mais aussi relancer une nouvelle partie. Nous utiliserons la touche Echap pour passer du jeu au menu et vice-versa.

3.1. Présentation du fichier d'entrée

Notre menu sera défini par une image de fond, une image pour le titre, une pour le bouton Nouveau et une pour le bouton Quitter. Voici le fichier de données :

Fichier de données du menu
Sélectionnez
data/menu.bmp
data/titre.bmp
20 30
data/nouveau.bmp
52 277
data/quitter.bmp
372 490

Ce fichier contient donc le nom de l'image de fond. Ensuite, nous avons, successivement, le nom et la position des fichiers contenant le titre, le bouton nouveau et le bouton quitter. Regardons comment est implémentée la gestion du menu.

3.2. Le fichier Menu.h

La classe Menu ne possède pas beaucoup de membres et pas beaucoup de fonctions. Il y a un tableau de SDL_Surface* pour les différentes images et des variables pour contenir les positions du titre et des boutons du menu. Voici leur déclaration :

Membres de la classe
Sélectionnez

//Surfaces pour le menu
SDL_Surface *images[4];

//Position et taille des boutons nouveau et quitter et du titre
SDL_Rect nouveau,
    quitter,
    titre;

Dans les fonctions membres de la classe menu, il y a le constructeur, le destructeur, la fonction d'initialisation, la fonction qui gère le clic de la souris et l'affichage. Voici leur déclaration :

Fonctions membres de la classe
Sélectionnez

//Constructeur
Menu();
//Destructeur
~Menu();

//Fonction d'initialisation
bool init(const std::string &file);

//Gestion du clic
void clic(int x, int y);

//Fonction d'affichage
void aff(SDL_Surface *screen);

3.3. Le fichier Menu.cpp

L'implémentation de la classe Menu est relativement simple. Nous allons, à présent, montrer les fonctions une à une.

3.3.1. Constructeur et Destructeur

Ces deux fonctions gèrent le tableau des surfaces. Le constructeur met le tableau à NULL :

Constructeur
Sélectionnez

//Constructeur
Menu::Menu()
{
    int i;
    for(i=0;i<4;i++)
        images[i] = NULL;
}

Le destructeur libère les surfaces :

Destructeur
Sélectionnez

Menu::~Menu()
{
    int i;
    for(i=0;i<4;i++)
       {
       SDL_FreeSurface(images[i]) , images[i] = NULL;
       }
}

3.3.2. La fonction init

La fonction init de la classe Menu ressemble à la fonction du même nom de la classe Jeu. On utilisera ce fichier pour initialiser le menu, voici son contenu :

Fichier de données pour le menu
Sélectionnez

data/menu.bmp
data/titre.bmp
20 30
data/nouveau.bmp
52 277
data/quitter.bmp
372 490

On commence par donner l'image de fond et ensuite les fichiers contenant les images du titre, du bouton nouveau et du bouton quitter. Voici les images séparées :

Image non disponible
Image de fond du menu
Image non disponible
Image du titre
Image non disponible
Image du bouton nouveau
Image non disponible
Image du bouton quitter

J'ai déjà montré le résultat au début du tutoriel, je mettrais juste le lien

Le code de la fonction init est assez simple. On ouvre le fichier, on charge les images et les positions du titre et des boutons. Je pense que les commentaires sont assez explicites et, avec les explications de la même fonction dans la classe Jeu, cette fonction est facile à comprendre. Je vais simplement montrer le code, les commentaires devraient être suffisants pour sa compréhension.

Fonction init
Sélectionnez

//Fonction d'initialisation
bool Menu::init(const string &file)
{
    //Ouverture du fichier de paramétrage
    ifstream input(file.c_str());

    //Test si le fichier s'est bien ouvert
    if(input==NULL)
        {
        std::cerr << "Erreur dans l'ouverture du fichier " << file << std::endl;
        return false;
        }

    //Récupération du nom de l'image pour le menu
    string nom;
    input >> nom;

    //On charge l'image de fond
    images[0] = SDL_LoadBMP(nom.c_str());

    //On récupére la surface de l'image "Titre"
    input >> nom;
    images[1] = SDL_LoadBMP(nom.c_str());

    //Initialisation des positions du menu
    input >> titre.x >> titre.y;

    //On récupére la surface de l'image "Nouveau"
    input >> nom;
    images[2] = SDL_LoadBMP(nom.c_str());

    //Initialisation de la position du bouton nouveau
    input >> nouveau.x >> nouveau.y;

    //On récupére la surface de l'image "Quitter"
    input >> nom;
    images[3] = SDL_LoadBMP(nom.c_str());

    //Mise en place de la transparence
    for(int i=1;i<4;i++)
        SDL_SetColorKey(images[i],SDL_SRCCOLORKEY,0);

    //On récupére la position du bouton quitter
    input >> quitter.x >> quitter.y;

    //On récupére la taille des images
    titre.w = images[1]->w;
    titre.h = images[1]->h;

    nouveau.w = images[2]->w;
    nouveau.h = images[2]->h;

    quitter.w = images[3]->w;
    quitter.h = images[3]->h;

    //On ferme le fichier
    input.close();

    //On retourne vrai
    return true;
}

3.3.3. La fonction clic

La fonction clic vérifie si la position de la souris se trouve dans le bouton nouveau ou le bouton quitter. Si c'est le bouton nouveau, alors on remet le jeu à zéro et on demande au moteur d'afficher le jeu. Si c'est le bouton quitter, on demande au moteur de terminer le programme.

La fonction clic
Sélectionnez

//Gestion du clic
void Menu::clic(int x, int y)
{
//Est-ce qu'on est dans le bouton nouveau?
if((nouveau.x<x)&&(nouveau.x+nouveau.w>x)&&(nouveau.y<y)&&(nouveau.y+nouveau.h>y))
    {
    moteur.initJeu();
    moteur.setFonctionsJeu();
    }
//Est-ce qu'on est dans le bouton quitter?
else if((quitter.x<x)&&(quitter.x+quitter.w>x)&&(quitter.y<y)&&(quitter.y+quitter.h>y))
    moteur.fin();
}

3.3.4. La fonction aff

La fonction d'affichage vérifie si nous avons les 4 surfaces pour faire le menu. Si c'est bien le cas, alors on les affiche.

L'ordre d'affichage est important. D'abord, on affiche l'image de fond du menu. Ensuite, grâce à l'utilisation de la transparence, on affiche l'image du titre, l'image du bouton Nouveau et du bouton Quitter.

La fonction aff
Sélectionnez

//Fonction d'affichage
void Menu::aff(SDL_Surface *screen)
{
    //Si on a une image, on l'affiche
    if(images[0] && images[1] && images[2] && images[3])
    {
        SDL_BlitSurface(images[0],NULL,screen,NULL);
        SDL_BlitSurface(images[1],NULL,screen,&titre);
        SDL_BlitSurface(images[2],NULL,screen,&nouveau);
        SDL_BlitSurface(images[3],NULL,screen,&quitter);
    }
}

4. La classe Moteur

Depuis la création et l'intégration de la classe Menu, on a dû compléter la classe Moteur pour qu'elle prenne en compte l'interaction entre l'ancienne classe Jeu et la nouvelle classe.

4.1. Le fichier Moteur.h

Nous remarquerons que nous avons deux nouveaux membres dans cette classe. Tout d'abord, un pointeur vers le menu et un booléen qui nous servira pour savoir si on est dans le menu ou non. Nous avons aussi mis le menu et le jeu en pointeur et nous utiliserons les opérateurs new/delete pour la gestion dynamique de la mémoire.

Membres de la fonction moteur
Sélectionnez

    //Pointeur de jeu
    Jeu *jeu;
    //Pointeur de menu
    Menu *menu;

    //booléen qui sert à savoir dans le menu
    bool dansMenu;

Dans la partie déclarant les fonctions membres, nous avons ajouté:

Les fonctions de la classe Moteur
Sélectionnez

    //Fonction qui initialise le jeu (et le remet à zéro)
    void initJeu();
    //Fonction qui demande de passer en mode Jeu
    void setFonctionsJeu();
    //Fonction qui demande de passer en mode Menu
    void setFonctionsMenu();
    //Fonction qui change d'état de jeu
    void echangeFonctions();

    //Fonction de fin (servira pour les sauvegardes)
    void fin();

Nous avons donc une fonction qui remet le jeu à zéro (donc qui vide les cases du morpion). Une qui demande d'afficher le jeu, une qui demande d'afficher le menu, une qui demande de changer entre le menu et le jeu et enfin une qui dit d'arrêter le moteur et donc le programme. Nous allons, à présent, voir l'implémentation de ces fonctions.

4.2. Le fichier Moteur.cpp

4.2.1. Le constructeur et destructeur

Puisque nous sommes maintenant en train d'utiliser de l'allocation dynamique pour le plateau de jeu et le menu, il est logique que dans le constructeur et le destructeur, nous nous occupons de la gestion mémoire.

Constructeur et Destructeur
Sélectionnez

Moteur::Moteur()
{
    //Création du jeu et du menu
    jeu = new Jeu();
    menu = new Menu();

    //Initialisation du booléen dansMenu
    dansMenu = true;
}

Moteur::~Moteur()
{
    delete jeu;
    delete menu;
}

4.2.2. La fonction init

La fonction init de la classe Moteur ouvre un troisième fichier de données. Dans ce fichier, on a mis les noms des fichiers pour paramétrer le jeu et le menu. C'est le dernier fichier qui permettra de paramétrer le programme. Voici son contenu :

Fichier de données pour le moteur
Sélectionnez

data/plateau.txt
data/menu.txt

Pour la troisième fois, on va voir comment ouvrir et lire un fichier. On l'ouvre, on teste s'il y a une erreur et passe les noms des fichiers au plateau de jeu et du menu.

Sélectionnez

bool Moteur::init()
{
    //Ouverture du fichier de paramétrage
    ifstream input("data/input.txt");

    if(input)
        {
        string s1,s2;
        //On récupére les noms des fichiers pour paramétrer le jeu et le menu
        input >> s1 >> s2;
        return (jeu->init(s1) && (menu->init(s2)));
        }
    return false;
}

Cette fonction, retourne vrai si l'initialisation du jeu et l'initialisation du menu retournent vraies.

4.2.3. La fonction clic

La fonction clic du moteur est légérement plus compliqué. On utilise la valeur du booléen dansMenu pour décider si c'est vers le menu ou si c'est vers le jeu qu'il faut envoyer l'information.

Fonction de clic
Sélectionnez

void Moteur::clic(int x, int y)
{
    //Si on est dans le menu
    if(dansMenu)
        {
        menu->clic(x,y);
        }
    else
        {
        if(jeu->getFini())
            {
            jeu->videJeu();
            }
        else
            jeu->clic(x,y);
        }
}

Il existe plusieurs techniques pour implémenter le moteur de jeu le plus rapidement possible. Une alternative serait d'utiliser des pointeurs de fonctions pour éviter l'utilisation de if. Mais, puisque nous avons seulement deux possibilités (entre le menu et le jeu), cette solution est parfaitement acceptable.

4.2.4. La fonction aff

La fonction d'affichage est exactement comme la fonction clic. On utilisera la valeur du booléen dansMenu pour décider si on affiche le menu ou le jeu.

Fonction d'affichage
Sélectionnez

void Moteur::aff(SDL_Surface *screen)
{
    //Si on est dans le menu
    if(dansMenu)
        {
        menu->aff(screen);
        }
    else
        {
        jeu->aff(screen);
        }
}

4.2.5. Les fonctions pour la gestion des modules

Nous pouvons présenter maintenant les fonctions qui serviront à dire au moteur si le programme est dans le mode jeu ou le mode menu. Je pense qu'elles sont facilement compréhensibles :

Autres fonctions
Sélectionnez

void Moteur::initJeu()
{
    jeu->videJeu();
}

void Moteur::echangeFonctions()
{
    dansMenu = !dansMenu;
}

void Moteur::setFonctionsJeu()
{
    //On met dansMenu false
    dansMenu = false;
}

void Moteur::setFonctionsMenu()
{
    //On met dansMenu true
    dansMenu = true;
}

On me dira qu'on pouvait faire tout ceci avec une fonction setdansMenu qui mettra directement à jour la valeur du booléen dansMenu. Mais, en l'écrivant comme je viens de la présenter, on pourra facilement passer à une version de programme ayant, par exemple, trois différents modules : un menu, le jeu du morpion et un jeu de pong!

L'utilisation d'un booléen n'est donc plus suffisante pour différencier les trois modules. C'est dans cette optique que je présente les fonctions comme ceci.

4.2.6. La fonction fin

La dernière fonction de cette classe est la fonction fin. Cette fonction a été mise en place pour que n'importe quel module puisse demander de quitter le programme. Il est préférable de passer par cette fonction pour que le moteur puisse faire les sauvegardes du jeu, demander les fermetures des fichiers et la libération de la mémoire avant la fin du programme.

Bien que dans cette version du programme, nous n'allons rien sauvegarder, rien fermer et il n'y a rien à libérer. Sa mise en place est importante et j'espère que cela vous apprendra quelque chose.

La fonction fin
Sélectionnez

void Moteur::fin()
{
    SDL_Event ev;
    //Le jeu est fini, on va émettre un événement SDL_QUIT
    //On pourrait mettre une phase de sauvegarde ici.
    ev.type = SDL_QUIT;
    SDL_PushEvent(&ev);
}

5. La fonction Main

La seule chose qu'on change dans la fonction main est l'ajout de la gestion de la touche Echap. On utilise la fonction echangeFonctions pour passer du menu au jeu (et vice-versa).

Evénement clavier
Sélectionnez

case SDL_KEYUP:
    if(event.key.keysym.sym==SDLK_q)
        moteur.fin();
    else if(event.key.keysym.sym==SDLK_ESCAPE)
        moteur.echangeFonctions();
    break;

6. Conclusion

Encore une fois, nous avons ici une partie assez imposante. Mais, de nouveau, nous avons fait beaucoup de choses pour rendre le jeu plus présentable et plus paramétrable. Maintenant, en modifiant les fichiers textes (sans devoir recompiler) nous pouvons changer tous les aspects du morpion.

Le fait de pouvoir tout changer sans devoir recompiler a beaucoup d'avantage. Premièrement, cela laisse n'importe quel utilisateur modifier et configurer le jeu comme bon lui semble. Cela vous permet de modifier la taille des cases, leur position.

Bien sûr, leur format est très simpliste et il serait bon de pouvoir mettre des commentaires dans ces fichiers textes.

Enfin, cette partie est la première qui a un rapport sur la programmation du jeu et moins sur le morpion. Dans la prochaine partie, nous allons nous attaquer à l'intelligence artificielle pour pouvoir jouer contre un ordinateur.

Jc

Liens
  • Sommaire du tutoriel;
  • Ouvrir une fenêtre SDL;
  • Lier la souris à la fenêtre et afficher des ronds à l'endroit cliqué;
  • Ajouter les règles du jeu et le finaliser;
  • Ajouter les classes Objet et Moteur pour rendre le jeu plus souple;
  • Ajouter un menu dans un jeu;
  • Ajouter une Intelligence Artificielle (Min-Max);
  • Ajouter une Intelligence Artificielle (Alpha-Beta);

7. Téléchargements

Voici le code source de ce tutoriel : zip (1.2 Mo)

Voici la version pdf : pdf (210 Ko)


Tutoriel SDL Morpion

Programmation de Jeux 2D: Un morpion en SDL, Sixième partie

  • 1. Introduction
  • 2. Généralités
    • 2.1. L'algorithme Min-Max
      • 2.1.a. Décision avec une profondeur 1
      • 2.1.b. Fonction d'évaluation
      • 2.1.c. Décision avec une profondeur 2
      • 2.1.d. Généralisation
    • 2.2. L'algorithme
      • 2.2.a. Présentation
      • 2.2.b. Discussion du choix de la profondeur
      • 2.2.c. La fonction évaluation
  • 3. La classe IA
    • 3.1. IA.h
    • 3.2. IA.cpp
      • 3.2.a. La fonction calcScore
      • 3.2.b. La fonction evalue
    • 3.3. L'algorithme du Min-Max
      • 3.3.a. La fonction calcIA
      • 3.3.b. La fonction calcMin
  • 4. La classe Jeu
    • 4.1. Le fichier Jeu.h
    • 4.2. Le fichier Jeu.cpp
  • 5. La classe Menu
    • 5.1. L'ajout des membres
    • 5.3. Les implications
      • 5.3.a. Les changements des fonctions existantes
      • 5.3.b. L'affichage et la gestion du clic
      • 5.3.c. L'ajout de getType
  • 6. La classe Rand et son intérêt
    • 6.1. L'intérêt
    • 6.2. L'implémentation
  • 7. Conclusion
  • 8. Téléchargements

Dans ce tutoriel nous allons montrer comment mettre en place une petite intelligence artificielle. Dans le jeu du morpion, la technique la plus courante est d'utiliser l'algorithme du Min-Max. C'est donc ce qu'on va présenter ici.

1. Introduction

Bienvenue à cette nouvelle partie sur la programmation d'un morpion. Nous allons introduire une intelligence artificielle (dans le reste de l'article, j'utiliserais aussi IA) pour pouvoir jouer contre un ordinateur. Bien sûr, pour aller encore plus loin, on va définir trois niveaux de difficulté pour l'IA et on va permettre à l'utilisateur de choisir s'il y a un ou deux ordinateurs.

L'algorithme le plus connu pour implémenter une intelligence artificielle pour ce type de jeu est appelé Min-Max. Nous allons présenter cet algorithme.

2. Généralités

2.1. L'algorithme Min-Max

Lorsque nous regardons le déroulement d'une partie, cela peut être vu comme l'élaboration d'un arbre de possibilités.

Lorsque l'ordinateur doit choisir son prochain coup, il parcourt toutes les différentes possibilités et choisira la meilleure possibilité. Bien sûr, comment fait-il pour décider? Si l'ordinateur a une technique pour évaluer une configuration, il peut parcourir toutes les possibilités et choisir la meilleure solution.

2.1.a. Décision avec une profondeur 1

Lorsque l'IA doit décider son prochain coup, il parcourt les différentes possibilités. Si nous limitions la recherche à une profondeur de un, cela veut dire que l'ordinateur va donc se restreindre à regarder le prochain coup.

L'image suivante montre comment l'ordinateur va choisir le prochain coup. En haut se trouve l'état courant du jeu et je l'appelle aussi la configuration courante.

Image non disponible
Etude avec profondeur 1

L'IA va regarder tous les coups possibles et va choisir le meilleur (nous supposons donc que l'ordinateur joue les ronds). Regardons maintenant comment on peut évaluer les configurations du jeu.

2.1.b. Fonction d'évaluation

Nous avons, jusqu'ici, passé sous silence comment évaluer une partie et nous verrons que c'est un des plus grands soucis de cet algorithme. Une mauvaise évaluation et l'ordinateur fera des mauvais choix. Une évaluation trop compliquée rendra l'algorithme trop complexe et trop lent.

Nous avons vu comment fonctionne une IA qui regarde seulement le prochain coup. Mais, vous vous doutez que dans le cas général, il sera plus intéressant de pouvoir regarder plusieurs coups à l'avance. Mais comment parcourir correctement l'arbre et comment choisir entre les différents coups possibles?

Nous avons déjà parlé de fonction d'évaluation. Supposons que nous avons une fonction evalue qui regarde un plateau de morpion et en donne un score. Par convention, on suppose qu'un entier peut parfaitement représenter une évaluation d'un jeu.

Dans le cas général, la fonction peut retourner n'importe quelle valeur entière. On suppose que 0 représente une impression d'égalité par la fonction, une valeur négative signifie que la fonction a une impression que l'ordinateur est en train de perdre et une valeur positive signifie que la fonction a une impression que l'ordinateur est en train de gagner.

Nous verrons dans la suite de ce tutoriel comment programmer correctement cette fonction. A présent regardons comment l'IA va parcourir les configurations possibles avec une profondeur 2.

2.1.c. Décision avec une profondeur 2

L'image suivante montre les possibilités avec une profondeur 2. A présent, l'ordinateur aura la connaissance des deux prochains coups pour faire son choix. Déjà, nous remarquons que si l'IA choisit le coup de droite, nous pourrons gagner la partie en jouant ensuite le coup de gauche (on aura donc trois X au milieu du morpion).

Image non disponible
Etude avec profondeur 2

C'est donc nécessaire que l'IA puisse voir qu'il ne faut pas aller vers la droite. D'ailleurs, vous remarquerez que si l'IA joue n'importe quel autre coup que celui de gauche, nous pourrons gagner la partie.

Pour résoudre ce problème, nous savons que nous devons utiliser la fonction d'évaluation. Comme évaluation basique nous allons définir:

  • évaluation(jeu) = 1000 si l'IA a gagné
  • évaluation(jeu) = -1000 si l'IA a perdu
  • évaluation(jeu) = 0 sinon

Donc si nous utilisons l'évaluation, la dernière image deviendra:

Image non disponible
Utilisation de l'évaluation

La fonction d'évaluation nous donne une valeur pour chaque configuration du jeu. Mais quelle valeur faut-il donner des noeuds de profondeur 1?

La solution est extrêmement logique. Les noeuds de profondeur 1 représentent les choix que feraient l'adversaire de l'ordinateur (généralement un humain mais cela pourrait être un autre ordinateur, c'est pour cela que je ferais référence d'adversaire). Bien sûr, l'IA va supposer que l'adversaire va choisir le meilleur coup possible. Donc pour chaque noeud qui représente le choix de l'adversaire, le coup qu'il choisirait devrait mettre l'IA dans la plus grande difficulté (donc devrait avoir le score le plus petit).

Donc en supposant cela, il est logique que la valeur des noeuds de profondeur 1 soit égale au minimum des valeurs de ses fils. On obtient donc:

Image non disponible
Evaluation des valeurs de profondeur 1

Donc pour terminer, l'IA aura comme choix le coup de gauche qui mènera vers une évaluation de 0 ou le coup de droite qui mène vers une évaluation de -1000. Si vous avez bien suivi, l'IA va bien sûr devoir choisir le coup de gauche. La valeur de ce noeud est donc le maximum de ses fils. Voici ce que cela donne:

Image non disponible
Evaluation de la valeur de la racine

2.1.d. Généralisation

Je pense que la section précédente a été bénéfique pour bien illustrer le fonctionnement de l'algorithme du Min-Max. Finalement, l'idée est de parcourir un arbre de configurations. Lorsqu'on arrive à une feuille de l'arbre, on utilise la fonction d'évaluation pour donner une valeur à la configuration.

Ensuite, pour les noeuds qui ne sont pas des feuilles. On utilise soit le minimum des valeurs des fils si le noeud représente le choix de l'adversaire (c'est donc un noeud Min), soit le maximum des valeurs des fils si le noeud représente le choix de l'IA (c'est donc un noeud Max).

2.2. L'algorithme

2.2.a. Présentation

Une première version de l'algorithme de l'évaluation de la valeur d'un noeud de l'arbre peut donc être donnée par :

Algorithme 1
Sélectionnez

- Si c'est une feuille, utiliser la fonction eval;
- Si c'est un noeud Max, la valeur sera le maximum des fils;
- Sinon, c'est un noeud Min, la valeur sera le minimum des fils.

On peut rendre cet algorithme plus proche d'un langage de programmation en écrivant ceci :

Algorithme 2
Sélectionnez

Fonction calculValeurNoeud(Noeud n)
{
Si n est une feuille alors
    retourner eval(n);
Sinon
    Si c'est un noeud max alors
        retourner le maximum des évaluations des fils du noeud
    sinon
        retourner le minimum des évaluations des fils du noeud
Fin Si
}

On remarque tout de suite l'aspect récursif de cet algorithme. Ceci est encore une version simplifiée puisque nous devons gérer les cas d'une partie terminée. A présent, nous allons présenter la nouvelle version de l'algorithme. Cet algorithme va se diviser en trois fonctions : calcIA, calcMin et calcMax.

Voici la fonction calcIA :

Algorithme calcIA
Sélectionnez

Fonction calculIA(Jeu *j, int prof)
{
Soit maxcourant = MINIMUM;

Pour chaque coup possible
     Faire
        Jouer le coup
        max = calcMax(j, prof-1);
        Si max > maxcourant alors
            maxcourant = max;
        FinSi
        Annuler le coup
    FinPour
Jouer le coup qui a eu le score maximum
}

Vous remarquerez que cette dernière version de l'algorithme commence à ressembler de plus en plus à un code C++. En effet, dans l'algorithme, nous avons réintroduit la classe Jeu et nous avons mis en place l'utilisation d'une profondeur. Et nous avons introduit la valeur MINIMUM qui sera définie comme la minoration stricte de la fonction d'évaluation.

La fonction d'entrée calcIA est assez simple : on regarde tous les coups possibles et on appelle la fonction calcMax. Cette fonction calcule l'évaluation d'un noeud de type Max. Voici son algorithme :

Algorithme calcMax
Sélectionnez

Fonction calcMax(Jeu *j, int prof)
{
Soit max = MINIMUM;

Si la partie est terminée ou si la profondeur == 0 alors
    retourner eval(n);
Sinon
    Pour chaque coup possible
        Faire
            Jouer le coup
            tmp = calcMin(j, prof-1);
            Si tmp > max alors
                max = tmp;
            FinSi
            Annuler le coup
        FinPour
    Retourner max;
}

Cette fonction ressemble beaucoup à la précédente. La seule différence est ce qu'on fait du maximum après le parcours. Dans la fonction calcIA, on joue le coup mais, dans la fonction calcMax, on retourne le maximum. La fonction calcMin est exactement la même sauf qu'on définit une valeur MAXIMUM et qu'on cherche un minimum à la place.

2.2.b. Discussion du choix de la profondeur

Le deuxième paramètre de l'algorithme Min-Max est la profondeur du parcours. En effet, nous avons parlé de profondeur 1 et 2 mais nous pouvons généraliser à une profondeur n.

Mais il y a un aspect de l'algorithme qu'il ne faut pas oublier: la complexité. En effet, la fonction d'évaluation peut être suffisamment compliqué que le simple nombre d'appel suffirait à rendre l'algorithme inutilisable. Prenons le simple jeu du morpion, si on calcule la taille de l'arbre, on obtient 9! noeuds (ce qui est égale à 362880).

Il n'y a pas de bonne réponse à la profondeur du parcours qu'il faudrait utiliser. Généralement, il faut utiliser la plus grande profondeur possible sans pour autant faire attendre le joueur 1h pour un coup de morpion.

Dans cette version du morpion, nous allons définir trois niveaux de difficulté de l'IA. En fait, ce sera la profondeur qui va être la définition de l'intelligence. Une profondeur 3 sera la difficulté Facile, 5 sera Moyen et 9 sera Difficile.

2.2.c. La fonction évaluation

Nous avons montré l'importance de la fonction d'évaluation. Si cette fonction est mal établie, alors l'IA ne va pas jouer correctement. Pour beaucoup de jeux, cette fonction sera très difficile à mettre en place. Heureusement, pour le morpion, c'est assez simple.

Une possibilité (et celle que j'utilise ici) pour le morpion est cette évaluation :

L'algorithme de la fonction d'évaluation
Sélectionnez

Si la partie est terminée alors
    Si c'est une égalité, alors on retourne 0
    Sinon
        Si l'ordinateur a gagné, alors retourne 1000 (le maximum possible);
        Sinon l'ordinateur a perdu, alors retourne -1000 (le minimum possible);
        Fin Si
    Fin Si
Sinon
    Soit la somme = 0
    Pour chaque ligne, chaque colonne, chaque diagonale,
       S'il y au moins un pion de chaque joueur, on ne fait rien
       Sinon
            S'il y a 2 pions de l'adversaire, on enlève 30 à somme
            sinon s'il y a 2 pions du joueur de l'IA, on ajoute 30 à somme
            sinon s'il y a 1 pion de l'adversaire, on enlève 10 à somme
            sinon s'il y a 1 pion de l'IA, on ajoute 10 à somme
            Fin Si
       Fin Si
    Fin Pour
Fin Si

On voit que si la partie est terminée, on retourne -1000, 0 et 1000 si la partie est respectivement perdue pour l'IA, une égalité ou gagnée pour l'IA. Ceux-ci sont les valeurs de références pour cette évaluation, ensuite on introduira des nuances.

Donc si la partie est bien partie, l'évaluation sera positive. Si l'IA est en train de perdre, l'évaluation sera donc négative. L'IA va donc tenter d'aller vers les configurations qui ont une évaluation positive et l'algorithme va supposer que son adversaire va vouloir aller vers les évaluations négatives.

3. La classe IA

Nous allons voir à présent comment nous allons implémenter la classe IA.

3.1. IA.h

La classe IA possède seulement un membre : joueurCourant. Cet élément représente le type de l'IA pour pouvoir calibrer correctement la fonction d'évaluation. En effet, si l'IA joue rond et que les ronds gagnent, alors la fonction devrait rendre 1000. Par contre, si le type de l'IA est croix mais c'est toujours les ronds qui gagnent alors on devrait obtenir -1000. C'est à cela que servira ce membre de la classe.

De plus, nous verrons que nous pouvons avoir deux IA qui jouent l'un contre l'autre. L'utilisation de ce membre permet de savoir pendant le parcours quel est le type de l'IA qui doit jouer (rond ou croix).

Classe IA
Sélectionnez

const int MINEVAL=-100000;
const int MAXEVAL=100000;

//Classe qui gère l'intelligence artificielle
class IA
{
    private:
        Case joueurCourant;

    public:
        IA();
        ~IA();

        //Fonction qui calcule le prochain coup
        void calcIA(Jeu *j, int prof);

        //Fonctions pour le calcul
        int calcMin(Jeu *j, int prof);
        int calcMax(Jeu *j, int prof);

        //Fonction qui évalue le jeu
        int calcScore(int, int);
        int evalue(Jeu *j);
        int comptePions(Jeu *j);
};

Les fonctions membres se divisent en deux groupes:

  1. L'évaluation à travers la fonction evalue qui retournera un entier et les fonctions comptePions, calcScore serviront pour ce calcul;
  2. Le parcours de l'arbre de configurations qui se fait avec les fonctions calcIA, calcMin et calcMax.

Nous allons maintenant pouvoir voir l'implémentation de cette classe.

3.2. IA.cpp

3.2.a. La fonction calcScore

Débutons cette sous-section par la présentation du calcul d'évaluation. Pour évaluer une configuration, le programme va regarder le nombre de pions et le nombre de pions de l'IA moins le nombre de pions de l'adversaire pour chaque horizontale, verticale et diagonale. Grâce à ces deux compteurs, nous pourrons définir ce qu'il faut pour calculer la participation à la somme de l'évaluation. Voici le code qui retourne cette participation :

Fonction calcScore
Sélectionnez

int IA::calcScore(int cntpion, int cntjoueur)
{
//On regarde le nombre de pions
switch(cntpion)
    {
    case 1:
        return 10*cntjoueur;
    case 2:
        return 30*cntjoueur;
    default:
        return 0;
    }
}

3.2.b. La fonction evalue

Mais il faut maintenant calculer ces deux compteurs pour chaque horizontal, chaque vertical et chaque diagonale. Le code de la fonction evalue est divisé en deux parties. Tout d'abord on regarde si le jeu est fini. Si c'est le cas, on retourne 1000 si l'IA a gagné, -1000 s'il a perdu et 0 sinon.

Début de la fonction evalue
Sélectionnez

    //Si le jeu est fini
    if(jeu->getFini())
        {
        //Si l'IA a gagné, on retourne 1000 - le nombre de pions
        if(jeu->getTypeGagne() == joueurCourant)
            {
            return 1000-comptePions(jeu);
            }
        else if(jeu->getTypeGagne() == Vide)
                {
                //Egalite -> on retourne 0
                return 0;
                }
        else
                {
                //Si l'IA a perdu, on retourne -1000 + le nombre de pions
                return -1000+comptePions(jeu);
                }
        }

    //On ajoute au score cette nouvelle participation
    score += calcScore(cntpion,cntjoueur);

Dans l'implémentation finale, j'ai rajouté quelque chose lorsqu'il a une défaite ou une victoire. En effet, il y a cette histoire de nombre de pions que j'ajoute (ou retire) à -1000 (ou 1000).

La raison de cet ajout est très simple à comprendre. S'il l'IA regarde avec une profondeur de 5 et qu'il y a deux victoires : une à la profondeur 1 et l'autre à la profondeur 5. Je préfère que l'IA aille pour la victoire directe au lieu de faire durer le plaisir. Rendre 1000 - nombre de pions permet de dire que dans le premier cas on aurait le score 999 et dans l'autre case 995. Donc l'IA favorisera bien la victoire directe.

Pour la défaite, on fait l'inverse. Si c'est sûr qu'on va perdre, on va aller vers la défaite la plus lointaine puisqu'il est tout de même possible que l'adversaire fasse une erreur.

La suite de la fonction evalue est le parcours des diagonales, des verticales et des horizontales. Nous allons montrer le code pour une diagonale mais sachez que c'est la même chose pour les autres alignements potentiels.

Evaluation d'une diagonale
Sélectionnez

    //On regarde chaque case
    for(i=0;i<3; i++)
        {
        //Si la case n'est pas vide
        if(!jeu->estVide(i,i))
            {
            //On incrémente d'un pion
            cntpion++;
            //Si c'est le même type du joueur courant
            if(jeu->getCase(i,i)==joueurCourant)
                //On incrémente le compteur
                cntjoueur++;
            else
                //On décrémente le compteur
                cntjoueur--;
            }
        }

Donc ce code regarde si la case est vide. Si ce n'est pas le cas, on incrémente le nombre de pions de cet alignement. Ensuite, on regarde si la case est occupée par un pion de l'IA ou de l'adversaire. Si c'est de l'IA, on incrémente cntjoueur, sinon on le décrémente. Finalement, cela revient à avoir comme possibilités pour les couples (nombre de pions, compteur du nombre des pions IA moins le nombre de pions de l'adversaire) :

  • (0,0) -> aucun pion;
  • (1,1) -> un seul pion, et c'est le pion de l'IA (c'est une bonne chose, il y a une possibilité lointaine de pouvoir gagné);
  • (1,-1) -> un seul pion mais c'est le pion de l'adversaire;
  • (2,-2) -> deux pions et c'est 2 pions de l'adversaire (vraiment pas bon, il pourrait gagner)
  • (2,0) -> deux pions mais un de chaque type, ceci ne mènera qu'à une égalité pour cet alignement.
  • (2,2) -> deux pions et 2 de l'IA (bonne chose, l'IA peut bientôt gagner).

Je pense que si vous regardez de nouveau la définition de la fonction calcScore, vous allez arriver à comprendre mieux son fonctionnement.

3.3. L'algorithme du Min-Max

Il faut, encore une fois de plus, compléter l'algorithme du Min-Max pour ce programme. Je vais maintenant passer directement à l'explication du code. L'implémentation du Min-Max est divisé en trois parties : la fonction qui permet de choisir entre les différents coups possibles, la fonction qui gère le choix des noeuds Max et celle qui gère le choix des noeuds Min.

3.3.a. La fonction calcIA

La fonction calcIA est le point d'entrée du programme qui choisira le prochain coup pour l'ordinateur. L'algorithme de la fonction peut se résumer en quelques phrases. On va parcourir toutes les possibilités de la configuration courante et calculer l'évaluation de l'IA pour chaque case. En prenant le maximum, on va donc avoir le meilleur choix que nous donne l'évaluation.

La seule chose dont on a n'a pas encore discuté est la technique utilisée pour parcourir les possibilités et surtout pour effectuer le parcours de l'arbre de configurations. La solution la plus simple est de simuler qu'on joue la partie et annuler les coups lorsqu'on veut tenter autre chose.

Le problème c'est qu'annuler un coup n'est pas toujours évident. Si ce n'est pas possible, une alternative est de copier la configuration, jouer le coup et ensuite on libère la mémoire.

Puisqu'on est en train de programmer un morpion, annuler un coup est facile. Mais pour un reversi, il serait probablement préférable de passer par des copies.

La fonction calcIA
Sélectionnez

void IA::calcIA(Jeu *jeu, int prof)
{
    int i,j,tmp;
    int max = MINEVAL;
    int maxi=-1,maxj=-1;

    //On met en place le joueur courant
    joueurCourant = jeu->getTour();

    //Si la profondeur est nulle ou la partie est finie,
    //on ne fait pas le calcul
    if((prof!=0)||(!jeu->getFini()))
        {
        //On parcourt les cases du morpion
        for(i=0;i<3; i++)
            for(j=0;j<3;j++)
                {
                //Si la case est vide
                if(jeu->estVide(i,j))
                    {
                    //On simule qu'on joue cette case
                    jeu->joue(i,j);
                    //On appelle la fonction calcMin pour lancer l'IA
                    tmp = calcMin(jeu, prof-1);
                    //Si ce score est plus grand
                    if((tmp>max)||((tmp==max)&&(Rand::randi(2))))
                        {
                        //On le choisit
                        max = tmp;
                        maxi = i;
                        maxj = j;
                        }
                    //On annule le coup
                    jeu->annuleCoup(i,j);
                    }
                }
        }
    //On jouera le coup maximal
    jeu->joue(maxi,maxj);
}

Lorsqu'on regarde le fonctionnement de cette fonction, on remarque qu'on parcourt les cases du morpion. Si une case est vide, on joue la case. Ensuite, on regarde quel serait le score de cette configuration. Puisque c'est un noeud Min (en effet, vu que, maintenant, c'est le tour de l'adversaire, celui-ci va essayer de minimiser l'évaluation).

Ensuite, lorsqu'on a la valeur du noeud, on le compare avec le maximum courant. Ce maximum est initialisé à une valeur qui minore strictement la fonction d'évaluation.

Remarquez que le choix d'un coup se fait donc grâce à ce test :

Sélectionnez

//Si ce score est plus grand
if((tmp>max)||((tmp==max)&&(Rand::randi(2))))

A quoi sert le ou logique? Si jamais l'évaluation obtenue est égale au maximum courant, on utilise un générateur aléatoire pour aider à choisir entre les deux possibilités. Nous ne cherchons pas à faire un générateur parfait, encore moins un équiprobable. Pour ce jeu, nous souhaitons simplement que l'IA ne semble pas jouer tout le temps les mêmes coups.

Une fois que l'algorithme a choisi le meilleur coup possible, il le joue avant de rendre la main.

3.3.b. La fonction calcMin

Pour qu'il n'y ait pas de jaloux, et parce que le code est assez simple, nous allons présenter la fonction calcMin puisque nous avons présenté l'algorithme pour la fonction calcMax.

Cette fonction gère le calcul de la valeur d'un noeud de type Min. Ce code ressemble énormément à la fonction calcMax. Voici son code :

La fonction calcMin
Sélectionnez

int IA::calcMin(Jeu *jeu, int prof)
{
    int i,j,tmp;
    int min = MAXEVAL;

    //Si on est à la profondeur voulue, on retourne l'évaluation
    if(prof==0)
        return evalue(jeu);

    //Si la partie est finie, on retourne l'évaluation de jeu
    if(jeu->getFini())
        return evalue(jeu);

    //On parcourt le plateau de jeu et on le joue si la case est vide
    for(i=0;i<3; i++)
        for(j=0;j<3;j++)
            {
            if(jeu->estVide(i,j))
                {
                //On joue le coup
                jeu->joue(i,j);
                tmp = calcMax(jeu, prof-1);
                if(tmp<min)
                    {
                    min = tmp;
                    }
                //On annule le coup
                jeu->annuleCoup(i,j);
                }
            }
    return min;
}

Comme vous le voyez, le code de la fonction calcMin ressemble étrangement à celui de calcIA.

4. La classe Jeu

Le reste de cette partie sera très rapide vu qu'il n'y a presque pas de changements. En effet, il y a simplement quelques ajouts du côté de la classe Jeu pour permettre à l'algorithme d'IA de faire son travail. Voici une présentation des modifications:

4.1. Le fichier Jeu.h

Afin d'avoir la possibilité de modifier le type de joueur des ronds et des croix, il nous faut définir une énumération prenant tous les types possibles.

Enumération du type du joueur
Sélectionnez

enum TypeJoueur {Humain, Facile, Moyen, Difficile};

Remarquez qu'on ne définit pas directement ici ce que veut dire Facile, Moyen ou Difficile. Ce sera le moteur du jeu qui s'en chargera directement (vu que c'est lui qui aura le contrôle de l'IA).

Ensuite, nous allons avoir deux nouveaux membres de la classe Jeu : le type de chaque joueur.

Nouveau membre de la classe Jeu
Sélectionnez

//Type des joueurs (humain/ordi)
TypeJoueur j1, j2;

Pour que l'IA puisse bien fonctionner, l'IA doit avoir une certaine maîtrise du jeu sous-jacent. Effectivement, on a surtout besoin de fonctions qui sondent le jeu et récupère l'état du jeu (qui a gagné? est-ce que la case (i,j) est vide?).

Nouvelles fonctions de la classe Jeu
Sélectionnez

        //Fonction qui retourne le joueur courant
        Case getTour();

        //Fonction qui retourne qui a gagné
        Case getTypeGagne();

        //Fonction qui retourne le type de la case
        Case getCase(int i, int j);

        //Fonction qui retourne si le joueur courant est un humain
        bool estHumain();

        //Fonction qui retourne le type du joueur courant
        TypeJoueur getTypeJoueur();

        //Fonction qui retourne l'état de la case
        bool estVide(int i, int j);

        //Fonction qui joue la case (i,j)
        void joue(int i,int j);

        //Fonction qui annule le dernier coup
        void annuleCoup(int a, int b);

        //Fonction qui met en place le type des joueurs
        void setTypeJoueur(int i, int type);

Je pense que les noms sont assez évocateurs pour ne pas devoir expliquer plus.

4.2. Le fichier Jeu.cpp

Bien qu'il y ait 9 nouvelles fonctions à cette classe, elles sont toutes extrêmement simples donc je ne vais pas allonger inutilement cet article (qui est déjà suffisamment long!). En effet, pour bien faire, je dois encore présenter la classe Menu et la classe Rand.

5. La classe Menu

Dans la classe Menu, nous allons ajouter une possibilité de choisir le type des joueurs. Une image sera sûrement bénéfique.

Image non disponible
Le nouveau menu du morpion

Nous allons donc ajouter deux zones textes comme le titre (donc des zones où l'on ne peut pas cliquer) et deux boutons comme le bouton nouveau (pour gérer le type de chaque joueur).

Cette partie va donc présenter l'ajout de ces éléments et comment on les intègre au jeu.

5.1. L'ajout des membres

Pour gérer correctement ces nouveaux éléments du menu, nous avons simplement à ajouter les variables SLD_Rect pour les positions et les SDL_Surface pour les nouvelles images.

Nouveaux membres de la classe Menu
Sélectionnez

    //Informations pour le type de joueur
    //Positions et surfaces du texte des joueurs
    SDL_Rect postxtj1, postxtj2;
    SDL_Surface* txtj[2];

    //Positions et surfaces des types des joueurs
    SDL_Rect postypej1, postypej2;
    SDL_Surface* typejoueur[4];

    //Type des joueurs
    int typej1, typej2;

Nous avons donc les positions et les surfaces des textes Joueur 1 et Joueur 2. Ensuite, nous définissons les positions et surfaces pour le type des joueurs (il y aura Humain, Facile, Moyen et Difficile).

Enfin, nous définissons le type des joueurs 1 et 2. Ces deux variables typej1 et typej2 vont servir pour savoir quelle surface dessiner du tableau typejoueur.

5.3. Les implications

Puisque déjà beaucoup de choses ont déjà été mis en place pour afficher le menu. Ajouter ces éléments ne demande pas beaucoup de changements.

5.3.a. Les changements des fonctions existantes

Si on regarde toutes les fonctions existantes de cette classe, il y a seulement quelques ajouts pour initialiser, détruire, gérer les clics et afficher ces nouveaux éléments. Nous n'allons pas montrer tout le nouveau code pour deux grandes raisons : cette partie est déjà assez longue et les ajouts ne présente pas d'intérêt. En effet, si vous avez compris le code de la partie précédente, il n'y aura pas de surprises ici.

5.3.b. L'affichage et la gestion du clic

Regardons d'abord comment nous affichons ces nouveaux éléments. Il suffit d'ajouter ceci :

Ajout pour l'affichage
Sélectionnez

    SDL_BlitSurface(txtj[0],NULL,screen,&postxtj1);
    SDL_BlitSurface(txtj[1],NULL,screen,&postxtj2);
    SDL_BlitSurface(typejoueur[typej1],NULL,screen,&postypej1);
    SDL_BlitSurface(typejoueur[typej2],NULL,screen,&postypej2);

La seule chose à noter est l'utilisation des valeurs typej1 et typej2 pour afficher la bonne surface pour le type de chaque joueur. Donc en changeant cette valeur, l'affichage est directement mis à jour.

Pour modifier le type des joueurs, l'utilisateur doit cliquer sur la surface en question. Voici comment on gère le clic :

Gestion des nouveaux boutons
Sélectionnez

//Est-ce qu'on est dans le bouton type joueur1?
else if((postypej1.x<x)&&(postypej1.x+typejoueur[typej1]->w>x)&&(postypej1.y<y)&&(postypej1.y+typejoueur[typej1]->h>y))
        {
        typej1= (typej1+1)%4;
        }
//Est-ce qu'on est dans le bouton type joueur2?
else if((postypej2.x<x)&&(postypej2.x+typejoueur[typej2]->w>x)&&(postypej2.y<y)&&(postypej2.y+typejoueur[typej2]->h>y))
        {
        typej2= (typej2+1)%4;
        }

Puisque le tableau typejoueur possède 4 éléments, les indices vont de 0 à 3. Pour cycler à travers les types, on incrémente la valeur courante. Bien sûr, lorsque la valeur vaut 4, il faut revenir à 0 donc on utilise un modulo.

5.3.c. L'ajout de getType

Nous allons tout de même ajouter une fonction à cette classe. En effet, lorsqu'on modifie le type d'un joueur, on peut passer par le moteur pour mettre à jour le type du joueur dans la classe Jeu. Rappelons, que nous avons à présent une redondance dans les données.

La fonction getType
Sélectionnez

//Fonction retourne le type des joueurs
int Menu::getType(int i)
{
        if(i==1)
                return typej1;
        else
                return typej2;
}

Effectivement, le type des joueurs se trouve dans la classe Menu et dans la class Jeu. Ceci est dû au fait que ces classes doivent passer par le moteur pour se parler. Il est donc plus facile à faire une petite redondance. Par contre, si nous avions plus de valeurs, il serait préférable de mettre en place un autre système.

Pourquoi éviter les redondances? Il faut éviter les redondances dans un code à cause des risques d'incohérences. Gardez comme règle générale qu'il faut mieux les éviter.

Ici, nous allons laisser le moteur décider le moment de mettre à jour le type des joueurs. Je présente cette solution, plutôt que de laisser le menu mettre directement l'instance du jeu à jour, parce que je trouve que celle-ci a plusieurs avantages:

  • La simple modification d'un paramètre du jeu peut engendrer beaucoup de calculs ou de modifications de l'état du jeu, il est donc préférable d'attendre le dernier moment avant de mettre à jour la valeur;
  • Si c'est le moteur qui gère les mises à jour, le menu n'a pas besoin de savoir qui a besoin de savoir qu'il y a eu un changement dans les paramètres.
  • Enfin, si on modifie plusieurs fois le type sans retourner dans le jeu, il n'est sûrement pas nécessaire de mettre à jour tout de suite les valeurs qui ont changées.

6. La classe Rand et son intérêt

J'ai parlé de l'utilisation d'un générateur de nombres aléatoires dans ce tutoriel. Logiquement, il nous faut une classe qui l'implémentera.

6.1. L'intérêt

Pourquoi centraliser le générateur de nombres aléatoires?

La difficulté d'un tel générateur est de le rendre le plus aléatoire possible. Centraliser ce code permet de commencer avec un générateur naïf et, comme d'habitude, si nous voulons avoir un meilleur générateur, il suffira de le changer.

Le reste du code n'a pas besoin de savoir comment le générateur de nombres aléatoires est implémenté.

6.2. L'implémentation

L'implémentation de ce générateur de nombres aléatoires est assez simple. Nous commencerons par définir la variable statique cnt. Cette variable va servir pour compter le nombre de fois qu'on appelle le générateur.

Lorsque ce générateur sera appelé un certain nombre de fois, on remet le générateur à zéro. Ceci se fait à travers la fonction newSeed.

Classe Rand
Sélectionnez

//Mis en place du compteur
int Rand::cnt=0;

//Vérification du compteur
void Rand::checkSeed()
{
        //Si on a trop appelé le générateur de nombres, on remet le tout à zéro
        if((!cnt)||(cnt>10000))
                {
                srand(time(NULL));
                cnt=0;
                }
}

//Générer une nouvelle séquence...
void Rand::newSeed(int s)
{
        srand(s);
}

//Générer un nombre aléatoire
int Rand::randi()
{
        checkSeed();
        return rand();
}

//Générer un nombre aléatoire entre [0,max-1]
int Rand::randi(int max)
{
        checkSeed();
        if(max)
                return rand()%max;
        return 0;
}

Finalement, ce code n'a rien de particulier. On utilise la fonction rand, ce qui nous permet d'avoir un générateur correct. Remarquez qu'au début de chaque fonction on appelle checkSeed. Ceci permet de refaire automatiquement une sélection du nombre de génération.

7. Conclusion

Encore une partie bien longue parce qu'avant de parler d'implémentation, il a fallu présenter la théorie et les idées de base. Une fois cette théorie en place, les spécificités du morpion ont ajouté quelques détails lors de l'implémentation.

De plus, pour faire une bonne implémentation d'une intelligence artificielle, il a fallu implémenter un générateur de nombres aléatoires.

Enfin, pour des jeux de plateau ou il y a des tours et des coups à jouer, l'algorithme du Min-Max s'implémente facilement. En effet, il faut parcourir toutes les configurations possibles et après, ce n'est qu'un parcours d'un arbre de configurations.

Dans la prochaine partie, nous verrons comment améliorer cet algorithme en limitant le nombre de configurations qui sont nécessaires pour calculer le prochain coup.

Jc

Liens
  • Sommaire du tutoriel;
  • Ouvrir une fenêtre SDL;
  • Lier la souris à la fenêtre et afficher des ronds à l'endroit cliqué;
  • Ajouter les règles du jeu et le finaliser;
  • Ajouter les classes Objet et Moteur pour rendre le jeu plus souple;
  • Ajouter un menu dans un jeu;
  • Ajouter une Intelligence Artificielle (Min-Max);
  • Ajouter une Intelligence Artificielle (Alpha-Beta);

8. Téléchargements

Voici la version pdf : pdf (293 Ko)

Voici le code source de ce tutoriel : zip (1.2 Mo)


Tutoriel SDL Morpion

Programmation de Jeux 2D: Un morpion en SDL, Septième partie

  • 1. Introduction
  • 2. Présentation de l'Alpha-Beta
    • 2.1. Présentation
    • 2.2. L'algorithme
    • 2.3. Exemple
    • 2.4. Discussion
  • 3. Implémentation
    • 3.1. La fonction calcMin
    • 3.2. La fonction calcIA
    • 3.3. Discussion
    • 3.4. Limitations
  • 4. Remerciements
  • 5. Conclusion
  • 6. Téléchargements

Dans ce tutoriel, nous avons présenté dans la partie précédente comment mettre en place une intelligence artificielle. Le problème de l'algorithme Min-Max est sa lenteur, il y a beaucoup d'évaluations inutiles. Dans cette partie, nous allons présenter une optimisation du Min-Max nommée Alpha-Beta.

1. Introduction

Le jeu du morpion est un mauvais exemple de la lenteur de l'algorithme Min-Max parce que la fonction d'évaluation est relativement rapide et le nombre de configurations n'est pas énorme.

Mais dès que vous allez tenter de faire tourner l'algorithme Min-Max sur un jeu d'échecs, vous comprendrez le problème. Une solution pour résoudre le problème est de limiter le nombre d'évaluations et de configurations qui seront à parcourir.

Cette partie va donc présenter la variante Alpha-Beta de l'algorithme Min-Max.

2. Présentation de l'Alpha-Beta

Nous allons commencer par présenter l'algorithme. Ensuite, nous présenterons comment modifier les fonctions de la classe IA pour implémenter cette variante.

2.1. Présentation

L'amélioration faite par l'algorithme Alpha-Beta consiste à limiter le nombre de configurations visitées lors du calcul du prochain coup. En effet, il n'est pas toujours nécessaire d'évaluer toutes les configurations.

Pour expliquer comment fonctionne l'algorithme Alpha-Beta, nous allons le faire en présentant rapidement les changements dans les fonctions calcIA, calcMin et calcMax et, ensuite, en montrant le déroulement de l'algorithme sur un exemple. Je pense que ce sera plus clair et plus facile à assimiler.

2.2. L'algorithme

Pour montrer les modifications de l'algorithme Alpha-Beta, nous allons remontrer la version de calcMax :

Ancien algorithme
Sélectionnez

Fonction calcMax(Jeu *j, int prof)
{
Soit max = MINIMUM;

Si la partie est terminée ou si la profondeur == 0 alors
    retourner eval(n);
Sinon
    Pour chaque coup possible
        Faire
            Jouer le coup
            tmp = calcMin(j, prof-1);
            Si tmp > max alors
                max = tmp;
            FinSi
            Annuler le coup
        FinPour
    Retourner max;
}

Voici la version pour l'Alpha-Beta :

Nouvel algorithme
Sélectionnez

Fonction calcMax (Jeu *j, int prof, int alpha, int beta)
{
Soit max = MINIMUM;

Si la partie est terminée ou si la profondeur==0 alors
    retourner eval(n);
Sinon
    Pour chaque coup possible
        Faire
            Jouer le coup
            alpha = max (alpha, calcIA(j, prof-1, alpha, beta))
            Si beta <= alpha alors
                retourner alpha
            Fin Si
            Annuler le coup
        Fin Pour
   retourner alpha
Fin Si

Quelle est donc la grande différence? C'est le test dans la boucle qui fait la grande différence. En effet, la partie :

Différence
Sélectionnez

            Si beta <= alpha alors
                retourner alpha
            Fin Si

Ceci permet donc de ne pas étudier toutes les configurations possibles. Pour mieux comprendre les implications de ces ajouts, la prochaine partie montre un exemple concret de parcours d'arbre. Il devrait vous illustrer comment on peut savoir s'il est nécessaire ou non d'étudier un sous-arbre.

2.3. Exemple▲

Pourquoi est-ce que ceci fait une si grande différence? Je m'excuse par avance de la longueur de cette sous-section. L'algorithme alpha-beta n'est pas difficile à mettre en place et pas difficile à comprendre lorsqu'on a un exemple. Cet exemple montre le déroulement de l'algorithme en 8 étapes. Voici l'arbre des configurations :

Image non disponible
Arbre de configuration

Comme pour l'algorithme du Min-Max, on commence par parcourir l'arbre. Rappelons que la racine de l'arbre est un noeud Max, ses fils sont des noeuds Min et ses petits fils sont des noeuds Max... Regardons l'image suivante :

Image non disponible
Parcours, étape 2

Lors de la première descente, on regarde les trois fils 5, 1 et 9. Etant donné que leur père est un noeud Max, sa valeur devient donc 9. Cette valeur devient donc la valeur intermédiaire de son propre père, c'est ce que montre l'image suivante :

Image non disponible
Parcours, étape 3

Ensuite, l'algorithme recommence la descente vers les noeuds 2, 12 et 9. Lorsque le noeud 2 est évalué, cette valeur intermédiaire remonte à son père. On le voit ici :

Image non disponible
Parcours, étape 4

Or lorsqu'on regarde le prochain noeud, la valeur 12 est rencontrée. Etant donné que son père est un noeud Max, il prend la valeur 12 (puisque 12>2).

Image non disponible
Parcours, étape 5

C'est à ce moment que l'algorithme Alpha-Beta réagit différemment que l'algorithme Min-Max. En effet, est-il nécessaire de regarder le troisième fils?

Le petit-fils 12 ne peut que monter. En effet, puisque c'est un noeud max, l'évaluation ne peut que monter. Mais son père est un noeud min, donc pour le moment, il choisira le noeud 9. Donc peu importe l'évolution de la valeur du petit-fils droit, le fils gauche choisira de toute facon le sous-fils gauche. Il est donc inutile de regarder le troisième noeud.

On a donc terminé l'évaluation du sous-arbre gauche. La racine récupère donc la valeur temporaire 9, comme le montre la prochaine image :

Image non disponible
Parcours, étape 6

On commence donc l'évaluation du sous-arbre droit. Le premier sous-fils prend la valeur temporaire 5.

Image non disponible
Parcours, étape 7

Donc la valeur remonte au fils droit qui prend cette valeur temporaire, comme le montre cette dernière image :

Image non disponible
Parcours, étape 8

Maintenant est-ce la peine de continuer les évaluations? La réponse est bien sûr non! L'évaluation du fils droit ne peut que descendre (puisque c'est un noeud Min). Donc la racine aura un choix entre l'évaluation 9 du fils gauche et le fils droit qui aura une évaluation forcément plus petite que 5. Puisque la racine est un noeud Max, il prendra donc la valeur 9. Il est donc inutile de regarder les autres noeuds.

2.4. Discussion

Donc finalement, en connaissant quelques valeurs clés, il est possible de savoir si l'évaluation d'un noeud ou même d'un sous-arbre est nécessaire ou pas.

La beauté de l'algorithme Alpha-Beta est sa simplicité pour permettre un tel mécanisme. La suite de cet article va donc montrer ce qui change dans le code.

3. Implémentation

L'implémentation de l'algorithme Alpha-Beta ressemble énormément à celle du Min-Max. On va commencer par montrer les prototypes des fonctions de la classe IA.

Prototype des fonctions
Sélectionnez

  //Fonction qui calcule le prochain coup*/
  void calcIA(Jeu *j, int prof);

  //Fonctions pour le calcul
  int calcMin(Jeu *j, int prof, int alpha, int beta);
  int calcMax(Jeu *j, int prof, int alpha, int beta);

Remarquez l'ajout des deux paramètres alpha et beta. En rappelant la présentation de la section précédente, l'algorithme de l'Alpha-Beta est exactement le même que celui du Min-Max. Les seules différences sont :

  • Nous n'initialisons plus les variables min et max avec des valeurs minimales et maximales. A la place, nous utiliserons les valeurs d'alpha et de beta.
  • La variable alpha est mise à jour dans les noeuds max.
  • La variable beta est mise à jour dans les noeuds min.
  • Nous arrêtons le parcours si la variable alpha se retrouve supérieure à beta.

3.1. La fonction calcMin

Puisque nous avons présenté le code de la fonction calcMax, nous allons montrer le code de la fonction calcMin. Je ne vais pas mettre tout le code puisque le début du code est exactement le même que l'algorithme Min-Max.

Modification de la fonction calcMin
Sélectionnez

for(i=0;i<3; i++)
  for(j=0;j<3;j++)
    {
    if(jeu->estVide(i,j))
      {
      jeu->joue(i,j);

      tmp = calcMin(jeu, prof-1,alpha,beta);

      jeu->annuleCoup(i,j);

      //Mis a jour de la valeur de alpha
      if(alpha<tmp)
        {
        alpha = tmp;
        }

      if(beta <= alpha)
        {
        return alpha;
        }
      }
    }

return alpha;

Si vous faites une comparaison avec la version de l'algorithme Min-Max, on ne fait qu'ajouter deux tests. La mise à jour de la variable alpha se fait comme la variable max de la version Min-Max. La grande différence est l'ajout du deuxième test qui permet de sortir du parcours des configurations. Si beta devient inférieur à la variable alpha, on sait qu'on peut sortir de la fonction.

3.2. La fonction calcIA

La seule nouveauté de la fonction calcIA de la version Alpha-Beta se retrouve dans le corps du nid de boucles. Voici les différences :

Nouveauté dans la fonction calcIA
Sélectionnez

//On appelle la fonction calcMin pour lancer l'IA
tmp = calcMin(jeu, prof-1,alpha,beta);

//Si ce score est plus grand
if(alpha<tmp)
  {
  //On le choisit
  alpha = tmp;
  maxi = i;
  maxj = j;
  }

Donc finalement, la seule différence est de passer la valeur de la variable alpha à la fonction calcMin. Ceci permet de réduire le nombre de configurations étudiées.

3.3. Discussion

Voilà, c'est tout! Je me doute que cela semble un peu magique. Il suffit de relire la deuxième section pour comprendre le principe et revoir cette section pour bien regarder comment l'ajout de ces quelques instructions permet de limiter le nombre de configurations.

Enfin, je pensais que ce serait intéressant de regarder le nombre de configurations étudiées lors du premier coup (l'IA jouera donc le premier coup) :

Niveau de l'IA | Min-Max | Alpha-Beta
IA Facile : | 504 | 125
IA Moyen : | 15120 | 1985
IA Difficile : | 255168 | 8453

Je pense que le tableau montre bien l'intérêt de l'algorithme Alpha-Beta.

3.4. Limitations

A cause de la spécificité de l'algorithme Alpha-Beta, nous ne pouvons plus utiliser l'amélioration de l'algorithme Min-Max pour avoir un jeu plus aléatoire. Rappelons cette modification :

Modification de l'algorithme
Sélectionnez

if((tmp>max)||((tmp==max)&&(Rand::randi(2))))

Ceci permet de choisir un coup parmi d'autres de la même valeur. Le problème dans l'algorithme Alpha-Beta est que ceci n'est plus possible puisque le retour des fonctions calcMin et calcMax n'est plus vraiment la valeur de l'évaluation du fils. Ce n'est donc pas forcément l'évaluation du noeud mais le retour de la valeur alpha ou beta.

La seule solution pour permettre de remettre cette possibilité d'aléatoire est d'enlever cette gestion de la valeur alpha et donc de perdre un peu l'efficacité de l'algorithme.

Voici donc le nouveau code de la fonction calcIA :

Nouveau code Alpha-Beta
Sélectionnez

//Si ce score est plus grand
if(max<tmp)
  {
  //On le choisit
  max = tmp;
  maxi = i;
  maxj = j;
  }

Donc finalement le code ressemble plus à la version Min-Max (honnêtement, c'est le code du Min-Max, à part le passage des variables alpha et beta à la fonction calcMax). Nous pouvons montrer le tableau qui compare les deux versions :

Niveau de l'IA | Nouveau Alpha-Beta | Alpha-Beta de Base
IA Facile : | 227 | 125
IA Moyen : | 2973 | 1985
IA Difficile : | 14113 | 8453

Ce tableau confirme donc qu'on perd un peu de performances avec cette nouvelle méthode, mais on gagne en aléatoire.

4. Remerciements

Enfin, j'aimerais prendre le temps de remercier toute l'équipe de Developpez.com, en particulier Laurent Gomila, Gege2061, Miles, ace.ice.loki, Loka et Ricky81.

Et bien sûr, je remercie ma femme qui me laisse tapoter sur le clavier presque tous les soirs!

5. Conclusion

Une petite partie qui clôture finalement ce programme de morpion. En effet, lorsqu'on met en place un algorithme d'intelligence artificielle, on n'a pas un grand pas à faire pour arriver à l'algorithme de l'Alpha-Beta.

Par contre, on voit facilement que c'est un grand avantage de le mettre en place. Cette optimisation permet d'avoir de bonnes performances.

Enfin, ceci finit ce tutoriel sur le morpion. A partir d'un des jeux les plus faciles à faire, on a tout de même fait un grand parcours. Nous avons un système de menu, une gestion d'objet basique, une gestion de la souris et du clavier et une intelligence artificielle.

J'espére que vous avez apprécié cette série, qu'elle vous a plu et que vous avez appris quelque chose. C'était un plaisir de les faire!

Jc

Liens
  • Sommaire du tutoriel;
  • Ouvrir une fenêtre SDL;
  • Lier la souris à la fenêtre et afficher des ronds à l'endroit cliqué;
  • Ajouter les règles du jeu et le finaliser;
  • Ajouter les classes Objet et Moteur pour rendre le jeu plus souple;
  • Ajouter un menu dans un jeu;
  • Ajouter une Intelligence Artificielle (Min-Max);
  • Ajouter une Intelligence Artificielle (Alpha-Beta);

6. Téléchargements

Voici la version pdf : pdf (253 Ko)

Voici le code source de ce tutoriel : zip (2 Mo)


Tutoriel SDL Morpion

Programmation de Jeux 2D: Un pong en SDL/OpenGL

Si vous avez suivi la partie sur le morpion, vous avez eu l'occasion d'avoir un avant goût de ce qui peut se retrouver dans un programme de jeu. Le morpion était un bon premier programme puisqu'il ne posséde pas d'animations, il ne faut pas se soucier de performances (du moins pas énormément) et on avait donc la liberté de s'intéresser plus facilement à l'esthétique du jeu, sans se perdre dans les détails. Dans un jeu de pong, il y a beaucoup plus à gérer. On doit arriver à garder le nombre d'images par secondes assez élevé (certes, ce n'est pas trop difficile) mais nous allons nous compliquer la tâche. En effet, cette série va proposer de montrer comment faire un pong qui aura plusieurs balles et sera en multi-joueur ! Bonne lecture!

Bienvenue à ce groupe de tutoriels sur le pong. Cette série sera plus compléte que celle sur le morpion puisque nous allons nous attaquer sur un jeu plus difficile. En espérant qu'ils répondront à vos attentes concernant la programmation de ce jeu, je vous laisse et vous souhaite une bonne lecture.

Tutoriels
  • Introduction;
  • Les bases du moteur;
  • Les collisions et un menu;
  • Améliorer les collisions;
  • Le score, la souris et les joueurs;
  • Le réseau;
  • Conclusion.

Pour toute remarque, suggestion, question ou correction, vous pouvez me contacter par message privé.


Tutoriel SDL Morpion

Programmation de Jeux 2D: Un pong en SDL/OpenGL, Première partie

  • 1. Introduction
    • 1.1. SDL
    • 1.2. OpenGL
  • 2. Programme de base
  • 3. Conclusion
  • 4. Téléchargements
  • 5. Remerciements

Le jeu de pong est sûrement un des jeux les plus refait. Cette série de tutoriels va présenter une implémentation possible mais en rendant la tâche plus difficile. En effet, nous voulons avoir plusieurs balles qui peuvent être en collision et un jeu en multi-joueur. Voici l'introduction de ce tutoriel, il montre comment ouvrir une fenêtre SDL/OpenGL.

1. Introduction

Bienvenue à cette nouvelle série sur la programmation de jeux en 2D. Nous allons nous lancer dans un projet nettement plus intéressant que le morpion. En effet, nous allons présenter le jeu Pong.

La version de base consiste à avoir deux raquettes et une balle. Il faut faire passer la balle derrière la raquette de l'adversaire et à ce moment là c'est gagné!

Encore un jeu suffisamment simple pour ne pas nous faire perdre nos cheveux mais nous allons tout de même aller un peu plus loin. Premièrement, nous allons ajouter le fait que le jeu va ajouter une balle de temps en temps, ce qui va bien sûr compliquer le jeu. Ensuite, nous allons ajouter une couche réseau pour pouvoir jouer à ce jeu en multijoueur.

1.1. SDL

Je vais bien sûr reprendre beaucoup de choses de la série sur le morpion, je vous conseille donc de le lire si vous n'êtes pas familier avec la bibliothèque SDL ou avec la programmation de jeu en général.

La bibliothèque SDL permet d'ouvrir la fenêtre et de gérer les entrées claviers et souris. Nous allons revoir rapidement dans la section 2 comment ouvrir la fenêtre. Mais, contrairement au morpion, nous allons utiliser la bibliothèque OpenGL pour l'affichage.

1.2. OpenGL

On pourra se demander pourquoi utiliser OpenGL pour faire de la 2D? Pourquoi ne pas utiliser les fonctions fournies par SDL?

Les réponses sont assez simples à comprendre : nous voulons afficher des balles de tailles différentes et de couleurs différentes.

La première solution serait d'utiliser une image comme celle-ci :

Image non disponible
Une image d'une balle

Ensuite, lors du rendu on utilise un filtre de couleur pour dessiner la balle dans la couleur voulue, et on modifie la taille pour obtenir les dimensions souhaitées.

La bibliothèque SDL ne permet pas facilement (et rapidement) de faire ces modifications, par contre c'est très facile en OpenGL.

La deuxième solution serait d'avoir une image contenant toutes les tailles et couleurs des balles souhaitées. Inutile de vous dire que ce serait un vrai travail de titan et on limiterait le programme inutilement.

Nous avons donc choisi d'utiliser SDL pour la gestion de la fenêtre et OpenGL pour le rendu.

2. Programme de base

Cette section va montrer comment ouvrir une fenêtre SDL/OpenGL correctement initialisée. Nous allons commencer par une fonction qui gère l'affichage OpenGL.

Fonction d'affichage
Sélectionnez

void affiche()
{
   //Mettre a zero le tampon de couleurs
   glClear(GL_COLOR_BUFFER_BIT);
   glLoadIdentity();

   SDL_GL_SwapBuffers();
}

Comme tout programme OpenGL, pour l'affichage, on utilise la fonction glClear pour mettre le tampon de couleur à zéro. C'est comme cela qu'on efface toute l'image qui se trouve dans le tampon. Cela nous permet donc de dessiner de nouveau sur le tampon. Ensuite, on remet le repère à zéro utilisant glLoadIdentity et, lorsque le code du dessin est terminé, on met à jour les tampons avec SDL_GL_SwapBuffers.

Ce tutoriel n'est en rien un tutoriel sur OpenGL. On suppose que vous connaissez un minimum sur OpenGL pour que cette fonction affiche ne soit pas une surprise. La suite de ce tutoriel est une présentation rapide de comment ouvrir une fenêtre OpenGL avec la bibliothèque SDL, pour avoir plus de détail sur les fonctions d'initialisation SDL, regardez le premier tutoriel SDL sur le morpion ou les tutoriels de loka.

La fonction main commence par des déclarations de variables locales.

Début du main
Sélectionnez

int main(int argc, char **argv)
{
    SDL_Event event;
    SDL_Surface *screen;
    int done = 0;
    int fps, last,now;

Voici la signification de ces variables :

  • event : pour gérer les événements;
  • screen : pour gérer la surface de la fenêtre;
  • done : pour savoir si le programme devait se terminer;
  • fps : pour calculer le nombre d'affichage par seconde;
  • last, now : pour gérer le temps (pour le calcul de fps).

Une fois les variables déclarées, nous allons commencer par initialiser SDL.

Initialisation
Sélectionnez

    //Initialisation
    if(SDL_Init(SDL_INIT_VIDEO)!=0) {
       cerr << "Probleme pour initialiser SDL: " << SDL_GetError() << endl;
       return EXIT_FAILURE;
    }

    //Mettre un titre à la fenetre
    SDL_WM_SetCaption("Pong Version 1.0", NULL);

    //Double tampon
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER,1);

    //Ouvrir une fenetre
    screen = SDL_SetVideoMode(WIDTH, HEIGHT, 32, SDL_OPENGL);
    if(screen==NULL)
          done = 1;

Comme vous le voyez, on initialise SDL, on met un titre à la fenêtre. Ensuite, la fonction SDL_GL_SetAttribute permet de dire qu'on utilise un double tampon pour l'affichage (sous SDL pur, nous utilisons le drapeau SDL_DOUBLEBUF; pour OpenGL, le double tampon est par défaut dans l'implémentation courante de SDL mais je trouve important de rajouter cet appel). Enfin, on utilise la fonction SDL_SetVideoMode pour dire qu'on veut utiliser SDL_OPENGL. C'est ici qu'il y a un changement par rapport à un programme pur SDL.

Ensuite, nous avons le code d'initialisation OpenGL. Puisque nous sommes en train de faire un jeu 2D, la mise en place d'une vue orthogonale est logique. Voici comment on la met en place :

Initialisation d'OpenGL
Sélectionnez

    //Remettre a zero la matrice de projection
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();

    // Mettre les matrices en place pour une vue orthogonale
    glOrtho(0,WIDTH,HEIGHT,0,-1,1);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

Nous allons commencer par remettre à zéro la matrice de projection. Ensuite, nous appelons glOrtho pour avoir une vue orthogonale. Les deux premiers paramètres sont les bornes de la largeur de la fenêtre. Les deux paramètres qui suivent sont les bornes de la hauteur de la fenêtre. Enfin, les deux derniers paramètres définissent les bornes de profondeur. Ici, WIDTH et HEIGHT sont définies dans un .h comme ceci :

Définition des variables
Sélectionnez

const int WIDTH=800;
const int HEIGHT=600;

Avant de commencer la boucle événementielle, nous initialisons last et fps. Prenons donc le temps d'expliquer l'utilité de ces deux variables. fps servira pour compter le nombre d'images par seconde. A chaque itération de la boucle événementielle, nous allons regarder l'heure actuelle en utilisant la fonction SDL_GetTicks qui fournit l'heure en nombre de millisecondes (donc la division par 1000 permet d'avoir ce temps en secondes). Nous allons ensuite comparer l'heure actuelle avec la valeur de last. Si la valeur est différente, nous allons pouvoir calculer le nombre d'affichages par seconde.

Initialisation des variables last et fps
Sélectionnez

    last = SDL_GetTicks()/1000;
    fps = 0;
Boucle événementielle
Sélectionnez

    //Boucle generale
    while(!done)
    {
        //Traiter les evenements
    while(SDL_PollEvent(&event))
    {
        switch(event.type)
        {
            case SDL_QUIT:
                done=1;
                break;
              case SDL_KEYUP:
                if(event.key.keysym.sym==SDLK_q)
                    done=1;
                break;
            default:
                break;
        }
    }

Rien de spécial dans cette boucle. Nous parcourons les événements qui sont en attente. Si l'événement est de type SDL_QUIT, on met la variable done à jour. Si l'événement est de type SDL_KEYUP et que la touche relâchée est la lettre 'q' alors on met done à jour pour quitter le programme.

A la fin de la boucle événementielle, on incrémente la variable fps. Et on vérifie l'heure actuelle. Enfin, on appelle la fonction affiche pour appeler l'affichage de la scène. Je pense que le code qui suit est assez explicite :

Fin de la boucle événementielle
Sélectionnez

    fps++;

    now = SDL_GetTicks()/1000;
    if(now!=last)
        {
        cout << "FPS: " << fps/(now-last) << endl;
        last = now;
        fps = 0;
        }

    affiche();
    }

La fin du programme se termine l'appel SDL_Quit.

Fin du programme
Sélectionnez

    SDL_Quit();
    return EXIT_SUCCESS;
}

3. Conclusion

Voilà pour cette première partie, nous avons fait une introduction de l'utilisation des bibliothèques SDL et OpenGL.

Comme vous le voyez, il est assez facile d'ouvrir une fenêtre et, pour ajouter du code d'affichage, il suffit de compléter la fonction affiche.

Cette première partie n'est qu'un début et on me dira que c'est encore un autre tutoriel d'ouverture d'une fenêtre SDL. Mais je préfère prendre le temps de le refaire encore une fois, ne serait-ce que parce que l'ajout de la gestion OpenGL change tout de même certaines choses. Les parties qui vont suivre vont gérer l'affichage des balles, leurs collisions et la gestion de la souris.

Jc

Liens
  • Sommaire du tutoriel;
  • Introduction;
  • Les bases du moteur;
  • Les collisions et un menu;
  • Améliorer les collisions; Image non disponible
  • Le score, la souris et les joueurs;
  • Le réseau;
  • Conclusion.

4. Téléchargements

Voici le code source pour ce premier tutoriel: (3 Ko).

Voici la version pdf de cet article: (35 Ko).

Si vous avez des suggestions, remarques, critiques, si vous avez remarqué une erreur, ou bien si vous souhaitez des informations complémentaires, n'hésitez pas à me contacter !

5. Remerciements▲

J'aimerais remercier loka pour sa double relecture de cet article !


Tutoriel SDL Morpion

Programmation de Jeux 2D: Un pong en SDL/OpenGL, Deuxième partie

  • 1. Introduction
  • 2. La modification du fichier Main.cpp
    • 2.1. La classe Moteur
    • 2.2. Gérer la fréquence d'affichage
      • 2.2.a. Version basique
      • 2.2.b. La version SDL_gfx
  • 3. La classe Moteur
    • 3.1. Moteur.h
    • 3.2. Moteur.cpp
  • 4. La classe Jeu
    • 4.1. Le fichier Jeu.h
      • 4.1.a. Les champs membres
      • 4.1.b. Les fonctions membres
    • 4.2. Le fichier Jeu.cpp
      • 4.2.a. Le Constructeur et le Destructeur
      • 4.2.b. La fonction initTextures
      • 4.2.c. Les autres fonctions d'initialisation
      • 4.2.d. La fonction addObjet
      • 4.2.e. Les fonctions d'affichage
      • 4.2.f. La fonction updateObjets
      • 4.2.g. La fonction gereSceneServeur
  • 5. La classe Objet
    • 5.1. Le fichier Objet.h
      • 5.1.a. Les champs membres
      • 5.1.b. Le fichier Objet.cpp
  • 6. La classe Vecteur
  • 7. La classe Rand
  • 8. Conclusion
  • 9. Téléchargements
  • 10. Remerciements

Cette partie pose les fondations du jeu du pong. A travers 3 classes, nous allons pouvoir définir les interactions nécessaires pour la gestion d'un jeu comme le pong. Une classe Objet pour chaque balle (et bientôt chaque raquette), une classe pour le jeu et une classe Moteur pour diriger le tout. Cet article présente donc ces classes.

1. Introduction

Bienvenue à cette deuxième partie sur la programmation du pong. Nous allons de nouveau ajouter beaucoup de choses dans cette partie mais rien de très surprenant. Si vous avez suivi la série du morpion, vous avez vu comment faire une classe Objet, une classe Jeu et une classe Moteur.

Cette partie présentera donc ces trois classes et leur interaction dans ce programme. Il y a beaucoup de choses à montrer donc je m'excuse déjà pour la longueur de cet article.

2. La modification du fichier Main.cpp

Il n'y a pas beaucoup de changements dans ce fichier. Mais nous allons présenter les quelques endroits où il y a des différences dans cette section.

2.1. La classe Moteur

Dans ce fichier Main.cpp, nous déclarons une variable globale :

Déclaration d'une variable globale
Sélectionnez

//Variable globale
Moteur moteur;

Comme vous le voyez, le programme aura une instance de la classe Moteur. Cette classe permettra de faire le lien entre le jeu et la fonction main. Nous verrons la déclaration de cette classe plus loin dans cet article.

Nous avons un appel à une fonction init de la classe moteur. Cet appel permettra de gérer toute l'initialisation du moteur.

Initialisation de la classe moteur
Sélectionnez

//Initialisation du moteur
if(moteur.init()==false)
    done = 1;

2.2. Gérer la fréquence d'affichage

2.2.a. Version basique

La bibliothèque SDL ne contient pas directement des fonctions pour gérer la fréquence d'affichage. En suivant la réponse de la question FAQ Comment gérer la vitesse d'affichage ?, nous allons mettre le code qui permet de limiter la fréquence d'affichage. Nous commencerons donc par initialiser la variable checkTime avant la boucle événementielle.

Initialisation de la variable checkTime
Sélectionnez

checkTime = SDL_GetTicks();

Ensuite, nous allons entourer le code d'affichage et le code de gestion de la variable fps avec le code de la FAQ :

Gestion de l'affichage
Sélectionnez

if(SDL_GetTicks() > (checkTime + 1000 / wantedfps) )
  {
  // On met a jour la variable checkTime
  checkTime = SDL_GetTicks();

  // On incremente la variable fps
  fps++;
  // Gerer l'affichage du fps
  now = time(NULL);
  if(now!=last)
    {
    cout << "FPS: " << fps/(now-last) << endl;
    last = now;
    fps = 0;
    }

  // Demander au moteur de dessiner la scene
  moteur.gereScene();
  }
else
  {
  // Attendre 5 millisecondes
  SDL_Delay(5);
  }

Comme vous le voyez, on ira dans le corps du if tous les 1000/wantedfps millisecondes. C'est maintenant à travers de la fonction membre gereScene que l'affichage et la gestion du jeu se fera.

Vous remarquerez que nous ajoutons une clause else. Ce code permet d'arrêter l'exécution pendant 5 millisecondes pour donner un peu de temps au système d'exploitation.

2.2.b. La version SDL_gfx

Si vous voulez une technique un plus facile à gérer, il vous faudra utiliser la bibliothèque SDL_gfx. En effet, cette extension permet d'avoir des fonctions en plus, dont celles qui vont nous servir ici. Les fonctions qui doivent être utilisées pour gérer la fréquence d'affichage sont définies dans le fichier d'en-tête SDL_framerate.h (par contre, utiliser l'option -lSDL_gfx pour la compilation).

Voici les fonctions définies dans ce fichier :

Les fonctions définies dans le fichier SDL_framerate.h
Sélectionnez

void SDL_initFramerate(FPSmanager * manager);
int SDL_setFramerate(FPSmanager * manager, int rate);
int SDL_getFramerate(FPSmanager * manager);
void SDL_framerateDelay(FPSmanager * manager);

Toutes ces fonctions utilisent un paramètre de type FPSmanager. Pour chaque programme, vous en définissez un (allouer statiquement ou dynamiquement) que vous passerez à chaque fonction. On va montrer maintenant comment on les utilise.

On commencera par définir une variable de type FPSmanager. Ensuite, en appelant SDL_initFramerate, on initialise la structure.

Définition et initialisation de FPSmanager
Sélectionnez

FPSmanager manager;

//Initialisation
SDL_initFramerate(&manager);

Par défaut, l'utilisation des ces fonctions mettent en place 30 images par seconde. Pour définir un autre taux d'affichage, il faut utiliser la fonction SDL_setFramerate. Pour récupérer le taux d'affichage, on utilise SDL_getFramerate.

Mettre 60 images par seconde
Sélectionnez

//Mettre le nombre d'images par secondes souhaite
SDL_setFramerate(&manager,35);

Bien sûr, si le programme n'arrive pas à garder un taux d'affichage aussi élevé, cette technique (comme la version naïve) ne pourra pas faire mieux. Il faut donc s'assurer que le code de rendu est assez rapide.

Enfin, pour mettre en place la gestion du taux d'affichage, on fait un appel à SDL_framerateDelay. Vous devez faire un appel pour chaque itération de la boucle globale. Cela ressemblerait donc à :

Mettre 60 images par seconde
Sélectionnez

while(!done)
{
  ...
  // Demander au moteur de dessiner la scene
  moteur.gereScene();
  SDL_framerateDelay(&manager);
}

3. La classe Moteur

Nous allons donc présenter la classe Moteur. Ceci sera fait en deux parties, d'abord le fichier d'en-tête puis le fichier source.

3.1. Moteur.h

Déclaration de la classe
Sélectionnez

class Moteur
{
  private:
    // Le jeu
    Jeu* jeu;

    // Sommes-nous dans le menu ?
    bool dans_menu;

    // Dessiner le jeu
    void dessineJeu();
    // Dessiner le menu
    void dessineMenu();

  public:
    // Le constructeur et destructeur
    Moteur();
    ~Moteur();

    // Gerer la scene (affichage + mise a jour)
    void gereScene();

    // Fonction d'initialisation
    bool init();
};

Comme vous le voyez, dans cette version du moteur du jeu, nous avons deux variables.

  • Nous avons une instance jeu de la classe Jeu. Comme vous le pouvez le voir, cette classe Moteur ne posséde rien de plus sur le jeu sous-jacent. Ceci est logique. Nous voulons garder cette classe aussi indépendante que possible.
  • Nous avons aussi un booléen menu qui permettra de savoir si nous sommes dans le menu ou dans le jeu. Bien que dans cette version, le menu ne sera pas implémenté, je pensais que ce serait bien de le mettre tout de suite en place.

Cette classe posséde aussi 4 fonctions membres (dont 2 sont en private):

  • gereScene : cette fonction est le point d'entrée pour l'affichage. Dépendant de la valeur du booléen menu on pourra appeler dessineJeu ou dessineMenu;
  • dessineJeu : cette fonction se charge de l'affichage lorsque le programme est en mode jeu;
  • dessineMenu : cette fonction se charge de l'affichage lorsque le programme est en mode menu;
  • init : cette fonction gére l'initialisation de cette classe et de la classe init.

3.2. Moteur.cpp

Cette classe n'est qu'à ses débuts, elle est assez vide pour le moment! Mais nous allons tout de même présenter les fonctions de cette classe.

Constructeur et Destructeur
Sélectionnez

//Constructeur
Moteur::Moteur()
{
  menu = false;
  jeu = new Jeu();
}

//Destructeur
Moteur::~Moteur()
{
  delete jeu;
}

Comme vous le voyez, la seule initialisation est de mettre la variable menu à false et allouer de la mémoire pour l'instance jeu. Le programme ira donc directement dans le mode jeu comme nous pouvons le voir dans la fonction gereScene :

La fonction gereScene
Sélectionnez

void Moteur::gereScene()
{
  // Effacer le tampon des couleurs
  glClear(GL_COLOR_BUFFER_BIT);
  glLoadIdentity();

  if(!menu)
    dessineJeu();
  else
    dessineMenu();

  SDL_GL_SwapBuffers();
}

Ensuite, viennent le tour des fonctions dessineMenu et dessineJeu. Pour le moment, la fonction dessineMenu est vide. Par contre, le code de la fonction dessineJeu contient les appels pour la classe jeu.

Les fonctions dessineMenu et dessineJeu
Sélectionnez

void Moteur::dessineMenu()
{
}

void Moteur::dessineJeu()
{
  jeu->affiche();
  jeu->gereSceneServeur();
}

A quoi sert la fonction gereScene ? Pour pouvoir ajouter des balles, mettre à jour les positions des balles, vérifier les collisions, ...

Pourquoi ne pas mettre ce code dans la fonction d'affichage ? C'est en effet une chose souvent faite. A chaque affichage, on met à jour la position des objets. Mais ce n'est pas très portable. Par exemple, un client du jeu a juste besoin d'avoir les positions des objets et les afficher. Le client n'a pas besoin de mettre à jour les objets, c'est le travail du serveur (bien sûr, dans certaines applications le client fait aussi une partie du travail).

Finalement, nous avons la fonction d'initialisation du moteur :

La fonction init
Sélectionnez

bool Moteur::init()
{
   return jeu->init();
}

Cette fonction appelle simplement la fonction d'initialisation pour l'instance du jeu et retourne son résultat.

4. La classe Jeu

La classe Jeu posséde toutes les informations du jeu. Nous allons présenter la classe Jeu dans cette section.

4.1. Le fichier Jeu.h

4.1.a. Les champs membres

Nous allons commencer par la présentation des champs membres de la classe Jeu :

Les membres de la classe du Jeu
Sélectionnez

  //Les objets
  std::vector<Objet> objets;

  //Pour la creation des balles
  unsigned int last;

Comme vous le voyez, nous avons un vecteur objets pour les objets définis par une constante (qui se trouvera dans le fichier Define.h). Le type vector nous permet bien sûr de pouvoir facilement ajouter des objets dans le jeu et aussi d'en enlever sans difficulté.

On utilise la variable last pour savoir quand ajouter une nouvelle balle dans la partie. Nous verrons plus en détail son utilité lors de la prochaine section.

4.1.b. Les fonctions membres

Voici les fonctions membres de la classe Jeu :

Les fonctions membres
Sélectionnez

private:
    ...

    //Initialisation des objets et des textures
    bool initObjets();
    bool initTextures();

public:
    //Constructeur et Destructeur
    Jeu();
    ~Jeu();

    //Initialisation de la classe
    bool init();

    //Ajouter un objet dans le jeu, retourne l'indice de l'objet ajoute
    int addObjet(Objet &o);
    //Retourner le nombre d'objets dans le jeu
    unsigned int getNObjets();

    //Afficher le jeu
    void affiche();
    void affObjets();

    //Mettre a jour les objets
    void updateObjets();

    //Gere la scene (mise a jour)
    void gereSceneServeur();

Nous avons mis les deux fonctions initObjets et initTextures en privé puisque ce sont des fonctions qui devraient être uniquement appelées par la fonction init

Je ne pense pas que ce soit nécessaire d'en dire plus sur ces fonctions, les commentaires doivent suffire.

4.2. Le fichier Jeu.cpp

Montrons l'implémentation de la classe Jeu. A part la fonction initTextures, il n'y a pas grand chose de difficile dans cette classe mais je pense que c'est important de présenter les grands points.

4.2.a. Le Constructeur et le Destructeur

Constructeur et Destructeur
Sélectionnez

//Constructeur
Jeu::Jeu()
{
    last = 0;
}

//Destructeur
Jeu::~Jeu()
{
}

Comme vous le voyez le constructeur met simplement à zéro les variables last.

4.2.b. La fonction initTextures

La fonction initTextures sert à initialiser les textures. Elle retourne true si tout s'est bien passé et false s'il y a eu un problème. Voici le code de cette fonction :

La fonction initTextures
Sélectionnez

bool Jeu::initTextures()
{
  int i,j,cnt,pcnt;
  GLuint txtballe;

  unsigned char *pixels, *tmppixels;

  //On charge l'image "data/balle.bmp"
  SDL_Surface *tmpballe = SDL_LoadBMP("data/balle.bmp");
  if(tmpballe==NULL)
    {
    std::cerr << "Erreur dans le chargement de l'image de la balle" << std::endl;
    return false;
    }

  //On cree une surface RGBA de meme dimension
  SDL_Surface *balle = SDL_CreateRGBSurface(SDL_SWSURFACE, tmpballe->w, tmpballe->h, 32,
                0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000);
  if(balle==NULL)
    {
    std::cerr << "Erreur dans la creation de l'image de la balle" << std::endl;
    return false;
    }

Pour dessiner les balles du jeu, nous allons utiliser une texture OpenGL. Ces textures sont définies par une variable de type GLuint. Pour notre jeu et pour nos balles, nous utilisons une variable nommée txtballe.

On commence donc cette fonction par le chargement de l'image d'une balle et de la création d'une surface de même dimension. Nous avons besoin de cette nouvelle surface pour mettre en place le canal alpha. En effet, si vous vous souvenez de l'image d'une balle :

Image non disponible
La balle

Cette image est carré mais nous voulons une image ronde lors du rendu. Comment faire ? La réponse est le canal alpha. Rappelons, qu'avec la bibliothèque SDL, on utilise la fonction SDL_SetColorKey. Cette fonction permet de définir la couleur transparente de l'image.

Sous OpenGL, on utilise la valeur du canal alpha pour définir quel pixel sera dessiné ou non. Voyons la suite de cette fonction. Pour regarder et modifier les pixels des surfaces, il suffit d'utiliser les pointeurs pixels et tmppixels.

La suite de la fonction
Sélectionnez

  pixels = (unsigned char*) balle->pixels;
  tmppixels = (unsigned char*) tmpballe->pixels;

  pcnt = 0;
  cnt = 0;

  //Pour chaque pixel
  for(i=0;i<tmpballe->w;i++)
    {
    for(j=0;j<tmpballe->h;j++)
      {
      //On copie la couleur
      pixels[pcnt] = tmppixels[cnt];
      pixels[pcnt+1] = tmppixels[cnt+1];
      pixels[pcnt+2] = tmppixels[cnt+2];

      //La couleur magenta sera transparente
      if((tmppixels[cnt]==255)&&(tmppixels[cnt+1]==0)&&(tmppixels[cnt+2]==255))
        {
        pixels[pcnt+3] = 255;
        }
      else
        {
        pixels[pcnt+3] = 0;
        }

      cnt += 3;
      pcnt += 4;
      }
    }

Je ne pense pas que ce code soit très difficile à comprendre. On parcourt chaque pixel, si la couleur est magenta, on met le canal alpha à 255 sinon il est mis à 0 (remarquez que normalement on utilise une couleur moins utilisée comme le magenta mais c'est un choix arbitraire).

La seule chose dont il faut faire attention, c'est l'incrémentation de 3 pour le compteur cnt et 4 pour le compteur pcnt. En effet, pour l'image source on a que trois canaux (donc trois octets par pixel) et pour l'image destination on en a quatre (donc quatre octets par pixel).

La suite de cette fonction gére simplement le chargement de la surface sous forme de texture OpenGL. Nous commencerons par générer un indice pour la texture. Ensuite, nous utiliserons la fonction glBindTexture pour dire que c'est cette texture que nous voulons modifier. Les fonctions glTexParameteri permettent de dire quels filtres nous allons utiliser (voir cette partie pour plus de détail). Enfin, la fonction glTexImage2D permet de vraiment passer le tableau de l'image. Voici le code associé :

Chargement de l'image pour OpenGL
Sélectionnez

    //On passe la texture a OpenGL

    //Generation d'un indice de texture
    glGenTextures(1,&txtballe);

    //On choisit la texture et on definit un simple filtre pour la texture
    glBindTexture(GL_TEXTURE_2D,txtballe);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);

    //Chargement de l'image
    glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,pion->w,pion->h,0,
          GL_RGBA,GL_UNSIGNED_BYTE,balle->pixels);

    //Liberation des surfaces
    SDL_FreeSurface(balle);
    SDL_FreeSurface(tmpballe);

Avant de terminer cette fonction, on va passer à la classe Objet l'indice de la texture générée.

Passer l'indice à la classe Objet
Sélectionnez

    //On met a jour la variable statique txtballe de la classe Objet
    Objet::setTxtBalle(txtballe);

    return true;
}

4.2.c. Les autres fonctions d'initialisation

Rien de bien particulier pour ces deux autres fonctions d'initialisation. En effet, la fonction init permet d'appeler les deux autres fonctions (initTextures que nous avons déjà montré et initObjets qui remet le nombre d'objets à zéro en utilisant la fonction clear de la classe vector.

Fonctions d'initialisation
Sélectionnez

//Initialisation du jeu
bool Jeu::init()
{
    return initObjets() && initTextures();
}

bool Jeu::initObjets()
{
    objets.clear();
    return true;
}

4.2.d. La fonction addObjet

La fonction addObjet permet d'ajouter une balle dans le jeu s'il y a de la place dans le tableau. Cette fonction retourne -1 s'il y a eu un problème ou l'indice de l'objet qui vient d'être ajouté si tout s'est bien passé.

La fonction addObjet
Sélectionnez

int Jeu::addObjet(Objet &o)
{
  // Si on a encore de la place
  if(getNObjets()<MAX_BALLES)
    {
    // On copie l'objet dans la case vide
    objets.push_back(o);
    //On rend l'indice de l'objet insere
    return getNObjets()-1;
    }
  return -1;
}

Cette fonction appelle la fonction getNObjets qui retourne simplement le nombre d'éléments dans le vecteur objets :

La fonction getNObjets
Sélectionnez

inline unsigned int Jeu::getNObjets()
{
   return objets.size();
}

4.2.e. Les fonctions d'affichage

Les fonctions d'affichage pour la classe Jeu se divisent en deux fonctions. La fonction affiche est le point d'entrée pour l'affichage du jeu. Pour le moment, elle appelle simplement la fonction affObjets.

Fonction d'affichage
Sélectionnez

void Jeu::affiche()
{
  //Afficher les objets
  affObjets();
}

void Jeu::affObjets()
{
  int i;

  //On met en place le test sur la valeur alpha
  glEnable(GL_ALPHA_TEST);
  //On dessine si la valeur alpha est inférieur
  glAlphaFunc(GL_LEQUAL,0.1f);

  //On affiche les objets
  for(i=0;i<getNObjets();i++)
    {
    objets[i].affiche();
    }

  //On desactive le test sur la valeur alpha
  glDisable(GL_ALPHA_TEST);
}

La fonction glEnable permet de mettre en place le test Alpha. La fonction glAlphaFunc permet de définir comment on gére le canal alpha. En résumé, on définit une valeur seuil (0.1f dans ce cas) et avec GL_LEQUAL, on définit qu'on dessine le pixel si le canal alpha est inférieur à 0.1f. Si vous vous souvenez de la fonction initTextures, on a mis 1 pour les pixels magentas et 0 pour les autres, donc les pixels qui ne sont pas de la couleur magenta seront dessinés.

La fonction affObjets permet donc d'afficher tous les objets. Comme vous le voyez, nous entourons la boucle d'instructions permettant de tester le canal alpha. Enfin, vous remarquerez que, comme pour la série sur le morpion, c'est à chaque objet de gérer son affichage.

4.2.f. La fonction updateObjets

La fonction updateObjets
Sélectionnez

void Jeu::updateObjets()
{
  int i;

  // On met à jour chaque objet
  for(i=0;i<getNObjets();i++)
    {
    objets[i].updatePos();
    }
}

Comme pour la fonction d'affichage, cette fonction est le point d'entrée pour la mise à jour des positions des objets. Comme vous le voyez, on parcourt simplement les objets et on appelle la fonction pour mettre à jour leur position.

4.2.g. La fonction gereSceneServeur

La fonction gereSceneServeur est la dernière fonction que nous allons présenter ici. C'est une fonction qui va appeler la fonction updateObjets et ensuite regarder s'il est temps d'ajouter une nouvelle balle.

La fonction gereSceneServeur
Sélectionnez

void Jeu::gereSceneServeur()
{
  int vx,vy;
  int r,g,b;
  Objet tmpobj;

  // On met à jour les objets
  updateObjets();

  // Si on doit ajouter un objet
  if(SDL_GetTicks()>last+1000)
    {
    last = SDL_GetTicks();

    // Definit la position du nouvel objet (milieu de l'ecran)
    tmpobj.setPos(WIDTH/2-TAILLE_BALLE, HEIGHT/2-TAILLE_BALLE);

    // Choisit aleatoirement une direction (refusant un 0 pour vx ou vy)
    vx = Rand::randi(5000) - 2500;
    if(vx == 0)
      vx++;
    vy = Rand::randi(5000) - 2500;
    if(vy == 0)
      vy++;
    tmpobj.setDirVitesse(vx,vy);

    // On definit une norme de 4 pour la vitesse
    tmpobj.setVitesse(4);
    // On definit la taille
    tmpobj.setTaille(TAILLE_BALLE,TAILLE_BALLE);

    // On choisit une couleur avec un minimum de 50 par couleur
    r = Rand::randi(205)+50;
    g = Rand::randi(205)+50;
    b = Rand::randi(205)+50;

    tmpobj.setCouleur(r/256.0f, g/256.0, b/256.0);

    // On ajoute l'objet
    addObjet(tmpobj);
    }
}

Comme vous le voyez, on met à jour la position des objets et ensuite on regarde s'il est temps d'ajouter un objet dans le jeu. Si c'est le cas, on crée un objet au milieu de l'écran et on choisit une direction pour la vitesse aléatoirement (par contre, on définit la norme comme étant égale à 4). Ensuite, on choisit aussi une couleur aléatoire. Enfin, on ajoute l'objet dans le jeu.

Pour ne pas avoir trop de balles dans la fenêtre, on définit une variable MAX_BALLES qui permet de limiter le nombre.

Je pense que les noms des fonctions de la classe Objet sont assez clairs mais je vais prendre deux phrases pour les expliquer. setDirVitesse permet de définir la direction de la balle. setVitesse, setTaille et setCouleur permettent respectivement de définir la vitesse, la taille et la couleur de la balle.

5. La classe Objet

Nous avons déjà présenté la classe Moteur et la classe Jeu. Nous allons maintenant prendre un moment pour présenter la classe Objet.

5.1. Le fichier Objet.h

La classe Objet est la base de tout le programme. En effet, c'est la plus petite entité dans le jeu (remarquez que dans cette partie, on parlera encore de classes Vecteur et Rand mais ce ne sont que des classes pour aider la définition des autres, pas des classes pour le jeu).

5.1.a. Les champs membres

Les champs membres de la classe Objet sont définis :

Les membres de la classe Objet
Sélectionnez

  static GLuint txtballe;

  // Position, vitesse et taille de l'objet
  Vecteur pos,
    vitdir,
    taille;

  // Couleur
  float r,g,b;

Comme vous le voyez, il n'y a pas grand chose de spécial dans cette classe. Nous avons une variable statique pour l'indice de la texture pour les balles. Ensuite, nous avons un vecteur pour la position, la direction de la balle et la taille de la balle. Enfin, nous avons trois variables pour la couleur.

Nous n'allons pas présenté chaque fonction de cette classe (il y en a beaucoup pour chaque élément qui doit être accédé ou modifié. Nous allons présenter ceux qui ont un intêrét :

Les fonctions membres
Sélectionnez

  static void setTxtBalle(GLuint t);

  void setPos(double, double);
  void setTaille(double, double);
  void setVitesse(double);
  void setCouleur(float,float,float);

  //Fonction d'affichage
  void affiche();

  //Fonction de gestion  de collision
  void updatePos();

La première fonction setTxtBalle est une fonction statique pour définir la valeur du champs statique txtBalle. Ensuite, nous avons quatre fonctions pour définir la position, la taille, la vitesse et la couleur de la balle.

Ensuite nous avons une fonction pour afficher la balle et une pour mettre à jour la position.

5.1.b. Le fichier Objet.cpp

Le code des fonctions de la classe Objet est assez simple, on va montrer les fonctions les unes après les autres. On commence par ne pas oublier de mettre la variable statique txtballe à zéro.

La variable statique
Sélectionnez

//Variable statique
GLuint Objet::txtballe = 0;

Ensuite nous avons la fonction qui définit la valeur de cette variable statique :

La fonction setTxtBalle
Sélectionnez

//Mise en place de la texture d'une balle
void Objet::setTxtBalle(GLuint t)
{
    txtballe = t;
}

Nous allons maintenant présenter les fonctions set :

Les fonctions set
Sélectionnez

//Mise en place de la position de l'objet
void Objet::setPos(double x1, double y1)
{
    pos.setX(x1);
    pos.setY(y1);
}

//Mise en place de la direction de l'objet
void Objet::setDirVitesse(double vx1, double vy1)
{
    vitdir.setX(vx1);
    vitdir.setY(vy1);
}

//Mise en place de la taille de l'objet
void Objet::setTaille(double w1, double h1)
{
    taille.setX(w1);
    taille.setY(h1);
}

Comme vous le remarquez, ces fonctions ne font que passer l'information aux variables de type Vecteur.

Il reste la fonction d'affichage et la fonction de mise à jour de la position. Tout d'abord, nous allons voir la fonction d'affichage :

La fonction affiche
Sélectionnez

// Fonction d'affichage
void Objet::affiche()
{
    glEnable(GL_TEXTURE_2D);
    glBindTexture(GL_TEXTURE_2D, txtballe);

    glColor3u(r,g,b);
    glBegin(GL_QUADS);
        glTexCoord2i(0,1);
        glVertex3f(pos.getX(),pos.getY(),0);               // En haut a gauche
        glTexCoord2i(1,1);
        glVertex3f(pos.getX()+taille.getX(),pos.getY(),0); // En haut a droite
        glTexCoord2i(1,0);
        glVertex3f(pos.getX()+taille.getX(),pos.getY()+taille.getY(),0); // En bas a droite
        glTexCoord2i(0,0);
        glVertex3f(pos.getX(),pos.getY()+taille.getY(),0); // En bas a gauche
    glEnd();
    glDisable(GL_TEXTURE_2D);
}

Le code est assez clair, on met en place l'utilisation des textures. On dit qu'on veut utiliser la texture des balles. On définit la couleur de cette balle et on dessine le quadrilatère de la balle.

La dernière fonction que nous allons présenter de cette classe Objet est la fonction updatePos. En utilisant la position courante et la vitesse, on peut calculer la nouvelle position. Une fois mise en place, on va tester pour voir si la balle est en train de sortir de l'écran, si c'est le cas, on la remet au bord et on modifie la vitesse pour la faire repartir dans la direction opposée.

La fonction updatePos
Sélectionnez

//Fonction de mise a jour
void Objet::updatePos()
{
    //Calcul de la nouvelle position
    double x = pos.getX() + vitdir.getX(),
           y = pos.getY() + vitdir.getY(),
           w = taille.getX(),
           h = taille.getY();

    //Mise a jour
    pos.set(x,y);

    //Verification de la position ie si on sort de l'ecran on "rebondit"
    //A gauche
    if(x<0)
        {
        pos.setX(1);
        if(vitdir.getX()<0)
            vitdir.setX(vitdir.getX()*(-1));

        }

    //A droite
    if(x+w>=WIDTH)
        {
        pos.setX(WIDTH-w-1);
        if(vitdir.getX()>0)
            vitdir.setX(vitdir.getX()*(-1));
        }

    //En haut
    if(y<0)
        {
        pos.setY(1);
        if(vitdir.getY()<0)
            vitdir.setY(vitdir.getY()*(-1));
        }

    //En bas
    if(y+h>=HEIGHT)
        {
        pos.setY(HEIGHT-h-1);
        if(vitdir.getY()>0)
            vitdir.setY(vitdir.getY()*(-1));
        }
}

Vous remarquerez le test sur la positivité de la direction. Cela permet d'être sûr qu'on modifie correctement la vitesse lorsque c'est nécessaire.

6. La classe Vecteur

La classe Vecteur est relativement simple, nous présenterons simplement la définition de la classe.

La classe Vecteur
Sélectionnez

class Vecteur
{
    private:
        double x, y;

    public:
        Vecteur();
        ~Vecteur();

        void set(double, double);
        void setX(double x);
        void setY(double y);
        void normalise();

        void setNormalise(double, double);

        double getX();
        double getY();
};

Je ne pense pas que ce soit la peine d'expliquer plus le code. Cette classe a deux membres x et y et les fonctions permettent de définir la valeur de ces membres. Les seules fonctions intéressantes sont normalise qui permet de normaliser le vecteur et la fonction setNormalise qui permet de mettre en place le vecteur placé en paramètre et ensuite on normalise le vecteur.

7. La classe Rand

La dernière classe que nous allons présenter est la même que celle qui a été présentée dans la série morpion. Regardez ici pour plus de détails.

8. Conclusion

Voilà, la deuxième partie d'une série est toujours la plus longue puisqu'on doit prendre le temps de bien présenter toutes les classes qui sont en jeu. Voici une image de ce que nous venons de faire :

Image non disponible
Une image du programme actuel

La classe Moteur permet de faire le lien entre les informations données par la fonction main (et donc la bibliothèque SDL) et le jeu sous-jacent. La classe Jeu permet de définir et de gérer la cohérence entre tous les objets sous-jacents. Par exemple, le moteur va dire au jeu d'afficher tous les objets.

Bien sûr chaque objet possède aussi son propre indépendance. Ils mettent à jour leur position, savent s'afficher à l'écran.

Ceci conclut donc cette partie du tutoriel. Nous allons montrer comment gérer simplement les collisions dans la prochaine partie.

Jc

Liens
  • Sommaire du tutoriel;
  • Introduction;
  • Les bases du moteur;
  • Les collisions et un menu;
  • Améliorer les collisions; Image non disponible
  • Le score, la souris et les joueurs;
  • Le réseau;
  • Conclusion.

9. Téléchargements

Voici le code source pour ce tutoriel: (15.6 Ko) (version naïve), (15.6 Ko) (version SDL_framerate).

Voici la version pdf de cet article: (115 Ko).

Si vous avez des suggestions, remarques, critiques, si vous avez remarqué une erreur, ou bien si vous souhaitez des informations complémentaires, n'hésitez pas à me contacter !


Tutoriel SDL Morpion

Programmation de Jeux 2D: Un pong en SDL/OpenGL, Troisième partie

Table des matières

  • 1. Introduction
  • 2. Les collisions
    • 2.1. Classe Physique
    • 2.2. Collision Cercle-Cercle
    • 2.3. Collision Rectangle-Rectangle
    • 2.4. La gestion des collisions entre les objets
    • 2.5. La fonction updateObjets
    • 2.6. Appel au moteur physique
    • 2.7. L'insertion des balles
  • 3. Le menu
    • 3.1. Les raccourcis du clavier
    • 3.2. Le bouton Continuer
  • 4. La classe Jeu
  • 5. La classe Moteur
  • 6. Conclusion
  • 7. Téléchargements
  • 8. Remerciements

Dans cette partie, nous allons nous intéresser aux collisions des balles et à la création d'un menu. Il existe beaucoup de solutions pour cette gestion mais nous allons voir ici une solution simple et facile à mettre en place. Nous allons présenter une classe Physique. Elle sera responsable pour le déplacement des objets, mais aussi des collisions entre les objets. Enfin, nous présenterons une solution rapide pour mettre en place un menu avec des raccourcis claviers.

1. Introduction

Bienvenue à cette troisième partie sur la programmation du pong. Dans cette partie, nous allons nous intéresser à la gestion des collisions. La solution proposée ici est assez simple pour être facilement mise en place.

Cette partie présentera comment gérer les collisions et on introduira un menu dans l'application.

2. Les collisions

Nous allons présenter dans cette section la gestion de collisions entre les objets. Ceci va se faire en intégrant une nouvelle classe Physique.

2.1. Classe Physique

La classe Physique sera entièrement composée de fonctions statiques. Il existe deux fonctions publiques :

  • updateObjets : cette fonction met à jour la position et la vitesse des objets (en tenant compte bien sûr des collisions)
  • isCollision : cette fonction permet de vérifier s'il y a une collision avec le rectangle (x,y,w,h)

Enfin, voici la déclaration de la classe :

La classe Physique
Sélectionnez

class Physique
{
        public :
                //Fonction qui met à jour les objets
                static void updateObjets(std::vector<Objet> &objets);

                //Fonction qui teste la collision entre les objets et le rectangle (x,y,w,h)
                static bool isCollision(std::vector<Objet> &objets,
                            double x, double y, double w, double h);

        private :
                //Fonction qui gere la collision avec l'objet i
                static int collisionObjet(std::vector<Objet> &objets, int i);
                //Fonction qui teste la collision entre deux rectangles
                static bool collisionRect(double x1,double y1,double w1, double h1,
                            double x2, double y2,double w2,double h2);
                //Fonction qui teste la collision entre deux cercles
                static bool collisionCercle(double cx1,double cy1,
                            double r1, double cx2, double cy2, double r2);
};

Nous allons maintenant voir les fonctions qui gére la collision cercle-cercle et rectangle-rectangle.

2.2. Collision Cercle-Cercle

La collision entre deux cercles est plus facile à gérer. En effet, il suffit de calculer la distance entre deux cercles et voir si elle est inférieure au rayon d'un des cercles. Ce type de collision va être utilisé pour la collision entre deux balles.

Collision Cercle-Cercle
Sélectionnez

//Collision entre deux cercles (cx1,cy1,r1) et (cx2,cy2,r2)
bool Physique::collisionCercle(double cx1, double cy1, double r1,
                                 double cx2, double cy2, double r2)
{
   //Distance entre les deux centres
   double d = (cx1-cx2)*(cx1-cx2) + (cy1-cy2)*(cy1-cy2);
   d = sqrt(d);
   return (d<r1+r2);
}

2.3. Collision Rectangle-Rectangle

Il se peut que notre programme ait besoin d'une gestion de collision entre deux rectangles. Dans notre cas, nous aurons besoin de ce type de collision pour gérer la collision entre les balles et la raquette du jeu.

Collisions entre deux rectangles
Sélectionnez

//Fonctions qui gerent les collisions
bool Physique::collisionRect(double x1, double y1, double w1, double h1,
                        double x2, double y2, double w2, double h2)
{
    if(x1+w1<x2) return false;
    if(x2+w2<x1) return false;
    if(y1+h1<y2) return false;
    if(y2+h2<y1) return false;

    return true;
}

Comme vous le voyez, ce n'est pas très compliqué. La question est de savoir si le rectangle (x1,y1,w1,h1) est en collision avec (x2,y2,w2,h2).

On commence par regarder si l'abscisse x2 est à gauche de x1+w1. Si c'est le cas, il peut y avoir une intersection si x1 n'est pas à droite de x2+w2. En effet, cela voudrait dire que soit x2 appartient à l'intervalle [x1, x1+w1], soit x1 appartient à l'intervalle [x2,x2+w2]. Dans ce cas, on regarde si y1 est inclu dans [y2, y2+h2] ou si y2 est inclu dans [y1, y1+h1].

En faisant un dessin, je pense que vous arriveriez à voir tous les cas possibles et voir que nous les gérons tous... Ce type de collision sera utilisé par la collision entre une balle et une raquette.

2.4. La gestion des collisions entre les objets

Nous savons à présent gérer les collisions entre les balles et les collisions entre une balle et une raquette. Bien que nous n'aurons pas encore de raquette dans le programme actuel, nous allons préparer le terrain pour son introduction qui sera fait dans la prochaine partie.

On va poser comme hypothèse qu'avant la mise à jour des objets, aucun objet n'est en collision. Lorsque nous bougeons un objet, nous allons vérifier s'il y a eu une collision. Dans le cas d'une collision, nous remettons l'objet en place (donc à un endroit sans collision) et nous modifions sa direction.

Voici la fonction gérant les collisions :

Fonction testant la collision
Sélectionnez

//Est-ce que l'objet d'indice idx est en collision avec les autres ?
int Physique::collisionObjet(std::vector<Objet> &objets, int idx)
{
    int i;
    int nobjets = objets.size();

    double x1,x2,y1,y2;
    double w1,h1,w2,h2;

    double cx1=0,
           cy1=0,
           cr1=0,
           cx2=0,
           cy2=0,
           cr2=0;

    //On recupere la position et taille de l'objet idx
    x1 = objets[idx].getX();
    y1 = objets[idx].getY();
    w1 = objets[idx].getW();
    h1 = objets[idx].getH();

    //Pour le moment on n'a que des balles, on recupere son centre et rayon
    cx1 = x1+w1/2;
    cy1 = y1+h1/2;
    cr1 = w1/2;

    //On parcourt tous les autres objets
    for(i=0;i<nobjets;i++)
        {
        if(idx!=i)
            {
            x2 = objets[i].getX();
            y2 = objets[i].getY();
            w2 = objets[i].getW();
            h2 = objets[i].getH();

            //Recuperation du deuxieme centre et rayon
            cx2 = x2 + w2/2;
            cy2 = y2 + h2/2;
            cr2 = w2/2;

            //Si on a une collision entre les deux objets
            if(collisionCercle(cx1,cy1,cr1,cx2,cy2,cr2))
                return i;
            }
        }
    return -1;
}

Cette fonction n'est pas très compliquée. On commence par récupérer la position de l'objet en question. Puisque, pour le moment, nous avons que des balle, on utilise la position, la largeur et hauteur pour calculer le centre du cercle et son rayon.

Ensuite, on parcourt chaque objet et on regarde si on a une collision. Si les deux objets sont des cercles (donc notre cas pour le moment) alors nous devons tester pour une collision cercle-cercle.

Le retour de cette fonction représente l'objet qui est en collision ou -1. Ceci permettra au moteur physique de gérer la position des deux objets et leur vitesse.

2.5. La fonction updateObjets

La dernière fonction que nous allons présenter est nommée updateObjets. Cette fonction met à jour la position des objets du jeu. L'algorithme de la fonction met à jour la position de chaque objet puis vérifie s'il y a eu ou non une collision.

Dans le cas d'une collision, on remet l'objet dans sa position initiale et on met à jour la direction de l'objet. Voici l'implémentation de cette fonction, nous commencerons par présenter les déclarations des variables :

La fonction updateObjets
Sélectionnez

void Physique::updateObjets(std::vector<Objet> &objets)
{
        int i;
        int res;
        double oldx, oldy;

        double vx,vy,newvx, newvy,n;
        double x1,x2,y1,y2;

Ensuite, nous avons une simple boucle qui parcours chaque objet. Le programme sauvegarde la position de l'objet avant de la mettre à jour. Ensuite, on vérifie s'il y a eu une collision :

Mise a jour de l'objet
Sélectionnez

        for(i=0;i<nobjets;i++)
                {
                //Sauvegarde de l'ancienne position
                oldx = objets[i].getX();
                oldy = objets[i].getY();

                //Mise a jour de la position
                objets[i].updatePos();

                //Est-ce qu'on a une collision
                res = collisionObjet(objets, i);

Une fois le calcul de collision a été fait, on peut regarder la valeur de res pour une éventuelle collision. Si nous avons une collision, nous allons remettre la position de l'objet en place.

Remettre la position de l'objet
Sélectionnez

                if(res>=0)
                        {
                        //Si on a une collision, on remet l'ancienne position
                        objets[i].setPos(oldx,oldy);

Maintenant, le code le plus compliqué de ce tutoriel : la gestion de la collision. En effet, nous savons à présent qu'il y a eu une collision entre l'objet courant i et l'objet res. Il faut maintenant mettre à jour la direction des objets.

Gestion de la vitesse
Sélectionnez

                        //Recupere le centre de l'objet i
                        x1 = oldx + objets[i].getW()/2;
                        y1 = oldy + objets[i].getH()/2;

                        //Recupere le centre de l'objet res
                        x2 = objets[res].getX() + objets[res].getW()/2;
                        y2 = objets[res].getY() + objets[res].getH()/2;

                        //Calcul le vecteur de direction entre les centres
                        vx = x1 - x2;
                        vy = y1 - y2;

                        //Normalise le vecteur
                        n = vx*vx + vy*vy;
                        n = sqrt(n);

                        vx /= n;
                        vy /= n;

Comme vous le voyez, le programme commence par calculer le centre des deux objets. On calcule ensuite le vecteur normalisé qui représente la direction de l'objet res vers l'objet i. Le code qui suit vérifie que l'objet est un cercle (ce qui, pour cette demo, sera toujours le cas) et met la vitesse à jour.

Mettre à jour la vitesse des objets
Sélectionnez

                        //Mis a jour de l'objet i
                        newvx = vx;
                        newvy = vy;

                        objets[i].setDirVitesse(objets[i].getVX()+newvx,
                                           objets[i].getVY()+newvy);

                        //Mis a jour de l'objet res
                        newvx = (-vx);
                        newvy = (-vy);

                        objets[res].setDirVitesse(objets[res].getVX()+newvx,
                                           objets[res].getVY()+newvy);
                        }
                }
}

Vous remarquerez ma technique pour mettre à jour la vitesse de l'objet. Ce n'est pas politiquement correct mais je préfére garder le code simple. Une somme de vitesses suffira pour ce programme.

2.6. Appel au moteur physique

La fonction gereSceneServeur va faire un appel au moteur physique passant les informations des objets et du nombre d'objets.

Appel du moteur physique
Sélectionnez

//Mis a jour des objets
Physique::updateObjets(objets);

Comme vous le voyez, la position et de la vitesse des objets sont gérées par le moteur physique et plus directement par la classe Jeu. Ceci permet d'avoir une moteur physique indépendant des objets sous-jacents.

Enfin, voici une image de notre pong avec 50 balles:

Image non disponible
Une image du pong avec 50 balles, aucune balle n'est en collision

2.7. L'insertion des balles

La dernière chose à remarquer en rapport avec les collisions concerne l'insertion de nouvelles balles. En effet, pour être sûr qu'une balle puisse être inséré et que cela ne provoquera pas une collision, il suffit d'ajouter ceci dans la fonction gereSceneServeur de la classe Jeu.

Test de collision pour la nouvelle balle
Sélectionnez

// Si on doit ajouter un objet
if((SDL_GetTicks()>last+1000)
   &&(!Physique::isCollision(objets, WIDTH/2-TAILLE_BALLE,HEIGHT/2-TAILLE_BALLE,
        TAILLE_BALLE,TAILLE_BALLE)))

3. Le menu

Comme dans la série du morpion, nous allons mettre un menu en place. Certes, ce sera juste un menu avec deux boutons. Par contre, nous allons afficher le menu entièrement avec OpenGL pour ne pas mélanger le code d'affichage OpenGL et la bibliothéque SDL.

Cette petite différence est tout de même de taille puisqu'il faut gérer le chargement des fichiers images pour le menu et les faire passer en textures OpenGL. Ce détail nous oblige à changer les dimensions de nos images pour qu'elles soient égales à une puissance de deux.

Mais sinon, le code est presque le même, l'affichage du menu ne contient rien de spécial et le code du moteur ressemble énormément au code présenté dans la partie 5 de la série sur le morpion.

Voici une image de notre menu :

Image non disponible
Notre menu

3.1. Les raccourcis du clavier

Il y a tout de même un ajout à ce menu qui le rend nettement plus interactif que celui pour le morpion. Nous avons ajouté des raccourcis clavier (la touche 'n' pour Nouvelle partie et la touche 'q' pour Quitter.

Ceci se fait facilement avec une fonction clavier qui retourne si oui ou non la touche a servie.

Gestion du clavier
Sélectionnez

//Gestion du clavier
bool Menu::clavier(unsigned char k)
{
        switch(k)
        {
                case 'c':
                        if(moteur.jeuEnCours()) {
                            moteur.echangeFonctions();
                        }
                        return true;
                case 'n':
                        moteur.initJeu();
                        moteur.setFonctionsJeu();
                        return true;
                default:
                        return false;
        }
}

Et là on remarque qu'un raccourci n'est pas présent lors de l'affichage du menu ! Et nous avons un appel à une fonction jeuEnCours. Comme son nom l'indique nous avons une fonction membre de la classe Moteur qui permet de dire si le jeu est en cours. Si le jeu est en cours, nous avons en fait un bouton supplémentaire.

3.2. Le bouton Continuer

Ce bouton Continuer apparaît seulement lorsque la fonction membre jeuEnCours retourne vrai. En effet, si nous regardons la fonction d'affichage du menu, on remarque que l'affichage se fait en utilisant les fonctions OpenGL et donc rien ne devrait être surprenant :

Affichage du menu
Sélectionnez

//Fonction d'affichage
void Menu::affiche()
{
        int i;
        glEnable(GL_TEXTURE_2D);
        glDisable(GL_DEPTH_TEST);

        glAlphaFunc(GL_LESS,10);
        glEnable(GL_ALPHA_TEST);

        glColor3f(1.0,1.0,1.0);

        //On dessine les diffentes images
        for(i=0;i<4;i++)
                {
                glBindTexture(GL_TEXTURE_2D, images[i]);
                glBegin(GL_QUADS);
                        glTexCoord2i(0,0);glVertex2i(positions[i].x,positions[i].y);
                        glTexCoord2i(1,0);glVertex2i(positions[i].x+positions[i].w,positions[i].y);
                        glTexCoord2i(1,1);glVertex2i(positions[i].x+positions[i].w,positions[i].y+positions[i].h);
                        glTexCoord2i(0,1);glVertex2i(positions[i].x,positions[i].y+positions[i].h);
                glEnd();
                }

        //Pour le dernier bouton, cela dépend si le jeu est en cours
        //Remarquez i vaut 4, donc c'est bien
        if(moteur.jeuEnCours())
        {
                glBindTexture(GL_TEXTURE_2D, images[i]);
                glBegin(GL_QUADS);
                        glTexCoord2i(0,0);glVertex2i(positions[i].x,positions[i].y);
                        glTexCoord2i(1,0);glVertex2i(positions[i].x+positions[i].w,positions[i].y);
                        glTexCoord2i(1,1);glVertex2i(positions[i].x+positions[i].w,positions[i].y+positions[i].h);
                        glTexCoord2i(0,1);glVertex2i(positions[i].x,positions[i].y+positions[i].h);
                glEnd();
        }

        glDisable(GL_TEXTURE_2D);
}

Comme vous le voyez, nous avons encore fait un cas particulier pour l'affichage du bouton Continuer. Si le jeu est en cours, nous l'affichons, sinon il n'apparaît pas.

La fonction jeuEnCours appelle simplement la fonction enCours de l'instance du jeu. La classe Jeu posséde un booléen qui représente si une partie est en cours. Si c'est le cas, ce booléen vaut vrai sinon il vaut faux. Comme toujours, la classe Moteur sert de lien entre le menu et le jeu.

Image non disponible
Notre menu avec le nouveau bouton

4. La classe Jeu

Dans la classe Jeu, il n'y a pas eu beaucoup de changements par rapport au deuxième tutoriel. Un des grands changements est la perte de la mise à jour des objets. En effet, cela se fait maintenant lors de l'appel au moteur Physique.

Il y a toutefois un ajout, la gestion des raccourcis clavier :

Sélectionnez

//Gestion du clavier
bool Jeu::clavier(unsigned char k)
{
        switch(k)
        {
                //On veut voir le menu
                case 'q':
                        moteur.echangeFonctions();
                        return true;
                default:
                        return false;
        }
}bool Jeu::clavier(unsigned char k)
{
        switch(k)
        {
                //On veut voir le menu
                case 'q':
                        moteur.echangeFonctions();
                        return true;
                default:
                        return false;
        }
}

Comme nous le voyons ici, seule la touche 'q' sert pour passer du jeu au menu. Comme la fonction clavier de la classe Menu, cette fonction retourne un booléen exprimant si la touche a été gérée ou non. Ceci permettra au moteur de savoir s'il doit encore géré la touche (si nécessaire) ou non.

Cette méthode nous permet donc de définir des touches que le jeu sous-jacent (qu'on soit dans le menu ou dans le jeu) peut décider de gérer ou laisser au moteur leur comportement par défaut.

Par exemple, nous pouvons supposer que par défaut la touche 'q' permet de quitter le programme. Dans ce cas, si nous sommes dans le jeu, nous voulons d'abord passer par le menu. Donc nous ajoutons un cas 'q' dans la fonction clavier de la classe Jeu. En retournant true, la classe Jeu dit "C'est bon, j'ai géré la touche".

Pour continuer cet exemple, vous avez vu que dans le menu, nous ne gérons pas la touche 'q'. Nous faisons ceci parce que par défaut, si une touche 'q' est appuyé, le moteur quittera le programme si l'événement n'est pas géré par le menu ou le jeu. Voici le code qui gére le clavier dans la classe Moteur :

La gestion du clavier
Sélectionnez

void Moteur::clavier(unsigned char k)
{
        int gere=false;
        if(dans_menu)
        {
                gere = menu->clavier(k);
        }
        else
        {
                gere = jeu->clavier(k);
        }

        //Si la touche n'est pas geree
        if(!gere) {
                switch(k)
                {
                        //On veut voir le menu
                        case 'q':
                                fin();
                                break;
                        default:
                                break;
                }
        }
}

5. La classe Moteur

La classe Moteur contient quelques nouvelles fonctions :

Les nouvelles fonctions de la classe Moteur
Sélectionnez

    //Gestion du clavier
    void clavier(unsigned char k);

    //Est-ce que la partie est finie ?
    bool estFini();
    //Terminer la partie
    void fin();

    //Echange entre menu et jeu
    void echangeFonctions();

    //Initialise la partie
    void initJeu();

    //Est-ce que le jeu est en cours
    bool jeuEnCours();

La plupart des fonctions sont assez claires. Le moteur est maintenant capable de dire si le programme sous-jacent se termine. On peut échanger d'état entre la partie et le menu.

La fonction initJeu permet de demander au jeu de recommencer la partie. Ceci permet au menu de faire savoir qu'on veut une nouvelle partie. Enfin, la fonction jeuEnCours nous fait savoir si une partie est en cours ou si elle n'a pas commencé ou si elle est terminée.

Nous avons aussi introduit la fonction fin qui permettra au moteur de faire le nécessaire côté sauvegarde, affichage (un bouton Etes-vous sûr? par exemple) et nettoyage.

Ceci modifie bien sûr la fonction main. En effet, à la place d'avoir une variable booléenne done, nous utilisons maintenant la fonction fin pour signaler la fin du programme dans la boucle événementielle et nous utilisons la fonction estFini pour savoir si nous devons continuer le programme.

6. Conclusion

Ceci conclut cette troisième partie sur un simple jeu comme un pong. Comme vous le remarquez, ajouter un pseudo-moteur physique n'est pas vraiment compliqué si nous prenons le soin de mettre en place certaines hypothèses.

En effet, en comparant les positions de chaque objet, il est possible d'en déduire les collisions. Ensuite, nous remettons l'objet en place et nous modifions la vitesse de chaque objet. Cette technique, ne suivant pas entièrement les lois physiques suffit pour ce genre de petit programme et est une bonne introduction au sujet.

Jc

Liens
  • Sommaire du tutoriel;
  • Introduction;
  • Les bases du moteur;
  • Les collisions et un menu;
  • Améliorer les collisions; Image non disponible
  • Le score, la souris et les joueurs;
  • Le réseau;
  • Conclusion.

7. Téléchargements

Voici le code source pour ce tutoriel: (781 Ko) zip.

Voici la version pdf de cet article: (248 Ko).

Si vous avez des suggestions, remarques, critiques, si vous avez remarqué une erreur, ou bien si vous souhaitez des informations complémentaires, n'hésitez pas à me contacter !


Tutoriel SDL Morpion

Programmation de Jeux 2D: Un pong en SDL/OpenGL, Quatrième partie

  • 1. Introduction
  • 2. Les collisions : version améliorée
    • 2.1. Motivations
    • 2.2. Présentation
    • 2.3. Algorithme
      • 2.3.a. Structure de données
      • 2.3.b. Calcul de zone
        • 2.3.b.1. La fonction enleverObjetDesZones
        • 2.3.b.2. La fonction recupereCoinObjet
        • 2.3.b.3. Les fonctions recupereZoneColonne et recupereZoneLigne
        • 2.3.b.4. Les fonctions enleverObjetZone
        • 2.3.b.5. Les fonctions pour ajouter un objet dans une zone
      • 2.3.c. Le calcul de collision
      • 2.3.d. La fonction updateObjets
      • 2.3.e. La fonction init
    • 2.4. Performances
    • 2.5. Allocation dynamique
  • 3. La vitesse des balles
  • 4. Les barres
    • 4.1. Chargement de la texture
    • 4.2. Affichage de l'objet
    • 4.3. Gestion de la collision
  • 5. Conclusion
  • 6. Téléchargements
  • 7. Remerciements

Nous n'avons jamais parlé de performances depuis le début de ce tutoriel donc je pense qu'il est temps que nous nous y attardions un peu. En faisant des tests de base, on voit qu'on perd beaucoup de temps dans la gestion des collisions. Cette partie va donc discuter d'une solution pour limiter cette perte et rendre notre code de gestion des collisions plus intelligent.

1. Introduction

Bienvenue à cette quatrième partie sur la programmation du pong. Dans cette partie, nous allons revenir sur la gestion des collisions dans le but de limiter son impact au niveau des performances.

Cette partie présentera donc une technique inspirée des QuadTrees pour ne tester qu'une partie des objets (ceux qui se trouvent dans les environs). Et, à la fin de ce tutoriel, nous allons introduire un nouveau type d'objet : la barre.

2. Les collisions : version améliorée

2.1. Motivations

Il est vrai que dans un jeu de pong, nous n'avons pas forcément besoin de chercher très loin ou d'utiliser un code ultra-performant. Néanmoins, il est souvent intéressant de trouver une meilleure technique qui pourra s'utiliser dans un projet plus complexe.

Question complexité, le code présenté dans la troisième partie n'était pas génial. En effet, si nous supposons que nous avons 50 objets dans le jeu, il fallait faire 2500 comparaisons. Cela ne semblera pas forcément énorme et pour étudier l'impact, j'ai fait quelques tests.

En utilisant le code de la troisième partie et en modifiant la taille de la fenêtre (la passant à 1024*768) et la taille des balles (passant de 30 à 20), voici un tableau récapitulatif du nombre d'images par seconde :

Nombre d'objets | Avec | Sans
50 | 490 | 490+
250 | 104 | 384
500 | 30 | 304

La colonne Avec est le nombre d'images par seconde en utilisant la gestion des collisions et la colonne Sans est le nombre sans cette gestion.

Comme vous le voyez, la gestion de collision demande tout de même beaucoup de ressources et ralentit tout de même le programme. Bien que cela reste raisonnable pour le cas des 50 objets, je pense que cela est plutôt dû au fait que la carte graphique et l'écran n'arrivent pas à afficher assez rapidement les images (d'où le + dans le tableau). Il faudra donc garder en tête ce problème de saturation dans la suite de l'article. Il faut aussi garder en tête que l'utilisation en force brute de la gestion de collision avec 500 objets nécessite le calcul de 500*500 calculs de collision (ce qui est égal à 250000 calculs...) par mise à jour de la position des objets.

Bien sûr, il faut relativiser l'importance de l'optimisation. Le problème, c'est que 250 objets qui sont en mouvement n'est pas forcément quelque chose d'anormal dans un grand jeu RTS et gérer les collisions ou les interactions entre les objets sont des choses primordiales. Si avec cette gestion, le nombre d'images par seconde est à 100, alors avec un affichage plus complexe qu'un pong, le jeu tournera à 1 ou 2 images par seconde...

2.2. Présentation

L'idée qui va nous permettre de gagner en nombre d'images par seconde et donc limiter les calculs de collisions est de classer les objets avec, comme facteur de tri, leur position dans le monde de jeu (dans notre cas, c'est donc la position dans la fenêtre).

Sachant bien sûr que nous allons donc tester les collisions d'un objet seulement avec les objets de sa zone. Voici une image montrant la fenêtre avec 10 balles :

Image non disponible
Un quadrillage possible

Le quadrillage montre que, par exemple, dans la zone en haut à gauche, seules deux balles sont potentiellement en collision. Lorsqu'on gérera une de ces balles, ce sera inutile de tester la collision avec les autres.

Par contre, que faire de la balle turquoise qui est sur la ligne ? Et bien, elle se retrouvera dans les deux zones en question. Dernière remarque, rien n'oblige d'avoir le même nombre de lignes que de colonnes dans le quadrillage.

2.3. Algorithme

Pour arriver à faire cette définition de zones, nous avons donc besoin de pouvoir calculer la zone dans laquelle se trouve un objet et connaître les autres objets de cette zone. Puisque nous déplacerons un seul objet à la fois, l'algorithme général de la mise à jour des objets deviendra donc :

Algorithme général
Sélectionnez

Enlever sa présence des zones auxquelles il appartient
Mis à jour de l'objet
Calcul de ses nouvelles zones
Calcul de collision avec les objets de ces zones

Bien entendu, la zone ne changera pas forcément à chaque mise à jour mais il faut bien savoir si nous avons changé de zone, donc il faut le vérifier à chaque mouvement.

2.3.a. Structure de données

Pour définir les structures de données nécessaires pour cet algorithme, nous allons introduire de nouvelles constantes dans le programme :

Constantes pour le quadrillage
Sélectionnez

const int QUADCOL = 5;
const int QUADROW = 5;

Nous avons donc défini un quadrillage de 5 colonnes et 5 lignes. Ensuite, pour gérer les objets de notre classe physique, nous devons ajouter un tableau à deux dimensions pouvant contenir ces objets.

Il faut remarquer que la taille minimale des zones est la moitié de la taille d'une balle + 1.

Puisqu'il est possible que tous les objets soient dans la même zone, nous devons avoir un tableau qui peut stocker tous les objets dans la même zone. Nous commencerons par montrer une allocation statique (plus simple d'un point de vue conception) définissant une structure qui contient le nombre d'objets dans la zone et un tableau contenant les numéros des objets.

Structure de zone
Sélectionnez

typedef struct sZone
{
int nobjets;
int objets[MAX_BALLES];
}SZone;

Ensuite nous pouvons déclarer, dans notre classe physique, notre tableau de zones. Remarquons que ceci déclare la taille maximale possible et ceci peut être amoindri si on utilise une allocation dynamique. Par contre, dans le cas d'un quadrillage de 5*5 avec 250 objets au maximum et 4 octets par entier, cette table utilisera approximativement 25k ce qui est raisonable.

Tableau de zones
Sélectionnez

//Tableau de zones
SZone tableau_zones[QUADLIGNE][QUADCOL];

2.3.b. Calcul de zone

Dans la suite logique des choses, nous devons maintenant pouvoir calculer la zone de chaque objet. En fait, pour ce calcul, il suffira de calculer la zone des quatre coins de l'objet.

Nous aurons donc deux fonctions, une qui demande d'enlever un objet des zones et une fonction qui ajoute un objet aux zones. Voici le code qui enlève un objet donné du tableau des zones.

2.3.b.1. La fonction enleverObjetDesZones

Enlever un objet des zones
Sélectionnez

void Physique::enleverObjetDesZones(Objet *objs, int idx)
{
      double x=0,y=0;
      int oldzonei=-1, oldzonej=-1;
      int newzonei,newzonej;
      //Pour chaque coin du rectangle
      for(int i=0;i<4;i++)
      {
            //Recupere le coin
            recupereCoinObjet(objs[idx],i,x,y);

            //Calcul de la zone
            newzonei = recupereZoneLigne(y);
            newzonej = recupereZoneColonne(x);

            //Si c'est une autre zone
            if((newzonei!=oldzonei)||(newzonej!=oldzonej))
                  {
                        oldzonei = newzonei;
                        oldzonej = newzonej;
                        enleverObjetZone(idx,newzonei,newzonej);
                  }
      }
}

Il y a bien sûr beaucoup de nouvelles fonctions dans ce code. Nous allons les voir dans l'ordre après avoir expliqué l'idée générale de la fonction.

Tout d'abord, nous allons étudier chaque coin de l'objet. Pour connaître la position de chaque coin, nous allons appeler la fonction recupereCoinObjet. Pour une question d'optimisation, cette fonction se trouve dans la classe Physique. On pourrait la mettre dans la classe Objet mais on perdrait la possibilité de mettre le code en inline (la plupart des compilateurs ne font pas d'inline entre fichiers...).

Une fois que nous avons la position du coin, on utilisera les fonctions recupereZoneLigne et recupereZoneColonne qui permettent respectivement d'avoir la ligne et la colonne correspondante pour le tableau des zones.

Le test qui suit nous permet de ne pas demander le retrait d'un objet si, d'un coin à un autre, nous sommes restés dans la même zone. Il est possible que nous demanderons tout de même deux fois le retrait d'une même zone mais cela est mieux que 4 fois!

Bref, présenté comme cela, cette fonction est assez directe mais en fait elle utilise 4 autres fonctions pour faire son travail. Présentons rapidement la fonction qui nous donne la position du coin.

2.3.b.2. La fonction recupereCoinObjet

La fonction recupereCoinObjet
Sélectionnez

inline void Physique::recupereCoinObjet(Objet &o, int i, double &x, double &y)
{
      //On regarde chaque coin
      switch(i)
      {
            case 0:
                  //En haut a gauche
                  x = o.getX();
                  y = o.getY();
                  break;
            case 1:
                  //En haut a droite
                  x = o.getX()+o.getW();
                  y = o.getY();
                  break;
            case 2:
                  //En bas a droite
                  x = o.getX()+o.getW();
                  y = o.getY()+o.getH();
                  break;
            case 3:
                  //En bas a gauche
                  x = o.getX();
                  y = o.getY()+o.getH();
                  break;
            default:
                  break;
      }

}

Comme vous le voyez, le paramètre i permet de définir quel coin nous intéresse. Ensuite, en utilisant un switch, la fonction est capable de mettre à jour les paramètres x et y.

2.3.b.3. Les fonctions recupereZoneColonne et recupereZoneLigne

Les fonctions recupereZoneColonne et recupereZoneLigne
Sélectionnez

inline int Physique::recupereZoneColonne(double x)
{
      const int PIXPARCOL = WIDTH/QUADCOL;
      int res = (int) (x/PIXPARCOL);
      if(res>=QUADCOL)
            {
            return QUADCOL-1;
            }
      return res;
}

inline int Physique::recupereZoneLigne(double y)
{
      const int PIXPARLIGNE = WIDTH/QUADLIGNE;
      int res = (int) y/PIXPARLIGNE;
      if(res>=QUADLIGNE)
            {
            return QUADLIGNE-1;
            }
      return res;
}

Ces deux fonctions sont relativement simples si vous vous souvenez de l'image du quadrillage. Le but est de savoir quel indice représente la zone d'une position en pixel. Si nous avons une largeur de 800 et 5 colonnes, chaque colonne fait donc 160 pixels. C'est par cela que commence ces fonctions : le calcul de la largeur (ou hauteur) d'une zone.

Ensuite, il suffit de calculer à quelle zone appartient un objet par une simple division et le test final permet de récupérer les objets qui sont aux extrêmes de l'écran (en effet, un effet de bord du calcul par entier, on perd un peu de précision et il y a donc un risque de débordement).

2.3.b.4. Les fonctions enleverObjetZone

Nous arrivons enfin à la fonction qui va enlever d'une zone l'indice d'un objet :

La fonction enleverObjetZone
Sélectionnez

void Physique::enleverObjetZone(int idx, int i, int j)
{
   int k;
   int *ptr = tableau_zones[i][j].objets,
       size = tableau_zones[i][j].nobjets;

   for(k=0;k<size;k++)
   {
      if(ptr[k]==idx)
         {
         //On efface en echangeant avec le dernier
         ptr[k] = ptr[size-1];
         tableau_zones[i][j].nobjets--;
         break;
         }
   }
}

Ce code est assez simple : on parcourt le tableau représentant la zone en cherchant l'indice de l'objet à enlever. Si on le trouve, on écrase la case avec le dernier objet du tableau et on décrémente le nombre d'objets dans la zone.

Nous avons enfin présenté chaque fonction qui sert à enlever un objet des zones auxquelles il appartient. Comme vous le voyez, il n'y a rien de particulier et rien de très difficile. Il suffit de faire un peu attention aux indices...

2.3.b.5. Les fonctions pour ajouter un objet dans une zone

Pour ajouter un objet, nous faisons les mêmes opérations sauf qu'à la place d'appeler enleverObjetZone, nous allons appeler une fonction ajouterObjetZone. Pour garder la longueur de ce tutoriel convenable, je ne vais pas mettre le code (surtout qu'il n'y a pas grand chose de compliqué dans le code !).

2.3.c. Le calcul de collision

Il faut maintenant changer la fonction collisionObjet pour qu'elle prenne en compte l'utilisation des zones. Pour garder les deux fonctions, nous nommerons la nouvelle fonction collisionObjetZone et voici son prototype :

Le prototype de la fonction collisionObjetZone
Sélectionnez

//Fonction qui gere la collision avec l'objet i mais en utilisant les zones
static int collisionObjetZone(std::vector<Objet> &objets, int idx);

Cette fonction prend le tableau des objets et l'indice de l'objet à considérer. Puisque nous n'avons plus besoin de parcourir le tableau, nous ne passons plus sa taille. Pour ne pas faire trop long, nous présenterons seulement la partie la plus intéressante de cette fonction (en gros, nous omettons la déclaration des variables et le dernier return) :

La fonction collisionObjetZone
Sélectionnez

      int oldzonei=-1, oldzonej=-1;

      x1 = objets[idx].getX();
      y1 = objets[idx].getY();
      w1 = objets[idx].getW();
      h1 = objets[idx].getH();

      //Si c'est un cercle, on recupere son centre
      if(objets[idx].getType()==CERCLE)
            {
            cx1 = x1+w1/2;
            cy1 = y1+h1/2;
            cr1 = w1/2;
            cercle = true;
            }

      //Pour chaque coin
      for(i=0;i<4;i++)
      {
            //Recupere le coin
            recupereCoinObjet(objets[idx],i,x,y);

            //Calcul de la zone
            newzonei = recupereZoneLigne(y);
            newzonej = recupereZoneColonne(x);

            //Si c'est une autre zone
            if((newzonei!=oldzonei)||(newzonej!=oldzonej))
                  {
                        oldzonei = newzonei;
                        oldzonej = newzonej;

                        //Ok on va regarder dans cette zone
                        nobjets_zone = tableau_zones[newzonei][newzonej].nobjets;
                        objets_zone = tableau_zones[newzonei][newzonej].objets;

                        //On parcourt tous les objets de cette zone
                        for(k=0;k<nobjets_zone;k++)
                        {
                              l = objets_zone[k];

                              x2 = objets[l].getX();
                              y2 = objets[l].getY();
                              w2 = objets[l].getW();
                              h2 = objets[l].getH();

                              //Si on a deux cercles
                              if( cercle && (objets[l].getType()==CERCLE))
                              {
                                    cx2 = x2 + w2/2;
                                    cy2 = y2 + h2/2;
                                    cr2 = w2/2;

                                    if(collisionCercle(cx1,cy1,cr1,cx2,cy2,cr2))
                                          return l;
                              }
                              else //Sinon on fait une collision entre rectangles
                              {
                                    if(collisionRect(x1,y1,w1,h1,x2,y2,w2,h2))
                                          return l;
                              }
                        }
                  }
      }
return -1;

Si on comparait avec la version qui n'utilisait pas de zone, on remarquerait que la boucle k interne est du même genre que l'originale. Nous avons simplement entouré cette boucle d'un calcul de zone pour chaque coin de l'objet.

Une petite différence est la disparition d'un test pour vérifier qu'on ne calcule pas la collision de l'objet sur lui-même. En effet, l'algorithme demande d'enlever l'objet du tableau de zone avant la gestion des collisions. Puisque cet indice n'est plus dans le tableau, le test est inutile.

Plus important est la présence d'un test sur le type de l'objet. En effet, à la fin de ce tutoriel, nous allons avoir un deuxième type d'objet : les barres ! Du coup, nous avons défini une énumération contenant tous les types. Ceci nous permettra de savoir quel genre de calcul de collision est nécessaire, mais aussi le genre de réaction qu'auront les objets lors de collisions.

L'énumération des types
Sélectionnez

enum Type {CERCLE, BARRE};

Ceci rajoute bien sûr un champs membre dans la classe Objet et une fonction qui permet de récupérer le type de l'objet.

2.3.d. La fonction updateObjets

Enfin la dernière chose à modifier est la fonction d'entrée updateObjets qui s'occupera de la gestion du tableau de zones et d'appeler la nouvelle fonction collisionObjetZone. Pour respecter l'algorithme, il suffira d'enlever l'objet du tableau avant la modification de sa position :

Enlever l'indice de l'objet
Sélectionnez

//Sauvegarde de l'ancienne position
oldx = objets[i].getX();
oldy = objets[i].getY();

//Enlever des zones
enleverObjetDesZones(objets, i);

//Mise a jour de la position
objets[i].updatePos();

Et bien sûr, avant de passer au prochain objet, il ne faut pas oublier de rajouter l'indice de l'objet dans le tableau :

Ajouter l'indice de l'objet
Sélectionnez

//Ajouter dans les zones
ajouterObjetDansZones(objets, i);

2.3.e. La fonction init

Notre classe Physique contient maintenant des informations sur l'état du jeu et la position des balles. Il faut donc pouvoir initialiser le tableau pour le mettre à zéro. Ceci se fait lors du début du programme ou lorsque l'utilisateur veut recommencer une partie.

La fonction init sert donc à mettre chaque case du maillage à zéro :

La fonction init
Sélectionnez

//Fonction d'initialisation
bool Physique::init()
{
int i,j;
for(i=0;i<QUADLIGNE;i++)
  for(j=0;j<QUADCOL;j++)
    {
    tableau_zones[i][j].nobjets = 0;
    }
return true;
}

Enfin, l'instance de jeu appellera l'initialisation du moteur physique dans la fonction recommence :

La fonction recommence de la classe Jeu
Sélectionnez

//Recommence la partie
void Jeu::recommence()
{
    objets.clear();
    Physique::init();
    jeuEnCours = true;
}

2.4. Performances

Voici le tableau que vous avez vu au début du tutoriel avec une nouvelle colonne. Zone donne le nombre d'images par seconde de cette nouvelle version de calcul de collisions.

Nombre d'objets | Avec | Zone | Sans
50 | 490 | 490+ | 490+
250 | 104 | 380 | 384
500 | 30 | 240 | 304

Comme vous le voyez, notre nouvelle version permet d'avoir le même comportement que la version sans gestion des collisions jusqu'à 250 objets. Ensuite, pour 500 objets, nous avons tout de même un gain considérable par rapport à la version de base.

Enfin, nous pouvons faire remarquer que, si l'utilisation mémoire n'est pas trop un facteur, passer à un quadrillage 15*15 permet d'avoir le même nombre d'images par secondes avec gestion de collisions qu'une version sans pour un programme qui fait interagir 500 objets ! Ceci en gardant en tête qu'il y a sûrement un facteur de saturation au niveau de la fréquence d'affichage, mais cela est nettement mieux qu'avoir 30 images par secondes...

A la fin de ce tutoriel, il y aura une version du pong qui contiendra cette allocation statique. L'allocation statique a l'avantage d'être plus facile à concevoir, par contre, le gaspillage de mémoire peut souvent être évité en utilisant l'allocation dynamique. C'est ce que nous allons montrer par la suite.

2.5. Allocation dynamique

Nous avons vu que les performances de ce système dépendent largement de la position des objets mais aussi de la taille du quadrillage. Si le quadrillage est plus fin, la gestion des collisions peut se faire plus rapidement. Par contre, notre utilisation mémoire est tout de même un peu gourmande, donc nous allons présenter ici comment résoudre ce problème en passant par une allocation dynamique.

Bien que ceci ne soit pas nécessaire, cela est tout de même intéressant de pouvoir garder le gain de temps et de limiter l'occupation mémoire. Bien qu'on n'en ait pas parlé, un quadrillage 15*15 avec 500 balles utilise tout de même 450 kO !

Pour limiter ce nombre, nous allons donc directement utiliser un tableau de type std::vector. Nous n'aurons plus besoin d'une structure puisque le type std::vector contient la méthode size qui nous le fournira.

Nouveau tableau de zones
Sélectionnez

//Tableau de zones
static std::vector<int> tableau_zones[QUADLIGNE][QUADCOL];

Ensuite, il faut mettre à jour le code pour gérer ce changement de type. Nous commençons par changer la déclaration de la variable tableau_zones :

Déclaration du tableau de zones
Sélectionnez

std::vector<int> Physique::tableau_zones[QUADLIGNE][QUADCOL];

Ensuite, à la place d'utiliser la variable nobjets, nous passons par la méthode size dans les fonctions enleverObjetZone, ajouterObjetZone et collisionObjetZone.

Voici le code pour enlever un objet d'une zone :

Enlever un objet d'une zone
Sélectionnez

void Physique::enleverObjetZone(int idx, int i, int j)
{
    int k;
    std::vector<int> &ptr = tableau_zones[i][j];
    int size = tableau_zones[i][j].size();

    for(k=0;k<size;k++)
    {
        if(ptr[k]==idx)
        {
            //On efface en echangeant avec le dernier
            ptr[k] = ptr[size-1];
            ptr.pop_back();
            break;
        }
    }
}

Comme vous le voyez, ce code copie le dernier objet du tableau et ensuite utilise la méthode pop_back pour diminuer la taille du vecteur.

Pour ajouter un objet dans une zone, on utilise cette nouvelle version de la fonction ajouterObjetZone :

Ajouter un objet d'une zone
Sélectionnez

void Physique::ajouterObjetZone(int idx, int i, int j)
{
    unsigned int k,
        size = tableau_zones[i][j].size();
    std::vector<int> &ptr = tableau_zones[i][j];

    for(k=0;k<size;k++)
    {
        if(ptr[k]==idx)
        {
            break;
        }
    }

    if((k==size)&&(size<MAX_BALLES))
        {
        ptr.push_back(idx);
        }
}

Cette nouvelle version utilise la fonction push_back pour ajouter un élément dans un vecteur. La dernière fonction qui a été modifiée est collisionObjetZone, voici la portion de la fonction qui a été modifiée :

Changement dans collisionObjetZone
Sélectionnez

    //Ok on va regarder dans cette zone
    nobjets_zone = tableau_zones[newzonei][newzonej].size();
    std::vector<int> &objets_zone = tableau_zones[newzonei][newzonej];

    //On parcourt tous les objets de la zone
    for(k=0;k<nobjets_zone;k++)
       {
       l = objets_zone[k];

Ce qui est intéressant est la facilité offerte par le type std::vector pour la gestion dynamique de la taille des vecteurs. Ce prochain tableau montre les performances avec 3000 objets de taille 5 (la mémoire est la mémoire utilisée par le programme en méga-octets) :

Version | Mémoire | Taux de rafraîchissement
Gestion naïve | 16 | 2
Statique | 60 | 65
Dynamique | 16 | 68
Sans | 16 | 150

Enfin, voici une image qui montre 3000 balles :

Image non disponible
3000 balles

Une dernière remarque sur cet algorithme pour dire qu'il ne faut pas non plus prendre n'importe quelle taille pour le quadrillage. En effet, il faut que la largeur (ou hauteur) soit plus grand que le rayon de la plus grande balle. En effet, on regarde les zones des coins des balles. Il faut donc qu'il n'y ait pas de trou entre deux coins. Sinon, une collision peut se produire sans que nous la détections. Une bonne solution est de définir les constantes comme ceci :

Définition du quadrillage
Sélectionnez

const int QUADCOL=WIDTH/TAILLE_BALLE;
const int QUADLIGNE=HEIGHT/TAILLE_BALLE;

A la fin de ce tutoriel, vous aurez le programme permettant d'avoir les 3000 balles gérées par une allocation statique (puisqu'à la fin de ce tutoriel, vous aurez le source utilisant entièrement l'allocation dynamique mais aussi les barres.

3. La vitesse des balles

Pour le moment, la mise à jour de la position de la balle se fait comme ceci :

Code pour la mise à jour des objets
Sélectionnez

//Calcul de la nouvelle position
double x = pos.getX() + vitdir.getX(),
      y = pos.getY() + vitdir.getY(),
      w = taille.getX(),
      h = taille.getY();

Comme vous le voyez, il n'y pas de gestion du temps. Ceci n'est pas une très bonne idée puisque, si ce programme est utilisé sur différents ordinateurs, la vitesse de la balle ne sera pas la même. En utilisant le temps, on harmonise les déplacements.

Cette solution n'a pas que des inconvénients : s'il y a une discontinuité dans le taux de rafraîchissement (le système d'exploitation fait autre chose par exemple), le programme continuera comme si de rien n'était. En utilisant le temps, après une discontinuité, il y aura un saut dans le déplacement. Lors d'un calcul de collision, cela n'est pas très pratique. Une solution hybride consiste à calculer la vitesse moyenne de temps en temps, et utiliser cette vitesse pour le déplacement.

En voulant garder le code relativement simple, nous allons tout de même utiliser la version utilisant le temps. Voici le code que nous allons utiliser :

Nouveau code pour la mise à jour des objets
Sélectionnez

//Heure actuelle
long tps = SDL_GetTicks() - derniertps;
//Calcul de la nouvelle position
double x = pos.getX() + vitdir.getX() * tps * RATIOVITESSE,
      y = pos.getY() + vitdir.getY() * tps * RATIOVITESSE,
      w = taille.getX(),
      h = taille.getY();

//Mise à jour derniertps
setDernierTps();

En ajoutant une variable derniertps qui représente la dernière mise à jour. En utilisant la différence entre le temps actuel et la dernière mise à jour, on est capable de calculer la distance de déplacement. Vous remarquerez la présence de la fonction setDernierTps qui permet de mettre à jour la variable dernierTps.

La mise en place de dernierTps
Sélectionnez

class Objet
{
  private :
     ...
     //Dernier temps de mise à jour
     long dernierTps;
  pubic :
     ...
     //Mettre a jour la variable derniertps
     void setDernierTps();

Le fait que la fonction setDernierTps soit déclaré comme une fonction publique n'est pas un hasard. En effet, nous allons avoir besoin de cette fonction lorsque l'utilisateur met le jeu en pause. Avant, nous mettions à jour la position à chaque mise à jour, indépendamment du temps entre les deux.

Cela veut dire que si l'utilisateur met le jeu en pause et revient 15 minutes plus tard. Lorsque le jeu reprend, les balles ne bougeront pas plus rapidement. Par contre, maintenant, vous imaginez bien que la variable tps va être trop grande, cela provoquera donc des sauts.

Une solution facile est de toucher les objets pour mettre à jour leur variable dernierTps. Nous déclarons donc une nouvelle fonction toucheObjets dans la classe Jeu pour les mettre à jour.

La fonction toucheObjets
Sélectionnez

void Jeu::toucheObjets()
{
    unsigned int i,
            nobjets = objets.size();

    for(i=0;i<nobjets;i++)
        {
        objets[i].setDernierTps();
        }
}

Cette fonction nous permettra donc de mettre toutes les variables dernierTps à jour et donc d'éviter les sauts. Bien sûr, il faut encore appeler cette fonction au bon moment. Ceci se fera lorsque le jeu reprend. Nous pouvons le gérer en complétant donc les fonctions de la classe Moteur :

La fonction echangeFonctions
Sélectionnez

//Echange entre menu et jeu
void Moteur::echangeFonctions()
{
    dans_menu = !dans_menu;
    if(!dans_menu) {
        jeu->toucheObjets();
    }
}

Dernière remarque, ceci modifie bien sûr la norme qui est utilisée pour les vecteurs. En effet, SDL_GetTicks retourne le temps en terme de millisecondes. Ceci veut donc dire qu'une norme correcte serait de l'ordre de 0.1 pour que l'objet se déplace d'une centaine de pixels en une seconde. C'est donc à cela que sert la constante RATIOVITESSE.

4. Les barres

La dernière chose (je le promets) que nous allons faire dans ce tutoriel est d'ajouter un nouveau type d'objet. Comme vous l'avez sûrement remarqué nous avions déjà préparé un peu le terrain pour cet objet. En effet, nous avons déclaré cet objet dans l'enum Type :

L'enum Type
Sélectionnez
enum Type {CERCLE, BARRE};

Ensuite, si vous vous souvenez de la fonction collisionObjetZone, nous avons commencé à intégrer la présence de types d'objets différents. En effet, en fonction du type des objets, nous utilisons la collision la plus adaptée.

Il y a donc un peu de travail nécessaire pour ajouter ce nouveau type. Il faudra gérer son affichage et la réaction qu'il peut y avoir face à une collision.

En ce qui concerne l'affichage, tout sera géré par la classe Objet. Il faudra bien sûr charger une nouvelle texture pour nos barres, nous allons ajouter une deuxième variable statique dans la classe Objet pour contenir son indice et lors de l'affichage de l'objet, son type décidera entre les deux indices pour l'appel glBindTexture.

4.1. Chargement de la texture

Nous avons donc besoin d'une deuxième variable statique pour la texture :

Le chargement de deux textures
Sélectionnez

static GLuint txtpion, txtbarre;

Pour ce qui est du chargement de la texture, cela se passe dans la fonction membre initTextures de la classe Jeu. Pour charger les deux textures, nous allons encapsuler le code existant dans une petite boucle qui, grâce à un if, chargera d'abord une texture puis la deuxième.

Le chargement de deux textures
Sélectionnez

    for(k=0;k<2;k++)
    {

        if(k==0)
        {
            //On charge l'image "data/balle.bmp"
            tmpsurf = SDL_LoadBMP("data/balle.bmp");
        }
        else
        {
            //On charge l'image "data/barre.bmp"
            tmpsurf = SDL_LoadBMP("data/barre.bmp");
        }

         //Gestion de l'erreur
         if(tmpsurf==NULL)
         {
               std::cout << "Erreur dans le chargement de la surface : "
                         << SDL_GetError() << std::endl;
               return false;
         }

Ensuite, nous avons le même code que pour la deuxième partie. Par contre, à la fin de la boucle, il faut passer à la classe Objet de l'indice de la texture, un autre if permet de gérer ce problème :

Interaction avec la classe Objet
Sélectionnez

        if(k==0)
        {
            //On passe la texture aux objets de type balle
            Objet::setTxtBalle(txtsurf);
        }
        else
        {
            //On passe la texture aux objets de type barre
            Objet::setTxtBarre(txtsurf);
        }

A ce point du code, les deux textures sont donc chargées et la classe Objet a maintenant les deux variables statiques txtpions et txtbarre sont à jour.

4.2. Affichage de l'objet

Lorsque nous allons vouloir afficher l'objet, il faudra simplement regarder le type de l'objet avant de faire l'appel au glBindTexture.

Interaction avec la classe Objet
Sélectionnez

if(type==CERCLE)
   {
   glBindTexture(GL_TEXTURE_2D, txtpion);
   }
else
   {
   glBindTexture(GL_TEXTURE_2D, txtbarre);
   }

Bien sûr, si nous avions plus de deux types, un tableau d'indice de textures OpenGL serait plus approprié.

4.3. Gestion de la collision

Enfin, pour la gestion de la collision, il suffit de dire que les barres ne sont pas affectées par les collisions et qu'une balle qui est en collision avec la barre repart dans la direction du centre de la barre vers la balle. En calculant la position du centre de la barre, nous avons facilement ce vecteur :

Sachant que pour la collision Cercle-Cercle nous avons déjà calculé cette direction, la seule chose qu'il reste à faire est de ne plus faire la somme des vitesses comme avant :

Collision avec une barre
Sélectionnez

//Si l'objet res n'est pas un cercle, on prend la direction (newx, newy)
//et met la vitesse à 0.1
if(objets[res].getType()!=CERCLE)
   {
   objets[i].setDirVitesse(newvx,newvy);

   }
else //Sinon on ajoute la direction (newx, newy)
   {
     objets[i].setDirVitesse(objets[i].getVX()+newvx,
          objets[i].getVY()+newvy);
   }
objets[i].setVitesse(0.1);

Donc, lorsqu'une balle rentre en collision avec une barre, son ancienne direction est remplacée par une direction qui dépend de la position de l'impact (un peu comme dans un casse brique).

Pour montrer l'utilisation des barres, nous allons ajouter pour ce tutoriel le code nécessaire à la création de 4 barres dans la fonction recommence :

La fonction recommence
Sélectionnez

//Recommence la partie
void Jeu::recommence()
{
    Objet o;

    objets.clear();
    Physique::init();
    jeuEnCours = true;

    //Premiere barre
    o.setCouleur(1.0,0.0,0.0);
    o.setPos(WIDTH/2-LARG_BARRE/2,5*HEIGHT/6 - HAUT_BARRE/2);
    o.setTaille(LARG_BARRE,HAUT_BARRE);
    addObjet(o);

    //2eme barre
    o.setCouleur(1.0,1.0,1.0);
    o.setPos(WIDTH/2-LARG_BARRE/2,HEIGHT/6-HAUT_BARRE/2);
    o.setTaille(LARG_BARRE,HAUT_BARRE);
    addObjet(o);

    //3eme barre
    o.setCouleur(0.0,1.0,0.0);
    o.setTaille(HAUT_BARRE,LARG_BARRE);
    o.setPos(WIDTH/6-HAUT_BARRE/2,HEIGHT/2 -LARG_BARRE/2);
    addObjet(o);

    //4eme barre
    o.setCouleur(0.0,1.0,1.0);
    o.setTaille(HAUT_BARRE,LARG_BARRE);
    o.setPos(5*WIDTH/6-HAUT_BARRE/2,HEIGHT/2 -LARG_BARRE/2);
    addObjet(o);
}

Comme vous le voyez, ce n'est pas difficile d'ajouter des barres dans le jeu. Nous avons simplement défini en plus la taille souhaitée des barres :

Taille des barres
Sélectionnez

const int LARG_BARRE=120;
const int HAUT_BARRE=25;

Bien sûr, qui dit taille de barre, dit taille du quadrillage pour les zones. La taille d'une barre étant plus grande qu'une balle, il faudrait mieux changer le calcul de QUADCOL et QUADLIGNE.

Voici une solution pour garder le code générique :

Calcul de QUADCOL et QUADLIGNE
Sélectionnez

const int QUADCOL=WIDTH/ (
            (TAILLE_BALLE>LARG_BARRE)?
                ((TAILLE_BALLE>HAUT_BARRE)?
                    TAILLE_BALLE:
                    HAUT_BARRE):
                ((HAUT_BARRE>LARG_BARRE)?
                    HAUT_BARRE:
                    LARG_BARRE)
           );
const int QUADLIGNE=HEIGHT/ (
            (TAILLE_BALLE>LARG_BARRE)?
                ((TAILLE_BALLE>HAUT_BARRE)?
                    TAILLE_BALLE:
                    HAUT_BARRE):
                ((HAUT_BARRE>LARG_BARRE)?
                    HAUT_BARRE:
                    LARG_BARRE)
           );

Certes, l'opérateur ternaire n'est pas l'opérateur favori de beaucoup de programmeurs mais il est pratique ici. En effet, nous avons trois tailles possibles pour ce jeu (et cela demeurera trois pour le reste de cette série) donc nous pouvons nous permettre de faire un calcul sur le maximum des trois tailles possibles : la taille d'une balle, la largeur et la hauteur d'une barre. Ce calcul nous permet de garder sous forme de constante la taille du quadrillage.

Une dernière image montre la présence de quatre barres dans la fenêtre avec vingt balles :

Image non disponible
L'introduction des barres

5. Conclusion

Nous avons vu trois grands concepts dans ce tutoriel. Premièrement, une solution pour réduire considérablement l'impact de la gestion des collisions a été mise en place. En utilisant une solution utilisant une allocation dynamique (utilisant le type std::vector), nous avons pu gérer correctement les collisions de 3000 balles...

Ensuite, nous avons amélioré le calcul des déplacements pour le rendre indépendant du taux de rafraîchissement. Cette solution a ajouté une petite fonction pour gérer la mise en pause de jeu.

Enfin, nous avons ajouté un nouveau type d'objet : la barre. Cette barre ne bouge pas comme les balles et n'est pas affectée par les collisions avec les balles. On a pu facilement gérer la nouvelle texture et le comportement des balles lorsqu'elles sont en contact avec la barre.

Jc

Liens
  • Sommaire du tutoriel;
  • Introduction;
  • Les bases du moteur;
  • Les collisions et un menu;
  • Améliorer les collisions;
  • Le score, la souris et les joueurs;
  • Le réseau;
  • Conclusion.

6. Téléchargements

Voici le code source pour ce tutoriel :

  • La version avec 3000 balles et allocation statique : (782 Ko) zip.
  • La version finale avec l'utilisation du temps dans le mouvement, l'allocation dynamique et l'introduction des barres : (790 Ko) zip.

Voici la version pdf de cet article: (237 Ko).

Si vous avez des suggestions, remarques, critiques, si vous avez remarqué une erreur, ou bien si vous souhaitez des informations complémentaires, n'hésitez pas à me contacter !


mateo21, pour Le Site du Zér0

ressources.c

1 ICON "caisse.ico"

main.c

/*
main.c
------

Par mateo21, pour Le Site du Zér0 (www.siteduzero.com)

Rôle : menu du jeu. Permet de choisir entre l'éditeur et le jeu lui-même.
*/

#include
#include
#include
#include

#include "constantes.h"
#include "jeu.h"
#include "editeur.h"

int main(int argc, char *argv[])
{
    SDL_Surface *ecran = NULL, *menu = NULL;
    SDL_Rect positionMenu;
    SDL_Event event;

    int continuer = 1;

    SDL_Init(SDL_INIT_VIDEO);

    SDL_WM_SetIcon(IMG_Load("caisse.jpg"), NULL); // L'icône doit être chargée avant SDL_SetVideoMode
    ecran = SDL_SetVideoMode(LARGEUR_FENETRE, HAUTEUR_FENETRE, 32, SDL_HWSURFACE | SDL_DOUBLEBUF);
    SDL_WM_SetCaption("Mario Sokoban", NULL);

    menu = IMG_Load("menu.jpg");
    positionMenu.x = 0;
    positionMenu.y = 0;

    while (continuer)
    {
        SDL_WaitEvent(&event);
        switch(event.type)
        {
            case SDL_QUIT:
                continuer = 0;
                break;
            case SDL_KEYDOWN:
                switch(event.key.keysym.sym)
                {
                    case SDLK_ESCAPE: // Veut arrêter le jeu
                        continuer = 0;
                        break;
                    case SDLK_KP1: // Demande à jouer
                        jouer(ecran);
                        break;
                    case SDLK_KP2: // Demande l'éditeur de niveaux
                        editeur(ecran);
                        break;
                }
                break;
        }

        // Effacement de l'écran
        SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 0, 0, 0));
        SDL_BlitSurface(menu, NULL, ecran, &positionMenu);
        SDL_Flip(ecran);
    }

    SDL_FreeSurface(menu);
    SDL_Quit();

    return EXIT_SUCCESS;
}


mateo21, pour Le Site du Zér0

jeu.c

/*
jeu.c
-----

Par mateo21, pour Le Site du Zér0 (www.siteduzero.com)

Rôle : fonctions du jeu.
*/

#include
#include
#include
#include

#include "constantes.h"
#include "jeu.h"

void jouer(SDL_Surface* ecran)
{
    SDL_Surface *mario[4] = {NULL}; // 4 surfaces pour chacune des directions de mario
    SDL_Surface *mur = NULL, *caisse = NULL, *caisseOK = NULL, *objectif = NULL, *marioActuel = NULL;
    SDL_Rect position, positionJoueur;
    SDL_Event event;

    int continuer = 1, objectifsRestants = 0, i = 0, j = 0;
    int carte[NB_BLOCS_LARGEUR][NB_BLOCS_HAUTEUR] = {0};

    // Chargement des sprites (décors, personnage...)
    mur = IMG_Load("mur.jpg");
    caisse = IMG_Load("caisse.jpg");
    caisseOK = IMG_Load("caisse_ok.jpg");
    objectif = IMG_Load("objectif.png");
    mario[BAS] = IMG_Load("mario_bas.gif");
    mario[GAUCHE] = IMG_Load("mario_gauche.gif");
    mario[HAUT] = IMG_Load("mario_haut.gif");
    mario[DROITE] = IMG_Load("mario_droite.gif");

    marioActuel = mario[BAS]; // Mario sera dirigé vers le bas au départ

    // Chargement du niveau
    if (!chargerNiveau(carte))
        exit(EXIT_FAILURE); // On arrête le jeu si on n'a pas pu charger le niveau

    // Recherche de la position de Mario au départ
    for (i = 0 ; i < NB_BLOCS_LARGEUR ; i++)
    {
        for (j = 0 ; j < NB_BLOCS_HAUTEUR ; j++)
        {
            if (carte[i][j] == MARIO) // Si Mario se trouve à cette position sur la carte
            {
                positionJoueur.x = i;
                positionJoueur.y = j;
                carte[i][j] = VIDE;
            }
        }
    }

    // Activation de la répétition des touches
    SDL_EnableKeyRepeat(100, 100);

    while (continuer)
    {
        SDL_WaitEvent(&event);
switch(event.type)
{
    case SDL_QUIT:
        continuer = 0;
        break;
    case SDL_KEYDOWN:
        switch(event.key.keysym.sym)
        {
            case SDLK_ESCAPE:
                continuer = 0;
                break;
            case SDLK_UP:
                marioActuel = mario[HAUT];
                deplacerJoueur(carte, &positionJoueur, HAUT);
                break;
            case SDLK_DOWN:
                marioActuel = mario[BAS];
                deplacerJoueur(carte, &positionJoueur, BAS);
                break;
            case SDLK_RIGHT:
                marioActuel = mario[DROITE];
                deplacerJoueur(carte, &positionJoueur, DROITE);
                break;
            case SDLK_LEFT:
                marioActuel = mario[GAUCHE];
                deplacerJoueur(carte, &positionJoueur, GAUCHE);
                break;
        }
        break;
}

        // Effacement de l'écran
        SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 255, 255, 255));

        // Placement des objets à l'écran
        objectifsRestants = 0;

        for (i = 0 ; i < NB_BLOCS_LARGEUR ; i++)
        {
            for (j = 0 ; j < NB_BLOCS_HAUTEUR ; j++)
            {
                position.x = i * TAILLE_BLOC;
                position.y = j * TAILLE_BLOC;

                switch(carte[i][j])
                {
                    case MUR:
                        SDL_BlitSurface(mur, NULL, ecran, &position);
                        break;
                    case CAISSE:
                        SDL_BlitSurface(caisse, NULL, ecran, &position);
                        break;
                    case CAISSE_OK:
                        SDL_BlitSurface(caisseOK, NULL, ecran, &position);
                        break;
                    case OBJECTIF:
                        SDL_BlitSurface(objectif, NULL, ecran, &position);
                        objectifsRestants = 1;
                        break;
                }
            }
        }

        // Si on n'a trouvé aucun objectif sur la carte, c'est qu'on a gagné
        if (!objectifsRestants)
            continuer = 0;

        // On place le joueur à la bonne position
        position.x = positionJoueur.x * TAILLE_BLOC;
        position.y = positionJoueur.y * TAILLE_BLOC;
        SDL_BlitSurface(marioActuel, NULL, ecran, &position);

        SDL_Flip(ecran);
    }

    // Désactivation de la répétition des touches (remise à 0)
    SDL_EnableKeyRepeat(0, 0);

    // Libération des surfaces chargées
    SDL_FreeSurface(mur);
    SDL_FreeSurface(caisse);
    SDL_FreeSurface(caisseOK);
    SDL_FreeSurface(objectif);
    for (i = 0 ; i < 4 ; i++)
        SDL_FreeSurface(mario[i]);
}

void deplacerJoueur(int carte[][NB_BLOCS_HAUTEUR], SDL_Rect *pos, int direction)
{
    switch(direction)
    {
        case HAUT:
            if (pos->y - 1 < 0) // Si le joueur dépasse l'écran, on arrête
                break;
            if (carte[pos->x][pos->y - 1] == MUR) // S'il y a un mur, on arrête
                break;
            // Si on veut pousser une caisse, il faut vérifier qu'il n'y a pas de mur derrière (ou une autre caisse, ou la limite du monde)
            if ((carte[pos->x][pos->y - 1] == CAISSE || carte[pos->x][pos->y - 1] == CAISSE_OK) &&
                (pos->y - 2 < 0 || carte[pos->x][pos->y - 2] == MUR ||
                carte[pos->x][pos->y - 2] == CAISSE || carte[pos->x][pos->y - 2] == CAISSE_OK))
                break;

            // Si on arrive là, c'est qu'on peut déplacer le joueur !
            // On vérifie d'abord s'il y a une caisse à déplacer
            deplacerCaisse(&carte[pos->x][pos->y - 1], &carte[pos->x][pos->y - 2]);

            pos->y--; // On peut enfin faire monter le joueur (oufff !)
            break;

        case BAS:
            if (pos->y + 1 >= NB_BLOCS_HAUTEUR)
                break;
            if (carte[pos->x][pos->y + 1] == MUR)
                break;

            if ((carte[pos->x][pos->y + 1] == CAISSE || carte[pos->x][pos->y + 1] == CAISSE_OK) &&
                (pos->y + 2 >= NB_BLOCS_HAUTEUR || carte[pos->x][pos->y + 2] == MUR ||
                carte[pos->x][pos->y + 2] == CAISSE || carte[pos->x][pos->y + 2] == CAISSE_OK))
                break;

            deplacerCaisse(&carte[pos->x][pos->y + 1], &carte[pos->x][pos->y + 2]);

            pos->y++;
            break;

        case GAUCHE:
            if (pos->x - 1 < 0)
                break;
            if (carte[pos->x - 1][pos->y] == MUR)
                break;

            if ((carte[pos->x - 1][pos->y] == CAISSE || carte[pos->x - 1][pos->y] == CAISSE_OK) &&
                (pos->x - 2 < 0 || carte[pos->x - 2][pos->y] == MUR ||
                carte[pos->x - 2][pos->y] == CAISSE || carte[pos->x - 2][pos->y] == CAISSE_OK))
                break;

            deplacerCaisse(&carte[pos->x - 1][pos->y], &carte[pos->x - 2][pos->y]);

            pos->x--;
            break;

        case DROITE:
            if (pos->x + 1 >= NB_BLOCS_LARGEUR)
                break;
            if (carte[pos->x + 1][pos->y] == MUR)
                break;

            if ((carte[pos->x + 1][pos->y] == CAISSE || carte[pos->x + 1][pos->y] == CAISSE_OK) &&
                (pos->x + 2 >= NB_BLOCS_LARGEUR || carte[pos->x + 2][pos->y] == MUR ||
                carte[pos->x + 2][pos->y] == CAISSE || carte[pos->x + 2][pos->y] == CAISSE_OK))
                break;

            deplacerCaisse(&carte[pos->x + 1][pos->y], &carte[pos->x + 2][pos->y]);

            pos->x++;
            break;
    }
}

void deplacerCaisse(int *premiereCase, int *secondeCase)
{
    if (*premiereCase == CAISSE || *premiereCase == CAISSE_OK)
    {
        if (*secondeCase == OBJECTIF)
            *secondeCase = CAISSE_OK;
        else
            *secondeCase = CAISSE;

        if (*premiereCase == CAISSE_OK)
            *premiereCase = OBJECTIF;
        else
            *premiereCase = VIDE;
    }
}


mateo21, pour Le Site du Zér0

fichiers.c

/*
fichiers.c
----------

Par mateo21, pour Le Site du Zér0 (www.siteduzero.com)

Rôle : fonctions de lecture / écriture de fichiers de niveau.
*/

#include
#include
#include
#include

#include "constantes.h"
#include "fichiers.h"

int chargerNiveau(int niveau[][NB_BLOCS_HAUTEUR])
{
    FILE* fichier = NULL;
    char ligneFichier[NB_BLOCS_LARGEUR * NB_BLOCS_HAUTEUR + 1] = {0};
    int i = 0, j = 0;

    fichier = fopen("niveaux.lvl", "r");
    if (fichier == NULL)
        return 0;

    fgets(ligneFichier, NB_BLOCS_LARGEUR * NB_BLOCS_HAUTEUR + 1, fichier);

    for (i = 0 ; i < NB_BLOCS_LARGEUR ; i++)
    {
        for (j = 0 ; j < NB_BLOCS_HAUTEUR ; j++)
        {
            switch (ligneFichier[(i * NB_BLOCS_LARGEUR) + j])
            {
                case '0':
                    niveau[j][i] = 0;
                    break;
                case '1':
                    niveau[j][i] = 1;
                    break;
                case '2':
                    niveau[j][i] = 2;
                    break;
                case '3':
                    niveau[j][i] = 3;
                    break;
                case '4':
                    niveau[j][i] = 4;
                    break;
            }
        }
    }

    fclose(fichier);
    return 1;
}

int sauvegarderNiveau(int niveau[][NB_BLOCS_HAUTEUR])
{
    FILE* fichier = NULL;
    int i = 0, j = 0;

    fichier = fopen("niveaux.lvl", "w");
    if (fichier == NULL)
        return 0;

    for (i = 0 ; i < NB_BLOCS_LARGEUR ; i++)
    {
        for (j = 0 ; j < NB_BLOCS_HAUTEUR ; j++)
        {
            fprintf(fichier, "%d", niveau[j][i]);
        }
    }

    fclose(fichier);
    return 1;
}


mateo21, pour Le Site du Zér0

editeur.c

/*
editeur.c
---------

Par mateo21, pour Le Site du Zér0 (www.siteduzero.com)

Rôle : gestion de l'éditeur de niveaux.
*/

#include
#include
#include
#include

#include "constantes.h"
#include "editeur.h"
#include "fichiers.h"

void editeur(SDL_Surface* ecran)
{
    SDL_Surface *mur = NULL, *caisse = NULL, *objectif = NULL, *mario = NULL;
    SDL_Rect position;
    SDL_Event event;

    int continuer = 1, clicGaucheEnCours = 0, clicDroitEnCours = 0;
    int objetActuel = MUR, i = 0, j = 0;
    int carte[NB_BLOCS_LARGEUR][NB_BLOCS_HAUTEUR] = {0};

    // Chargement des objets et du niveau
    mur = IMG_Load("mur.jpg");
    caisse = IMG_Load("caisse.jpg");
    objectif = IMG_Load("objectif.png");
    mario = IMG_Load("mario_bas.gif");

    if (!chargerNiveau(carte))
        exit(EXIT_FAILURE);

    // Boucle infinie de l'éditeur
    while (continuer)
    {
        SDL_WaitEvent(&event);
        switch(event.type)
        {
            case SDL_QUIT:
                continuer = 0;
                break;
            case SDL_MOUSEBUTTONDOWN:
                if (event.button.button == SDL_BUTTON_LEFT)
                {
                    // On met l'objet actuellement choisi (mur, caisse...) à l'endroit du clic
                    carte[event.button.x / TAILLE_BLOC][event.button.y / TAILLE_BLOC] = objetActuel;
                    clicGaucheEnCours = 1; // On active un booléen pour retenir qu'un bouton est enfoncé
                }
                else if (event.button.button == SDL_BUTTON_RIGHT) // Le clic droit sert à effacer
                {
                    carte[event.button.x / TAILLE_BLOC][event.button.y /TAILLE_BLOC] = VIDE;
                    clicDroitEnCours = 1;
                }
                break;
            case SDL_MOUSEBUTTONUP: // On désactive le booléen qui disait qu'un bouton était enfoncé
                if (event.button.button == SDL_BUTTON_LEFT)
                    clicGaucheEnCours = 0;
                else if (event.button.button == SDL_BUTTON_RIGHT)
                    clicDroitEnCours = 0;
                break;
            case SDL_MOUSEMOTION:
                if (clicGaucheEnCours) // Si on déplace la souris et que le bouton gauche de la souris est enfoncé
                {
                    carte[event.motion.x / TAILLE_BLOC][event.motion.y / TAILLE_BLOC] = objetActuel;
                }
                else if (clicDroitEnCours) // Pareil pour le bouton droit de la souris
                {
                    carte[event.motion.x / TAILLE_BLOC][event.motion.y / TAILLE_BLOC] = VIDE;
                }
                break;
            case SDL_KEYDOWN:
                switch(event.key.keysym.sym)
                {
                    case SDLK_ESCAPE:
                        continuer = 0;
                        break;
                    case SDLK_s:
                        sauvegarderNiveau(carte);
                        break;
                    case SDLK_c:
                        chargerNiveau(carte);
                        break;
                    case SDLK_KP1:
                        objetActuel = MUR;
                        break;
                    case SDLK_KP2:
                        objetActuel = CAISSE;
                        break;
                    case SDLK_KP3:
                        objetActuel = OBJECTIF;
                        break;
                    case SDLK_KP4:
                        objetActuel = MARIO;
                        break;
                }
                break;
        }

        // Effacement de l'écran
        SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 255, 255, 255));

        // Placement des objets à l'écran
        for (i = 0 ; i < NB_BLOCS_LARGEUR ; i++)
        {
            for (j = 0 ; j < NB_BLOCS_HAUTEUR ; j++)
            {
                position.x = i * TAILLE_BLOC;
                position.y = j * TAILLE_BLOC;

                switch(carte[i][j])
                {
                    case MUR:
                        SDL_BlitSurface(mur, NULL, ecran, &position);
                        break;
                    case CAISSE:
                        SDL_BlitSurface(caisse, NULL, ecran, &position);
                        break;
                    case OBJECTIF:
                        SDL_BlitSurface(objectif, NULL, ecran, &position);
                        break;
                    case MARIO:
                        SDL_BlitSurface(mario, NULL, ecran, &position);
                        break;
                }
            }
        }

        // Mise à jour de l'écran
        SDL_Flip(ecran);
    }

    SDL_FreeSurface(mur);
    SDL_FreeSurface(caisse);
    SDL_FreeSurface(objectif);
    SDL_FreeSurface(mario);
}