20 junio 2008

Programar videojuegos con la librería SDL (9)

CREANDO EL JUEGO DE PLATAFORMAS

El juego de plataformas que vamos a crear va a estar en una ventana de 640 x 480 con el siguiente fondo:


Para dibujar las plataformas voy a dividir toda la pantalla en piezas (tiles) de 40 x 40, lo que nos da 16 piezas horizontales y 12 verticales:


El juego va a constar de los siguientes piezas:


Vamos a tener dos clases de suelo (roca y madera), unas escaleras y las joyas que tiene que recoger el heroe. La primera pieza (la que está en negro) es el fondo transparente que por ahora no voy a utilizar.

CREANDO UN NUEVO PROYECTO

Vamos a crear un nuevo proyecto y aprovecharemos las unidades del proyecto anterior siguiendo estos pasos:

1. Creamos una nueva carpeta y un nuevo proyecto en Delphi.

2. Eliminamos el formulario principal de la aplicación (Unit1.pas).

3. Guardamos el proyecto con el nombre Juego.dpr.

4. Cerramos el proyecto.

5. Copiamos a este directorio las siguientes unidades del juego anterior: Juego.pdr, UJuego.pas.

6. Volvemos a abrir el proyecto y añadimos la unidad UJuego.pas.

7. Añadimos a la ruta de búsqueda del proyecto los siguientes directorios:

D:\Desarrollo\Librerias\sdl\SDL\Pas
D:\Desarrollo\Librerias\sdl\SDL_ttf\Pas
D:\Desarrollo\Librerias\sdl\SDL_Mixer\Pas
D:\Desarrollo\Librerias\sdl\smpeg\Pas
D:\Desarrollo\Librerias\sdl\SDL_Image\Pas

Como vimos en artículos anteriores, estos directorios forman parte de la librería SDL. Por último, necesitamos copiar las librerías dinámicas DLL del directorio anterior:


Con esto ya tenemos nuestra librería estándar para crear un nuevo juego.

CREANDO UNA NUEVA UNIDAD PARA EL JUEGO

Vamos a crear una nueva unidad llamada UPlataformas.pas. A esta unidad le vamos a vincular la ruta de las librerías SDL y de nuestro juego:

uses
SysUtils, Classes, UJuego, SDL, sdl_ttf;

En la unidad del proyecto (Juego.pdr) eliminamos la unidad UArcade.pas del juego anterior:


Ahora vamos a definir en la unidad UPlataformas.pas unas unidades globales para guardar los sprites del fondo y las piezas:

var
Fondo, Temporal, Piezas: TSprite;

He creado también una pantalla temporal donde vamos a mezclar el fondo con las piezas. Ahora implementamos el procedimiento encargado de cargar los sprites:

procedure CargarSprites;
begin
Fondo := TSprite.Create;
Fondo.CargarSuperficie( ExtractFilePath( ParamStr( 0 ) ) + 'fondo.jpg' );

Temporal := TSprite.Create;
Temporal.CargarSuperficie( ExtractFilePath( ParamStr( 0 ) ) + 'fondo.jpg' );

Piezas := TSprite.Create;
Piezas.CargarSuperficie( ExtractFilePath( ParamStr( 0 ) ) + 'piezas.png' );
Piezas.bSubsprites := True;
Piezas.iAnchoSub := 40;
Piezas.iAltoSub := 40;
end;

El fondo y la pantalla temporal son sprites normales, pero las piezas las hemos definido con subsprites de 40 x 40 cada uno.

También necesitamos el procedimiento encargado de destruir los sprites cuando termina el juego:

procedure DestruirSprites;
begin
Piezas.Free;
Temporal.Free;
Fondo.Free;
end;

Otro procedimiento que vamos a crear es el encargado de controlar los eventos en el juego. Como por ahora no vamos a hacer nada, lo dejamos vacío.

procedure ControlarEventos;
begin
end;

Y por último implementamos el procedimiento DibujarSprites que lo único que va a hacer por ahora es dibujar el fondo de pantalla:

procedure DibujarSprites;
begin
Temporal.Dibujar;
end;

Con esto ya podemos ejecutar el juego:


COMO CREAR EL MAPA CON LAS PIEZAS

Como dije al principio, el mapa va a constar de 16 piezas horizontales y 12 piezas verticales. Para almacenar la piezas vamos a crear en la unidad UPlataformas.pas un array bidimensional para almacenar donde va cada pieza:

var
Fondo, Temporal, Piezas: TSprite;
Mapa: array[1..16,1..12] of char;

La pantalla del juego la vamos a almacenar en un archivo de texto llamado pantalla1.txt y va a tener los siguientes datos:

1111111111111111
1000000000000001
1000400000000001
1222222222322221
1000000000300001
1000000000300041
1232222222222221
1030000000000001
1030004000000001
1222222222232221
1000000000030041
1111111111111111

El número 1 representa a los bloques de piedra, el número 2 el suelo de madera, el número 3 las escaleras y el número 4 son las joyas que tenemos que recoger.

El procedimiento CargarPantalla va a recoger los datos del archivo de texto y los va a meter en nuestro array bidimensional:

procedure CargarPantalla( sPantalla: String );
var
S: TStringList;
i, j: Integer;
begin
S := TStringList.Create;
S.LoadFromFile( ExtractFilePath( ParamStr( 0 ) ) + sPantalla );

for j := 1 to 12 do
for i := 1 to 16 do
Mapa[i,j] := S[j-1][i];

S.Free;
end;

Hasta ahora hemos utilizado el método Dibujar de la clase TSprite para dibujar los sprites en pantalla. Necesitamos a hora un método similar que dibuje en la superficie que yo le diga. Para ello vamos a ampliar la clase TSprite que está en la unidad UJuego.pas con el siguiente método:

procedure TSprite.DibujarEn( SuperficieDestino: PSDL_Surface );
var
Origen, Destino: TSDL_Rect;
begin
// ¿Está visible el sprite?
if bVisible then
// ¿Tiene subsprites?
if bSubSprites then
begin
Origen.x := iSubX * iAnchoSub;
Origen.y := iSubY * iAltoSub;
Origen.w := iAnchoSub;
Origen.h := iAltoSub;
Destino.x := x;
Destino.y := y;
Destino.w := iAnchoSub;
Destino.h := iAltoSub;

// Dibujamos el subsprite seleccionado
if SDL_BlitSurface( Superficie, @Origen, SuperficieDestino, @Destino ) < 0 then
begin
ShowMessage( 'Error al dibujar el sprite "' + sNombre + '" en pantalla.' );
Exit;
end;
end
else
begin
Origen.x := 0;
Origen.y := 0;
Origen.w := iAncho;
Origen.h := iAlto;
Destino.x := x;
Destino.y := y;
Destino.w := iAncho;
Destino.h := iAlto;

// Lo dibujamos entero
if SDL_BlitSurface( Superficie, @Origen, SuperficieDestino, @Destino ) < 0 then
begin
ShowMessage( 'Error al dibujar el sprite "' + sNombre + '" en pantalla.' );
Exit;
end;
end;
end;

Este método es similar al anterior pero permite dibujar el sprite en la superficie que le pasemos como parámetro. Esto lo necesitamos para dibujar las piezas del mapa encima de la pantalla temporal, antes de que todo el fondo se dibuje en pantalla. Esto lo vamos a hacer con el procedimiento DibujarPiezasEnTemporal que vamos a definir en la unidad UPlataformas.pas:

procedure DibujarPiezasEnTemporal;
var
i, j: Integer;
begin
for j := 1 to 12 do
for i := 1 to 16 do
begin
Piezas.x := ( i - 1 ) * 40;
Piezas.y := ( j - 1 ) * 40;
Piezas.iSubY := StrToInt( Mapa[i,j] );
Piezas.DibujarEn( Temporal.Superficie );
end;
end;

Después ampliamos el procedimiento CargarSprites para que cargue la pantalla del archivo y la dibuje en la pantalla temporal:

procedure CargarSprites;
begin
Fondo := TSprite.Create;
Fondo.CargarSuperficie( ExtractFilePath( ParamStr( 0 ) ) + 'fondo.jpg' );

Temporal := TSprite.Create;
Temporal.CargarSuperficie( ExtractFilePath( ParamStr( 0 ) ) + 'fondo.jpg' );

Piezas := TSprite.Create;
Piezas.CargarSuperficie( ExtractFilePath( ParamStr( 0 ) ) + 'piezas.png' );
Piezas.bSubsprites := True;
Piezas.iAnchoSub := 40;
Piezas.iAltoSub := 40;

CargarPantalla( 'pantalla1.txt' );
DibujarPiezasEnTemporal;
end;


Y este es el resultado al ejecutar el juego:


Aquí tenéis todo el proyecto (con el ejecutable incluido ) comprimido con zip en tres servidores distintos:

https://mega.nz/file/tRhS1CpQ#3KlM5s6Rda7S2ld765UqgyTSAGw7JoBKzX9SPAjszlI

En el próximo artículo veremos como mover el héroe por pantalla para que recoja las joyas incluyendo rutinas de salto teniendo en cuenta la gravedad.

Pruebas realizadas en Delphi 7.

13 junio 2008

Programar videojuegos con la librería SDL (8)

REPRODUCIR SONIDOS CON LA LIBRERÍA SDL

La otra parte que hace especial a un videojuego es el sonido. Aunque la librería estándar de SDL lleva para reproducir archivos WAV hay que reconocer que esto se queda corto hoy en día, necesitamos algo más potente.

Para ello vamos a utilizar la librería auxiliar llamada SDL_Mixer que permite reproducir sonidos WAV, MIDI, MP3 y OGG (este formato es similar al MP3 permitiendo sonidos y músicas de gran calidad en un formato muy reducido). La librería SDL_Mixer utiliza a su vez la librería smpeg:


Así que lo vinculamos al directorio de búsqueda del proyecto:


Y nos bajamos de Internet las librerías dinámicas para descomprimirlas al lado del ejecutable:

http://www.libsdl.org/projects/SDL_mixer/release/SDL_mixer-1.2.8-win32.zip

El archivo zip de compone de estas librerías:


Comencemos vinculando la librería SDL_mixer a nuestra unidad genérica UJuego.pas:

uses
Windows, SysUtils, Dialogs, SDL, sdl_ttf, SDL_Mixer;


Después vamos a definir dos constantes para controlar el número máximo de músicas y sonidos que podemos tener cargados en memoria:

const
MAX_SONIDOS = 10;
MAX_MUSICAS = 10;

Para controlar tanto las músicas del juego como los sonidos vamos a crear una clase global en la unidad UJuego.pas:

TSonido = class
Sonidos: array[1..MAX_SONIDOS] of PMix_Chunk;
iTmpSonido: array[1..MAX_SONIDOS] of UInt32; // Controla el tiempo de cada sonido
iDuracionSonido: array[1..MAX_SONIDOS] of UInt32; // Controla la duración del sonido

Musicas: array[1..MAX_MUSICAS] of PMix_Music;
iTmpMusica: array[1..MAX_MUSICAS] of UInt32; // Controla el tiempo de cada musica
iDuracionMusica: array[1..MAX_MUSICAS] of UInt32; // Controla la duración de cada musica

constructor Create;
destructor Destroy; override;
procedure CargarSonido( iNumSon: Integer; sArchivo: String; iDuracion: Integer );
procedure ReproducirSonido( iNumSon: Integer );
procedure CargarMusica( iNumMus: Integer; sArchivo: String; iDuracion: Integer );
procedure ReproducirMusica( iNumMus: Integer );
end;

El constructor de esta clase va a inicializar los sonidos:

constructor TSonido.Create;
var
i: Integer;
begin
for i := 1 to MAX_SONIDOS do
begin
Sonidos[i] := nil;
iTmpSonido[i] := 0;
end;

// Inicializamos SDL Mixer
if Mix_OpenAudio( 22050, AUDIO_S16, 2, 4096 ) > 0 then
begin
ShowMessage( 'Error al inicializar la libreria SDL Mixer\n' );
Mix_CloseAudio;
end;
end;

El destructor eliminará de memoria los sonidos y músicas cargados:

destructor TSonido.Destroy;
var
i: Integer;
begin
// Liberamos de memoria todos los sonidos cargados
for i := 1 to MAX_SONIDOS do
if Sonidos[i] <> nil then
Mix_FreeChunk( Sonidos[i] );

// Liberamos de memoria todos las musicas cargadas
for i := 1 to MAX_MUSICAS do
if Musicas[i] <> nil then
Mix_FreeMusic( Musicas[i] );

inherited;
end;

Después implementamos el método que carga un archivo WAV en memoria:

procedure TSonido.CargarSonido( iNumSon: Integer; sArchivo: String;
iDuracion: Integer );
begin
if ( iNumSon >= 0 ) and ( iNumSon <= MAX_SONIDOS ) then
begin
iTmpSonido[iNumSon] := 0;
iDuracionSonido[iNumSon] := iDuracion;
Sonidos[iNumSon] := Mix_LoadWAV( PChar( sArchivo ) );

if Sonidos[iNumSon] = nil then
ShowMessage( 'Error al cargar el sonido: ' + sArchivo );
end;
end;

Y el método que se encarga de reproducirlo:

procedure TSonido.ReproducirSonido( iNumSon: Integer );
begin
if ( iNumSon >= 1 ) and ( iNumSon <= MAX_SONIDOS ) then
if Sonidos[iNumSon] <> nil then
if SDL_GetTicks - iTmpSonido[iNumSon] > iDuracionSonido[iNumSon] then
begin
Mix_PlayChannel( -1, Sonidos[iNumSon], 0 );
iTmpSonido[iNumSon] := SDL_GetTicks;
end;
end;

Igualmente vamos a hacer dos métodos para cargar música MP3 y para reproducirla:

procedure TSonido.CargarMusica( iNumMus: Integer; sArchivo: String;
iDuracion: Integer );
begin
if ( iNumMus >= 0 ) and ( iNumMus <= MAX_MUSICAS ) then
begin
iTmpMusica[iNumMus] := 0;
iDuracionMusica[iNumMus] := iDuracion;
Musicas[iNumMus] := Mix_LoadMUS( PChar( sArchivo ) );

if Musicas[iNumMus] = nil then
ShowMessage( 'Error al cargar la musica: ' + sArchivo );
end;
end;

procedure TSonido.ReproducirMusica(iNumMus: Integer);
begin
if ( iNumMus >= 1 ) and ( iNumMus <= MAX_MUSICAS ) then
if Musicas[iNumMus] <> nil then
if SDL_GetTicks - iTmpMusica[iNumMus] > iDuracionMusica[iNumMus] then
begin
Mix_PlayMusic( Musicas[iNumMus], 1 );
iTmpMusica[iNumMus] := SDL_GetTicks;
end;
end;

Con esto ya tenemos una librería de sonido estándar para cualquier juego.

EL SONIDO DEL AVIÓN DISPARANDO Y CON EXPLOSIONES

Para poder utilizar el sonido dentro de nuestra unidad UArcade.pas primero tenemos que cargarlo. Esto lo vamos a hacer al final del procedimiento CargarSprites:

procedure CargarSprites;
begin
...
Sonido.CargarSonido( 1, ExtractFilePath( ParamStr( 0 ) ) + 'disparo.wav', 10 );
Sonido.CargarSonido( 2, ExtractFilePath( ParamStr( 0 ) ) + 'explosion.wav', 100 );
end;

He cargado el sonido del disparo y le he asignado el número 1. Luego he cargado el sonido de la explosión y le he asignado el número 2. Después modificamos el procedimiento Disparar para que reproduzca el sonido:

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

Sonido.ReproducirSonido( 1 );

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;

También vamos a reproducir una explosión cuando un disparo alcance un avión enemigo. Para ello modificamos el procedimiento ControlarDisparos:

procedure ControlarDisparos;
var
i, j: 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;

for j := 1 to 10 do
if Enemigos[j].bActivo then
if ( Disparos[i].x + 16 >= Enemigos[j].x ) and
( Disparos[i].x + 16 <= Enemigos[j].x + 95 ) and
( Disparos[i].y + 9 >= Enemigos[j].y ) and
( Disparos[i].y + 9 <= Enemigos[j].y + 73 ) then
begin
Enemigos[j].bActivo := False;
Enemigos[j].bExplotando := True;
Enemigos[j].iTmpExplosion := SDL_GetTicks;
Disparos[i].bActivo := False;
rPuntuacion := rPuntuacion + 1;
Sonido.ReproducirSonido( 2 );
end;
end;
end;

Del mismo modo podemos cargar música MP3 utilizando los métodos CargarMusica y ReproducirMusica.

CARGANDO SPRITES EN FORMATO PNG Y JPG

La librería SDL viene por defecto para cargar imágenes de archivos BMP. El problema que tiene este tipo de archivos es que ocupa mucho en disco. Lo ideal es tener un formato que comprima al máximo la imagen pero sin perder calidad.

Para los sprites vamos a utilizar el formato PNG (que ocupa tan poco como el GIF y no tiene pérdida de calidad) y para los fondos de pantalla e imágenes grandes vamos a utilizar el formato JPG (que comprime muy bien imágenes grandes con una pérdida mínima de calidad, aunque no vale para los sprites porque estropea las transparencias).

Para poder cargar en estos formatos vamos a utilizar la librería SDL_image que se encuentra en este subdirectorio:


Esta carpeta también la vamos a vincular como ruta de búsqueda al proyecto:


También necesitamos bajarnos de Internet la librería dinámica SDL_Image.dll para descomprimirla al lado de nuestro ejecutable. Aquí tenéis el acceso directo para su descarga:

http://www.libsdl.org/projects/SDL_image/release/SDL_image-1.2.6-win32.zip

Dentro lleva comprimidos estos archivos:


En la unidad UJuego.pas vamos a vincular la librería SDL_image:

uses
Windows, SysUtils, Dialogs, SDL, sdl_ttf, SDL_Mixer, SDL_Image;

También vamos a modificar el método CargarSuperficie de la clase TSprite para que utilice el procedimiento IMG_Load para cargar cualquier tipo de extensión:

procedure TSprite.CargarSuperficie( sArchivo: string );
begin
Superficie := IMG_Load( PChar( sArchivo ) );
Superficie.flags := SDL_HWSURFACE;

if Superficie = nil then
begin
ShowMessage( 'No se encuentra el archivo "' + sArchivo + '.' );
Exit;
end;

iAncho := Superficie.w;
iAlto := Superficie.h;

// Fijamos el negro como color transparente
if bTransparente then
SDL_SetColorKey( Superficie, SDL_SRCCOLORKEY, 0 );
end;

Lo siguiente que he hecho es convertir todos los sprites BMP a PNG utilizando el programa de dibujo Paint:

avion.bmp -> avion.png
disparo.bmp -> disparo.png
enemigo.bmp -> enemigo.png
explosion.bmp -> explosion.png

Y la pantalla de fondo, como ocupa más de 500 KB en PNG la vamos a convertir a JPG, ya que no tiene transparencias:

fondo.bmp -> fondo.jpg

En resumen, de 1 MB que me ocupaban en disco los otros gráficos ahora se me queda en 84 KB. Esto es muy importante si vamos a hacer juegos para distribuirlos por Internet.

Por último, vamos a cambiar el procedimiento CargarSprites que se encuentra en la unidad UArcade.pas para cargar los nuevos formatos gráficos:

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

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

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

Enemigo := TSprite.Create;
Enemigo.CargarSuperficie( ExtractFilePath( ParamStr( 0 ) ) + 'enemigo.png' );

Explosion := TSprite.Create;
Explosion.CargarSuperficie( ExtractFilePath( ParamStr( 0 ) ) + 'explosion.png' );
Explosion.bSubsprites := True;
Explosion.iAnchoSub := 95;
Explosion.iAltoSub := 73;
Explosion.iSubX := 0;
Explosion.iSubY := 0;
Verdana := CrearFuente( 'verdana.ttf', 20 );
Sonido.CargarSonido( 1, ExtractFilePath( ParamStr( 0 ) ) + 'disparo.wav', 10 );
Sonido.CargarSonido( 2, ExtractFilePath( ParamStr( 0 ) ) + 'explosion.wav', 100 );
end;


Al ejecutar el juego tiene que funcionar exactamente igual sin ningún problema.

Aquí tenéis todo el proyecto comprimido con zip en Rapidshare, Megaupload y Hyperupload:

https://mega.nz/file/4JgUwYJC#afjcV-Jf4os5yDRJ9zl3l3Cbj5Bjt-TCCBb5t6P94gs

Con esto damos más o menos por finalizado nuestro juego de aviones y ahora vamos a ver como sería crear un juego de plataformas utilizando tiles (piezas).

CREANDO UN JUEGO CON TILES

Un juego de plataformas sería un juego bidimensional con este estilo:


La pantalla de fondo no se suele dibujar entera, ya que si el juego 200 pantallas entonces habría que dibujar 200 bitmaps, que aunque estén en formato JPG es demasiado.

Lo que se suele hacer en estos casos en dividir la pantalla en piezas (tiles) horizontal y verticalmente cuya posición almacenamos en un array bidimensional. Estas piezas también nos van a servir como mapa de durezas para comprobar si los personajes del juego chocan con las paredes o pueden subir escaleras.

Entonces cuando se ejecuta el juego, lo que hacemos es dibujar cada pieza donde corresponda en la pantalla de fondo y luego utilizamos todo el fondo en pantalla. De este modo ahorramos muchísimo en gráficos y podemos crear infinidad de pantallas sin coste alguno.

Otro estilo de juego donde se utilizan los tiles es en los juegos con perspectiva isométrica:


Aunque en este tipo de juegos que casi son 3D se utilizaría un array tridimensional (ancho, largo y alto). Si os interesa la programación isométrica aquí tenéis un buen artículo:

http://www.wired-weasel.com/users/serhid/tutos/tut5.html

En el próximo artículo vamos a ver como crear un nuevo juego de plataformas a partir de nuestra librería Juego.dpr y UJuego.pas.

Pruebas realizadas en Delphi 7.

06 junio 2008

Programar videojuegos con la librería SDL (7)

CREANDO UN SCROLL VERTICAL

Al igual que nos juegos clásicos de arcade, vamos a crear un scroll vertical hacia abajo para que parezca que estamos avanzando sobre el mar. Realmente vamos a utilizar la misma textura del mar, pero la vamos a dividirla en dos trozos: uno que baja y otro que lo alimenta por arriba:


Mientras hacemos que el fondo baje por la pantalla, arriba se nos va quedando un hueco que tenemos que completar con el mismo fondo. Para hacer esto vamos a implementar en nuestra unidad genérica UJuego.pas un nuevo procedimiento para copiar trozos de imagen de una superficie a otra:

procedure CopiarImagen( Origen, Destino: PSDL_Surface;
x1, y1, x2, y2, iAncho, iAlto: Integer );
var
RO, RD: TSDL_Rect; // rectángulos origen y destino
begin
RO.x := x1;
RO.y := y1;
RO.w := iAncho;
RO.h := iAlto;
RD.x := x2;
RD.y := y2;
RD.w := iAncho;
RD.h := iAlto;
SDL_BlitSurface( Origen, @RO, Pantalla, @RD );
end;

También necesitamos una variable glogal en la unidad UArcade.pas para controlar la posición del scroll:

var
iScroll: Integer;

Y por último modificamos el procedimiento DibujarSprites para conseguir el efecto deseado:

procedure DibujarSprites;
begin
// Parte inferior del scroll
CopiarImagen( Fondo.Superficie, Pantalla, 0, 0, 0, iScroll, 640, 480 - iScroll );

// Parte superior del scroll
CopiarImagen( Fondo.Superficie, Pantalla, 0, 480 - iScroll, 0, 0, 640, iScroll );

// Movemos el fondo hacia abajo
Inc( iScroll );

if iScroll > 480 then
iScroll := 0;

Avion.Dibujar;
ControlarDisparos;
ControlarEnemigos;
end;

Así quedaría al ejecutar el juego:


Si os fijáis en la imagen se puede apreciar el corte de donde empieza y termina la textura del mar verticalmente. Lo ideal sería conseguir una textura simétrica para que no se note dicho escalón. En las páginas sobre texturas 3D que hay por Internet se puede encontrar gran cantidad de material.

ACCEDIENDO A LAS FUENTES PARA ESCRIBIR TEXTO

Renderizar texto en una superficie no es tan fácil como imprimir un mensaje en Windows utilizando el canvas. La librería SDL dispone de dos formas de escribir texto: utilizando una fuente predefinida en un bitmap o utilizar las fuentes reales de Windows mediante archivos TTF.

Yo me he decantado por la segunda opción, ya que aunque es más difícil, sí que nos da más libertad al escribir texto (cambiando la fuente, el estilo, etc.). Para cumplir este objetivo vamos a utilizar una librería asociada a la SDL llamada SDL_TTF. Esta librería se encuentra en un subdirectorio donde hemos instalado la SDL:


Así que ese directorio tenemos que añadirlo a los directorios de búsqueda del proyecto:


También necesitamos bajarnos de Internet la DLL donde está esta librería y guardarla en el directorio de nuestra aplicación:

http://www.libsdl.org/projects/SDL_ttf/release/SDL_ttf-2.0.9-win32.zip

Ese zip lleva estos archivos:

La versión de esa DLL es la 2.0.9, mientras que nuestras cabeceras de SDL del proyecto Delphi son de la versión 1.5, aunque todo funciona a la perfección.

Ahora vamos a hacer una serie de rutinas estándar en nuestra unidad UJuego.pas para poder imprimir texto encima de cualquier superficie. Lo primero es añadir la nueva unidad sdl_ttf en la sección uses:

uses Windows, SysUtils, Dialogs, SDL, sdl_ttf;;

Para poder utilizar las fuentes hay que llamar a los procedimientos TTF_Init y TTF_Quit. Esto lo vamos a encapsular en dos procedimientos:

procedure InicializarFuentes;
begin
TTF_Init;
end;

procedure FinalizarFuentes;
begin
TTF_Quit;
end;

Estos procedimientos hay que llamarlos en bucle principal de programa, en la unidad juego.dpr. Lo llamamos después de inicializar el modo de vídeo:

begin
InicializarSDL( 'Mi primer juego en SDL' );
ModoVideo( 640, 480, 16, True );
InicializarFuentes;
...

y cuando nos salimos del juego:

...
FinalizarFuentes;
FinalizarSDL;
end.


Ahora necesitamos un procedimiento para abrir una fuente de Windows y que se quede en memoria y otro para cerrarla al salir de juego (en la unidad UJuego.pas):

function CrearFuente( sNombre: String; Tamano: Integer ): PTTF_Font;
begin
Result := TTF_OpenFont( PChar( 'c:\windows\fonts\' + sNombre ), Tamano );
end;

procedure EliminarFuente( Fuente: PTTF_Font );
begin
TTF_CloseFont( Fuente );
end;

Como puede verse he utilizado el puntero a una fuente PTTF_Font para crearla dando además la ruta del directorio de Windows. Aunque para crear un juego profesional independiente habría que copiar la fuente que vamos a utilizar en nuestro directorio local y abrirla desde ahí, no vaya a ser que el usuario no tenga esa fuente instalada en Windows.

El siguiente procedimiento que vamos a crear va a dibujar el texto en la superficie que le pasemos como parámetro:

procedure EscribirTexto( Superficie: PSDL_Surface; x, y: Integer; sTexto: String;
Fuente: PTTF_Font; Color: TSDL_Color );
var
Texto: PSDL_Surface;
begin
Texto := TTF_RenderText_Solid( Fuente, PChar( sTexto ), Color );
CopiarImagen( Texto, Superficie, 0, 0, x, y, Texto.w, Texto.h );
SDL_FreeSurface( Texto );
end;

La función TTF_RenderText_Solid escribe el texto en una superficie, pero hay que llevar cuidado, ya que lo que hace realmente es crear una nueva superficie y escribir texto en ella. Luego hay que acordarse de liberarla de memoria llamando al procedimiento SDL_FreeSurface.

DIBUJANDO LOS MARCADORES DE PUNTUACIÓN

Ahora que ya tenemos todo lo que necesitamos para escribir texto en una superficie vamos a crear la fuente en nuestra unidad UArcade.pas. Lo primero es vincular también la unidad sdl_ttf al principio de la unidad:

uses SysUtils, UJuego, SDL, sdl_ttf;

Después creamos las variables para almacenar la fuente, el color de la misma y la puntuación que llevamos acumulada en el juego:

var
...
Verdana: PTTF_Font;
Blanco: TSDL_Color = ( r: $FF; g: $FF; b: $FF; unused: 0 );
rPuntuacion: Real;

Los colores en la librería SDL se almacenan en la estructura de datos TSDL_Color especificando los colores rojo (red -> r), verde (green -> r ) y azul (blue -> b). En este caso, como voy a escribir la puntuación en color blanco, he puesto cada valor a $FF.

Lo siguiente es ampliar el procedimiento CargarSprites para que cargue la fuente verdana:

procedure CargarSprites;
begin
...
Verdana := CrearFuente( 'verdana.ttf', 20 );
end;

Y también nos encargamos de liberarla en el procedimiento DestruirSprites:

procedure DestruirSprites;
begin
EliminarFuente( Verdana );
...
end;

Para dibujar el marcador de puntuación en pantalla encima de todo lo demás, tenemos que hacerlo después de que se dibujen todos los sprites (fondo, aviones, etc.). Por ello vamos a dibujar el marcador de puntuación al final del procedimiento DibujarSprites:

procedure DibujarSprites;
begin
...
EscribirTexto( Pantalla, 450, 0, 'Puntuación: ' +
FormatFloat( '0000', rPuntuacion ), Verdana, Blanco );
end;

Por último, tenemos que modificar el procedimiento ControlarDisparos para que incremente la puntuación en 1 cuando un disparo alcanza al enemigo:

procedure ControlarDisparos;
var
i, j: 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;

for j := 1 to 10 do
if Enemigos[j].bActivo then
if ( Disparos[i].x + 16 >= Enemigos[j].x ) and
( Disparos[i].x + 16 <= Enemigos[j].x + 95 ) and
( Disparos[i].y + 9 >= Enemigos[j].y ) and
( Disparos[i].y + 9 <= Enemigos[j].y + 73 ) then
begin
Enemigos[j].bActivo := False;
Enemigos[j].bExplotando := True;
Enemigos[j].iTmpExplosion := SDL_GetTicks;
Disparos[i].bActivo := False;
rPuntuacion := rPuntuacion + 1;
end;
end;
end;


Y este es el resultado final al ejecutar el juego:


En este artículo tenía previsto cómo reproducir sonido y música utilizando la librería SDL, pero como es bastante extenso lo vamos a dejar para la semana que viene.

En el próximo artículo veremos también como cargar los gráficos utilizando los formatos JPG y PNG (los más avanzados) y veremos un ejemplo que como crear un juego de plataformas mediante piezas (tiles).

Aquí tenéis todo el proyecto en RapidShare, Megaupload y Hyperupload:

https://mega.nz/file/4JgUwYJC#afjcV-Jf4os5yDRJ9zl3l3Cbj5Bjt-TCCBb5t6P94gs

Pruebas realizadas en Delphi 7.

30 mayo 2008

Programar videojuegos con la librería SDL (6)

CREANDO LOS ENEMIGOS

Lo siguiente que vamos a crear en nuestro pequeño videojuego son los aviones enemigos. El sprite del enemigo va a ser el siguiente:


Ahora vamos a crear un registro en la unidad UArcade.pas para controlar cada enemigo:

TEnemigo = record
x, y: Integer;
bActivo: Boolean;
end;

En la misma unidad también tenemos que declarar una variable para el sprite del enemigo así como un array de enemigos:

var
Avion, Fondo, Disparo, Enemigo: TSprite;
Disparos: array[1..10] of TDisparo;
iTmpDisparo: UInt32;
Enemigos: array[1..10] of TEnemigo;
iTmpEnemigos: UInt32;

La variable iTmpEnemigos la vamos a utilizar para contar el tiempo que va a tardar en salir en siguiente enemigo. Ahora tenemos que hacer que el procedimiento CargarSprites cargue el sprite del enemigo:

procedure CargarSprites;
begin
.....
Enemigo := TSprite.Create;
Enemigo.CargarSuperficie( ExtractFilePath( ParamStr( 0 ) ) + 'enemigo.bmp' );
end;

Y como no podía ser menos, hay que liberar el sprite cuando termina el juego:

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

Ahora tenemos que hacer un procedimiento que muestra los enemigos en pantalla. Vamos a hacer que aparezcan por la parte superior de la pantalla y que vayan descendiendo hasta que desaparezca por la parte inferior:

procedure ControlarEnemigos;
var
i: Integer;
begin
// Cada 3 segundos saco un nuevo enemigo
if SDL_GetTicks - iTmpEnemigos > 1000 then
begin
Randomize;
iTmpEnemigos := SDL_GetTicks;

for i := 1 to 10 do
if not Enemigos[i].bActivo then
begin
Enemigos[i].bActivo := True;
Enemigos[i].x := Random( 550 ) + 10;
Enemigos[i].y := -73;
break;
end;
end;

for i := 1 to 10 do
if Enemigos[i].bActivo then
begin
Inc( Enemigos[i].y, 3 );

if Enemigos[i].y > 480 then
Enemigos[i].bActivo := False;

// Dibujamos el sprite del enemigo en pantalla
Enemigo.x := Enemigos[i].x;
Enemigo.y := Enemigos[i].y;
Enemigo.Dibujar;
end;
end;

Al principio del juego considero que los 10 enemigos están inactivos e invisibles. Entonces la primera parte del procedimiento anterior espera un segundo hasta sacar el siguiente enemigo (de la lista de 10 enemigos). Cuando un enemigo aparece en pantalla lo sitúo en una coordenada horizontal aleatoria y verticalmente lo escondo en la parte superior de la pantalla para que aparezca suavemente.

Después el segundo bucle se encarga de mover hacia abajo los enemigos que están activos y los dibuja en pantalla. Si se salen de la pantalla lo deja inactivos y listos para la siguiente aparición.

Ahora sólo hay que llamar al procedimiento ControlarEnemigos dentro del procedimiento DibujarSprites:

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

Al ejecutar el juego tienen que ir apareciendo nuevos enemigos cada segundo:


DESTRUYENDO LOS ENEMIGOS

Para que quede bien la destrucción de los enemigos necesitamos una serie de fotogramas que simulen el efecto de explosión. Buscando un poco con Google pude encontrar estos gráficos y los reajusté con el programa de dibujo Gimp:


En esta ocasión nuestro sprite va a tener 4 subsprites (4 verticales y 1 horizontal). Para ello definimos primero la variable del sprite de la explosión:

var
Avion, Fondo, Disparo, Enemigo, Explosion: TSprite;
...

Ampliamos el procedimiento CargarSprites:

procedure CargarSprites;
begin
...
Explosion := TSprite.Create;
Explosion.CargarSuperficie( ExtractFilePath( ParamStr( 0 ) ) + 'explosion.bmp' );
Explosion.bSubsprites := True;
Explosion.iAnchoSub := 95;
Explosion.iAltoSub := 73;
Explosion.iSubX := 0;
Explosion.iSubY := 0;
end;

A diferencia de los sprites simples, le hemos dicho que va a tener subsprites de tamaño 95 x 73 cada uno. El subsprite seleccionado va a ser el primero comenzando por la izquierda (iSubX=0, iSubY = 0).

Y nos encargamos también de destruirlo:

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

Cuando disparemos al enemigo lo que tenemos que hacer es que explote. Para ello voy a ampliar el registro del enemigo para introducir una variable booleana que controla si el enemigo está explotando:

TEnemigo = record
x, y: Integer;
bActivo, bExplotando: Boolean;
iTmpExplosion: UInt32;
end;

También he añadido la variable iTmpExplosion para controlar los milisegundos que han pasado desde que empezó a explotar. Esto nos va a permitir el ir cambiando cada fotograma de la explosión cada 100 milisegundos.

Lo siguiente que toca es modificar el procedimiento ControlarEnemigos para añadirle el efecto de la explosión:

procedure ControlarEnemigos;
var
i: Integer;
iTmpTranscurrido: UInt32;
begin
// Cada 3 segundos saco un nuevo enemigo
if SDL_GetTicks - iTmpEnemigos > 1000 then
begin
Randomize;
iTmpEnemigos := SDL_GetTicks;

for i := 1 to 10 do
if not Enemigos[i].bActivo and not Enemigos[i].bExplotando then
begin
Enemigos[i].bActivo := True;
Enemigos[i].x := Random( 550 ) + 10;
Enemigos[i].y := -73;
break;
end;
end;

for i := 1 to 10 do
begin
if Enemigos[i].bActivo then
begin
Inc( Enemigos[i].y, 3 );

if Enemigos[i].y > 480 then
Enemigos[i].bActivo := False;

// Dibujamos el sprite del enemigo en pantalla
Enemigo.x := Enemigos[i].x;
Enemigo.y := Enemigos[i].y;
Enemigo.Dibujar;
end;

if Enemigos[i].bExplotando then
begin
iTmpTranscurrido := SDL_GetTicks - Enemigos[i].iTmpExplosion;

if iTmpTranscurrido <>= 100 ) and ( iTmpTranscurrido <>= 200 ) and ( iTmpTranscurrido <> 400 then
Enemigos[i].bExplotando := False;
end;
end;
end;

En el segundo bucle encargado de dibujar a cada enemigo también controlo si está explotando, con lo cual según los milisegundos que han pasado voy asignando el fotograma correspondiente:

De 0 a 99 milisegundos -> fotograma 0
De 100 a 199 milisegundos -> fotograma 1
De 200 a 299 milisegundos -> fotograma 2
De 300 en adelante -> fotograma 3

Ahora vamos a ampliar el procedimiento de ControlarDisparos para que compruebe si cada disparo choca con cada avión activo:

procedure ControlarDisparos;
var
i, j: 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;

for j := 1 to 10 do
if Enemigos[j].bActivo then
if ( Disparos[i].x + 16 >= Enemigos[j].x ) and
( Disparos[i].x + 16 <= Enemigos[j].x + 95 ) and
( Disparos[i].y + 9 >= Enemigos[j].y ) and
( Disparos[i].y + 9 <= Enemigos[j].y + 73 ) then
begin
Enemigos[j].bActivo := False;
Enemigos[j].bExplotando := True;
Enemigos[j].iTmpExplosion := SDL_GetTicks;
Disparos[i].bActivo := False;
end;
end;
end;

Con el bucle i recorro todos los disparos y con el bucle j controlo si ese disparo choca con algún enemigo. En el caso de que choque lo pongo como inactivo y activo su estado de explotando. También me apunto el tiempo inicial (los tics del reloj del sistema) de cuando ha empezado a explotar ese enemigo.

Al ejecutar el juego ya podemos acribillar a los enemigos:


CAMBIANDO EL TITULO DE LA VENTANA

Una cosa que se me olvidó mencionar en artículos anteriores es que podemos modificar el título de la ventana de nuestro juego. Para ello se utiliza el siguiente procedimiento de la librería SDL:

procedure SDL_WM_SetCaption( const title : PChar; const icon : PChar);

El primer parámetro es el título de la ventana y el segundo es el icono del juego. Yo sólo he utilizado el primero, ya que cuando terminemos el juego lo vamos a poner a pantalla completa y el icono no se va a ver.

Ahora sólo queda modificar el procedimiento InicializarSDL:

procedure InicializarSDL( sTitulo: String );
begin
// Inicializamos la librería SDL
if SDL_Init( SDL_INIT_VIDEO or SDL_DOUBLEBUF or SDL_INIT_JOYSTICK ) < 0 then
begin
ShowMessage( 'Error al inicializar la librería SDL.' );
SDL_Quit;
bSalir := True;
Exit;
end;

SDL_WM_SetCaption( PChar( sTitulo ), 0 );
bSalir := False;
end;

Luego en la unidad principal juego.dpr tenemos que llamar a esta función con el título de la ventana:

InicializarSDL( 'Mi primer juego en SDL' );
ModoVideo( 640, 480, 16, True );
...



EL PROYECTO SUBIDO A INTERNET

Como el proyecto se va volviendo más complicado, os he subido a Internet el proyecto con el ejecutable incluido. Si queréis compilarlo ya sabéis que necesitáis la librería SDL como mencioné en capítulos anteriores, aunque podéis ejecutarlo conforme está.

Lo he subido a tres servidores distintos, por si acaso no os funciona alguno: 

https://mega.nz/file/4JgUwYJC#afjcV-Jf4os5yDRJ9zl3l3Cbj5Bjt-TCCBb5t6P94gs
tml

Si lo descargáis del último servidor (hyperupload) veréis que le ha quitado la extensión zip. Sólo tenéis que ponérsela y listo.

En el próximo artículo veremos como crear un scroll de pantalla, cómo dibujar los marcadores para la puntuación y veremos como reproducir sonidos.

Pruebas realizadas en Delphi 7.

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.

16 mayo 2008

Programar videojuegos con la librería SDL (4)

Hoy vamos a ver como mover el sprite del avión por pantalla utilizando el teclado. Lo primero que vamos a hacer es centrar el avión en pantalla modificando el procedimiento CargarSprites:

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

Ahora vamos a crear un procedimiento llamado ControlarEventos que va a encargarse de mover el avión según las pulsaciones del teclado:

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

if Teclado.bIzquierda and ( Avion.x > 0 ) then
Dec( Avion.x );

if Teclado.bArriba and ( Avion.y > 0 ) then
Dec( Avion.y );

if Teclado.bAbajo and ( Avion.y < 407 ) then
Inc( Avion.y );
end;

Después tenemos que llamar a este procedimiento desde el bucle principal del juego dentro de la unidad juego.dpr:

Al ejecutar el juego nos va a surgir uno de los grandes problemas en los juegos 2D:

Al moverse el sprite por pantalla va dejando rastro como los caracoles. Para solucionar este problema necesitamos un fondo que se dibuje antes que el sprite. Por ejemplo un fondo de dimensiones 640 x 480:

Para esta textura vamos a crear un sprite llamado Fondo a nivel global dentro de la unidad UArcade:

var
Avion, Fondo: TSprite;

Y ahora volvemos a modificar el procedimiento CargarSprites para cargar el fondo:

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' );
end;

Se supone que el archivo fondo.bmp es un bitmap de 640 x 480 que está al lado de nuestro ejecutable. Otra cosa importante es liberar el fondo cuando termina el juego. Para ello ampliamos el procedimiento DestruirSprites:

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

Y por último, el procedimiento DibujarSprites debe dibujar el fondo antes que el avión:

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

Al ejecutar el juego este sería el resultado:


LA TÉCNICA DEL DOBLE BUFFER

Con esto quedan solucionadas las manchas a la hora de dibujar el avión. Primero dibuja el fondo y después el avión. Lo normal es estos casos es que se produjera un parpadeo (ya que se mezclarían el avión con el fondo), pero no es así.

La librería SDL utiliza una técnica de doble buffer donde utiliza dos pantallas realizando los pasos siguientes:

Primero dibuja el fondo y el avión en la pantalla 1.

Al ejecutar el comando SDL_Flip( Pantalla ) intercambia la pantalla 1 por la 2.

Después dibuja el fondo y el avión en la pantalla 2.

Intercambia la pantalla 2 por la 1.

Vuelve al paso .

De este modo se provoca el engaño de parecer una animación cuando realmente la tarjeta de vídeo va intercambiando ambas pantallas 25 veces por segundo sin que el usuario se percate de lo ocurrido:


ACELERANDO EL MOVIMIENTO DEL AVIÓN

Inicialmente podemos ver que el avión no se mueve muy rápido que digamos (así no esquivamos ni una bala). Para acelerar su movimiento tenemos que modificar los incrementos en el procedimiento ControlarEventos:

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

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

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

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

Al mover el avión de 5 en 5 pixels ya se le puede considerar a este juego un arcade.

MOVER EL SPRITE CON EL RATÓN

Últimamente se está haciendo común el mover los sprites de este tipo de juegos con el ratón. Para no perder la costumbre vamos a hacer también una clase en la unidad UJuego:

TRaton = class
x, y, ux, uy: Integer; // coordenadas del ratón y última posición

constructor Create;
procedure Leer;
end;


En las variables x e y guardo las coordenadas actuales del ratón. Las variables ux, uy las voy a utilizar para detectar si el usuario ha movido el ratón. Voy a utilizar el constructor de la clase TRaton para ocultar la flecha del cursor que por defecto me muestra la librería SDL:

constructor TRaton.Create;
begin
SDL_ShowCursor( 0 );
end;

La función SDL_ShowCursor oculta o muestra el cursor del ratón dependiendo de si le pasamos un 0 o un 1. Y para leer el estado del ratón aquí tenemos el método:

procedure TRaton.Leer;
begin
SDL_GetMouseState( x, y );
end;

La función SDL_GetMouseState toma como parámetro dos números enteros que hay que pasar como variables y almacena ahí la posición del ratón. Ahora tenemos que crear una variable global dentro de la misma unidad para almacenar el objeto Raton:

var
bSalir: Boolean;
Pantalla: PSDL_Surface;
Teclado: TTeclado;
Raton: TRaton;
Temporizador: TTemporizador;

Y en el bucle principal del juego inicializamos el ratón, lo leemos y posteriormente lo eliminamos:

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

while not bSalir do
begin
Temporizador.Actualizar;

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

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

Para terminar sólo hay que modificar el procedimiento ControlarEventos para leer el teclado, o el ratón indistintamente:

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

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

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

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

// Movemos el avión por el ratón sólo si lo ha movido el usuario
if ( Raton.ux <> Raton.x ) or ( Raton.uy <> Raton.y ) then
begin
Avion.x := Raton.x;
Avion.y := Raton.y;

// Si el avión se sale de pantalla lo corregimos
if Avion.x > 545 then
Avion.x := 545;

if Avion.y > 407 then
Avion.y := 407;

// Guardamos las últimas coordenadas del ratón
Raton.ux := Raton.x;
Raton.uy := Raton.y;
end;
end;

Con esto ya tenemos el control absoluto de los dispositivos de entrada. En el próximo artículo veremos como hacer que dispare el avión controlando la multitarea.

Pruebas realizadas en Delphi 7.

09 mayo 2008

Programar videojuegos con la librería SDL (3)

Después de crear el núcleo central del programa vamos a ver como cargar los gráficos para su posterior visualización.

LOS SPRITES

En un videojuego en dos dimensiones (2D) los protagonistas principales son los llamados Sprites. Un sprite es una figura gráfica en movimiento que puede tener una o más posiciones (frames). Por ejemplo, en un juego de naves tipo Galaxian, la nave principal sólo tiene una posición: la vista desde arriba. En cambio en un juego de plataformas el personaje tiene muchas posiciones (andar, saltar, agacharse, etc.).

Cuando yo era novato en estos temas, lo que hacía era crear un sprite por cada personaje y posición. Eso formaba una cantidad de ficheros gráficos impresionante donde al final no me aclaraba que archivo iba para cada posición.

Otros programadores suelen guardar todos los gráficos en una sola imagen y van capturando sólo el trozo que les interesa:


Si el juego esta planificado al máximo desde el principio entonces ésta es la solución ideal. Pero como los seres humanos somos perezosos por naturaleza, empezamos las cosas con un planteamiento inicial vago y luego decimos: ¿y si añadimos esto? ¿y si añadimos lo otro? Pasa lo mismo que con los programas de gestión, empiezas a improvisar un poco por aquí y un poco por allá y de un proyecto serio acaba convertido en churros con chocolate.

Así que me decanté por una solución intermedia que me permitiera improvisar en el futuro: cada sprite en su fichero pero con todas sus posiciones. Así es como lo hacen hoy en día la mayoría de juegos 3D (por lo menos así pude verlo en el código fuente de Quake 3).

LA CLASE TSPRITE

Dentro de nuestra unidad genérica UJuego.pas vamos a crear una clase para manejar todo lo que necesita un sprite. La clase la vamos a llamar TSprite:

TSprite = class
sNombre: string; // Nombre del archivo del sprite
rx, ry: Real; // Coordenadas del sprite en coma flotante
x, y: Integer; // Coordenadas del sprite en pantalla
iAncho, iAlto: Integer; // Altura y anchura del sprite
bVisible, bTransparente: Boolean;
bSubsprites: Boolean; // ¿Tiene subsprites?
iNumSubX, iNumSubY: Integer; // Nº de subsprites a lo ancho y a lo alto
iAnchoSub, iAltoSub: Integer; // Ancho y alto del subsprite
iSubX, iSubY: Integer; // Coordenada del subsprite dentro del sprite
Superficie: PSDL_Surface;

constructor Create;
destructor Destroy; override;
procedure CargarSuperficie( sArchivo: string );
procedure Transparente;
procedure NoTransparente;
procedure Dibujar;
end;


Veamos detenidamente para que sirve cada variable:

sNombre: nombre del archivo BMP asociado al sprite.
rx, ry: coordenadas del sprite en pantalla en números reales.
x,y: coordenadas reales del sprite en pantalla (transformadas de rx y ry).
iAncho, iAlto: ancho y alto del sprite en pixels.
bVisible: ¿Está visible en pantalla?
bTransparente: En un sprite transparente sólo son transparentes ciertas zonas del sprite.

Muchos os preguntaréis ¿Qué hace este tío guardando las coordenadas del sprite en formato flotante si en la pantalla se guardan las coordenadas como números enteros?. La explicación es sencilla: permite controlar la velocidad de movimiento con más precisión y es imprescindible cuando vamos a crear rutinas físicas para controlar la gravedad, la aceleración, etc.

Para solucionar el tema de que un sprite pueda tener muchas posiciones, se me ocurrió el crear subsprites dentro del sprite. Es como dividir un sprite en trozos horizontal y verticalmente:


Para ello utilizo estas variables dentro de la clase TSprite:

bSubsprites: ¿Tiene subsprites o es un sprite simple? Por defecto está a False.
iNumSubX, iNumSubY: Nº de subsprites horizontal y verticalmente.
iAnchoSub, iAltoSub: Ancho y alto máximo de cada uno de los subsprites.
iSubX, iSubY: subsprite seleccionado actualmente (columna y fila)
Superficie: Es el bitmap en memoria de video que contiene la imagen BMP cargada.

Vamos a ver la hora la implementación de la clase TSprite. Primero tenemos el constructor que inicializa todos los valores:

constructor TSprite.Create;
begin
Superficie := nil;
bVisible := True;
bTransparente := True;
rx := 0;
ry := 0;
x := 0;
y := 0;
iAncho := 0;
iAlto := 0;
bSubsprites := False;
iNumSubX := 0;
iNumSubY := 0;
iAnchoSub := 0;
iAltoSub := 0;
iSubX := 0;
iSubY := 0;
end;

Depués tenemos el desctructor que se encargar de eliminar la superficie (si esta cargada en memoria de vídeo):

destructor TSprite.Destroy;
begin
// Destruimos la superficie cargada
if Superficie <> nil then
begin
SDL_FreeSurface( Superficie );
Superficie := nil;
end;

inherited;
end;

Como las superficies son objetos especiales creados por la librería SDL no podemos llamar al método Free o FreeAndNil como si tal cosa. Hay que llamar a la función SDL_FreeSurface que se encarga de liberarla ya sea de la memoria de vídeo o de la memoria RAM.

Lo siguiente que necesitamos es la rutina que carga el sprite BMP del disco duro:

procedure TSprite.CargarSuperficie( sArchivo: string );
begin
Superficie := SDL_LoadBMP( PChar( sArchivo ) );

if Superficie = nil then
begin
ShowMessage( 'No se encuentra el archivo "' + sArchivo + '.' );
Exit;
end;

iAncho := Superficie.w;
iAlto := Superficie.h;

// Fijamos el negro como color transparente
if bTransparente then
SDL_SetColorKey( Superficie, SDL_SRCCOLORKEY, 0 );
end;

Lo que hace el método CargarSuperficie es cargar el bitmap del archivo y le asigna un color transparente si hemos puesto anteriormente la variable bTransparente a True.

También necesitamos un par de métodos para poder convertir en tiempo real un sprite a transparente o para hacerlo opaco. Un caso donde se suele dar es en los juegos de rol. En pantalla el héroe recoge el objeto transparente (por ejemplo una espada) y después la dibujamos en los marcadores sin que sea transparente. Para no tener que crear dos gráficos iguales (uno transparente y otro opaco) he creado estos dos métodos:

procedure TSprite.NoTransparente;
begin
// Lo hacemos opaco
SDL_SetColorKey( Superficie, SDL_SRCCOLORKEY, 255 );
end;

La función SDL_SetColorkey se encarga de decirle a una superficie cual va a ser el color transparente. Viene definida en la librería SDL del siguiente modo:

function SDL_SetColorKey(surface: PSDL_Surface; flag, key: UInt32) : Integer;

Sus parámetros son los siguientes:

Surface: superficie que queremos hacer transparente u opaca.
flag: nivel de transparencia (permite también invertir colores y hacer mezclas).
Key: Color que va a tomarse como transparente (si ponemos 255 es opaco).

procedure TSprite.Transparente;
begin
// Fijamos el negro como color transparente
SDL_SetColorKey( Superficie, SDL_SRCCOLORKEY, 0 );
end;

Yo he elegido el color negro como transparente pero puede ser cualquier otro. Se suele utilizar también mucho el color rosa ya que no es utilizado generalmente en los gráficos.

Y ahora viene el procedimiento más importante de todos:

procedure TSprite.Dibujar;
var
Origen, Destino: TSDL_Rect;
begin
// ¿Está visible el sprite?
if bVisible then
// ¿Tiene subsprites?
if bSubSprites then
begin
Origen.x := iSubX * iAnchoSub;
Origen.y := iSubY * iAltoSub;
Origen.w := iAnchoSub;
Origen.h := iAltoSub;
Destino.x := x;
Destino.y := y;
Destino.w := iAnchoSub;
Destino.h := iAltoSub;

// Dibujamos el subsprite seleccionado
if SDL_BlitSurface( Superficie, @Origen, Pantalla, @Destino ) < 0 then
begin
ShowMessage( 'Error al dibujar el sprite "' + sNombre + '" en pantalla.' );
Exit;
end;
end
else
begin
Origen.x := 0;
Origen.y := 0;
Origen.w := iAncho;
Origen.h := iAlto;
Destino.x := x;
Destino.y := y;
Destino.w := iAncho;
Destino.h := iAlto;

// Lo dibujamos entero
if SDL_BlitSurface( Superficie, @Origen, Pantalla, @Destino ) < 0 then
begin
ShowMessage( 'Error al dibujar el sprite "' + sNombre + '" en pantalla.' );
Exit;
end;
end;
end;

El procedimiento lo he dividido en dos partes principales. Si el sprite tiene subsprites entonces sólo capturo el trozo que me interesa y lo dibujo. En caso contrario dibujo todo el sprite.

Para dibujar un sprite con la librería SDL utilizo la función SDL_BlitSurface. Esta función viene definida en la librería SDL así:

function SDL_BlitSurface(src: PSDL_Surface; srcrect: PSDL_Rect; dst: PSDL_Surface; dstrect: PSDL_Rect): Integer;

Estos son sus parámetros:

Src: superficie origen.
srcrect: coordenadas del rectángulo origen.
dst: superficie destino.
dstrect: coordenadas del rectángulo destino.

PSDL_Rect es realmente un puntero a una estructura de datos (record en Delphi) definida de este modo:

TSDL_Rect = record
x, y: SInt16;
w, h: UInt16;
end;

Si tuviera que buscar una equivalencia entre dibujar con la librería SDL y con el GDI de Windows sería la siguiente:

Canvas = Superficie
TRect = TSDL_Rect

Así de simple y así de fácil.

PASAMOS A LA ACCIÓN

El juego lo vamos crear en una unidad aparte para no estropear nuestras rutinas estándar situadas en Juego.dpr y Juego.pas. Hay que pensar siempre en que si tengo que empezar un juego nuevo, copio estas dos unidades y listo.

Creamos una unidad nueva que se llame UArcade.pas y le vamos a vincular nuestra unidad estándar:

uses SysUtils, UJuego;

También voy a definir una variable global para guardar el gráfico de este avión:


Lo definimos en la sección interface:

var
Avion: TSprite;

El primer procedimiento que vamos a crear para esta unidad va a encargarse de cargar los sprites:

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

Doy por supuesto que el archivo avion.bmp se encuentra al lado de nuestro ejecutable. Este sprite no tiene subsprites, por lo que no hay que hacer nada en especial con el mismo.

También necesitamos otro procedimiento para eliminar los sprites de todo el juego:

procedure DestruirSprites;
begin
Avion.Free;
end;

Y otro para dibujarlos:

procedure DibujarSprites;
begin
Avion.Dibujar;
end;

MODIFICANDO EL BUCLE PRINCIPAL DE JUEGO

Después de toda la parafernalia que he montado vamos a ver los resultados reales. Para ello necesitamos llamar a estos nuevos procedimientos desde el bucle principal del programa situado en la unidad Juego.dpr:

program juego;

uses
Windows,
Dialogs,
SysUtils,
UJuego in 'UJuego.pas',
UArcade in 'UArcade.pas';

{$R *.res}

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

while not bSalir do
begin
Temporizador.Actualizar;

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

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

Si os fijáis en la sección uses he añadido la nueva unidad UArcade.pas.

Todo el bucle está igual que el artículo anterior anterior con la salvedad de que he llamado a los procedimientos CargarSprites, DibujarSprites y DestruirSprites.

Al ejecutar el juego tiene que aparecer el avión en la esquina superior izquierda de la ventana:


En el siguiente artículo veremos como mover el sprite por la pantalla incluso con un fondo. También veremos como moverlo utilizando el teclado y el ratón.


Pruebas realizadas en Delphi 7.

Publicidad