[edit: histoire de vivre avec son temps j'ai recompilé le projet en c++. ça ne change strictement rien au code source, y'a juste l'extension des fichiers qui a été modifiée]
Pour faire ce second tuto vous devez d'abord avoir fait le tuto 1 qui explique comment télécharger Code::Blocks et tester un hello world.
La librairie SDLSimple Directmedia Layer est un projet open-source collaboratif qui permet de faciliter une partie problématique de la programmation multimédia, à savoir l'intégration OS. SDL gère pour vous l'intégration dans windows, mac, linux, android, etc... de façon beaucoup plus accessible que glut & dxut. (de façon moins subtile et moins optimisée, mais on verra comment améliorer ça lors d'un prochain tutoriel sur le polishing spécifique à windows)
Cette librairie est utilisée aussi bien dans les jeux amateur que dans les jeux professionnels.
Elle possède un moteur graphique facile d'utilisation mais pour les tutoriels qui vont suivre, notre but n'étant pas de devenir des spécialistes de SDL, on va utiliser la méthode bien connue dans les jeux amateur, que j'appellerai "ms-dos-like".
La méthode DOS-likeCette méthode consiste à recréer les conditions de programmation des jeux sur ms-dos: on change simplement la couleur des pixels de l'ecran et on écrit directement des ondes dans le buffer audio.
Si on emploie beaucoup la technique dos-like dans les jeux amateur, c'est qu'à l'époque des jeux ms-dos, leur développement ne coûtait quasiment rien, c'est donc adapté aux faibles délais des dev amateur. Cette méthode est beaucoup plus efficace que d'utiliser une librairie qui dessine et joue la musique à votre place: il est plus rapide d'improviser des techniques de tracé maison et de mixage maison, plutôt que d'apprendre toute la doc d'un moteur graphique et d'un moteur de son. C'est à dire que vous ne faites que du calcul pur (et tant que vous ne faites que des jeux 2d, ça reste des maths niveau 6ème). Vous pouvez donc préparer vos algorithmes sur papier quand vous êtes dans le train, au bistro, etc... pas besoin d'être devant le pc à apprendre de la doc.
Le calcul pur a aussi l'avantage d'être très facile à porter, il est indépendant du hardware, de l'os, etc, on peut donc le trimballer facilement d'un environnement de développement à l'autre, y compris celui qui nous intéresse, c'est à dire flash (on verra ça dans les prochains tutoriels).
Partir d'un template codeblocksCe tuto s'adresse à des débutants donc je n'expliquerai pas ici comment configurer le projet. (Ca n'est pas très compliqué à faire si on sait se servir de google, si vous voulez un tutoriel sur la configuration d'un projet sdl sur code::blocks demandez moi.)
On va donc démarrer d'un petit template simple qui se trouve ici:
[Vous devez être inscrit et connecté pour voir ce lien]Vérifiez qu'il se compile bien avant de poursuivre. (Si jamais ça plante reportez-moi le bug que j'étudie ça.)
Le template contient trois pages de programme: "main.cpp" qui s'occupe de SDL, "hart.c" qui dimensionne les buffers video/audio/clavier et gère la communication entre sdl et le jeu, et "game.cpp" où l'on programme le jeu proprement dit.
La seule page sur laquelle vous allez travailler est "game.cpp", vous n'avez besoin d'ouvrir que celle-là.
- Code:
-
// hart data ponters
long game_scrW = 320, game_scrH = 240;
unsigned long * game_screen;
float * game_buf4096;
char * game_keys;
// game data
long game_scrollx=0, game_scrolly=0, game_speedx=0, game_speedy=0;
void game_init( char * keys, unsigned long * screen, long scrW, long scrH, float * buf_4096_64 ) {
// alloc pointers
game_scrW = scrW;
game_scrH = scrH;
game_screen = screen;
game_buf4096 = buf_4096_64;
game_keys = keys;
}
void game_close() {
// dealloc pointers
game_screen = 0;
game_buf4096 = 0;
game_keys = 0;
}
void game_logicRoutine( ) {
char * keys = game_keys;
game_speedx = keys[1]-keys[0];
game_speedy = keys[3]-keys[2];
game_scrollx += 4*game_speedx;
game_scrolly += 4*game_speedy;
}
void game_graphicRoutine()
{
unsigned long * screen = game_screen;
long scrW=game_scrW,scrH=game_scrH,i,j,p=0,scrollx=game_scrollx,scrolly=game_scrolly,vy; // caching and local
for ( j=0; j<scrH; j++ ) {
vy = j+scrolly;
for ( i=0; i<scrW; i++, p++ ) { // scanline loop should be optimized with assembler
screen[p] = (i+scrollx)&vy;
}
}
}
long audiopos=0;
void game_audioRoutine()
{
float * buf = game_buf4096;
double val = 0, volLeft = (game_speedx<0), volRight = (game_speedx>0);
long s=0, ap = audiopos, perShift = 8+game_speedy;
for (s=0; s<4096; s+=2, ap++ ) { // audio buffer loop, should be optimized with assembler
val = 0.01 * ( ( ( ( ap >> perShift ) & 1 ) << 1 ) - 1 ) ;
buf[s+0] = val * volLeft;
buf[s+1] = val * volRight;
}
audiopos = ap;
}
Je vais expliquer brièvement le code.
Tout d'abord on a 5 fonctions:
- game_init()
- game_close()
- game_logicroutine()
- game_graphicroutine()
- game_audioroutine()
Les fonctions game_init() et game_close() sont l'équivalent des constructeurs / destructeurs dans les langages objet. On référence et déréférence les pointeurs des zones mémoire écran/son/clavier.
Les pointeursSi vous faites de l'actionscript / javascript, vous savez sans le savoir ce que c'est qu'un pointeur. En actionscript ou javascript, toute variable est un pointeur, c'est à dire qu'elle n'enregistre pas directement les données, mais elle enregistre une adresse mémoire qui permet d'accéder un objet qui contient les données.
Dans la fonction init() vous aurez remarqué qu'il y'a des variables avec une syntaxe bizzare:
- Code:
-
char * keys, unsigned long * screen, .... double * buf_4096_64
Lorsqu'en c/c+ on met une astérix devant la déclaration de variable, c'est qu'il s'agit d'un pointeur.
Grâce à ces pointeurs, on connait l'adresse mémoire de tableaux qui ont été déclarés sur une autre page du code (dans le fichier "hart.c"). Pour être exact, le pointeur contient l'adresse de la première case du tableau.
Les pointeurs C/C++ se comportent exactement comme des tableaux, on accède aux données en écrivant
pointeur[i], à quelques petites différences près:
- un pointeur ne permet pas de connaitre la taille du tableau avec la fonction
sizeof()- un pointeur permet aussi de stocker l'adresse mémoire d'une simple variable, il fonctionne alors comme un tableau à une seule case. Pour ce faire on utilise l'éperulette:
pointeur = &variable.
- un pointeur permet d'optimiser certains parcours de tableaux en faisant directement des calculs sur les adresses mémoire. Pour ça on remplace la syntaxe tableau par la syntaxe pointeur. Au lieu d'écrire
pointeur[i]=10, on va écrire
*(pointeur+i)=10La différence des pointeurs en langages natif C/C+ avec les langages managés java/c#/javascript/actionscript, c'est que les pointeurs des langages managés ne permettent pas d'accéder directement à l'adresse mémoire. Ces pointeurs qui masquent l'adresse ram sont appelés des "références".
Les fonctions routinesCe sont des fonctions appelées sur un intervalle de temps régulier. En flash vous connaissez la routine "enterFrame" appelée à chaque rafraichissement d'image, ou la routine "sampleDataEvent" appelée à chaque mise à jour de buffer audio.
Sur cet exemple j'en ai mis trois.
- game_logicroutine()
- game_graphicroutine()
Elles sont toutes les deux appelées sur le rafraichissement d'image, mais je les ai volontairement séparées car en programmation de jeux on sépare toujours la phase logique de la phase graphique.
La première lit le tableau clavier (qui dans mes exemples ne gère que les 4 touches haut/bas/gauche/droite), elle en déduit un déplacement (scrolling) d'image.
La seconde dessine un bête motif x&y. Elle sert juste à tester que l'affichage des pixels fonctionne.
- game_audioroutine()
Appelée sur l'update du buffer audio.
Elle fait un bruit bête en encodant une onde dite "square-wave" (carrée) qui devrait vous rappeler les vieux buzzers des premiers ordinateurs et consoles. C'est la forme d'onde la plus facile à programmer.
J'ai dimensionné le buffer audio en 4096 nombres décimaux de type "double" (64 bits) afin qu'il soit plus facile à porter sur flash (qui utilise un buffer de taille minimale 4096 décimales de 64 bit). Il y'a donc 2048 "samples", on divise 4096 par deux car pour chaque sample d'onde il faut deux nombres: un pour le haut-parleur gauche, un pour le haut-parleur droite.
Et maintenant on passe au pong.Bien, pour le moment vous n'avez fait que lire mon bavardage, maintenant il faut commencer à bosser.
Vous allez d'abord faire un copier-coller du dossier du template et le renommer "pong".
Ensuite, modifiez le nom du fichier "sdl_template.cbp" en "pong.cbp"
Dans la colonne gauche de codeblocks, faites un clic-droit sur la racine du projet (qui n'a pas encore été renommée en "pong") et cliquez dans le menu tout en bas sur "properties"
Dans le premier onglet "project settings", changez le champ "Title" et écrivez "pong".
On va également renommer les fichiers exe. Cliquez sur l'onglet "build target". Dans la colonne de gauche vous pouvez selectionner la version debug et la version release du programme. Pour les deux versions, modifiez le champ "output filename", et mettez "bin\Debug\pong.exe" et "bin\Release\pong.exe"
Le programme du pong.Maintenant ouvrez le fichier "game.cpp", vous allez remplacer le code du sample par celui-ci:
- Code:
-
// hart data ponters
long game_scrW, game_scrH;
unsigned long * game_screen;
float * game_buf4096;
char * game_keys;
// game data
long pad_x, pad_y, ball_x, ball_y, ball_speed_x, ball_speed_y;
long audiopos=0; // audio stuff
double vol=0;
// game internal functions -------------------------------------------------------
void round_start() {
pad_x = game_scrW / 2; pad_y = game_scrH - 16;
ball_x = game_scrW / 2; ball_y = game_scrH / 2;
ball_speed_x = 4; ball_speed_y = 4;
}
void game_clearScreen()
{
long nPix = game_scrW * game_scrH; // caching
unsigned long * screen = game_screen;
long p;
for ( p=0; p<nPix; p++ ) screen[p] = 0x0; // optimize this shit with assembler
}
void game_whiteRect(long left, long top, long right, long bottom) {
long i,j,scrW=game_scrW,scrH=game_scrH,rowFirstPix;
unsigned long * screen = game_screen;
if (left<0) left=0;
if (top<0) top=0;
if (right>scrW) right=scrW;
if (bottom>scrH) bottom=scrH;
for (j=top;j<bottom;j++) {
rowFirstPix = j * scrW;
for (i=left;i<right;i++) screen[rowFirstPix+i] = 0xffffff; // optimize this shit with assembler
}
}
void noise()
{
vol = 1;
}
// init and close -----------------------------------------------------------------------------
void game_init( char * keys, unsigned long * screen, long scrW, long scrH, float * buf_4096_64 ) {
// alloc pointers
game_scrW = scrW; game_scrH = scrH; game_screen = screen; game_buf4096 = buf_4096_64; game_keys = keys;
// first round
round_start();
}
void game_close() {
// dealloc pointers
game_screen = 0; game_buf4096 = 0; game_keys = 0;
}
// routines ----------------------------------------------------------------------------
void game_logicRoutine( ) {
// pad behavior
pad_x += 8*(game_keys[1]-game_keys[0]);
if (pad_x<16) pad_x=16; else if (pad_x>game_scrW-16) pad_x=game_scrW-16;
// ball behavior
ball_x += ball_speed_x;
ball_y += ball_speed_y;
if ( ball_x < 4 ) {
ball_x = 4;
ball_speed_x = 4;
noise();
}
if ( ball_x > game_scrW-4 ) {
ball_x = game_scrW-4;
ball_speed_x = -4;
noise();
}
if ( ball_y < 4 ) {
ball_y = 4;
ball_speed_y = 4;
noise();
}
if ( ball_y > game_scrH ) ball_y -= game_scrH;
// ball hit pad
if ( ball_y > pad_y-4 && ball_y < pad_y+12 && ball_x > pad_x-20 && ball_x < pad_x+20 ) {
ball_y = pad_y -4;
ball_speed_y = -4;
noise();
}
}
void game_graphicRoutine()
{
game_clearScreen();
game_whiteRect(pad_x-16,pad_y,pad_x+16,pad_y+8); // draw pad
game_whiteRect(ball_x-4,ball_y-4,ball_x+4,ball_y+4); // draw ball
}
void game_audioRoutine()
{
float * buf = game_buf4096;
double val = 0, volLeft = (double)(-ball_x+game_scrW)/game_scrW, volRight = (double)(ball_x)/game_scrW;
long s = 0, ap = audiopos, perShift = 8;
for (s=0; s<4096; s+=2, ap++ ) { // audio buffer loop, should be optimized with assembler
vol *= 0.999; //fade
val = 0.02 * ( ( ( ( ap >> perShift ) & 1 ) << 1 ) - 1 ) ;
buf[s+0] = val * volLeft * vol;
buf[s+1] = val * volRight * vol;
}
audiopos = ap;
}
A priori je n'ai rien à vous expliquer, c'est un exercice suffisemment simple pour que vous compreniez le code de vous-même. Mais si vous avez des questions à poser, n'hésitez pas.
Je vais quand même fournir quelques explications sur le code.
Vous avez du voir que j'ai ajouté 4 fonctions. Je les ai mises au début car en C il faut créer une fonction avant de s'en servir (c'est un langage simple donc bête).
- round_start()
Cette fonction ne sert à rien ici mais elle prévoit qu'on puisse rejouer une partie. L'initialisation de la partie se fait dedans.
- game_clearScreen()
Première fonction de mon moteur graphique. Celle-ci repeint tout l'écran en noir pour pouvoir retracer les sprites par dessus.
- game_whiteRect()
Deuxième fonction du moteur graphique. Elle dessine un rectangle blanc.
- noise()
Unique fonction du moteur audio. Elle déclenche un bruit. Le reste est géré dans la routine audio.
P.S. Vous avez du remarquer aussi un truc bizzare..Dans les fonctions de dessin et de bruit, y'a des variables globales qui sont copiées dans des variables locales. Cette technique s'appelle le caching, elle permet d'accélérer les grosses boucles de calculs car l'accès aux variables locales est plus rapide. En as/js ça accélère la totalité des calculs. En c/c+ ça marche plus ou moins, le compileur va essayer de stocker un maximum de variables locales dans les registres (la mémoire processeur) plutôt que dans la ram, mais on abordera ces questions dans les tutoriels sur l'assembleur.
Voilà... vous avez un point de départ pour faire des jeux dos-like en C.
N'hésitez pas à bidouiller le code et à poser des questions.
Dans le prochain tutoriel on verra comment faire marcher ça dans le logiciel flash.
[edit: ce tuto a été mis à jour. j'ai remplacé les double (64bit) du audiobuffer de la zone calcul par des float (32bit), pour être en accord avec le format de l'audiobuffer de flash]