Mostrando las entradas para la consulta aviones ordenadas por relevancia. Ordenar por fecha Mostrar todas las entradas
Mostrando las entradas para la consulta aviones ordenadas por relevancia. Ordenar por fecha Mostrar todas las entradas

04 julio 2008

Programar videojuegos con la librería SDL (y 11)

Hoy voy a terminar la sección dedicada a la programación con SDL en Delphi hablando de la creación de enemigos en un juego de plataformas.

CREANDO LOS ENEMIGOS

Para crear los sprites enemigos voy a utilizar este gráfico:


Como en el juego voy a insertar 6 enemigos, no es necesario cargar 6 sprites con los mismos gráficos. Al igual que hicimos en el juego de aviones, con un solo sprite enemigo podemos dibujar el resto.

Para ello voy a crear una nueva estructura de datos para guardar la posición de cada enemigo y su estado:

TEnemigo = record
x, y, iSubX, iSubY: Integer;
iPosicion: Integer;
bActivo, bIzquierda: Boolean;
end;

Este registro va a guardar las coordenadas de un enemigo, los subsprites que va a dibujar en ese momento, la posición al andar, si está activo y si anda hacia la izquierda o hacia la derecha.

Al igual que hicimos con los aviones vamos a añadir un array global en la unidad UPlataforma.pas para controlar a varios enemigos:

var
Fondo, Temporal, Piezas, Heroe, Enemigo: TSprite;
Enemigos: array[1..5] of TEnemigo;
...

También vamos a ampliar nuestro procedimiento cargar sprites para cargar los enemigos:

procedure CargarSprites;
begin
...
Enemigo := TSprite.Create;
Enemigo.CargarSuperficie( ExtractFilePath( ParamStr( 0 ) ) + 'enemigo.png' );
Enemigo.bSubsprites := True;
Enemigo.iAnchoSub := 40;
Enemigo.iAltoSub := 40;

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

Y por supuesto no nos olvidamos de liberarlo de memoria:

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

SITUANDO LOS ENEMIGOS EN PANTALLA

Una vez ya tenemos nuestro sprite enemigo creado vamos a crear un procedimiento encargado de colocar los enemigos en pantalla:

procedure SituarEnemigos;
var
i: Integer;
begin
Enemigos[1].x := 40;
Enemigos[1].y := 320;
Enemigos[2].x := 400;
Enemigos[2].y := 320;
Enemigos[3].x := 440;
Enemigos[3].y := 200;
Enemigos[3].bIzquierda := True;
Enemigos[4].x := 160;
Enemigos[4].y := 200;
Enemigos[4].bIzquierda := True;
Enemigos[5].x := 200;
Enemigos[5].y := 80;
Enemigos[6].x := 440;
Enemigos[6].y := 80;

for i := 1 to 6 do
Enemigos[i].bActivo := True;
end;

A este procedimiento lo podemos llamar al final del procedimiento CargarSprites:

procedure CargarSprites;
begin
...
CargarPantalla( 'pantalla1.txt' );
DibujarPiezasEnTemporal;
SituarEnemigos;
end;

Lo más difícil es lo que viene ahora. Tenemos que mover todos los enemigos a la vez:

procedure MoverEnemigos;
var
i, xMapa, yMapa: Integer;
begin
for i := 1 to 6 do
begin
yMapa := ( Enemigos[i].y + 40 ) div 40 + 1;

if Enemigos[i].bActivo then
begin
if Enemigos[i].bIzquierda then
begin
Enemigos[i].iSubY := 1;
Dec( Enemigos[i].x );
xMapa := ( Enemigos[i].x - 1 ) div 40 + 1;
if ( Mapa[xMapa,yMapa] = '0' ) or ( Mapa[xMapa,yMapa] = '1' ) then
Enemigos[i].bIzquierda := False;
end
else
begin
Enemigos[i].iSubY := 0;
Inc( Enemigos[i].x );
xMapa := ( Enemigos[i].x + 40 ) div 40 + 1;
if ( Mapa[xMapa,yMapa] = '0' ) or ( Mapa[xMapa,yMapa] = '1' ) then
Enemigos[i].bIzquierda := True;
end;

// cambiamos de fotograma
Inc( Enemigos[i].iPosicion );

if Enemigos[i].iPosicion > 4 then
begin
if Enemigos[i].iSubX = 0 then
Enemigos[i].iSubX := 1
else
Enemigos[i].iSubX := 0;

Enemigos[i].iPosicion := 0;
end;
end;
end;
end;

Este procedimiento recorre todos los enemigos y los mueve a la derecha o hacia la izquierda hasta que choquen con un obstáculo. Cuando choca cambia de dirección hasta que vuelva a chocar. También nos encargamos de cambiar de fotograma para que se produzca la animación al andar.

Ahora sólo tenemos que modificar el procedimiento DibujarSprites para que dibuje a los enemigos y los mueva:

procedure DibujarSprites;
var
i: Integer;
begin
Temporal.Dibujar;
Heroe.Dibujar;

for i := 1 to 6 do
if Enemigos[i].bActivo then
begin
Enemigo.x := Enemigos[i].x;
Enemigo.y := Enemigos[i].y;
Enemigo.iSubX := Enemigos[i].iSubX;
Enemigo.iSubY := Enemigos[i].iSubY;
Enemigo.Dibujar;
end;

MoverEnemigos;
end;

Este sería el resultado:


COMPROBANDO LAS COLISIONES ENTRE SPRITES

Por último vamos a hacer que si el sprite del héroe choca con algún enemigo tenga que volver a empezar desde el principio (incluyendo volver a recoger las joyas). Eso lo vamos a hacer con el siguiente procedimiento:

procedure ComprobarEnemigos;
var
i: Integer;
begin
for i := 1 to 6 do
begin
if Enemigos[i].bActivo then
if ( Heroe.x + 20 >= Enemigos[i].x ) and ( Heroe.x + 20 <= Enemigos[i].x + 39 ) and
( Heroe.y + 20 >= Enemigos[i].y ) and ( Heroe.y + 20 <= Enemigos[i].y + 39 ) then
begin
// volvemos a colocar el equipo y los enemigos
Heroe.rx := 40;
Heroe.ry := 400;
SituarEnemigos;
CargarPantalla( 'pantalla1.txt' );

// volvemos a redibujar la pantalla de fondo
Fondo.DibujarEn( Temporal.Superficie );
DibujarPiezasEnTemporal;
end;
end;
end;

Para comprobar si el héroe choca con el enemigo lo que hago es comprobar si el centro de las coordenadas del héroe (heroe.x + 20, heroe.y + 20 ) están dentro de la cuadrícula de 40 x 40 que forma parte el enemigo. Cuando chocan entonces vuelvo a colocar al héroe, a los enemigos y las joyas, volviendo a dibujar la pantalla de fondo.

Este procedimiento hay que llamarlo desde DibujarSprites:

procedure DibujarSprites;
begin
...
MoverEnemigos;
ComprobarEnemigos;
end;


Otras cosas que se le podían haber añadido al juego es el control de vidas de modo que cada vez que nos maten decremente una vida. También haría falta meterle sonidos y una música de fondo, pero la falta de tiempo me impide cerrar el círculo.

Aquí os dejo el proyecto en RapidShare:

https://mega.nz/file/5FhU2SyD#ZavwnR9Amwcpub4wGaTI5zLsqEOPmUxuy201rtIUaOA

EL FIN DE UNA SERIE

Con esto doy más o menos por finalizado el curso de introducción a la programación de videojuegos con la librería SDL. Podía haber hecho muchos más ejemplos (juegos de estrategia, juegos de carreras, juegos de inteligencia tipo tetris, etc.), pero la falta de tiempo me impide llevarlo a cabo. Quizás en un futuro si encuentro un buen patrocinador que haga que no vuelva a trabajar podría abarcar todos estos temas (que bueno es soñar, jejeje).

Otro tema que también dejo pendiente para un futuro es la programación de videojuegos 3D con OpenGL, donde la librería SDL también da un buen soporte, pero todavía no lo tengo muy preparado y me gustaría presentarlo bien, ya que es un tema algo complicado pero apasionante.

Aprovechando también la librería OpenGL veremos también como realizar juegos 2D utilizando polígonos, lo que hace que los juegos sean espectaculares con sprites supergigantes y con los efectos que brindan las tarjetas 3D (iluminación, realzado de texturas, rotaciones, etc.).

Cuando tenga una librería bien montada crearé otra serie de artículos dedicados a la OpenGL en Delphi. En el próximo artículo voy a comenzar hablando de las novedades de RAD Studio 2007 respecto a Delphi 7. Ya va siendo hora de guardar nuestro querido Delphi 7 en el cajón y aprovechar las nuevas ventajas del nuevo IDE de CodeGear. Con Embarcadero Technologies comienza una nueva era para Delphi. Espero que sea para mejor.

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.

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.

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.

Publicidad