Introducción a DirectDraw
Las librerías DirectX han sido creadas por Microsoft para
facilitar la creación de juegos para el entorno Windows, ya que
hasta su aparición casi todos los juegos se desarrollaban para DOS.
Existen otras librerías gráficas para Windows, siendo una
de las más conocidas OpenGL
Básicamente DirectX está dividido en las siguientes partes:
-
DirectDraw: utilizado para dibujar gráficos en dos dimensiones.
-
DirectSound: para efectos de sonido y música.
-
DirectInput: para leer el teclado, joystick, ratón, etc.
-
Direct3D: para hacer gráficos en 3D.
-
DirectPlay: para jugar por Internet, por modem, por cable serie,
etc.
Este artículo pretende ser una introducción a DirectDraw,
el sistema de dibujo de gráficos en 2D y animación. Hoy en
día parece que todo lo que no sea un juego en 3D no sirve para nada,
pero todavía se pueden hacer buenos juegos en 2D, muy divertidos
y adictivos, que no tienen nada que envidiar a un mega-juego en 3D que
ocupa tropecientos megas. (el que no me crea, que le eche un vistazo
a Bubble
Puzzle )
En este artículo vamos a crear un programa
que nos va a permitir mover un pequeño comecocos por un fondo de
estrellas. Aprenderemos a inicializar DirectDraw, reservar espacio en memoria
para incluir los sprites y el fondo, y a hacer la sencilla animación
mediante el conocido page-flipping (intercambio de páginas).
Para su creación he utilizado Borland C++ 4.5, aunque
el programa debería funcionar perfectamente con otros compiladores
de C como Visual C++ o Watcom C. También tengo que
decir que he utilizado la versión 3 de DirectX: hace poco
ha aparecido la versión 5 de DirectX, pero todavía
no la he mirado. La verdad es que no sé si este programa se compilará
utilizando el SDK de DirectX-5, pero el ejecutable por lo menos supongo
que funcionará.
1. Inicialización de DirectDraw:
Lo primero que tenemos que hacer es inicializar DirectDraw. He agrupado
todo lo que hay que hacer en la función DirectDrawInit() que
devuelve un valor VERDADERO si no encuentra ningún problema, y FALSO
si hay algún problema, como por ejemplo que DirectX no esté
instalado en el ordenador, o que no haya suficiente memoria. Si hay algún
problema, antes de cerrar el programa se presenta un mensaje al usuario.
Lo primero que tenemos que hacer es crear un objeto DirectDraw,
que representa la pantalla del ordenador, de la siguiente forma:
LPDIRECTDRAW lpDD; // este es el objeto DirectDraw
HRESULT ddrval; // esta variable contendrá el valor devuelto
// por las funciones DirectDraw
ddrval = DirectDrawCreate(NULL, lpDD, NULL );
if( ddrval != DD_OK )
return CleanupAndExit("Error en DirectDrawCreate!");
La función CleanupAndExit() es una función que
hemos creado nosotros, que lo que hace es borrar todos los objetos DirectDraw
creados y luego presentar al usuario un mensaje de error. Utilizaremos
la variable ddrval para almacenar el valor devuelto por las distintas
funciones de DirectDraw. Si el valor es DD_OK no ha habido ningún
problema. En caso contrario, ddrval nos indicará cuál
ha sido el problema (aunque en este caso no lo analizamos, en un programa
serio debería hacerse)
Ahora tenemos que decirle a DirectDraw cómo va a comportarse
nuestro programa. Para un juego lo normal es que queramos un acceso
exclusivo a la pantalla y utilizando la pantalla completa:
ddrval = lpDD->SetCooperativeLevel(hwnd,
DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN );
if( ddrval != DD_OK )
return CleanupAndExit("Error en SetCooperativeLevel!!");
Si por ejemplo quisiéramos que nuestro juego se ejecutase en
una ventana Windows, cambiariámos los flags que hemos utilizado
en SetCooperativeLevel(), utilizando por ejemplo DDSCL_NORMAL.
Ahora tenemos que elegir la resolución gráfica
que queramos, lo cual es bastante simple.
ddrval = lpDD->SetDisplayMode(AnchoPixels, AltoPixels, BitsPorPixel )
if( ddrval != DD_OK )
return CleanupAndExit("Error en SetDisplayMode!");
Los parámetros de llamada a la función son bastante evidentes,
¿no? Valores típicos serían 640x480x8 (8 bits significa
256 colores), 320x200x8 (que es el modo 13h), etc. Como siempre, en ddrval
obtendremos el valor devuelto por la función, que puede ser DDERR_INVALIDMODE,
indicándonos que el modo gráfico que hemos pedido no está
disponible. Para saber todos los modos gráficos que soporta un ordenador,
se puede usar la función EnumDisplayModes().
Ahora tenemos que crear las superficies (surfaces en inglés)
en las que vamos a dibujar. Para ello primero tenemos una variable
(lpDDSPrimary) que apunte a la superficie que vamos a crear, y otra
variable más ( ddsd ) que vamos a utilizar para decir a DirectDraw
cómo queremos que sea la superficie.
LPDIRECTDRAWSURFACE lpDDSPrimary; // Superficie primaria
DDSURFACEDESC ddsd;
// Poner a 0 todos los bytes de la estructura
memset( &ddsd, 0, sizeof(ddsd));
// Hay que decirle el tamaño de la estructura
// (por si cambia en futuras versiones de DirectX)
ddsd.dwSize = sizeof( ddsd );
// Ahora le decimos cuáles de todos los campos de la
// estructura DDSURFACEDESC vamos a definir en esta llamada.
ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
// Los flags para decirle que queremos crear una superficie
// primaria, compleja (?), y con capacidad de Flip.
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE
| DDSCAPS_FLIP | DDSCAPS_COMPLEX;
// Sólo vamos a poner un back-buffer
// (podemos poner más, tantos como quepan en la memoria de la tarjeta)
ddsd.dwBackBufferCount = 1;
// Ahora creamos la superficie primaria
ddrval = lpDD->CreateSurface( &ddsd,&lpDDSPrimary, NULL );
if( ddrval != DD_OK )
return CleanupAndExit("Error en CreateSurface Superficie Primaria!");
Bueno, ahora que ya tenemos la superficie primaria vamos a obtener mediante
la función GetAttachedSurface() un puntero al back buffer
que hemos creado en esa superficie primaria:
LPDIRECTDRAWSURFACE lpDDSBack; //Back Buffer
ddscaps.dwCaps = DDSCAPS_BACKBUFFER;
ddrval = lpDDSPrimary->GetAttachedSurface(&ddscaps, &lpDDSBack );
if( ddrval != DD_OK )
return CleanupAndExit("Error en GetAttachedDurface!");
Bueno, si hemos llegado hasta aquí, ya tenemos DirectDraw incializado,
hemos puesto la resolución de pantalla que queremos, y tenemos una
superficie primaria y un back-buffer en los que podemos dibujar e intercambiar
las páginas para hacer animaciones.
2 - Dibujar en una superficie DirectDraw:
Está claro que las superficies no nos sirven de nada si no podemos
dibujar en ellas. Para esto tenemos varios métodos:
a) Obtener el DC (Device Context) de la superficie, y
utilizar las funciones GDI de Windows. El siguiente ejemplo utiliza
GDI para escribir MiTexto en la superficie primaria:
HDC hdc;
if ( lpDDSPrimary->GetDC(&hdc) == DD_OK)
{
SetBkColor(hdc, RGB(0, 0, 255));
SetTextColor(hdc, RGB(255, 255, 0));
TextOut(hdc, 0, 0, MiTexto , lstrlen(MiTexto) );
lpDDSPrimary->ReleaseDC(hdc);
}
Bastante evidente, ¿no? No hay que olvidarse de liberar el DC mediante
ReleaseDC(). Bueno, he supuesto que sabes utilizar las funciones
GDI, que son funciones de Windows de toda la vida, seguro que en la ayuda
de tu compilador vienen explicadas, no tienen mayor misterio.
b) Obtener un puntero lineal a la superficie. Con este método
obtendremos un puntero lineal a la superficie (igual que en el modo 13h),
y utilizando dicho puntero podremos dibujar lo que queramos:
DDSD ddsd;
unsigned char *pointer;
while (lpDDSBack->Lock(NULL, &ddsd,0, NULL)
== DDERR_WASSTILLDRAWING );
pointer = (unsigned char *) ddsd.lpSurface;
// aquí dibujamos lo que queramos.
// por ejemplo, una línea horizontal desde (0,0)
// hasta (50,0), en el color 100
for(i=0; i<=50; i++); *(pointer+i) = 100;
// ahora una línea vertical desde (50,0)
// a (50,50), en el color 150
for(i=0; i<=50; i++) *(pointer + 50 + i*AnchoEnPixels)= 150;
lpDDSBack->Unlock(ddsd.lpSurface);
Bueno, es bastante sencillo. Lo bueno de este método es que
puedes crear tus propias rutinas de dibujo, y aprovechar las rutinas que
ya tienes hechas para DOS. Incluso se pueden programar en ensamblador.
Es importante no olvidarse del Unlock() del final, para liberar
el puntero. Si queremos volver a dibujar, tendremos que volver a obtener
el puntero con Lock(), no nos vale el anterior.
3 - Intercambio de páginas ( page-flipping )
Supongamos que ya hemos dibujado en el back-buffer todo lo que queremos
ver en pantalla. Ahora lo que tenemos que hacer es intercambiar el back-buffer
y el front-buffer. Para ello sólo tenemos que llamar a lpDDSPrimary->Flip(NULL,
0). Ahora el back-buffer está en la pantalla y lo que antes
era el front-buffer ahora está como back-buffer, para que dibujemos
el siguiente frame. Básicamente es como he dicho, pero la llamada
al Flip() es mejor hacerla de la siguiente forma:
while(1) {
HRESULT ddrval;
ddrval = lpDDSPrimary->Flip(NULL, 0);
if(ddrval == DD_OK) break;
if(ddrval == DDERR_SURFACELOST) {
ddrval = lpDDSPrimary->Restore();
if(ddrval != DD_OK) break;
if(ddrval != DDERR_WASSTILLDRAWING) break;
}
Si lo hacemos de esta forma no aseguramos que no haya problemas con el
flip. Por un lado, si la función Flip() nos devuelve el mensaje
DDERR_WASSTILLDRAWING, eso significa que todavía no se ha
terminado de hacer la última operación en este surface, por
lo que tenemos que volver a llamar a Flip(). Por otro lado, si Flip()
nos devuelve DDERR_SURFACELOST significa que DirectDraw ha perdido
la memoria reservada para ese surface, por lo que debe reservarla otra
vez mediante Restore().
4 - Crear superficies off-screen
En cualquier juego que hagamos, tendremos que reservar un espacio para
guardar los bitmaps y el fondo que vamos a dibujar. Para ello vamos a utilizar
las superficies off-screen (no sé cómo traducirlo al español).
Crear una superficie de este tipo es bastante parecido a crear una superficie
primaria, lo que ya hicimos en el punto 1.
LPDIRECTDRAWSURFACE lpDDSSprite;
// Aqui guardaremos los sprites
ddsd.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH;
ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN;
ddsd.dwWidth = 38;
// esta es la anchura del bitmap del ejemplo.
ddsd.dwHeight = 43;
// y esta la altura
ddrval = lpDD->CreateSurface( &ddsd, &lpDDSSprite, NULL );
if( ddrval != DD_OK )
return CleanupAndExit("Error en CreateSurface lpDDSSprite!");
En dwFlags le hemos dicho que vamos a fijar la anchura y altura
de la superficie, además de sus características. En
dwCaps le decimos que va a ser un buffer off-screen, y en dwWidth
y dwHeight le damos las medidas. En este caso le damos las medidas
del bitmap (PACMAN.BMP) que incluye los bitmaps
del pacman. En el programa de ejemplo se crea otra superficie off-screen
para el fondo, que en ese caso tendrá la anchura y altura de la
pantalla completa.
En las superficies off-screen puedes dibujar de la misma forma que
en la superficie primaria o en el back-buffer (ver punto 2). En el programa
de ejemplo, puedes mirar las rutinas SetupFondo() que dibuja en
una superficie off-screen un fondo negro con unas "estrellas" al azar,
y también la rutina LoadPacmanBitmap() que lee el fichero
PACMAN.BMP y lo dibuja en otra superficie off-screen.
Nuestro siguiente paso es cómo hacer que aparezca en la pantalla
algo que tenemos en una superficie off-screen. Lo que haremos será
primero copiar de la superficie off-screen al back-buffer, y luego haremos
un Flip() como ya hemos aprendido.
Para copiar de una superficie off-screen al back-buffer ( o a cualquier
otra superficie ) utilizamos Blt() o BltFast(). Por ejemplo,
en el programa de ejemplo la función CopyFondo2Back() copia
en el back-buffer el fondo que hemos preparado en la superficie lpDDSFondo,
de la siguiente forma:
RECT rc = {0,0,XSize,YSize};
while( 1 ) {
ddrval =;
lpDDSBack->BltFast(0, 0, lpDDSFondo, &rc, FALSE);
if( ddrval == DD_OK ) break;
if( ddrval != DDERR_WASSTILLDRAWING) return;
}
Bueno, es bastante simple, ¿no? Fijaos en que tenemos que
asegurarnos de que siempre que las funciones de DirectDraw nos devuelvan
el valor DDERR_WASSTILLDRAWING tenemos que volver a llamar a la
función, porque si no lo hacemos no consiguemos hacer lo que queríamos
(en este caso, la copia del fondo).
Los parámetros a la función BltFast() son las coordenadas
de destino (0,0), la superficie de origen (lpDDSFondo), el rectángulo
que queremos copiar (rc) y el tipo de transferencia.
¿Qué es eso de tipo de transferencia? Nos sirve para indicarle
a BltFast() un color-key (color-clave o algo así) para que
DirectDraw al hacer el blit ignore ese color, es decir, lo utilizamos para
dibujar bitmaps transparentes. En el fichero pacman.bmp del ejemplo, las
partes que no son del pacman están en color negro (número
0), y no queremos que DirectDraw dibuje lo que está en negro, porque
el pacma aparecería como un cuadrado cuando en realidad es redondo.
Para ello, primero le decimos a DirectDraw que en la superficie lpDDSSprite
el color-key va a ser el color número 0:
DDCOLORKEY ddck;
ddck.dwColorSpaceLowValue =
ddck.dwColorSpaceHighValue = ddck.dwColorSpaceLowValue;
lpDDSSprite->SetColorKey(DDCKEY_SRCBLT,&ddck);
Ahora, cuando hagamos el blit para copiar de lpDDSSprite al
back-buffer, le diremos a la función BltFast() que utilice
el color-key de la superficie de origen:
ddrval = lpDDSBack->BltFast(XPos, YPos, lpDDSSprite,
&rc, DDBLTFAST_SRCCOLORKEY);
También podemos utilizar el color-key de la superficie de destino
en caso de que nos interese.
El programa de ejemplo muestra un pacman que se mueve por la pantalla
con las teclas del cursor. Con las teclas + / - puedes
variar la velocidad a la que se mueve. Como podrás ver, tenemos
una superficie primaria y un back-buffer, además de dos superficies
off-screen, una que contiene el fondo que nosotros mismos creamos, y otra
que contiene los distintos bitmaps del pacman (mirando hacia arriba, hacia
la derecha, etc., con la boca abierta, cerrada). Para cada "frame" lo que
hacemos es copiar primero el fondo en el back-buffer, luego copiamos el
pacman en el back-buffer de forma "transparente" (ignorando el color negro),
y luego hacemos un flip para que el back-buffer se vea por la pantalla.
Al principio del programa hay dos "defines", XSIZE e YSIZE, que puedes
cambiar para probar con distintas resoluciones de pantalla
Pulsa aquí para llevarte el programa
de ejemplo (incluye código fuente y ejecutable).
Algunos links de recomendada visita:
Microsoft DirectX SDK (Software
Development Kit) : Aquí está todo lo que necesitas para hacer
un programa en DirectX. Acaba de salir la versión 5. Cuidado, porque
son más de 30 megas...
DirectX
en Yahoo
Game Programming
Megasite: tiene muchas cosas de programación de juegos, y también
una sección de DirectX
x2ftp.oulu.fi:
el mejor ftp-site de programación de juegos tiene también
un directorio de DirectX. Todavía no hay muchas cosas, pero supongo
que las habrá.
Antes de terminar, tengo que dar las gracias a pipero (de #programación
y #programacion_d_juegos) que lo sabe absolutamente todo sobre DirectX
y programación de juegos.