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