23 octubre 2009

Mi primer videojuego independiente (y 6)

Voy a terminar esta serie de artículos comentando otras dificultades que encontré en el juego como pueden ser el dibujado de piezas con scroll y la colisión con las mismas.

CARGANDO LAS PIEZAS DEL JUEGO

Como dije anteriormente, la pantalla de 640 x 480 la he dividido en piezas de 40 x 40 pixels lo que me dan un total de 16 piezas horizontales y 12 verticales. Estas piezas las guardo en un gran sprite vertical:

El problema está en que como tengo que meterlo en texturas que no superen los 256 de altura lo he partido en texturas de 64 x 256 (me sobra 24 horizontalmente y 16 verticalmente). En cada textura me caben 6 bloques de 40 x 40.

Entonces realizo el proceso de carga de este modo:

procedure CargarSpritesPequepona;
var
Zip: TZipForge;
Stream: TMemoryStream;
Comprimido: PSDL_RWops;
i, yp: Integer;
begin
Piezas := TSprite.Create('Piezas', True, 40, 2120, 40, 40);

// Extraemos las piezas del archivo zip
AbrirZip('graficos.zip', Stream, Zip, Comprimido);
DescomprimirSprite(Stream, Zip, Comprimido, Piezas, True, 40, 40, 'piezas.png');

// Partimos el sprite en texturas y las subimos a la memoria de vídeo
yp := 0;
for i := 1 to 10 do
begin
Piezas.CrearTextura(0, yp, 64, 256, 0, 0, 40, 240);
Inc(yp, 240);
end;
Piezas.LiberarSuperficie;

Zip.Free;
Stream.Free;
end;

Una vez que le mandamos las texturas a OpenGL podemos liberar la superficie que hemos extraído del archivo zip y lo cerramos.

DIBUJANDO LAS PIEZAS CON SCROLL HORIZONTAL

La función de dibujar en pantalla realiza un barrido horizontal y vertical por toda la pantalla y va dibujando las piezas de 40 x 40 y encima los objetos. A continuación explicaré cada parte:

procedure DibujarPantalla;
var
p, i, j, x, y: Integer;
bDibujar: Boolean;
begin
for p := 1 to iNumPan do
begin
for j := 1 to 12 do
begin
for i := 1 to 16 do
begin
x := (i-1)*40+iScroll+(p-1)*640;
y := (j-1)*40;

// Comprobamos que la pieza a dibujar esté dentro de la pantalla

if (x+40 >= 0) and (x <= 640) then
begin
if Pantallas[p].Piezas[i,j] > 0 then
begin
Piezas.x := x;
Piezas.y := y;
Piezas.iSubY := Pantallas[p].Piezas[i, j];
Piezas.Dibujar;
end;

// Caso especial para la llave

if Pantallas[p].Objetos[i,j] = LLAVE then
DibujarAnimacionLlave(x, y );

// Dibujamos el resto de objetos
bDibujar := False;

// Sólo dibujamos los objetos principales
if (Pantallas[p].Objetos[i,j] > LLAVE) and
(Pantallas[p].Objetos[i,j] < RATONDER) then
bDibujar := True;

// Sólo dibujamos los objetos a recoger
if (Pantallas[p].Objetos[i,j] = PANZACOCO) or
(Pantallas[p].Objetos[i,j] = ESCUDO) or
(Pantallas[p].Objetos[i,j] = MUELLE) or
(Pantallas[p].Objetos[i,j] = MUELLE2) or
(Pantallas[p].Objetos[i,j] = RELOJ) then
bDibujar := True;

// El tope de los enemigos no lo sacamos
if Pantallas[p].Objetos[i,j] = TOPE then
bDibujar := False;

if bDibujar then
begin
Objetos.x := x;
Objetos.y := y;
Objetos.iSubY := Pantallas[p].Objetos[i, j];
Objetos.Dibujar;
end;

// Caso especial para la puerta
if Pantallas[p].Objetos[i,j] = PUERTA2 then
DibujarAyuda(x, y);
end;
end;
end;
end;
end;

La primera parte comienzo con un bucle de recorre todas las pantallas y dentro de cada una las piezas horizontal (i) y verticalmente (j):

for p := 1 to iNumPan do
begin
for j := 1 to 12 do
begin
for i := 1 to 16 do begin
x := (i-1)*40+iScroll+(p-1)*640;
y := (j-1)*40;

Aquí la parte más importante es la variable x, que dependiendo de la pantalla p y el desplazamiento de la variable iScroll determinaremos que pantalla dibujar y desde donde comenzamos. No conviene dibujar más de lo normal ya que podríamos ralentizar la librería OpenGL.

Después de convertir las coordenadas de las pantallas virtuales a la pantalla de vídeo entonces compruebo si puedo dibujarlas o no:

if (x+40 >= 0) and (x <= 640) then
begin
if Pantallas[p].Piezas[i,j] > 0 then
begin
Piezas.x := x;
Piezas.y := y;
Piezas.iSubY := Pantallas[p].Piezas[i, j];
Piezas.Dibujar;
end;

Las pantallas las almaceno en este array bidimensional:

Pantallas: array[1..MAX_PAN] of TPantalla;

Donde TPantalla es un registro que almacena todas las piezas:

TPantalla = record
Piezas, Objetos: array[1..16, 1..12] of byte;
end;

Lo que viene a continuación es dibujar los objetos, pero aquí hay que tener cuidado. Los objetos especiales como los topes de los enemigos, las puertas o las llaves tienen su propia rutina de dibujado por lo que las evito mediante la variable bDibujar:

// Caso especial para la llave

if Pantallas[p].Objetos[i,j] = LLAVE then
DibujarAnimacionLlave(x, y );

// Dibujamos el resto de objetos
bDibujar := False;

// Sólo dibujamos los objetos principales
if (Pantallas[p].Objetos[i,j] > LLAVE) and
(Pantallas[p].Objetos[i,j] < RATONDER) then
bDibujar := True;

// Sólo dibujamos los objetos a recoger
if (Pantallas[p].Objetos[i,j] = PANZACOCO) or
(Pantallas[p].Objetos[i,j] = ESCUDO) or
(Pantallas[p].Objetos[i,j] = MUELLE) or
(Pantallas[p].Objetos[i,j] = MUELLE2) or
(Pantallas[p].Objetos[i,j] = RELOJ) then
bDibujar := True;

// El tope de los enemigos no lo sacamos
if Pantallas[p].Objetos[i,j] = TOPE then
bDibujar := False;

if bDibujar then
begin
Objetos.x := x;
Objetos.y := y;
Objetos.iSubY := Pantallas[p].Objetos[i, j];
Objetos.Dibujar;
end;

// Caso especial para la puerta
if Pantallas[p].Objetos[i,j] = PUERTA2 then
DibujarAyuda(x, y);

La variable iScroll la inicializo a cero al comenzar el juego:

iScroll := 0;

Entonces cuando pulso la tecla hacia la derecha incremento el scroll:

// Si el heroe pasa a la mitad derecha de la pantalla
// entonces comprobamos si tiene que hacer scroll:
if Heroe.rx+30 > 320 then
ScrollDerecha;

La variable rx son las coordenadas del heroe en pantalla en números reales. Antes de moverlo he comprobado que si estamos en la primera pantalla y no pasamos de la mitad de la misma no haga scroll.

Entonces el scroll hacia la derecha decrementa la variable iScroll para que las pantallas se vayan desplazando hacia la izquierda mientras nuestro personaje permanece en el centro de la pantalla:

procedure ScrollDerecha;
begin
if Abs(iScroll) < (iNumPan-1)*640 then
Dec(iScroll, Heroe.iVelocidad);
end;

La variable iNumPan es el número de pantallas totales que tiene esta fase. La variable iVelocidad la he fijado en 3 para realizar el scroll aunque a veces de incrementa cuando estamos andando por una plataforma móvil en su misma dirección: Ahora para movernos a hacia la izquierda comprobamos si no estamos en la última pantalla:

if Heroe.rx > 320 then
ScrollIzquierda
else
if iScroll <= 0 then
ScrollIzquierda;

Y entonces muevo todas las pantallas hacia la derecha controlando de que no me pase de la primera pantalla:

procedure ScrollIzquierda;
begin
if (iScroll<0) and (Heroe.Sprite.x+30<320) then
Inc(iScroll, Heroe.iVelocidad);
end;

Básicamente tenemos aquí todo lo que necesitamos para hacer un scroll horizontal y bidireccional. El fallo que cometí fue no haber diseñado un mundo gigante horizontalmente en vez de haberlo partido todo en pantallas. Hubiese sido más fácil así.

CONTROLANDO EL CHOQUE CON PAREDES Y SUELOS

Cuando movemos el personaje por pantalla tenemos que controlar que no choque con los bloques de 40 x 40 que hacen de pared o de suelo. Para ello hice una distinción en el editor para saber los bloques que hacen de suelo y los que hacen de pared:


Con los objetos que hacen de suelo sólo podemos chocar al caer verticalmente pero no horizontalmente. Y con los que hacen de suelo-pared chocamos por todos lados. Lo primero que tenemos que hacer es una función de tal modo que si le pasamos un par de coordenadas nos diga que pieza hay en esa coordenada:

function LeerPieza(x,y: Integer): Byte;
var
xMapa, yMapa, iPantalla: Integer;
begin
LeerCoordenadasMapa(x, y, xMapa, yMapa, iPantalla);
if (xMapa>=1) and (xMapa<=16) and (yMapa>=1) and (yMapa<=12) then
Result := Pantallas[iPantalla].Piezas[xMapa,yMapa]
else
Result := 0;
end;

Pero antes llama al procedimiento LeerCoordenadasMapa que transforma esas coordenadas a pantalla según el scroll:

procedure LeerCoordenadasMapa(x,y: Integer; var xMapa, yMapa, iPantalla: Integer);
begin
iPantalla := x div 640+1;
// ¿La pieza a mirar está en la pantalla de la izquierda?
if x < 0 then
x := (iPantalla-1)*640+x;

// ¿La pieza a mirar está en la pantalla de la derecha?
if x > 640 then
x := x-(iPantalla-1)*640;

xMapa := x div 40+1;
yMapa := y div 40+1;
end;

El peligro de esta función esta en que hay que saber si nos hemos pasado de pantalla y hay que asomarse a la primera columna de la siguiente pantalla.

Una vez sabemos la pieza que hay en un determinado lugar podemos saber si chocamos con ella:

function ChocaConSuelo(x,y: Integer): Boolean;
var
i, iPieza: Integer;
begin
iPieza := LeerPieza(x,y);
Result := False;
for i := 1 to iNumSue do
if iPieza = Suelos[i] then
Result := True;
end;

El array Suelos contiene la numeración de suelos con los que chocamos según lo que hicimos en el editor de pantallas. De la misma forma he operado para comprobar el choque con las paredes, recoger los objetos, etc.

DIBUJANDO LOS NUMEROS DEL MARCADOR

Aunque la librería SDL permite escribir caracteres utilizando las fuentes de Windows, os recomiendo no utilizarlo porque salen las letras en monocromo y pixeladas. Lo mejor es generar los sprites con los caracteres que necesitamos:

Entonces creamos un procedimiento para imprimir dichos números utilizando estos sprites:

procedure DibujarNumero(x,y: Integer; iNumero: Integer);
begin
if ( iNumero <> 99 ) then
Exit;

Numeros.x := x;
Numeros.y := y;
Numeros.iSubX := iNumero div 10;
Numeros.Dibujar;
Inc(Numeros.x,36);
Numeros.iSubX := iNumero mod 10;
Numeros.Dibujar;
end;

Como solo hay que imprimir una puntuación de 0 a 99 entonces cojo el primer dígito y lo divido por 10 y del segundo dígito calculo el resto. También lo podíamos haber realizado convirtiendo el número a string y luego recorriendo la cadena de caracteres e imprimiendo un dígito por cada carácter.

FINALIZANDO

Así como he mostrado estos ejemplos podía haber escrito miles de líneas más sobre enemigos, animaciones, etc., pero necesitaría un tiempo que no tengo. Creo que lo más importante de lo que he hablado es la renderización de polígonos con OpenGL.

Actualmente estoy pasando este motor a 1024 x 768 y 60 fotogramas por segundo y para probarlo estoy haciendo un pequeño juego gratuito que ya os mostraré por aquí cuando lo termine.

Después de todo esto que os he soltado, ¿quién se anima a hacer videojuegos? No diréis que no os lo he puesto a huevo.

Pruebas realizadas en Delphi 7.

Publicidad