09 octubre 2009

Mi primer videojuego independiente (4)

Después de preparar toda la artillería que necesitamos para dibujar sprites en OpenGL vamos a implementar por fin la clase TSprite. Vamos a ver como se carga la textura de un sprite desde un archivo comprimido con zip.


CARGANDO SPRITES

Esta clase es similar a la que hice en cursos anteriores pero ahora comento lo que he añadido nuevo respecto a OpenGL:

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;
rEscalaX, rEscalaY: Real; // Escala de ampliación X e Y

Textura: array[1..MAX_TEXTURAS] of TTextura; // Texturas disponibles para este sprite
iNumTex: Integer; // Nº de texturas
iTexAct: Integer; // Si es mayor de cero sólo se imprime esta textura (sin subsprites)
iTexSub: Integer; // Si es mayor de cero sólo se imprime esta textura (con subsprites)

constructor Create(sNombreSprite: String; bSubsprites: Boolean;
iAncho, iAlto, iAnchoSub, iAltoSub: Integer);
destructor Destroy; override;
procedure CargarSuperficieComprimida(Comprimido: PSDL_RWops);
procedure Dibujar;
procedure DibujarEn(SuperficieDestino: PSDL_Surface);
procedure Crear;
procedure CrearTextura(xInicial, yInicial, iAncho, iAlto,
iIncrementoX, iIncrementoY, iAnchoTex, iAltoTex: Integer );
procedure LiberarSuperficie;
end;

Las primeras variables contienen un nombre único que le asigno cada sprite, sus dimensiones y las coordenadas en pantalla en números enteros y en números reales:

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

Después tenemos tres variables booleanas para saber si el sprite esta visible, si es transparente o tiene subsprites:

bVisible, bTransparente: Boolean;
bSubsprites: Boolean; // ¿Tiene subsprites?

¿Qué es un subsprite? Pues un sprite dentro de otro sprite:

Como la clase TSprite la utilizo para todo, lo mismo nos toca dibujar toda la pantalla con un solo sprite compuesto de muchas texturas de 256 x256 (lo que yo llamo un supersprite) que lo mismo sólo tenemos que dibujar una parte del sprite (subsprite), como en el caso de la tortuga que aprovecho la misma textura para poner todas las posiciones.

En el caso de que nuestro sprite tenga subsprites entonces debemos darle las dimensiones de los mismos:

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

Las variables iSubX e iSubY las utilizo para saber que subsprite voy a imprimir antes de llamar al procedimiento Dibujar. Después tenemos la superficie donde vamos a cargar la textura del sprite antes de subirla a la memoria de vídeo:

Superficie: PSDL_Surface;
rEscalaX, rEscalaY: Real; // Escala de ampliación X e Y

Las variables rEscalaX y rEscalaY las utilizo para aumentar o disminuir el tamaño del sprite. Esto lo hice para aumentar el tamaño de los botones del menú principal:

Aquí añado algunas novedades pertenecientes a OpenGL:

Textura: array[1..MAX_TEXTURAS] of TTextura; // Texturas disponibles para este sprite
iNumTex: Integer; // Nº de texturas
iTexAct: Integer; // Si es mayor de cero sólo se imprime esta textura (sin subsprites)
iTexSub: Integer; // Si es mayor de cero sólo se imprime esta textura (con subsprites)

Creo un array de texturas donde le pongo por ejemplo un máximo de 100 texturas por sprite:

const
MAX_TEXTURAS = 100; // Nº máximo de texturas por sprite

Y las variables iTexAct y iTexSub las utilizo para sprites que son más grandes de 256 x 256 cuando tengo que seleccionar que textura voy a imprimir. Ya lo veremos en el siguiente artículo cuando muestre las rutinas de impresión de polígonos.

El constructor del sprite inicializa todas estas variables así como las que le pasamos como parámetro para crear el sprite:

constructor TSprite.Create(sNombreSprite: String; bSubsprites: Boolean;
iAncho, iAlto, iAnchoSub, iAltoSub: Integer);
var
i: Integer;
begin
sNombre := sNombreSprite;
Superficie := nil;
bVisible := True;
bTransparente := True;
rx := 0;
ry := 0;
x := 0;
y := 0;
Self.iAncho := iAncho;
Self.iAlto := iAlto;
Self.bSubsprites := bSubSprites;
iNumSubX := 0;
iNumSubY := 0;
Self.iAnchoSub := iAnchoSub;
Self.iAltoSub := iAltoSub;
iSubX := 0;
iSubY := 0;
rEscalaX := 1;
rEscalaY := 1;
iNumTex := 0;
rProfundidadZ := 0;
iTexSub := 0;

// Inicializamos el array de texturas
for i := 1 to MAX_TEXTURAS do
Textura[i] := nil;
end;

Por ejemplo, para cargar el fondo de pantalla creo este sprite:

Fondo := TSprite.Create('Fondo', False, 640, 480, 0, 0);

En cambio, para el pájaro violeta necesito crear subsprites:

PajaroVioleta := TSprite.Create('PajaroVioleta', True, 180, 56, 45, 28);

O puede haber sprites como la pausa donde todo es una sola textura:

Pausa := TSprite.Create('Pausa', False, 303, 292, 0, 0);

El destructor de esta clase debe eliminar todas las texturas y objetos que haya creados en memoria:

destructor TSprite.Destroy;
var
i: Integer;
begin
LiberarSuperficie;

// Destruimos las texturas
for i := 1 to MAX_TEXTURAS do
if Textura[i] <> nil then
FreeAndNil(Textura[i]);

iNumTex := 0;
inherited;
end;

El procedimiento LiberarSuperficie elimina la superficie de OpenGL que una vez que subimos a memoria de vídeo ya no necesitamos:

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

CARGANDO IMÁGENES COMPRIMIDAS CON ZIP

Aquí entramos en una parte importante de esta clase. Para proteger nuestros gráficos vamos a cargar las texturas PNG que están comprimidas en un archico ZIP con contraseña. De este modo evitamos que otra gente utilice nuestros gráficos para hacer otros juegos cutres. Esta sería la rutina de carga:

procedure TSprite.CargarSuperficieComprimida(Comprimido: PSDL_RWops);
begin
if Superficie <> nil then
begin
SDL_FreeSurface(Superficie);
Superficie := nil;
end;

Superficie := IMG_Load_RW(Comprimido, 1);

if Superficie = nil then
begin
ShowMessage('Error al cargar el archivo comprimido ' + sNombre + '.');
Exit;
end;

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

// Fijamos el negro como color transparente
if bTransparente then
SDL_SetColorKey(Superficie, SDL_SRCCOLORKEY or SDL_RLEACCEL,
SDL_MapRGBA(Pantalla.format, 255, 0, 255, 1));
end;

El parámetro Comprimido es del tipo PSDL_RWops, un tipo especial definido en la librería SDL para cargar gráficos desde un buffer de memoria. Luego os mostraré como lo creo al descomprimir el archivo ZIP.

Al principio del procedimiento compruebo si ya la hemos cargado anteriormente para liberarla:

if Superficie <> nil then
begin
SDL_FreeSurface( Superficie );
Superficie := nil;
end;

A continuación cargo la superficie y almaceno el ancho y alto del sprite:

Superficie := IMG_Load_RW( Comprimido, 1 );

if Superficie = nil then
begin
ShowMessage( 'Error al cargar el archivo comprimido ' + sNombre + '.' );
Exit;
end;

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

Y en el caso de que sea transparente le asigno como color transparente el rosa o el canal alfa:

// Fijamos el negro como color transparente
if bTransparente then
SDL_SetColorKey( Superficie, SDL_SRCCOLORKEY or SDL_RLEACCEL,
SDL_MapRGBA( Pantalla.format, 255, 0, 255, 1 ) );

Para cargar los sprites hago esto:

var
Zip: TZipForge;
Stream: TMemoryStream;
Comprimido: PSDL_RWops;
PajaroVioleta: TSprite;
begin
AbrirZip('graficos.zip', Stream, Zip, Comprimido);
DescomprimirSprite(Stream, Zip, Comprimido, PajaroVioleta, True, 45, 28,
'pajaro_violeta.png');
...

Para ello creé un procedimiento para abrir archivos Zip utilizando el componente ZipForge:

procedure AbrirZip(sArchivo: string; var Stream: TMemoryStream;
var Zip: TZipForge; var Comprimido: PSDL_RWops);
begin
Stream := TMemoryStream.Create;
Zip := TZipForge.Create(nil);
Zip.Password := 'miclave';
Zip.FileName := sRuta+sArchivo;
Zip.TempDir := sRuta;
zip.BaseDir := sruta;
Zip.OpenArchive(fmOpenRead);
Comprimido := nil;
end;

Y una vez abierto voy descomprimiendo texturas:

procedure DescomprimirSprite(Stream: TMemoryStream; Zip: TZipForge;
Comprimido: PSDL_RWops; Sprite: TSprite; bSubSprites: Boolean;
iAncho, iAlto: Integer; sNombre: string);
begin
Stream.Clear;
Zip.ExtractToStream(sNombre, Stream);
Comprimido := SDL_RWFromMem(Stream.Memory, Stream.Size);
Sprite.CargarSuperficieComprimida(Comprimido);
Sprite.bSubsprites := bSubSprites;

if bSubSprites then
begin
Sprite.iAnchoSub := iAncho;
Sprite.iAltoSub := iAlto;
end;
end;

Este procedimiento va extrayendo del archivo zip cada textura y la carga en la superficie del sprite. Entonces después de descomprimir la textura del archivo zip me la llevo de la superficie SDL a la memoria de vídeo:

var
Zip: TZipForge;
Stream: TMemoryStream;
Comprimido: PSDL_RWops;
PajaroVioleta: TSprite;
begin
AbrirZip('graficos.zip', Stream, Zip, Comprimido);
DescomprimirSprite(Stream, Zip, Comprimido, PajaroVioleta, True, 45, 28,
'pajaro_violeta.png');
Sprite.CrearTextura(0, 0, 256, 64, 0, 0, 180, 56);
Sprite.LiberarSuperficie;
Zip.Free;
Stream.Free;
end;

El procedimiento de crear la textura sube mediante OpenGL la textura a la memoria de vídeo:

procedure TSprite.CrearTextura(xInicial, yInicial, iAncho, iAlto,
iIncrementoX, iIncrementoY, iAnchoTex, iAltoTex: Integer);
begin
// Creamos la textura a partir de esta imagen
if iNumTex < MAX_TEXTURAS then
begin
Inc(iNumTex);
Textura[iNumTex] := TTextura.Create(Superficie, xInicial, yInicial,
iAncho, iAlto, iIncrementoX, iIncrementoY, iAnchoTex, iAltoTex,
iAnchoSub, iAltoSub, bSubsprites);
Textura[iNumTex].sNombre := sNombre;
end;
end;

Después la podemos liberar así como el archivo ZIP y el stream que hemos utilizado como intermediario. No os podéis imaginar las explosiones que me dio Delphi antes de conseguirlo. En el próximo artículo veremos como dibujar el sprite en pantalla según si es un sprite normal, un subsprite o un supersprite.

Pruebas realizadas en Delphi 7.

Publicidad