Un Tetris rudimentaire.

Un Tetris rudimentaire.

Voir version :

Pas de dépendances

Télécharger :

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sdl/sdl.h>

#pragma comment(lib,"sdl.lib")
#pragma comment(lib,"sdlmain.lib")

#define TILESIZE 16
#define NBPIECES 7
#define XRES 400
#define YRES 400
#define NLINE 21
#define NCOL 12
#define STARTXZONEJEU 4
#define STARTYZONEJEU 2
#define VITESSE 500   // temps en millisecondes avant prochaine descente.

#define DOUBLEFOR(i,imax,j,jmax) for(i=0;i<imax;i++) for(j=0;j<jmax;j++)

struct State
{
    int x,y,rotate;
};

struct STetris
{
    SDL_Surface* tiles;
    Uint8 zone[NCOL][NLINE];
    Uint8 pieces[NBPIECES][4][4][4];
    int numpiece,numpiecenext;
    struct State Scurrent;
    struct State Scand;
    int gameover,vitesse;
    Uint32 nextmove;
};

typedef struct STetris Tetris;

int SetPixel32(SDL_Surface* srf,int x,int y,Uint32 color)
{
    Uint32 *p = (Uint32*)(((Uint8*)srf->pixels) + y * srf->pitch + x * 4);
    *p = color;
    return 0;
}

int BuildTileSet(SDL_Surface* srf)
{
    Uint32 colors[NBPIECES+2][3]={{0,0,0},
        {128,128,128},{255,0,0},{0,255,0},{0,0,255},
        {255,255,0},{255,0,255},{0,255,255},{255,128,0}
    };
    Uint32 Tileshem[TILESIZE]={
        0xFFFFFFFD,0xFFFFFFF5,0xFAAAAAA5,0xFAAAAAA5,
        0xFAAAAAA5,0xFAAAAAA5,0xFAAAAAA5,0xFAAAAAA5,
        0xFAAAAAA5,0xFAAAAAA5,0xFAAAAAA5,0xFAAAAAA5,
        0xFAAAAAA5,0xFAAAAAA5,0xD5555555,0x55555555
    };
    int i,j,k;
    SDL_LockSurface(srf);
    for(k=0;k<NBPIECES+2;k++)
    {
        Uint32 LocalPalette[4];
        for(i=0;i<4;i++)
            LocalPalette[i] = SDL_MapRGBA(srf->format,(colors[k][0]/3)*i,(colors[k][1]/3)*i,(colors[k][2]/3)*i,0);
        DOUBLEFOR(i,TILESIZE,j,TILESIZE)
        {
            int ndat = j*TILESIZE+i;
            Uint32 ind = ((Tileshem[ndat/16]<<((ndat%16)*2))&0xC0000000)>>30;
            SetPixel32(srf,k*TILESIZE+i,j,LocalPalette[ind]);
        }
    }
    SDL_UnlockSurface(srf);
    //SDL_SaveBMP(srf,"plouf.bmp");
    return 0;
}

int BuildPieces(Tetris* T)
{
    Uint16 shem[NBPIECES][4] = {
        {0xC600,0x2640,0xC600,0x2640},
        {0x6C00,0x4620,0x6C00,0x4620},
        {0x8E00,0x6440,0x0E20,0x44C0},
        {0x6600,0x6600,0x6600,0x6600},
        {0x4E00,0x4640,0x0E40,0x4C40},
        {0x0F00,0x2222,0x0F00,0x2222},
        {0x2E00,0x4460,0x0E80,0xC440}
    };
    int i,j,k,l;
    DOUBLEFOR(i,NBPIECES,j,4)
        DOUBLEFOR(k,4,l,4)
        {
            int dat = ((shem[i][j]<<(l*4+k))&0x8000)>>15;
            T->pieces[i][j][k][l] = dat*(i+2);
        }
    return 0;
}

int NouvellePiece(Tetris* T)
{
    T->numpiece = T->numpiecenext;
    T->numpiecenext = rand()%NBPIECES;
    T->Scurrent.rotate = 0;
    T->Scurrent.x = (NCOL-4)/2;
    T->Scurrent.y = 0;
    T->nextmove = SDL_GetTicks()+T->vitesse;
    return 0;
}

int Tetris_Init(Tetris* T)
{
    int i;
    T->numpiecenext = T->gameover = T->nextmove = 0;
    T->vitesse = VITESSE;
    NouvellePiece(T);
    NouvellePiece(T);
    T->tiles = SDL_CreateRGBSurface(0,(NBPIECES+2)*TILESIZE,TILESIZE,32,0,0,0,0);
    BuildTileSet(T->tiles);
    BuildPieces(T);
    memset(T->zone,0,NCOL*NLINE);
    for(i=0;i<NCOL;i++)
        T->zone[i][NLINE-1] = 1; // ligne de protection basse
    for(i=0;i<NLINE;i++)
    {
        T->zone[0][i] = 1; // lignes de protections latérales.
        T->zone[NCOL-1][i] = 1;
    }
    return 0;
}

int Tetris_End(Tetris* T)
{
    SDL_FreeSurface(T->tiles);
    return 0;
}

int Cube(SDL_Surface* srf,int i,int j,int numcube)
{
    SDL_Rect Rsrc,Rdest;
    Rdest.x = i*TILESIZE;
    Rdest.y = j*TILESIZE;
    Rsrc.x = numcube*TILESIZE;
    Rsrc.y = 0;
    Rsrc.w = TILESIZE;
    Rsrc.h = TILESIZE;
    SDL_BlitSurface(srf,&Rsrc,SDL_GetVideoSurface(),&Rdest);
    return 0;
}

int RenderFond(Tetris* T)
{ // 2 fois pour toutes, double buffering, 
    int i,j,k;
    for(k=0;k<2;k++)
    {
        DOUBLEFOR(i,XRES/TILESIZE,j,YRES/TILESIZE)
            Cube(T->tiles,i,j,1);
        SDL_Flip(SDL_GetVideoSurface());
    }
    return 0;
}

int RenderPiece(Tetris* T,int x,int y,int npiece,int rotate,int transparence)
{
    int i,j;
    DOUBLEFOR(i,4,j,4)
    {
        if (transparence && T->pieces[npiece][rotate][i][j]==0)
            continue;
        Cube(T->tiles,x+i,y+j,T->pieces[npiece][rotate][i][j]);
    }
    return 0;
}

int Render(Tetris* T)
{
    int i,j;
    DOUBLEFOR(i,NCOL,j,NLINE)
        Cube(T->tiles,i+STARTXZONEJEU,j+STARTYZONEJEU,T->zone[i][j]);
    RenderPiece(T,STARTXZONEJEU+NCOL+2,4,T->numpiecenext,0,0);
    RenderPiece(T,STARTXZONEJEU+T->Scurrent.x,STARTYZONEJEU+T->Scurrent.y,T->numpiece,T->Scurrent.rotate,1);
    SDL_Flip(SDL_GetVideoSurface());
    SDL_Delay(1);
    return 0;
}

#define PIECECARRE(T,i,j) (T->pieces[T->numpiece][T->Scand.rotate][i][j])

int Valide(Tetris* T)
{
    int i,j;
    DOUBLEFOR(i,4,j,4)
    {
        Uint8 carre = PIECECARRE(T,i,j);
        if (carre!=0 && T->zone[T->Scand.x+i][T->Scand.y+j]!=0)
             return 0;
    }
    T->Scurrent = T->Scand;
    return 1;
}

int AncrePiece(Tetris* T)
{
    int i,j;
    T->Scand = T->Scurrent;
    DOUBLEFOR(i,4,j,4)
    {
        Uint8 carre = PIECECARRE(T,i,j);
        if (carre!=0)
            T->zone[T->Scand.x+i][T->Scand.y+j] = carre;
    }
    return 0;
}

int VirerLigne(Tetris* T,int y)
{
    int i,j;
    for(i=0;i<NCOL;i++)
    {
        for(j=y;j>0;j--)
            T->zone[i][j] = T->zone[i][j-1];
        T->zone[i][0] = 0;
    }
    T->zone[0][0] = 1;
    T->zone[NCOL-1][0] = 1;
    return 0;
}

int VirerLignes(Tetris* T,int ystart)
{
    int i,j;
    for(i=0;i<4;i++)
    {
        int cpt = 0;
        if (i+ystart==NLINE-1)
            break; // on ne vire pas la ligne de protection basse.
        for(j=0;j<NCOL;j++)
            if (T->zone[j][i+ystart]!=0)
                cpt++;
        if (cpt==NCOL)
            VirerLigne(T,i+ystart);
    }
    return 0;
}

int Descente(Tetris* T)
{
    T->Scand.y++;
    if (Valide(T))
    {
        T->nextmove = SDL_GetTicks()+T->vitesse;
        return 0;
    }
    else
    {
        if (T->Scurrent.y == 0)
            T->gameover = 1;
        AncrePiece(T);
        VirerLignes(T,T->Scurrent.y);
        NouvellePiece(T);
    }
    return 0;
}

int Jeu(Tetris* T)
{
    SDL_Event E;
    SDL_EnableKeyRepeat(200,50);
    while(!T->gameover)
    {
        T->Scand = T->Scurrent;
        if (SDL_GetTicks()>=T->nextmove)
            Descente(T);
        while (SDL_PollEvent(&E))
        {
            if (E.type!=SDL_KEYDOWN)
                continue;
            if (E.key.keysym.sym == SDLK_DOWN && T->nextmove-SDL_GetTicks()>20) // drop
                T->nextmove = SDL_GetTicks()+20;
            if (E.key.keysym.sym == SDLK_LEFT)
                T->Scand.x--;
            if (E.key.keysym.sym == SDLK_RIGHT)
                T->Scand.x++;
            if (E.key.keysym.sym == SDLK_SPACE)
                T->Scand.rotate = (T->Scand.rotate+1)%4;
            Valide(T);
        }
        Render(T);
    }
    return 0;
}

int main(int argc,char** argv)
{
    Tetris T;
    srand((unsigned int)time(NULL));
    SDL_Init(SDL_INIT_VIDEO);
    SDL_SetVideoMode(XRES,YRES,32,SDL_DOUBLEBUF);
    Tetris_Init(&T);
    RenderFond(&T);
    Jeu(&T);
    Tetris_End(&T);
    SDL_Quit();
    return 0;
}



Commentaires

	Un petit Tetris rudimentaire.

	Tout d'abord, lancez le jeu, pour voir. 
	Les touches sont les flèches pour diriger la pièce, le bas pour le drop, et la touche espace.

	Si vous avez été observateur, vous me direz :

	"Il y a des graphismes dans le jeu, mais pas de fichier BMP nécessaire, ni autre fichier !"

	Tout est il donc dans le code ? 
	-> Oui, je me suis amusé à le faire comme ça !

	Cherchez la ligne "SDL_SaveBMP", et activez la. Vous verrez en relançant le jeu qu'un BMP se sera créé.
	Un BMP avec les briques de couleur : mes seuls graphismes.

	Je vais commencer à expliquer le programme.

	Regardons le main. 
	Tout d'abord, la structure Tetris qui contiendra mes données :
	- SDL_Surface* tiles : le tileset (le fichier BMP que vous avez exporté, donc mes cubes de couleur)
	- la zone de jeu : un tableau à deux dimensions qui contiendra, pour chaque case de la zone de jeu,
	si elle est occupée ou pas, et par quelle brique.
	- pieces : ce sera la description de chaque piece, on détaillera plus tard.
	- numpiece : la piece en cours.
	- numpiecenext : la prochaine pièce (et oui, dans Tetris, on sait quelle est la prochaine piece)
	- State :  une sous structure qui dit quelle est la position de la piece dans la zone (x,y), et sa rotation :
	en effet, chaque piece peut avoir 4 sens différent.
	Il y aura deux sous structures State : Scurrent, représentant une position de pièce réelle, et Scand,
	un candidat pour les collisions, nous verrons plus tard
	- gameover : passe à 1 si le jeu s'arrête.
	- nextmove : un chrono qui dit quand arrive la prochaine descente (dépend de la vitesse de descente de piece voulue)

	Regardons le main, l'initialisation de la structure se fait dans Tetris_Init.
	Dedans, la mise à zéro des variables, on va dire triviales.

	Puis deux appels à "NouvellePiece". Cette fonction va remplacer la pièce courante par celle en attente (numpiecenext)
	Va la mettre en place en haut au centre, et définir une nouvelle pièce suivante aléatoire.
	Elle va également fixer le chrono avant la prochaine descente de pièce.
	J'appelle deux fois cette fonction pour etre sur de finalement générer deux pièces aléatoires (l'active et la suivante)

	Ensuite la construction du tileset (l'image). Elle pourrait être remplacée par un simple SDL_LoadBMP si j'avais voulu.
	Je vais expliquer ma création. Si ça ne vous intéresse pas, zappez le paragraphe suivant :

	BuildTileSet
	************

	Cette fonction va créer le tileset. Avant la fonction, on alloue une surface avec SDL_CreateRGBSurface, de la 
	bonne taille. et a 32 bits par pixels.
	Ensuite, je fais SDL_LockSurface : pour permettre un accès aux pixels. A la fin de la fonction, je ferai un SDL_UnLockSurface

	La fonction contient un tableau fixe colors.
	Ce tableau est la liste des couleurs que je souhaite. {0,0,0} = noir (cas particulier, première brique)
	{128,128,128} = gris 
	{255,0,0} = rouge, etc...

	Tileshem est la bitmap compressé.
	En effet, regardez l'image BMP que vous avez généré : chaque brique contient 4 couleurs.
	Si on regarde la rouge par exemple :
	au milieu un rouge moyen. Au coin supérieur gauche, un rouge vif, au coin inférieur droit un rouge foncé.
	3 couleurs utiles.

	Le schéma pourrait être le suivant :

	3333333333333331
	3333333333333311
	3322222222222211

	etc... avec 3 la couleur claire, 2 la couleur moyenne, 1 la couleur sombre.
	Je dois donc coder des nombres entre 1 et 3 (disons entre 0 et 3).
	J'ai besoin de 2 bits.

	Ainsi, la première ligne :

	3333333333333331

	peut s'écrire, en binaire :
	111111111111111111111111111101
	Ce qui, en hexa, se code :
	FFFFFFFD

	On retrouve ce code au début de mon tableau shem.

	Avec ces codes, je dessine mes briques. La ligne Uint32 ind va chercher l'index (entre 1 et 3 donc).
	La palette locale est une simple division de la couleur locale, définissant des nuances.

	A la fin, j'obtiens mon tileset.

	On retourne dans la fonction init, et j'appelle :

	BuildPieces
	***********

	Cette fonction va remplir le champs

	Uint8 pieces[NBPIECES][4][4][4];

	Regardons l'image suivante :
	http://www.colinfahey.com/tetris/tetris_diagram_pieces_orientations_new.jpg

	J'ai NBPIECES (j'en ai 7), 4 positions possibles par piece (meme si certaines se répètent, dans l'idée, j'en ai 4)
	Puis chaque position est un tableau de 4x4.

	Dans ce tableau, je vais mettre 0 s'il n'y a pas de matière, et une autre valeur s'il y en a. 
	(je vais mettre le numéro de la pièce +2, je réserve le 0 pour le noir, et le 1 pour le cube gris (le bord))

	Le codage est simple :
	Pour chaque grille de 4x4, soit j'ai de la matière, soit je n'en ai pas.
	Donc soit 0 soit 1 en soit.
	4*4 = 16 , j'ai besoin de 16 bits, un Uint16 pour décrire une pièce.

	Si on regarde la première valeur que j'ai mis dans le tableau : 0xC600

	Cela fait, en binaire : 
	1100 0110 0000 0000

	Si on y met sous forme de tableau :

	1100
	0110
	0000
	0000

	Et oui, c'est une pièce de Tetris.

	Voila comment je remplis mon tableau.

	Fin de l'init
	-------------

	La fin de la fonction Tetris_Init va simplement remplir la zone de 0 (cases libres), sauf la ligne du bas, 
	et deux lignes de coté. Ainsi, au lieu d'avoir un cas particulier de détection des bords et du bas, je 
	considère qu'il y a des blocs déjà en place. Ils sont de code 1 (cube gris) donc on ne les différencie 
	pas du décor.

	Si vous mettez = 2; au lieu de = 1; vous verrez ces lignes en rouge.

	Voila, l'initialisation est terminée.
	Retournons voir dans le main.

	Rendu du fond
	-------------

	la fonction RenderFond est ensuite appelée. 
	Dedans, je vais quadriller l'écran de cubes gris. 
	(Notez la fonction cube que je me suis faite qui blit un cube de type (=couleur) demandé sans avoir
	à récréer des SDL_Rect à chaque fois.)

	Je le fais deux fois : en effet, je suis en double buffering : je vais le faire pour les deux buffers.
	Le but est ensuite de ne reblitter que la zone de jeu, jamais le fond de nouveau.

	Le macro DOUBLEFOR que j'ai fait se comprend bien. Un gain de temps.

	Le jeu
	------

	Nous arrivons au jeu.
	Le jeu, c'est un while. Dedans, je teste mes touches.

	Je teste d'abord si mon chrono dépasse le chrono de la pièce.
	Si c'est le cas, c'est que la pièce doit descendre d'un cran.

	J'appelle alors la fonction Descente.

	Descente
	--------

	J'appelle descente le fait qu'une pièce descende d'un cran.
	Quand on commence la descente, je fait descendre un candidat de la pièce, c'est à dire sa position
	finale hypothétique.
	J'ai ainsi conservé la position actuelle dans Scurrent, et la position finale dans Scand.
	
	La fonction Valide me dit si le candidat n'est pas dans un mur, tout simplement.
	Pour cela, il teste chaque brique de la matrice 4x4 de la pièce actuelle avec celles de la zone de jeu, en
	fonction de la ou elle est (x,y).

	Si Valide retourne 1, c'est que je peux descendre. Je mets à jour le chrono pour la prochaine descente.

	Sinon, c'est que j'ai touché. Dans ce cas, je regarde :
	Si j'ai touché et que ma position y=0 (je suis tout en haut) -> gameover
	
	Sinon, j'ancre la pièce (donc je la pose dans la zone), je vérifie si je fais disparaitre des lignes
	(fonction VirerLignes) puis j'appelle "nouvellepièce"

	Le fonction VirerLignes est simple : elle regarde pour les 4 possibilités si la ligne est pleine ou non.
	Puis elle décale les données de la zone en fonction.

	On retourne a Jeu :
	Je teste les déplacements et les rotations, mais j'appelle la fonction Valide pour savoir si j'ai le droit de
	le faire : si par exemple, je suis au bord du jeu, ou bien collé contre une pièce, je ne peux pas aller plus loin
	Je ne peux pas non plus faire certaines rotations si je suis dans ce cas.

	Voila, je crois que j'ai fait le tour.
	Ah si, la fonction Render, toute petite, qui affiche la zone de jeu, puis la pièce qu'on joue, et la pièce 
	en attente, flip et fait un délai de 1 milliseconde pour décharger le CPU.

	Il faut toujours bien séparer le rendu du contexte du jeu (une structure ou hiérarchie de structure)
	La structure (ici Tetris) décrit mon jeu.
	La fonction Render ne fait qu'illustrer la structure.

	Et dans ce cas, la fonction Render ne fait que quelques lignes...


Laissez un commentaire / post a comment