Aula Macedonia


Artículos Varios


Artículo realizado por
Iñaki Ecenarro.





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:
 

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:

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.




AULA MACEDONIA
a
MACEDONIA Magazine