Leçon 3 : Animation
Présentation
Pour ce tutoriel, nous allons commencer pas charger une image une surface et de l'afficher à l'écran avant de créer de nouvelles surfaces, avec gestion de la transparence, qui se déplaceront sur la surface primaire. A la fin de ce tutorial, nous aurons une scène en 2D animée que nous pourrons faire évoluer vers un jeu jouable dans le cours suivant.
Charger une image
Nous allons appliquer le principe de blitting vue dans le cours précédent, c'est à dire que le programme travaillera sur les surfaces en arrière plan avant de les blitter et de les afficher à l'écran. Pour ce faire donc, nous devrons créer plusieurs surfaces :
LPDIRECTDRAWSURFACE Primaire = NULL; LPDIRECTDRAWSURFACE Image = NULL;
Bon, nous avons déclaré les surfaces, il faut maintenant charger la bitmap dans le fichier. Nous avons déjà débuté un programme en 640x480x32, nous allons maintenant charger une bitmap de même dimensions comme image de fond. Dans les sources du cours j'ai inclu une bitmap parfaite pour l'occasion (un peu de pub ne fait jamais de mal). Nous allons maintenant créer un ensemble de fonctions qui créeront et rempliront la surface offscreen avec les données de la bitmap. Nous allons créer tout d'abord une fonction qui chargera la bitmap :
IDirectDrawSurface * DDLoadBitmap(IDirectDraw *pdd, LPCSTR szBitmap) { HBITMAP hbm; BITMAP bm; IDirectDrawSurface *pdds; hbm = (HBITMAP)LoadImage(NULL, szBitmap, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE | LR_CREATEDIBSECTION); if (hbm == NULL) return NULL; GetObject(hbm, sizeof(bm), &bm); pdds = CreateOffScreenSurface(pdd, bm.bmWidth, bm.bmHeight); if (pdds) DDCopyBitmap(pdds, hbm, bm.bmWidth, bm.bmHeight); DeleteObject(hbm); return pdds; }
Bon, je vous explique désormais le fonctionnement de cette fonction. A vrai dire, elle n'est pas des plus simple, et je l'ai moi-même reçu d'un internaute. Au début de la fonction, on créé un handle et un objet sur la bitmap - consultez le site de Microsoft ou, si vous en disposez, l'aide de Visual C++.NET, pour obtenir plus d'informations sur ce type de variables. Ensuite, on définie la surface à créer. Puis, on récupère un handle sur l'image en utilisant une fonction de Windows nommée LoadImage. Ceci fait, on récupère les données de la BITMAP, son contenu et ses dimensions, grâce à la fonction GetObject. Ensuite, il ne reste plus qu'à créer la surface grâce à la fonction appropriée et à la remplier. Voilà, nous avons chargé la bitmap mais il nous faut désormais créer une surface offscreen.
IDirectDrawSurface *CreateOffScreenSurface(IDirectDraw *pdd, int dx, int dy) { DDSURFACEDESC ddsd; IDirectDrawSurface *pdds; ZeroMemory(&ddsd, sizeof(ddsd)); ddsd.dwSize = sizeof(ddsd); ddsd.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH; ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN; ddsd.dwWidth = dx; ddsd.dwHeight = dy; if (pdd->CreateSurface(&ddsd, &pdds, NULL) != DD_OK) return NULL; else return pdds; }
Nous avons ici une fonction qui créé une surface offscreen adaptée à la taille de l'image chargée. Elle prend les valeurs de l'objet Direct Draw et les dimensions de la bitmap. Après les premières définitions, un descripeut de surface et la surface à créer, on remplit la structure du descripteur et on précise les dimensions de la surface et on lui donne la valeur offscreen. On applique ensuite la fonction de création des surfaces pour créer notre surface offscreen.
Désormais il ne nous reste plus qu'à remplir la surface avec la bitmap via son handle. L'opération de remplissage utilisée sera BitBlt, une opération de blitting de Windows.
HRESULT DDCopyBitmap(IDirectDrawSurface *pdds, HBITMAP hbm, int dx, int dy) { HDC hdcImage; HDC hdc; HRESULT hres; HBITMAP hbmOld; hdcImage = CreateCompatibleDC(NULL); hbmOld = (HBITMAP)SelectObject(hdcImage, hbm); if ((hres = pdds->GetDC(&hdc)) == DD_OK) { BitBlt(hdc, 0, 0, dx, dy, hdcImage, 0, 0, SRCCOPY); pdds->ReleaseDC(hdc); } SelectObject(hdcImage, hbmOld); DeleteDC(hdcImage); return hr; }
Le fonctionnement assez complexe de cette fonction peut se résumer en quelques termes : on initialise en sélectionnant le handle de bitmap, le contexte prend alors la forme et la taille de la bitmap. Dans la suite de la fonction on ne travaille pas sur la surface mais sur son DC. Alors, on termine de finaliser l'objet. Ceci fait, nous disposons d'une fonction de chargement de bitmap dans une surface Direct Draw. Il suffit alors de l'appeler après avoir créé la surface primaire :
Désormais, il ne nous reste plus qu'à charger une bitmap dans la surface primaire. Nous utiliserons le code suivant :
Image = DDLoadBitmap(lpDD, "fond.bmp");
Désormais, il ne nous reste qu'à utiliser le code suivant dans la fonction Affichage() afin d'afficher l'image à l'écran :
RECT rcFrom; rcFrom.left = 0; rcFrom.top = 0; rcFrom.right = 640; rcFrom.bottom = 480; if (FAILED (Primaire->BltFast (0, 0, Image, &rcFrom, DDBLTFAST_WAIT))) return FALSE;
RECT rcForm est un rectangle de blitting, un rectangle qui contiendra la surface. On donne la position en x (left) et en y (top), ainsi que sa résolution, sa largeur (right) et sa hauteur (bottom). Ensuite, on affiche l'image sur la surface primaire où lon blitte la surface Image en 0 en x et en 0 en y. Désormais, vous pouvez essayer de compiler le programme. Nous savons afficher une image à l'écran : nous allons ajouter d'autres surfaces et animer la scène.
Animation 2D
Le programme que nous allons faire ici donc est une scène 2D composée de surfaces complexes, c'est à dire d'un ensemble de surfaces appliquées avec transparence sur la surface primaire. Comme vous pouvez le voir dans le programme, Tux se déplace en zig-zag par dessus le fond (un peu de pub il est vrai) avec effet de transparence. Tout d'abord, nous allons devoir modifier le code de création de la surface offscreen afin qu'il gère la transparence au niveau de la couleur vert pur RGB(0, 255, 0).
ZeroMemory(&ddsd, sizeof(ddsd)); ddsd.dwSize = sizeof(ddsd); ddsd.dwFlags = DDSD_CAPS|DDSD_HEIGHT|DDSD_WIDTH|DDSD_CKSRCBLT; ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN; ddsd.dwWidth = dx; ddsd.dwHeight = dy; ddsd.ddckCKSrcBlt.dwColorSpaceLowValue = RGB(0,255,0); ddsd.ddckCKSrcBlt.dwColorSpaceHighValue = RGB(0,255,0);
Ici, on modifie les paramètres afin de définir l'intervalle correspondant à la couleur de transparence, le vert. Ceci fait, une surface au fond vert pur donnera un effet de transparence. Ensuite, il ne vous reste plus qu'à charger l'image "fond.bmp" en fond comme nous l'avons vu plus haut. Nous allons nous permettre d'insérer du texte sur la surface. Cette fonction n'est pas de moi mais elle fonctionne à merveille :
int DDText(LPDIRECTDRAWSURFACE lpdds, char* text, int x, int y, COLORREF color) { // D'abord, on récupère le contexte graphique HDC hdc; if (FAILED(lpdds->GetDC(&hdc))) return 0; // Puis on définit les couleurs du texte et du fond SetTextColor(hdc, color); SetBkMode(hdc, TRANSPARENT); // On affiche notre texte grâce à TextOut TextOut(hdc, x, y, text, strlen(text)); // Enfin, on libère le contexte graphique lpdds->ReleaseDC(hdc); }
Ici, on récupère le contexte graphique via HDC. Ensuite, on définit la couleur du texte avec SetTextColor, ainsi que la couleur du fond du texte, ici transparente. Il ne reste plus qu'à afficher le texte grâce à la fonction TextOut de Windows, sans oublier de libérer le contexte graphique. En utilisant correctement cette fonction nous pourrons afficher du texte sur l'image de fond, ainsi :
Fond = DDLoadBitmap(lpDD,"fond.bmp"); DDText(Fond, "Linux Forever", 320, 240, RGB(80,0,0));
Nous pourrons donc afficher le texte "Linux Forever" en pourpre sur la surface de fond. Il ne nous reste plus qu'à blitter les surfaces ci-dessus sur la surface primaire. Nous utiliserons le code suivant dans la fonction Affichage() :
RECT rc; SetRect(&rc, 0, 0, 640, 480); if (FAILED (lpDDSBack->BltFast (0, 0, Fond, &rc, DDBLTFAST_WAIT | DDBLTFAST_SRCCOLORKEY))) return FALSE; SetRect(&rc,0,0,115,88); if (FAILED (lpDDSBack->BltFast (posx, posy, Linux, &rc, DDBLTFAST_WAIT | DDBLTFAST_SRCCOLORKEY))) return FALSE;
Nous affichons ainsi les surfaces à l'écran. A noter que posx et posy sont les positions en x et en y du pingouin à l'écran, qui sont variables car nous le ferons se déplacer. Malheureusement nous ne pouvons pas encore utiliser le programme tel qu'il est car nous blittons directement les surfaces offscreen sur la surface primaire, ce qui a pour effet de créer un scintillement désagréable à l'écran. Pour palier cet effet nous allons utiliser un Buffer d'Arrière-plan, soit Backbuffer en anglais. L'idée est de blitter toutes les surfaces offscreen sur une surface auxiliaire complète qui sera elle-même blittée sur la surface primaire. Ainsi nous n'aurons pas cet effet de déchirement. On appelle cette technique le flipping, car une fois l'opération effectuée on flippe le backbuffer et la surfaces primaire, et les rôles sont inversés. Pour mettre place ce système, il ne faut plus simplement définir la surface primaire en tant que telle, mais en tant que surface complexe (une surface primaire associée à un backbuffer), faisant partie d'une structure de flipping :
bool InitSurf() { DDSURFACEDESC desc; HRESULT ddrval; DDSCAPS ddscaps; ZeroMemory(&desc, sizeof(desc)); desc.dwSize = sizeof(desc); desc.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT; desc.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | DDSCAPS_FLIP | DDSCAPS_COMPLEX; desc.dwBackBufferCount = 1; ddrval = lpDD->CreateSurface(&desc,&lpDDSPrim,NULL); if (ddrval != DD_OK) exit(0); ddscaps.dwCaps = DDSCAPS_BACKBUFFER; ddrval = lpDDSPrim->GetAttachedSurface(&ddscaps, &lpDDSBack); if (ddrval != DD_OK) { lpDDSPrim->Release(); lpDD->Release(); return(false); } return true ; }
Ici on définit notre surface primaire sans oublier de créer un backbuffer associé à la surface primaire (dwBackBufferCount = 1). Ensuite on créé la surface primaire et on récupère un backbuffer associé. Il ne reste plus alors qu'à créer les surfaces offscreen et le tour est joué ! Mais désormais, quand nous blitterons les surfaces offscreen cela ne sera plus sur la surface primaire mais sur le backbuffer. Voici pour finir ce cours le code source de la fonction Affichage() finale :
bool Affichage() { RECT rc; SetRect(&rc, 0, 0, 640, 480); if (FAILED (Back->BltFast (0, 0, Fond, &rc, DDBLTFAST_WAIT | DDBLTFAST_SRCCOLORKEY ))) return FALSE; SetRect(&rc,0,0,115,88); if (FAILED (Back->BltFast (posx, posy, Linux, &rc, DDBLTFAST_WAIT | DDBLTFAST_SRCCOLORKEY ))) return FALSE; Primaire->Flip(NULL, DDFLIP_WAIT); if (posx<=50) posy-=2; if (posx>50 && posx<=100) posy+=2; if (posx>100 && posx<=150) posy-=2; if (posx>150 && posx<=200) posy+=2; if (posx>200 && posx<=250) posy-=2; if (posx>250 && posx<=300) posy+=2; if (posx>350 && posx<=400) posy-=2; if (posx>400 && posx<=450) posy+=2; posx++; if(posx>=502) posx=0; return true; }
Conclusion
Vous devriez comprendre désormais le début de la fonction, on blitte ici les surfaces offscreen sur le backbuffer qui sera lui-même flippé avec la surface primaire. Ensuite, vu que la position du pingouin est défini en y et en x par des variables on agit dessus : si la position du pingouin en x est inférieur à 50 pixels du bord gauche on le fait monter, si elle est comprise en 50 et 100 elle descend etc. Quand à la position en x elle ne cesse d'augmenter durant tout le programme jusqu'à ce qu'elle ateigne le bord droit auquel cas elle revient au bord gauche. Voilà, il ne vous reste plus qu'à compiler et à admirer le résultat. Dans le prochain cours nous verrons comment, partant de là, vous pouvez réaliser un jeu complet.
Vous pouvez télécharger les fichiers sources de ce cours.