02 mayo 2008

Programar videojuegos con la librería SDL (2)

Antes de continuar desarrollando nuestro juego necesitamos bajarnos de Internet la librería SDL.DLL donde realmente están las rutinas SDL. Hay que tener en cuenta que la librería SDL está desarrollada en C y que el projecto JEDI-SDL contiene sólo las cabeceras de la librería en pascal.

Aquí tenéis el enlace directo para descargarse los archivos DLL que necesitamos:

http://www.libsdl.org/release/SDL-1.2.13-win32.zip

Este archivo hay que descomprimirlo al lado del ejecutable de nuestro juego (juego.exe). Se puede poner también dentro del directorio C:\Windows\System32\ pero es mejor dejarlo en nuestro directorio del proyecto, así hacemos el juego más portable pudiendo incluso llevarlo en un llavero USB.

Ahora vamos a seguir añadiendo rutinas genéricas a nuestro juego.

LEER EL TECLADO CON LA LIBRERÍA SDL

Otra cosa básica que necesita un juego mientras se está desarrollando es poder salir en cualquier momento del mismo con la tecla ESCAPE. Para controlar los eventos de teclado vamos a crear dentro de la unidad UJuego.pas una clase llamada TTeclado para encapsular todo lo referente respecto a este dispositivo.

Primero declaramos en la interfaz de la unidad la clase TTeclado:

type
TTeclado = class
bArriba, bAbajo, bDerecha, bIzquierda: Boolean; // Direcciones del cursor
bEspacio, bIntro, bCtrl_Izq: Boolean; // Teclas principales
Evento: TSDL_Event; // Clase evento de la librería SDL
iUltTec: Integer; // Última tecla pulsada

procedure Leer;
end;

He creado una variable booleana por cada tecla pulsada. Para un juego las teclas principales son los cursores del teclado, la tecla espacio, la tecla intro y la tecla Control (en la librería SDL se pueden leer indistintamente las teclas CTRL derecho e izquierdo). También he creado una variable llamada Evento de la clase TSDL_Event encargada de recoger las teclas pulsadas. Y por último, la variable iUltTec es el código de la última tecla pulsada por el usuario que nos será de utilidad más adelante.

Para capturar los eventos del teclado he creado el método Leer :

procedure TTeclado.Leer;
begin
// ¿Se ha producido un evento?
if SDL_PollEvent( @Evento ) > 0 then
begin
// ¿Ha pulsado una tecla?
if Evento.type_ = SDL_KEYDOWN then
begin
iUltTec := Evento.key.keysym.sym;

case Evento.key.keysym.sym of
SDLK_ESCAPE: bSalir := True;
SDLK_RIGHT: bDerecha := True;
SDLK_LEFT: bIzquierda := True;
SDLK_UP: bArriba := True;
SDLK_DOWN: bAbajo := True;
SDLK_SPACE: bEspacio := True;
SDLK_RETURN: bIntro := True;
SDLK_LCTRL: bCtrl_Izq := True;
end;
end;

// ¿Ha levantado una tecla?
if Evento.type_ = SDL_KEYUP then
begin
iUltTec := 0;

case Evento.key.keysym.sym of
SDLK_RIGHT: bDerecha := False;
SDLK_LEFT: bIzquierda := False;
SDLK_UP: bArriba := False;
SDLK_DOWN: bAbajo := False;
SDLK_SPACE: bEspacio := False;
SDLK_RETURN: bIntro := False;
SDLK_LCTRL: bCtrl_Izq := False;
end;
end;
end;
end;

Para leer el teclado realizamos una llamada a la función SDL_PollEvent que viene definida de esta manera:

function SDL_PollEvent( event: PSDL_Event ): Integer;

Lo que hace es leer un evento cualquiera y lo deposita en la variable event que le pasamos como parámetro. Al igual que ocurre con los eventos de teclado en la librería VCL de Delphi, en la librería SDL se distingue cuando se pulsa una tecla y cuando se suelta.

En un videojuego esto es muy importante ya que cada PC trabaja a una velocidad diferente dependiendo del procesador, la memoria RAM, la tarjeta gráfica, etc. Los videojuegos son muy sensibles a estos cambios ya que nuestros algoritmos se ejecutan entre 25 y 30 veces por segundo.

Si en un juego tipo matamarcianos el usuario deja pulsada la tecla de disparo, las ráfagas que salen de nave deben ir a la misma velocidad indistintamente del hardware que tenga nuestro PC . Y os puedo asegurar con rotundidad que la lectura de teclado de un PC a otro cambia una barbaridad (según teclado: Genius, Logitetch, etc.). Por tanto utilizo variables lógicas para saber en todo momento cuando esta pulsada una tecla y cuando no.

Para ello he utilizado la variable type_ que lleva el objeto Evento:

// ¿Ha pulsado una tecla?
if Evento.type_ = SDL_KEYDOWN then
...

// ¿Ha levantado una tecla?
if Evento.type_ = SDL_KEYUP then
...

Después leo la tecla pulsada según su código:

case Evento.key.keysym.sym of
SDLK_RIGHT: bDerecha := False;
SDLK_LEFT: bIzquierda := False;
.....

Las definiciones de todas las teclas de la librería están definidas dentro de la unidad SDL.pas siendo estas las más importantes para un videojuego las siguientes:

SDLK_RIGHT -> Cursor derecho
SDLK_LEFT -> Cursor izquierdo
SDLK_UP -> Cursor arriba
SDLK_DOWN -> Cursor abajo
SDLK_SPACE -> Espacio
SDLK_RETURN -> Intro
SDLK_LCTRL -> CTRL izquierdo
SDLK_RCTRL -> CTRL derecho
SDLK_LSHIFT -> Mayúsculas izquierdo
SDLK_RSHIFT -> Mayúsculas derecho
SDLK_LALT -> ALT izquierdo
SDLK_RALT -> ALT derecho
SDLK_F1 -> Tecla de función F1

SDLK_F12 -> Tecla de función F12
SDLK_INSERT -> Insertar
SDLK_HOME -> Inicio
SDLK_END -> Fin
SDLK_PAGEUP -> Avanzar página
SDLK_PAGEDOWN -> Retroceder página
SDLK_a -> Tecla A

SDLK_z -> Tecla Z


COMO CONTROLAR EL TIEMPO EN DISTINTOS ORDENADORES

Otro de los problemas que suele darse en un videojuego es que las figuras gráficas (sprites) suelen moverse a distinta velocidad según la tarjeta de video, por lo tanto debemos provocar ralentizaciones a propósito para que el juego funcione a 25 frames por segundo. Para conseguir esto vamos a crear una clase llamada TTemporizador:

TTemporizador = class
iTmpActual, iTmpUltimo: Integer;

constructor Create;
procedure Actualizar;
procedure Incrementar;
function Activado: Boolean;
procedure Esperar;
end;

Las variables iTmpActual y iTmpUltimo las voy a utilizar para que el bucle principal del juego se ejecute exactamente cada 25 milisegundos. En el constructor de la clase inicializo los temporizadores:

constructor TTemporizador.Create;
begin
iTmpUltimo := SDL_GetTicks;
end;

El método Actualizar va a encargarse de refrescar el temporizador:

procedure TTemporizador.Actualizar;
begin
iTmpActual := SDL_GetTicks;
end;

Con el método Incrementar controlo el tiempo que han consumido nuestras rutinas:

procedure TTemporizador.Incrementar;
begin
iTmpUltimo := iTmpUltimo + iTmpActual - iTmpUltimo;
end;

El método Activado nos dice cuado podemos ejecutar nuestras rutinas:

function TTemporizador.Activado: Boolean;
begin
Result := iTmpActual - iTmpUltimo >= 25;
end;

Y método Esperar le deja el control del procesador a Windows cuando no estemos en nuestros 25 milisegundos de proceso:

procedure TTemporizador.Esperar;
begin
SDL_Delay( 25 - ( iTmpActual - iTmpUltimo ) );
end;

La función SDL_Delay hace que nuestro juego se espere el número de milisegundos que le pasamos como parámetro. Lo que hace es devolverle el control a Windows para que baje el consumo de procesador. Equivale a provocar el evento OnIdle en Delphi.

También vamos a añadir un procedimiento global para actualizar la pantalla de video muestras se está ejecutando el juego:

procedure ActualizarPantalla;
begin
SDL_Flip( Pantalla );
end;

Este procedimiento refresca la pantalla de video mediante la técnica de doble buffer que veremos más adelante.

CREANDO EL BUCLE PRINCIPAL DEL JUEGO

Dentro de la unidad Juego.dpr vamos a inicializar el juego y mantener su control hasta el usuario pulse la tecla ESCAPE:

program juego;

uses
Windows, Dialogs, SysUtils, UJuego;

{$R *.res}

begin
InicializarSDL;
ModoVideo( 640, 480, 16, True );
Teclado := TTeclado.Create;
Temporizador := TTemporizador.Create;

while not bSalir do
begin
Temporizador.Actualizar;

if Temporizador.Activado then
begin
Teclado.Leer;
ActualizarPantalla;
Temporizador.Incrementar;
end
else
Temporizador.Esperar;
end;

Temporizador.Free;
Teclado.Free;
FinalizarSDL;
end.

Como puede apreciarse en el código fuente ni siquiera hemos utilizado el objeto Application para inicializar el juego. El juego corre de manera pura en Win32 sin utilizar las librerías ni los objetos estándar de Delphi. De este modo, el juego funciona en ensamblador puro a toda su potencia tal como si estuviera programado en Visual C/C++ de Microsoft, tal como están hechos la mayoría de los videojuegos profesionales.

Vamos a ver parte por parte que de está compuesto el módulo principal del juego.

Vinculamos las unidades estándar de Windows y por supuesto nuestra unidad especial UJuego.pas.

uses
Windows, Dialogs, SysUtils, UJuego;

Inicializamos la librería SDL y creamos el juego a una resolución de 640 por 480 pixels con 16 bits de color por píxel (65535 colores) y en modo ventana:

InicializarSDL;
ModoVideo( 640, 480, 16, True );

Creamos los objetos que van a encargarse de controlar el teclado y el tiempo:

Teclado := TTeclado.Create;
Temporizador := TTemporizador.Create;

Este es el bucle principal de la aplicación que va a ejecutarse 25 veces por segundo:

while not bSalir do
begin
Temporizador.Actualizar;

if Temporizador.Activado then
begin
Teclado.Leer;
ActualizarPantalla;
Temporizador.Incrementar;
end
else
Temporizador.Esperar;
end;

Mientras que el usuario no pulse la tecla ESCAPE la variable bSalir estará a False. Después actualiza el temporizador y cada 25 milisegundos lee el teclado, actualiza la pantalla y incrementa el contador de milisegundos. Cuando haya pasado nuestro intervalo de 25 milisegundos de ejecución devolvemos a Windows el control mediante el método Esperar del objeto Temporizador.

Cuando el usuario pulse ESCAPE liberamos los objetos creados y finalizamos la librería SDL:

Temporizador.Free;
Teclado.Free;
FinalizarSDL;

Al ejecutar el juego debe salir una pantalla de 640 por 480 con el fondo negro esperando a que el usuario pulse ESCAPE:

Mientras el juego está en ejecución el consumo de procesador por parte del mismo debe ser mínimo:



Una vez tenemos desarrollada la parte más importante del juego (el núcleo central) ya podemos pasar a crear en juego en cuestión.

Eso tocará en el siguiente artículo.

Pruebas realizadas en Delphi 7.

Publicidad