AnteriorPosterior

10.5. Juegos multiplataforma: SDL

  Curso: Fundamentos de programación en C, por Nacho Cabanes

10.5. Juegos multiplataforma: SDL

10.5.1. Dibujando una imagen de fondo y un personaje

Existen distintas bibliotecas que permiten crear gráficos desde el lenguaje C. Unas son específicas para un sistema, y otras están diseñadas para ser portables de un sistema a otro. Por otra parte, unas se centran en las funciones básicas de dibujo (líneas, círculos, rectángulos, etc), y otras se orientan más a la representación de imágenes que ya existan como fichero.

Nosotros veremos las nociones básicas del uso de SDL, que es una de las bibliotecas más adecuadas para crear juegos, porque es multiplataforma (existe para Windows, Linux y otros sistemas) y porque esta orientada a la manipulación de imágenes, que es algo más frecuente en juegos que el dibujar líneas o polígonos.

No veremos detalles de su instalación, porque en los sistemas Linux debería bastar un instalar el paquete SDL-Dev (que normalmente tendrá un nombre como libsdl1.2-dev), y en Windows hay entornos que permiten crear un ?proyecto de SDL? tan sólo con dos clics, como CodeBlocks.

Vamos a ver algunas de las operaciones básicas, y un primer ejemplo:

Para poder utilizar SDL, debemos incluir SDL.h, así

#include <SDL/SDL.h>

 

Ya dentro del cuerpo del programa, el primer paso sería tratar de inicializar la biblioteca SDL, y abandonar el programa en caso de no conseguirlo:

if ( SDL_Init ( SDL_INIT_VIDEO ) < 0 ) {

printf ( "No se pudo inicializar SDL: %s\n" , SDL_GetError ());

exit ( 1 );

}

 

Al terminar nuestro programa, deberíamos llamar a SQL_Quit:

SDL_Quit ();

 

Para escoger modo de pantalla de 640x480 puntos, con 16 bits de color haríamos

if ( screen == NULL ) {

printf ( "Error al entrar a modo grafico: %s\n" , SDL_GetError () );

SDL_Quit ();

return - 1 ;

}

 

Podemos cambiar simplemente el texto (caption) de la ventana:

SDL_WM_SetCaption ( "Prueba 1 de SDL" , "Prueba 1 de SDL" );

 

Para mostrar una imagen en pantalla: deberemos declararla del tipo SDL_Surface, cargarla con SDL_LoadBMP, y volcarla con SDL_BlitSurface usando como dato auxiliar la posición de destino, que será de tipo SDL_Rect:

SDL_Surface * protagonista ;

protagonista = SDL_LoadBMP ( "protag.bmp" );

SDL_Rect destino ;

destino . x = 320 ;

destino . y = 400 ;

SDL_BlitSurface ( protagonista , NULL , screen , & destino );

 

Con SDL_Flip hacemos toda la imagen visible:

SDL_Flip ( screen );

 

Y para esperar 5 segundos y que nos dé tiempo a comprobar que todo ha funcionado, utilizaríamos:

SDL_Delay ( 5000 );

 

Todo esto, junto en un programa, quedaría:

/*---------------------------*/
/*  Ejemplo en C son SDL     */
/*  sdl01.c                  */
/*                           */
/*  Ejemplo de SDL (1)       */
/*                           */
/*  Curso de C,              */
/*    Nacho Cabanes          */
/*---------------------------*/
 
#include <stdio.h>
#include <stdlib.h>
#include <SDL/SDL.h>
 
int main()
{
    SDL_Surface *screen;
    SDL_Surface *fondo;
    SDL_Surface *protagonista;
    SDL_Rect destino;
    int i, j;
 
    /* Tratamos de inicializar la biblioteca SDL */
    if (SDL_Init(SDL_INIT_VIDEO) < 0) {
        printf("No se pudo inicializar SDL: %s\n", SDL_GetError());
        exit(1);
    }
 
    /* Preparamos las imagenes a mostrar */
    fondo = SDL_LoadBMP("fondo.bmp");
    protagonista = SDL_LoadBMP("protag.bmp");
 
    /* Si todo ha ido bien, hacemos algo: 
        entrar a modo grafico y cambiar el título de la ventana */
    screen = SDL_SetVideoMode( 640, 480, 16, SDL_HWSURFACE );
    if( screen == NULL ) {
        printf( "Error al entrar a modo grafico: %s\n", SDL_GetError() );
        SDL_Quit();
        return -1;
    }
 
    /* Titulo de la ventana */
    SDL_WM_SetCaption( "Prueba 1 de SDL", "Prueba 1 de SDL" );
 
    /* Dibujamos la imagen de fondo */
    destino.x=0;
    destino.y=0;
    SDL_BlitSurface(fondo, NULL, screen, &destino);
 
    /* Dibujamos el protagonista */
    destino.x=320;
    destino.y=400;
    SDL_BlitSurface(protagonista, NULL, screen, &destino);
 
    /* Actualizamos la pantalla */
    SDL_Flip(screen);
 
    /* Y esperamos antes de salir */
    SDL_Delay( 5000 );
 
    /* Finalmente, preparamos para salir */
    SDL_Quit();
    return 0;
}
 

En principio, si sólo usamos SDL, las imágenes tendrán que ser en formato BMP, pero hay otras bibliotecas adicionales, como SDL_Image, que permiten mostrar también imágenes en formatos PNG, JPG, etc.

 

El tipo SDL_Rect representa un rectángulo, y se trata de un registro (struct), que tiene como campos:

* x: posición horizontal

* y: posición vertical

* w: anchura (width)

* h: altura (height)

(nosotros no hemos usado la anchura ni la altura, pero podrían ser útiles si no queremos volcar toda la imagen, sino sólo parte de ella).

 

Para compilar este fuente en Linux deberíamos teclear lo siguiente:

cc -o sdl01 sdl01.c `sdl-config --cflags --libs`

Hay cosas que ya conocemos: el compilador (cc), el nombre para el ejecutable (-o sdl01) y el nombre del fuente (sdl01.c). Las opciones adicionales debemos indicarlas tal y como aparecen, entre acentos graves (acentos ?hacia atrás?): `sdl-config --cflags --libs`

En Windows (con entornos como CodeBlocks), bastaría con pulsar el botón ?compilar el proyecto? de nuestro entorno de desarrollo.

Ejercicios propuestos :

  • Crea (o busca en Internet) una imagen de fondo de tamaño 800x600 que represente un cielo negro con estrellas, y tres imágenes de planetas con un tamaño menor (cercano a 100x100, por ejemplo). Entra a modo gráfico 800x600 con 24 bits de color usando SDL, dibuja la imagen de fondo, y sobre ella, las de los tres planetas. El título de la ventana deberá ser ?Planetas?. Las imágenes deberán mostrarse durante 7 segundos.

 

10.5.2. Un personaje móvil

Para poder mover a ese protagonista por encima del fondo de forma ?no planificada? necesitamos poder comprobar qué teclas se están pulsando. Lo podemos conseguir haciendo:

teclas = SDL_GetKeyState ( NULL );

if ( teclas [ SDLK_UP ])

{

posicionY -= 2 ;

}

 

Donde la variable "teclas", será un array de "unsigned int". La forma normal de declararla será:

Uint8 * teclas ;

 

Eso sí, antes de poner acceder al estado de cada tecla, deberemos poner en marcha todo el sistema de comprobación de sucesos ("events", en inglés). Al menos deberemos comprobar si hay alguna petición de abandonar el programa (por ejemplo, pulsando la X de la ventana), a lo que corresponde el suceso de tipo SDL_QUIT. De paso, podríamos comprobar en este mismo paso si se ha pulsado la tecla ESC, que es otra forma razonable de indicar que queremos terminar el programa:

while ( SDL_PollEvent (& suceso )) {

if ( suceso . type == SDL_QUIT ) terminado = 1 ;

if ( suceso . type == SDL_KEYDOWN )

if ( suceso . key . keysym . sym == SDLK_ESCAPE ) terminado = 1 ;

}

donde la variable suceso se declararía con

SDL_Event suceso ;

 

Ejercicios propuestos :

  • Amplía el ejercicio anterior, añadiendo la imagen de una nave espacial, que deberá moverse cada vez que el usuario pulse una de las flechas del cursor. Ya no se saldrá al cabo de varios segundos, sino cuando el usuario pulse la tecla ESC.

Existen distintas bibliotecas que permiten crear gráficos desde el lenguaje C. Unas son específicas para un sistema, y otras están diseñadas para ser portables de un sistema a otro. Por otra parte, unas se centran en las funciones básicas de dibujo (líneas, círculos, rectángulos, etc), y otras se orientan más a la representación de imágenes que ya existan como fichero.

10.5.3. Imágenes transparentes, escribir texto y otras mejoras

Hemos visto cómo dibujar imágenes en pantalla, y cómo comprobar qué teclas se han pulsado, para hacer que una imagen se mueva sobre la otra.

Pero tenía un defecto, que hacía que no quedara vistoso: si la imagen del ?protagonista? tiene un recuadro negro alrededor, al moverse tapará parte del fondo. Esto se debe evitar: una imagen que se mueve en pantalla (lo que se suele llamar un ?sprite?) debería tener zonas transparentes , a través de las que se vea el fondo.

Es algo fácil de conseguir con SDL: podemos hacer que un color se considere transparente, usando

SDL_SetColorKey ( surface , SDL_SRCCOLORKEY ,

SDL_MapRGB ( surface -> format , r , g , b ));

donde "surface" es la superficie que queremos que tenga un color transparente (por ejemplo, nuestro protagonista), y "r, g, b" son las componentes roja, verde y azul del color que queremos que se considere transparente (si es el color negro el que queremos que no sea vea, serían 0,0,0).

Así, con nuestro protagonista haríamos

/* Preparamos las imagenes a mostrar */

fondo = SDL_LoadBMP ( "fondo.bmp" );

protagonista = SDL_LoadBMP ( "protag.bmp" );

/* El protagonista debe tener contorno transparente */

SDL_SetColorKey ( protagonista , SDL_SRCCOLORKEY ,

SDL_MapRGB ( protagonista -> format , 0 , 0 , 0 ));

Si usamos la biblioteca adicional SDL_Image, podremos usar imágenes PNG con transparencia, y entonces no necesitaríamos usar esta orden.

 

Escribir texto con SDL no es algo ?trivial?: A no ser que empleemos la biblioteca adicional SDL_TTF, no tendremos funciones predefinidas que muestren una frase en ciertas coordenadas de la pantalla.

Pero podríamos hacerlo ?a mano?: preparar una imagen que tenga las letras que queremos mostrar (o una imagen para cada letra), y tratarlas como si fueran imágenes. Podemos crearnos nuestras propias funciones para escribir cualquier texto. Podríamos comenzar por una función ?escribirLetra(int x, int y, char letra)?, y apoyándonos en ella, crear otra ?escribirFrase(int x, int y, char *frase)?

 

Si queremos que nuestro juego funcione a pantalla completa , los cambios son mínimos: basta añadir SDL_FULLSCREEN a la lista de parámetros que indicamos al escoger el modo de pantalla. Estos parámetros se deben indicar separados por una barra vertical (|), porque entre ellos se va a realizar una operación OR (suma lógica) a nivel de bit:

 

screen = SDL_SetVideoMode ( 640 , 480 , 16 , SDL_FULLSCREEN | SDL_HWSURFACE );

 

Ejercicios propuestos :

  • Amplía el ejercicio anterior, para que la imagen de la nave espacial tenga el contorno transparente (y quizá también alguna zona interior).
  • Crea una imagen que contenga varias letras (al menos H, O, L, A), y úsala para escribir las frases HOLA y OH en modo gráfico con SDL.

 

10.5.4. El doble buffer

Si intentamos mover varias imágenes a la vez en pantalla, es probable que el resultado parpadee.

El motivo es que mandamos información a la pantalla en distintos instantes, por lo que es fácil que alguno de todos esos bloques de información llegue en un momento que no coincida con el barrido de la pantalla (el movimiento del haz de electrones que redibuja la información que vemos).

Una solución habitual es preparar toda la información, trozo a trozo, en una ?imagen oculta?, y sólo volcar a la pantalla visible cuando la imagen está totalmente preparada. Esta técnica es la que se conoce como ?usar un doble buffer?.

El segundo paso es sincronizar con el barrido, algo que en la mayoría de bibliotecas hace una función llamada ?retrace? o ?sync?, y que en SDL se hace automáticamente cuando volcamos la información con ?SDL_Flip?.

 

Ya en la práctica, en SDL, comenzaremos por añadir el parámetro correspondiente (SDL_DOUBLEBUF) cuando entramos a modo gráfico:

screen = SDL_SetVideoMode ( 640 , 480 , 16 , SDL_HWSURFACE | SDL_DOUBLEBUF );

 

A la hora de dibujar, no lo hacemos directamente sobre ?screen?, sino sobre una superficie (?surface?) auxiliar. Cuando toda esta superficie está lista, es cuando la volcamos a la pantalla, así:

SDL_BlitSurface ( fondo , NULL , pantallaOculta , & destino );

SDL_BlitSurface ( protagonista , NULL , pantallaOculta , & destino );

...

SDL_BlitSurface ( pantallaOculta , NULL , screen , & destino );

SDL_Flip ( screen );

 

Sólo queda un detalle: ¿cómo reservamos espacio para esa pantalla oculta?

 

Si la pantalla oculta es del mismo tamaño que nuestro fondo o que alguna otra imagen, nos puede bastar con cargar la imagen :

fondo = SDL_LoadBMP ( "fondo.bmp" );

 

Si no es el caso (por ejemplo, porque el fondo se forme repitiendo varias imágenes de pequeño tamaño), podemos usar "SDL_CreateRGBSurface", que reserva el espacio para una superficie de un cierto tamaño y con una cierta cantidad de colores, así:

pantallaOculta = SDL_CreateRGBSurface ( SDL_SWSURFACE , 640 , 480 , 16 ,

0 , 0 , 0 , 0 );

(el parámetro SDL_SWSURFACE indica que no se trabaje en memoria física de la tarjeta, sino en memoria del sistema; 640x480 es el tamaño de la superficie; 16 es la cantidad de color -16bpp = 655356 colores-; los otros 0,0,0,0 se refieren a la cantidad de rojo, verde, azul y transparencia -alpha- de la imagen).

 

Ejercicios propuestos :

  • Amplía el ejercicio de la nave, para que emplee un doble buffer que permita evitar parpadeos.

 

10.5.5. El bucle de juego (game loop)

Para abordar un juego completo, es frecuente no tener claro cómo debería ser la estructura del juego: qué repetir y cuando.

Pues bien, en un juego típico encontraríamos:

  • Cosas que no se repiten, como la inicialización para entrar a modo gráfico, o la liberación de recursos al final del programa.
  • Cosas que no deberían repetirse, como la lectura de las imágenes que vamos a usar (si no son muy grandes, bastará con leerlas al principio y memorizarlas).
  • Cosas que sí deberán repetirse, como dibujar el fondo, comprobar si el usuario ha pulsado alguna tecla, mover los elementos de la pantalla si procede, etc.

Esta parte que se repite es lo que se suele llamar el ?bucle de juego? (en inglés, ?game loop?). Su apariencia exacta depende de cada juego, pero en la mayoría podríamos encontrar algo parecido a:


Inicializar
Mientras (partida en marcha)
    Comprobar sucesos (teclas / ratón / joystick)
    Dibujar fondo (en pantalla oculta)
    Actualizar posición de personajes (en pantalla oculta)
    Comprobar colisiones y otras situaciones del juego
    Corregir personajes según la situación
    Mostrar pantalla oculta
    Atender a situaciones especiales (una vida menos, etc)
Fin Mientras
Liberar recursos

 

 

El orden no tiene por qué ser exactamente éste, pero habitualmente será parecido. Vamos a detallarlo un poco más:

  • Al principio del programa, toda la inicialización, que no se repite.
  • Mientras la partida esté en marcha, una de las cosas que haremos es comprobar si hay que atender a alguna orden del usuario, que haya pulsado alguna tecla o utilizado su ratón o su joystick/gamepad para indicar alguna acción del juego.
  • Otra de las cosas que hay que hacer siempre es "componer" la imagen del juego (fondo + personajes), generalmente en pantalla oculta, para evitar parpadeos.
  • Normalmente, antes de dibujar (quizá incluso antes del paso anterior) deberemos comprobar si ha habido alguna colisión o alguna otra situación que atender. Por ejemplo, si un disparo enemigo ha chocado con nuestra nave, no deberíamos dibujar la nave, sino una explosión. O quizá nuestro protagonista recoja un objeto y cambie su apariencia.
  • Cuando ya queda claro cómo debe ser la pantalla, la dibujaremos.
  • También hay situaciones especiales que se pueden dar durante la partida, y que pueden suponer interrupciones del bucle normal de juego, como cuando el usuario pulsa la tecla de pausa, o pide ayuda, o pierde una vida.
  • Finalmente, hay que recordar que cuando acaba la partida, en casi cualquier biblioteca de funciones que usemos deberemos liberar los recursos que hemos usado (en SDL, con SDL_Quit).

 

 

Ejercicios propuestos :

  • Amplía el ejercicio de la nave, para que emplee tenga una función ?buclePrincipal?, que siga la apariencia de un bucle de juego clásico, llamando a funciones con los nombres ?comprobarTeclas?, ?dibujarElementos?, ?moverEstrellas?, ?mostrarPantallaOculta?. Existirán algunas estrellas, cuya posición cambiará cuando se llame a ?moverEstrellas? y que se dibujarán, junto con los demás componentes, al llamar a en ?dibujarElementos?. Al final de cada ?pasada? por el bucle principal habrá una pausa de 20 milisegundos, para que la velocidad del ?juego? no dependa del ordenador en que se ejecute.
  • Ampliar el ejercicio anterior, para que también exista un ?enemigo? que se mueva de lado a lado de la pantalla.
  • Ampliar el ejercicio anterior, para que haya 20 ?enemigos? (un array) que se muevan de lado a lado de la pantalla.
  • Ampliar el ejercicio anterior, para añadir la posibilidad de que nuestro personaje ?dispare?.
  • Ampliar el ejercicio anterior, para que si un ?disparo? toca a un ?enemigo?, éste deje de dibujarse.
  • Ampliar el ejercicio anterior, para que la partida termine si un ?enemigo? toca a nustro personaje.

 

 

Actualizado el: 16-01-2015 00:08

AnteriorPosterior