23 mayo 2008

Programar videojuegos con la librería SDL (5)

Después de realizar los movimientos básicos del avión mediante teclado y ratón vamos a ver como disparar. El sprite del disparo va a llamarse disparo.bmp y será el siguiente:

Al igual que hicimos con el fondo vamos a crear la variable global Disparo de tipo TSprite en la unidad UArcade.pas:

var
Avion, Fondo, Disparo: TSprite;

Ahora modificamos los procedimientos encargados de cargar y destruir sprites:

procedure CargarSprites;
begin
Avion := TSprite.Create;
Avion.x := 273;
Avion.y := 204;
Avion.CargarSuperficie( ExtractFilePath( ParamStr( 0 ) ) + 'avion.bmp' );

Fondo := TSprite.Create;
Fondo.CargarSuperficie( ExtractFilePath( ParamStr( 0 ) ) + 'fondo.bmp' );

Disparo := TSprite.Create;
Disparo.CargarSuperficie( ExtractFilePath( ParamStr( 0 ) ) + 'disparo.bmp' );
end;

procedure DestruirSprites;
begin
Avion.Free;
Fondo.Free;
Disparo.Free;
end;

Como el avión debe realizar muchos disparos lo que vamos a hacer es utilizar el mismo sprite para todos los disparos. Para almacenar la posición de cada disparo vamos a crear una estructura de datos con un máximo de 10 disparos simultáneos. Esto lo vamos a declarar también en la unidad UArcade.pas:

type
TDisparo = record
x, y: Integer;
bActivo: Boolean;
end;

La variable bActivo controla si un disparo está en movimiento o no. Debajo también vamos a declarar un array de disparos:

var
...
Disparos: array[1..10] of TDisparo;

El siguiente procedimiento que vamos a crear se va a encargar de disparar una ráfaga:

procedure Disparar;
var
i: UInt32;
begin
if SDL_GetTicks - iTmpDisparo < 150 then
Exit;

iTmpDisparo := SDL_GetTicks;

// Buscamos un disparo que no esté activo
i := 1;
while Disparos[i].bActivo and ( i < 10 ) do
Inc( i );

// ¿ha encontrado un disparo no activo?
if not Disparos[i].bActivo then
begin
// Lo activamos
Disparos[i].bActivo := True;
Disparos[i].x := Avion.x + 29;
Disparos[i].y := Avion.y - 20;
end;
end;

Para controlar el tiempo entre disparo y disparo tenemos que crear la variable global iTmpDisparo:

var
...
iTmpDisparo: UInt32;

Como también he utilizado la función SDL_GetTicks tenemos que añadir SDL en la sección uses. El modo de controlar los disparos es el siguiente: considero que tengo una bolsa con 10 disparos sin gastar. Entonces cada vez que el usuario pulsa la barra de espacio miro si en mi bolsa de disparos (array) hay alguno disponible. Si es así, entonces disparo y lo pongo como activo y no volverá a ser inactivo hasta que desaparezca de pantalla (eso lo veremos más adelante).

En el momento que hago activo un disparo lo pongo justo delante de la posición del avión, para que parezca que sale del mismo. El siguiente procedimiento que tenemos que crear es el encargado de mover los disparos que estén activos:

procedure ControlarDisparos;
var
i: Integer;
begin
for i := 1 to 10 do
if Disparos[i].bActivo then
begin
Disparo.x := Disparos[i].x;
Disparo.y := Disparos[i].y;
Disparo.Dibujar;

Dec( Disparos[i].y, 20 );

if Disparos[i].y < -20 then
Disparos[i].bActivo := False;
end;
end;

Lo que hace el procedimiento es recorrer los 10 disparos y si hay alguno activo lo decrementa verticalmente hasta que desaparezca de pantalla. Ahora sólo tenemos que comprobar al final del procedimiento ControlarEventos si el usuario ha pulsado la barra de espacio:

procedure ControlarEventos;
begin
...
if Teclado.bEspacio then
Disparar;
end;

Y en la rutina de dibujar sprites controlamos los disparos y los dibujamos:

procedure DibujarSprites;
begin
Fondo.Dibujar;
Avion.Dibujar;
ControlarDisparos;
end;

Este es el resultado de disparar con la barra de espacio:


CONTROLANDO LOS BOTONES DEL RATÓN

Hasta ahora, lo único que hemos hecho con el ratón es leer sus coordenadas x e y para mover el avión. Ahora vamos a ampliar la clase TRaton para leer el estado de los botones izquierdo y derecho:

TRaton = class
x, y, ux, uy: Integer; // coordenadas del ratón y última posición
bIzquierdo, bDerecho: Boolean; // Estado de los botones del ratón

constructor Create;
procedure Leer;
end;

El constructor se queda igual, pero el método Leer lo ampliamos:

procedure TRaton.Leer;
var
Estado: Uint8;
begin
Estado := SDL_GetMouseState( x, y );
bIzquierdo := ( Estado and SDL_BUTTON( SDL_BUTTON_LEFT ) ) > 0;
bDerecho := ( Estado and SDL_BUTTON( SDL_BUTTON_RIGHT ) ) > 0;
end;

De este modo, con sólo leer las variables bIzquierdo y bDerecho sabemos si el usuario ha pulsado ambos botones. Para que nuestro avión dispare con el botón izquierdo del ratón o con la barra de estado modificamos las últimas líneas del procedimiento ControlarEventos:

procedure ControlarEventos;
begin
...
...

if Teclado.bEspacio or Raton.bIzquierdo then
Disparar;
end;

MOVER EL SPRITE CON EL JOYSTICK

Controlar el estado del joystick no es tal fácil como parece. Eso se debe a que los joystick en PC son analógicos. Cuando movemos la palanca del joystick o en gamepad según la presión devuelve un número u otro. Lo primero que hay que hacer es inicializar la librería SDL activando la función para joystick. Esto lo vamos a hacer en el procedimiento InicializarSDL:

procedure InicializarSDL;
begin
// Inicializamos la librería SDL
if SDL_Init( SDL_INIT_VIDEO or SDL_DOUBLEBUF or SDL_INIT_JOYSTICK ) < 0 then
begin
...
...
end;

Ahora vamos a crear una clase para almacenar el estado de las direcciones el joystick y sus botones:

TJoystick = class
bArriba, bAbajo, bDerecha, bIzquierda: Boolean; // Direcciones del joystick
Boton: array[0..9] of Boolean; // Asumimos que tiene un máximo de 10 botones
iNumJoy, iNumBot, iNumEjes: Integer; // Nº de joystick del sistema, Nº botone y Nº de ejes
iEjeX, iEjeY: Sint16; // Movimiento de los ejes
Joystick: PSDL_Joystick; // Estructura que almacena los datos del joystick
Evento: TSDL_Event; // Lectura de eventos del joystick

constructor Create;
destructor Destroy; override;
procedure Leer;
end;

En el constructor inicializamos el estado del joystick:

constructor TJoystick.Create;
var i: Integer;
begin
bArriba := False;
bAbajo := False;
bDerecha := False;
bIzquierda := False;

// Inicializamos los 10 botones del yoystick
for i := 0 to 9 do
Boton[i] := False;

// Leemos el nº de joystick en el sistema
iNumJoy := SDL_NumJoysticks();

// Sólo nos interesa el primer joystick
if iNumJoy > 0 then
begin
// Abrimos el primer joystick
Joystick := SDL_JoystickOpen( 0 );
SDL_JoystickEventState( SDL_ENABLE );

// Leemos el nº de botones
iNumBot := SDL_JoystickNumButtons( Joystick );

// Leemos el nº de ejes
iNumEjes := SDL_JoystickNumButtons( Joystick );
end;
end;

Lo primero que he hecho en el constructor es leer el número de joystick que hay instalados en Windows. Si hay por lo menos uno entonces me lo quedo y lo habilito. También leo el número de botones que tiene y el número de ejes (para un futuro).

En el destructor tengo que soltar el joystick para que Windows pueda asignarlo a otro programa:

destructor TJoystick.Destroy;
begin
if iNumJoy > 0 then
if SDL_JoystickOpened( 0 ) = 1 then
SDL_JoystickClose( Joystick );

inherited;
end;

Y ahora viene lo más difícil de todo. Leer el estado del joystick:

procedure TJoystick.Leer;
var
i: Integer;
begin
if iNumJoy > 0 then
begin
// Leemos el eje horizontal y vertical
iEjeX := SDL_JoystickGetAxis( Joystick, 0 );
iEjeY := SDL_JoystickGetAxis( Joystick, 1 );

// Si el eje esta centrado horizontalmente entonces no
// se mueve ni a la derecha ni a la izquierda
if ( iEjeX > -2000 ) and ( iEjeX < 2000 ) then
begin
bIzquierda := False;
bDerecha := False;
end;

// Si el eje esta centrado verticalmente entonces no
// se mueve ni arriba ni abajo
if ( iEjeY > -2000 ) and ( iEjeY < 2000 ) then
begin
bArriba := False;
bAbajo := False;
end;

// Se mueve hacia la izquierda
if iEjeX < -3000 then
begin
bIzquierda := True;
bDerecha := False;
end;

// Se mueve hacia la derecha
if iEjeX > 3000 then
begin
bDerecha := True;
bIzquierda := False;
end;

// Se mueve hacia la arriba
if iEjeY < -3000 then
begin
bArriba := True;
bAbajo := False;
end;

// Se mueve hacia la abajo
if iEjeY > 3000 then
begin
bAbajo := True;
bArriba := False;
end;

// Leemos si están pulsados cada uno de los botones
for i := 0 to 9 do
Boton[i] := SDL_JoystickGetButton( Joystick, i ) > 0;
end;
end;

Primero leo el estado de los ejes. Considero que si los ejes son menor que 3000 entonces considero la palanca del joystick centrada. Después determino el movimiento del joystick dependiendo de hacia donde se tuerce la palanca. Por último, también leo el estado de los 10 primeros botones.

Ahora tenemos de crear en la unidad UJuego.pas una variable global para controlar el joystick:

var
...
Joystick: TJoystick;

También tenemos que modificar el bucle principal del programa para inicilizar el joystick, leerlo y después eliminarlo:

begin
InicializarSDL( 'Mi primer juego en SDL' );
ModoVideo( 640, 480, 16, True );
Teclado := TTeclado.Create;
Joystick := TJoystick.Create;
Raton := TRaton.Create;
Temporizador := TTemporizador.Create;
CargarSprites;

while not bSalir do
begin
Temporizador.Actualizar;

if Temporizador.Activado then
begin
Teclado.Leer;
Joystick.Leer;
Raton.Leer;
ControlarEventos;
DibujarSprites;
ActualizarPantalla;
Temporizador.Incrementar;
end
else
Temporizador.Esperar;
end;

DestruirSprites;
Temporizador.Free;
Teclado.Free;
Joystick.Free;
Raton.Free;
FinalizarSDL;
end.

Ahora sólo tengo que modificar el procedimiento ControlarEventos de la unidad UArcade.pas para que el avión se mueva tanto por el teclado como con el joystick:

procedure ControlarEventos;
begin
if ( Teclado.bDerecha or Joystick.bDerecha ) and ( Avion.x < 545 ) then
Inc( Avion.x, 5 );

if ( Teclado.bIzquierda or Joystick.bIzquierda ) and ( Avion.x > 0 ) then
Dec( Avion.x, 5 );

if ( Teclado.bArriba or Joystick.bArriba ) and ( Avion.y > 0 ) then
Dec( Avion.y, 5 );

if ( Teclado.bAbajo or Joystick.bAbajo ) and ( Avion.y < 407 ) then
Inc( Avion.y, 5 );

....

Y para comprobar si dispara con la barra de espacio, el ratón o el joystick lo hacemos de este modo:
 
if Teclado.bEspacio or Raton.bIzquierdo or Joystick.Boton[1] then
Disparar;

Como podéis ver, un videojuego no es tan fácil como un programa de bases de datos. Debemos dar al usuario la posibilidad de manejar cualquier dispositivo sin que por ello el juego pierda adicción. En el siguiente artículo veremos como crear enemigos y que los disparos colisionen con los mismos.

Pruebas realizadas en Delphi 7.

Publicidad