16 octubre 2009

Mi primer videojuego independiente (5)

Vamos a terminar de ver el resto de la clase TSprite con el procedimiento más importante del mismo: el encargado de dibujar los polígonos.

DIBUJANDO POLÍGONOS EN PANTALLA

Primero voy a volcar todo el procedimiento de Dibujar y luego explico cada parte:

procedure TSprite.Dibujar;
var
i, j, k: Integer;
rDespX, rDespY: Real; // Desplazamiento X e Y respecto al eje de coordenadas
rEscalaTexX, rEscalaTexY: Real;
rTexX, rTexY: Real; // Coordenadas de la esquina superior izquierda de la textura (0,0)
iSubX2, iSubY2: Integer; // reajustamos los subsprites según la textura
iNumSprHor, iNumSprVer: Integer; // Nº de sprites que caben en la textura horizontal y verticalmente
begin
// Si no tiene texturas no hacemos nada
if iNumTex = 0 then
Exit;

if bTransparente then
begin
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
end
else
glDisable(GL_BLEND);

iSubX2 := 0;
iSubY2 := 0;

// ¿Tiene subsprites?
if bSubSprites then
begin
if iTexSub > 0 then
iTexAct := iTexSub
else
iTexAct := 1;

// Entonces calculamos el ancho, alto y posición de cada subsprite

iNumSprHor := Textura[1].iAnchoTex div iAnchoSub;

// ¿El subsprite se sale de la textura actual?
if (iSubX+1) > iNumSprHor then
begin
if iNumSprHor <> 0 then
begin
// Calculamos en que textura va a caer nuestro subsprite
iTexAct := iSubX div iNumSprHor+1;

// Calculamos en que subsprite cae dentro de esa textura
iSubX2 := iSubX mod iNumSprHor;
end
else
begin
iTexAct := 1;
iSubX2 := 0;
end;
end
else
iSubX2 := iSubX;

// Si el subsprite se pasa de la textura pasamos a la siguiente textura vertical
// Calculamos cuantos sprites caben verticalmente
iNumSprVer := Textura[1].iAltoTex div iAltoSub;

// ¿El subsprite se sale de la textura actual?
if (iSubY+1) > iNumSprVer then
begin
if iNumSprVer <> 0 then
begin
// Calculamos en que textura va a caer nuestro subsprite
iTexAct := iSubY div iNumSprVer+1;

// Calculamos en que subsprite cae dentro de esa textura
iSubY2 := iSubY mod iNumSprVer;
end
else
begin
iTexAct := 1;
iSubY2 := 0;
end
end
else
iSubY2 := iSubY;
end;

// Dibujamos todas las texturas del sprite (o sólo la actual)

for i := 1 to iNumTex do
begin
// ¿Hay que imprimir sólo una textura?
if iTexAct > 0 then
// ¿No es esta textura?
if i <> iTexAct then
// Pasamos a la siguiente textura
Continue;

// Calculamos desde donde comienza la textura
rDespX := x+Textura[i].iIncX;
rDespY := y+Textura[i].iIncY;

// Dibujamos todos los triángulos del objeto que contiene cada sprite

for j := 1 to 2 do
begin
glBindTexture(GL_TEXTURE_2D, Textura[i].ID);
glBegin(GL_TRIANGLES);
for k := 1 to 3 do
begin
// ¿Tiene subsprites?
if bSubsprites then
begin
rTexX := CompToDouble(iSubX2*iAnchoSub)/CompToDouble(Textura[i].iAncho);
rTexY := CompToDouble(iSubY2*iAltoSub)/CompToDouble(Textura[i].iAlto);
rEscalaTexX := CompToDouble(iAnchoSub)/CompToDouble(Textura[i].iAncho);
rEscalaTexY := CompToDouble(iAltoSub)/CompToDouble(Textura[i].iAlto);
glTexCoord2f(rTexX+Textura[i].Triangulo[j].Vertice[k].u*rEscalaTexX,
rTexY+Textura[i].Triangulo[j].Vertice[k].v*rEscalaTexY);
end
else
begin
// Cortamos toda la textura
glTexCoord2f(Textura[i].Triangulo[j].Vertice[k].u,
Textura[i].Triangulo[j].Vertice[k].v);
end;

glVertex3f(Textura[i].Triangulo[j].Vertice[k].x*rEscalaX+rDespX,
480-(Textura[i].Triangulo[j].Vertice[k].y*rEscalaY+rDespY), // la coordenada Y va al revés
Textura[i].Triangulo[j].Vertice[k].z+rProfundidadZ);
end;

glEnd();
end;
end;

rProfundidadZ := rProfundidadZ + 0.01;
glDisable(GL_BLEND);
end;

Lo primero que hacemos es declarar estas variables:

i, j, k: Integer;
rDespX, rDespY: Real;
rEscalaTexX, rEscalaTexY: Real;
rTexX, rTexY: Real;
iSubX2, iSubY2: Integer;
iNumSprHor, iNumSprVer: Integer;

Las variables i, j, k las voy a utilizar para recorrer las texturas, los triángulos y los vértices de este sprite.

Luego tenemos las variables rDesX, rDespY que serán las coordenadas de las esquina superior izquierda de la textura que vamos a capturar dentro del sprite.

Después declaramos rEscalaTexX, rEscalaTexY para calcular las coordenadas de OpenGL dentro de la textura, que como dije anteriormente van de 0 a 1. Haciendo una regla de tres podemos averiguar su proporción.

Como paso intermedio he declarado las variables rTexX, rTexY, iSubX2, iSubY2, iNumSprHor, iNumSprVer para extraer las texturas de cada subsprite.

Al comenzar el procedimiento lo primero que hago es asegurarme de que el sprite tenga texturas y habilitar la transparencia si el sprite tiene esta propiedad:

begin
// Si no tiene texturas no hacemos nada
if iNumTex = 0 then
Exit;

if bTransparente then
begin
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
end
else
glDisable(GL_BLEND);

La transparencia la realizamos con el canal alfa, por lo que es necesario que las texturas seran de 32 bits de color con los componentes (R,G,B,A) siendo A el canal alfa. Es importante utilizar un programa de diseño gráfico que soporte este canal (como GIMP).

En el siguiente bloque comprobamos si el sprite tiene subsprites, por lo que extraemos el trozo que nos interesa según las propiedades iSubX e iSubY del sprite:

// ¿Tiene subsprites?
if bSubSprites then
begin
if iTexSub > 0 then
iTexAct := iTexSub
else
iTexAct := 1;

// Entonces calculamos el ancho, alto y posición de cada subsprite

iNumSprHor := Textura[1].iAnchoTex div iAnchoSub;

// ¿El subsprite se sale de la textura actual?
if (iSubX+1) > iNumSprHor then
begin
if iNumSprHor <> 0 then
begin
// Calculamos en que textura va a caer nuestro subsprite
iTexAct := iSubX div iNumSprHor+1;

// Calculamos en que subsprite cae dentro de esa textura
iSubX2 := iSubX mod iNumSprHor;
end
else
begin
iTexAct := 1;
iSubX2 := 0;
end;
end
else
iSubX2 := iSubX;

// Si el subsprite se pasa de la textura pasamos a la siguiente textura vertical
// Calculamos cuantos sprites caben verticalmente
iNumSprVer := Textura[1].iAltoTex div iAltoSub;

// ¿El subsprite se sale de la textura actual?
if (iSubY+1) > iNumSprVer then
begin
if iNumSprVer <> 0 then
begin
// Calculamos en que textura va a caer nuestro subsprite
iTexAct := iSubY div iNumSprVer+1;

// Calculamos en que subsprite cae dentro de esa textura
iSubY2 := iSubY mod iNumSprVer;
end
else
begin
iTexAct := 1;
iSubY2 := 0;
end
end
else
iSubY2 := iSubY;
end;

Aquí pueden ocurrir dos casos: que los subsprites no superen el tamaño de la textura o que si la superan entonces me encargo de averiguar en que textura cae el subsprite que necesito.

Ahora viene lo mas gordo. En un primer nivel voy recorriendo todas las texturas de las que se compone este sprite:

for i := 1 to iNumTex do
begin
// ¿Hay que imprimir sólo una textura?
if iTexAct > 0 then
// ¿No es esta textura?
if i <> iTexAct then
// Pasamos a la siguiente textura
Continue;

// Calculamos desde donde comienza la textura
rDespX := x+Textura[i].iIncX;
rDespY := y+Textura[i].iIncY;

En el segundo nivel pasamos a recorrer los dos triángulos de la textura (si fuera un juego 3D habría que expandir este bucle según el número de polígonos del objeto 3D):

for j := 1 to 2 do
begin
glBindTexture(GL_TEXTURE_2D, Textura[i].ID);
glBegin(GL_TRIANGLES);

La función glBindTexture de dice a OpenGL que vamos a dibujar texturas 2D y le pasamos el identificador de la textura y luego mediante glBegin(GL_TRIANGLES) le indicamos que vamos a dibujar polígonos de tres vértices.

En el tercer y último nivel recorremos los tres vértices de cada triángulo y dependiendo si tiene subsprites o no recorto la textura a su medida:

for k := 1 to 3 do
begin
// ¿Tiene subsprites?
if bSubsprites then
begin
rTexX := CompToDouble(iSubX2*iAnchoSub)/CompToDouble(Textura[i].iAncho);
rTexY := CompToDouble(iSubY2*iAltoSub)/CompToDouble(Textura[i].iAlto);
rEscalaTexX := CompToDouble(iAnchoSub)/CompToDouble(Textura[i].iAncho);
rEscalaTexY := CompToDouble(iAltoSub)/CompToDouble(Textura[i].iAlto);
glTexCoord2f(rTexX+Textura[i].Triangulo[j].Vertice[k].u*rEscalaTexX,
rTexY+Textura[i].Triangulo[j].Vertice[k].v*rEscalaTexY);
end
else
begin
// Cortamos toda la textura
glTexCoord2f(Textura[i].Triangulo[j].Vertice[k].u,
Textura[i].Triangulo[j].Vertice[k].v);
end;

Y esta función es la que dibuja realmente los polígonos en pantalla:

glVertex3f(Textura[i].Triangulo[j].Vertice[k].x*rEscalaX+rDespX,
480-(Textura[i].Triangulo[j].Vertice[k].y*rEscalaY+rDespY), // la coordenada Y va al revés
Textura[i].Triangulo[j].Vertice[k].z+rProfundidadZ);

Como las coordenadas en OpenGL van en coordenadas ortogonales he tenido que invertir la coordenada Y para ajustarla a coordenadas de pantalla.

Para finalizar le indicamos que hemos terminado de dibujar triángulos y desactivo el efecto GL_BLEND.

end;

glEnd();
end;
end;

rProfundidadZ := rProfundidadZ + 0.01;
glDisable(GL_BLEND);
end;

La variable rProfundidadZ va incrementandose poco a poco para determinar el orden de los sprites en pantalla mediante el buffer de profundidad Z. Este buffer lo inicializo antes de comenzar a dibujar sprites, como veremos más adelante. Esto evita la superposición de polígonos en pantalla (y parpadeos).

OTROS PROCEDIMIENTOS DEL SPRITE

He mantenido el procedimiento DibujarEn por si necesitamos copiar el contenido de una superficie en otra antes de subir la textura a la memoria de vídeo:

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.' );
bSalir := True;
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.' );
bSalir := True;
Exit;
end;
end;
end;

Luego está el procedimiento Crear que crea una superficie vacía dentro de un sprite que no viene de ningún archivo de disco:

procedure TSprite.Crear;
begin
// Creamos una superficie vacía
Superficie := SDL_CreateRGBSurface( SDL_HWSURFACE, iAncho, iAlto, 32, 0, 0, 0, 0 );
end;

Se puede crear un sprite vacío en el que luego se va escribiendo la puntuación de los marcadores antes de llevarlos a pantalla.

DIBUJANDO SPRITES EN PANTALLA

Con este sistema es como dibujo todos los sprites del juego en pantalla:

procedure DibujarSprites;
begin
rProfundidadZ := 0.0;

case iEscena of
ESC_LOGOTIPO: DibujarSpritesLogotipo;
ESC_PRESENTA: DibujarSpritesPresentacion;
ESC_OPCIONES: DibujarSpritesOpciones;
ESC_MAPA: DibujarSpritesMapa;
ESC_JUEGO: DibujarSpritesJuego;
ESC_GAMEOVER: DibujarSpritesGameOver;
ESC_CASTILLO: DibujarCastillo;
ESC_PEQUEPONA: DibujarPequepona;
ESC_INTRO: DibujarIntro;
end;
end;

Inicializo primero el buffer de profundidad y luego determino que tengo de dibujar. Para hacer el juego lo más modular posible lo he dividido todo en escenas, de modo que llamo a cada escena según corresponda. Este sería el procedimiento dentro del juego:

procedure DibujarSpritesJuego;
begin
Fondo.Dibujar;
DibujarPantalla;
DibujarPlataformas;
DibujarEnemigos;
Heroe.Dibujar;
DibujarFresa;
DibujarVida;
DibujarLlave;
DibujarCoco;
Estrellas;

if bPausa then
DibujarPausa;
end;

De este modo vamos descomponiendo el dibujado de sprites en forma de árbol respetando la prioridad de cada sprite.

DIBUJANDO SUPERSPRITES

¿Cómo podemos dibujar toda la pantalla mediante texturas de 256x256? Pues troceándola en texturas de potencia de 2. Por ejemplo, este sería el proceso de carga de la pantalla de presentación:

procedure CargarSpritesPresentacion;
var
Zip: TZipForge;
Stream: TMemoryStream;
Comprimido: PSDL_RWops;
begin
// Creamos los sprites que van a cargar los datos comprimidos

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

AbrirZip('gfcs\presentacion.gfcs', Stream, Zip, Comprimido);
DescomprimirSprite(Stream, Zip, Comprimido, Presentacion, False, 0, 0, 'presentacion.png');

CrearPantalla640x480(Presentacion);

Zip.Free;
Stream.Free;
end;

Por si acaso había que hacer más pantallas de 640 x 480 me hice un prodecimiento general que partía toda la pantalla y las enviaba a la memoria de vídeo:

procedure CrearPantalla640x480(Sprite: TSprite);
begin
// Fila de arriba
Sprite.CrearTextura( 0, 0, 256, 256, 0, 0, 256, 256);
Sprite.CrearTextura(256, 0, 256, 256, 256, 0, 256, 256);
Sprite.CrearTextura(512, 0, 128, 256, 512, 0, 128, 256);

// Fila central
Sprite.CrearTextura( 0, 256, 256, 128, 0, 256, 256, 256);
Sprite.CrearTextura(256, 256, 256, 128, 256, 256, 256, 256);
Sprite.CrearTextura(512, 256, 128, 128, 512, 256, 128, 256);

// Penúltima fila de abajo
Sprite.CrearTextura( 0, 384, 256, 64, 0, 384, 256, 64);
Sprite.CrearTextura(256, 384, 256, 64, 256, 384, 256, 64);
Sprite.CrearTextura(512, 384, 128, 64, 512, 384, 256, 64);

// Última fila de abajo
Sprite.CrearTextura( 0, 448, 256, 32, 0, 448, 256, 32);
Sprite.CrearTextura(256, 448, 256, 32, 256, 448, 256, 32);
Sprite.CrearTextura(512, 448, 128, 32, 512, 448, 256, 32);

Sprite.LiberarSuperficie;
end;

Esa sería la pantalla troceada:

Después para dibujarla sólo tengo que hacer esto:

Presentacion.Dibujar;

Y ya se encargan mis rutinas de dibujar las 12 texturas del sprite. Es lo que yo llamo un supersprite. En el siguiente artículo comentaré como resolví el tema del scroll horizontal así como otros problemas similares (el héroe, los enemigos, etc.).

Pruebas realizadas con Delphi 7.

Publicidad