06 noviembre 2009

El experto cnPack (2)

Vamos a entrar en profundidad con todas las características que tiene este fantástico experto para Delphi comenzando con la barra de herramientas que aparece en la parte superior del editor cuando estamos modificando un formulario.

ALINEAR LOS COMPONENTES DEL FORMULARIO

Al editar un formulario en Delphi 2007 nos aparece una barra con tantos botones en la parte superior que debemos esconder los paneles laterales:

Para verlo detenidamente hay que hacerlo por grupos de izquierda a derecha. El primer grupo de botones que tenemos a la izquierda es el encargado de alinear dos más componentes gráficos que tengamos seleccionados:

Estos botones nos evitar tener que utilizar la alineación de componentes de Delphi que resulta más incómoda porque hay que sacarla con el botón derecho del ratón:

Y para colmo luego hay que utilizar este formulario:

Con estos tres botones que tenemos en la barra de cnPack podemos alinear a la izquierda, a la derecha o centrarlos. Los siguientes tres botones hacen exactamente lo mismo pero con la alineación vertical:

Los siguientes tres botones son los encargados de espaciar verticalmente los componentes seleccionados manteniendo exactamente igual el espacio entre los mismos:

Aunque hay una diferencia respecto al IDE de Delphi: el primer botón permite realizar un espaciado de componentes diciéndole nosotros cuantos pixels queremos exactamente de separación entre componentes. Por ejemplo, supongamos que queremos espaciar verticalmente estos componentes con 7 pixels entre ellos:

Los seleccionamos y pulsamos este botón:

Entonces se abrirá este cuadro de diálogo preguntándonos cuantos pixels queremos de separación:

Después de pulsar Ok cumplirá con su objetivo a la perfección:

Esto es ideal cuando se trabaja con un grupo amplio de programadores y cada uno monta los formularios como quiere. El jefe de proyecto puede obligar a los programadores a que diseñen la interfaz con las mismas medidas.

Los otros dos componentes son para que exista el mismo espacio entre todos los seleccionados o para pegarlos todos quitando espacios:
Tenemos exactamente los mismos tres botones pero para la separación horizontal:

REDIMENSIONAR LOS COMPONENTES DEL FORMULARIO

Los siguientes cuatro botones serán los encargados de redimensionar los controles seleccionados según nuestras preferencias:
Los dos primeros botones incrementan o decrementan el tamaño de los componentes en unidades de la rejilla. Por ejemplo, si tenemos estas dimensiones:

Al pulsar este botón:
Incrementará su tamaño vertical por su parte inferior:

El botón de la lado hace el efecto contrario. Luego está este botón que amplia el tamaño de todos al más grande (verticalmente):
Y el último los disminuye verticalmente al más pequeño:

Los siguientes cuatro botones cumplen el mismo cometido pero horizontalmente:
Pasemos al siguiente grupo:
El primer botón es bastante interesante porque permite establecer el tamaño horizontal y vertical de los componentes seleccionados a partir del primero seleccionado. Por ejemplo, vamos a hacer que este los dos componentes de abajo tengan el mismo tamaño que el primero que hemos seleccionado:

En vez de seleccionar los tres componentes a la vez, los seleccionamos secuencialmente y luego pulsamos el botón:
Y los igualará en tamaño al primer componente:

Los siguientes dos botones centran en el formulario todos los componentes seleccionados pero manteniendo su posición original entre ellos:
Si originalmente tenía así el formulario:

Después de seleccionar los tres componentes y pulsar ambos botones lo centrará todo:

Y los dos últimos botones de este grupo se encargan de traer al frente o enviar al fondo los componentes seleccionados:

ALINEAR COMPONENTES RESPECTO A LA REJILLA

Vamos con el siguiente grupo:
El primer botón se encarga de ajustar la posición de la esquina superior izquierda del componente seleccionado a la cuadrícula de la rejilla:
Y el segundo ajusta a la rejilla el tamaño horizontal y vertical del componente:

El siguiente botón también es muy interesante porque funciona en estado apagado o encendido. Cuando esta activado, al redimensionar un componente con el ratón veremos que se ajusta a la cuadrícula:

Sin embargo, podemos ampliar el tamaño del mismo de píxel en píxel a la vieja usanza con el teclado: tecla SHIFT + CURSORES. Este otro botón activa o desactiva la guía que permite alinear unos componentes respecto a otros:
Es la guía que incorporan por defecto las últimas versiones de Delphi:

Este botón no está disponible en Delphi 7. Y el último botón de este grupo bloquea los controles del formulario para que no puedan ni moverse ni redimensionarse:

SELECCIONANDO COMPONENTES

Vamos con el último grupo de la barra superior:
El primer botón nos sirve para seleccionar el formulario donde estamos situados cuando el foco lo tenemos sobre otro componente:
Esto viene bastante bien cuando diseñamos nuestra propia piel para el formulario y tenemos tantos controles encima del mismo que no podemos ni hacer clic para poder acceder a sus eventos a través del inspector de objetos. Si el formulario ya se encuentra seleccionado entonces se desactiva este botón.

El segundo botón copia los nombres de los componentes (propiedad Name) seleccionados al portapapeles:

El tercer botón si es bastante interesante. Nos devuelve el código fuente de los componentes seleccionados como si los hubiésemos creado por código:
Por ejemplo, al seleccionar estos componentes:

Y pulsar luego dicho botón aparecerá este formulario:

Nos devuelve el código de creación de los componentes con todas sus propiedades, algo muy útil cuando queremos esconder algún componente del archivo DFM como por ejemplo: registrar el producto, introduzca la clave de licencia, etc. Sólo hay que copiar este código en el evento OnCreate del formulario y eliminar los componentes del mismo.

El cuarto botón es bastante curioso:
Hace invisibles en tiempo de diseño los componentes no visuales como por ejemplo un TIBQuery:

Al pulsar este botón pasa esto:
Para que se vuelva invisible primero hay que seleccionar otro componente. La única utilidad que le veo a esto es cuando queremos mostrar a otros programadores o clientes como va el diseño de nuestro formulario sin que se vean estos componentes. Si volvemos a pulsar este botón se harán visibles de nuevo. Esto no afecta para nada a la aplicación, sólo a la fase de diseño.

El último botón si que tiene bastante utilidad:
Cuando tenemos varios componentes no visuales desordenados en el formulario:

Podemos reorganizarlos todos pulsando este botón y nos aparecerá un formulario para preguntarnos donde los queremos colocar y la separación entre los mismos:

Por defecto los coloca en la esquina superior izquierda del formulario:

Continuando con nuestro tema de selección de componentes vamos a comenzar con el primer grupo de la barra inferior:

El primer botón activa la selección múltiple:
Al pulsarlo nos aparece este mensaje:

Cuya traducción es esta:

Bienvenido a selector de componentes!

Este asistente puede seleccionar componentes en forma actual por nombre, tipo o otras condiciones a pesar de su visibilidad. Por ejemplo, si queremos cambiar la altura de todos los botones de 25 a 21, podemos seleccionar todos los botones y cambiar su altura una vez. Se admiten varios filtros y clasificación. A veces es útil modificar los pedidos de componentes seleccionados.

Después de pulsar el botón Continue nos aparece este formulario:

Solo hay que pasar de la lista de la izquierda a la lista de la derecha los nombres de los componentes que queremos seleccionar:

Pulsamos el botón Ok y volverá al formulario seleccionando los componentes que hemos elegido. Ya podemos ir al inspector de objetos a cambiar sus propiedades.

El segundo botón es un corrector de propiedades donde podemos configurar como queremos que ciertas propiedades de los componentes se cumplan:
Un ejemplo que lleva es que todos los botones (TButton) del formulario deberían tener una altura de 21 pixels. Al pulsar este botón aparecerá esta ventana en la que debemos pulsar el botón Search para buscar componentes que no se ajustan a nuestras reglas:

En este caso nos dice que el botón tiene una altura de 25 en vez de 21. Debemos cambiarlo nosotros manualmente en el inspector de objetos (hubiese sido ideal que lo hiciera él por nosotros). ¿Dónde están esas reglas? En el botón Options:

Podemos añadir cualquier componente y establecer el valor de debería tener cierta propiedad. Es otra herramienta ideal para forzar a los programadores a hacer las cosas del mismo modo que el resto del equipo. Al pulsar el botón Add debemos rellenar todo esto:


RENOMBAR COMPONENTES

Habréis visto que cuando copiamos o insertamos un componente en el formulario aparece un nuevo asistente que nos indica el prefijo que debemos dar al componente según su clase (notación húngara). Por ejemplo, al insertar un botón Aceptar, su nombre debería ser btnAceptar:

Si en la misma ventana pulsamos el botón Modify Prefix podemos elegir otro prefijo para esta clase:

Pues bien, si a partir de ahora queremos utilizar esta metodología y no queremos pegarnos la paliza cambiando el nombre de cada componente podemos utilizar este botón:

Al pulsarlo nos describe primero para que se utiliza:

Al pulsar el botón Continue nos aparece otro cuadro de diálogo donde podemos especificar si queremos cambiar solo este formulario o todos los formularios del proyecto:

Al pulsar el botón Process veremos una lista con los componentes que ha encontrado que no se ajustan a esta normativa y el nuevo nombre que les va a dar:

Al pulsar Ok hará todo el trabajo por nosotros. Lo único que no me hace gracia de este sistema este también me cambia la propiedad Caption de los mismos:

Y he estado mirando sus opciones (Settings) pero no he encontrando nada que desactive esto:

El último botón de este grupo se utiliza para renombrar el componente seleccionado sin tener que recurrir al inspector de objetos:
Nos mostrará la ventana que vimos anteriormente:

Esto sería para hacerlo uno a uno. Y también nos hace la faena de cambiarnos las propiedades Caption o Text, según el componente.

CONFIGURAR LA TABULACIÓN

Vamos con el penúltimo grupo:
El primer botón que aparece seleccionado sirve para activar o desactivar el número de tabulación que aparece en cada componente del formulario:

Si lo desactivamos se queda como cualquier Delphi sin cnPack:

El siguiente botón es bastante bueno para reordenar el orden de tabulación de los componentes:

Si tenemos el orden de tabulación desordenado:

Con sólo pulsar este botón nos quitará mucho trabajo:

Aunque parezca que no funciona al principio, pinchamos sobre cualquier componente y refrescará el formulario. Si este botón lo dejamos activado se encargará automáticamente de hacer la tabulación de izquierda a derecha y de arriba abajo.

El siguiente botón reordena también las tabulaciones pero no se queda activado:

Y con último botón de este grupo podemos arreglar la tabulación te todos los formularios del proyecto:


BUSCANDO FORMULARIOS Y COMPONENTES

Por fin llegamos al último grupo:
El primer botón es para la búsqueda de formularios en el proyecto:

El segundo botón si que es realmente útil para buscar componentes en el formulario actual (sobre todo cuando tenemos más capas que una cebolla):

El último botón es el más avanzado de todos:
Al pulsarlo aparece un menú contextual con estas tres opciones:

Mediante la opción ScriptWizard podemos incrustar dentro de nuestro programa un lenguaje script llamado RemObjects Pascal Script que permite modificar el comportamiento de nuestro programa sin tener que compilar cada vez:

Al pulsar el botón New a script nos creará una unidad nueva como si se tratase de un programa independiente:

También podemos pulsar el botón + para insertar script predefinidos que lleva dentro:

No voy a hablar sobre como crear estos script porque la ayuda que lleva el mismo es bastante buena y con muchos ejemplos. Quizás si me sobra tiempo, al final de esta serie de artículos pueda abarcar este tema, ya que tiene muy buena pinta. La opción Browse Demo nos muestra gran cantidad de ejemplos que han añadido los creadores de este experto.

En el próximo artículo veremos la barra de herramientas que se muestra cuando estamos editando código fuente.

Pruebas realizadas en Delphi 7 y Delphi 2007.

30 octubre 2009

El experto cnPack (1)

Pese a que las últimas versiones de Delphi (sobre todo Delphi 2010) incorporan gran cantidad de características en su IDE, hay que reconocer que algunas de ellas todavía no están a la altura de otros expertos realizados por terceros, como por ejemplo EurekaLog.

cnPack incorpora tal cantidad de ampliaciones al IDE de Delphi que le dan una gran fortaleza al programador con características que se encuentran en otros entornos como Visual Studio. Y lo mejor de todo: es completamente gratuito y funciona para todas las versiones de Delphi (incluidas de la 7 hacia atrás).

DONDE DESCARGAR CNPACK

Aunque los programadores de este experto son chinos, si entramos en su dominio nos redireccionará automáticamente a una página en inglés:

http://www.cnpack.org

He marcado en un círculo rojo la última versión que hay disponible a la fecha de este artículo.

INSTALAR CNPACK

La instalación tiene un tamaño de 10,8 MB y debemos cerrar todas las versiones de Delphi que tengamos abiertas antes de continuar:

En uno de los pasos de la instalación nos marcará en negrita todas las versiones de Delphi que ha encontrado instaladas en nuestro equipo y por defecto estarán todas seleccionadas:

Si la versión que tenéis de Delphi 2007 no tiene instalados los últimos Updates puede que os aparezca un mensaje advirtiendo que el IDE podría ser inestable:


Aunque yo lo tengo instalados estos parches y va perfectamente. Se me ha colgado alguna vez el IDE pero no creo que haya sido precisamente por el cnPack. Antes también lo hacía, así que…

TRABAJANDO CON CNPACK

Vamos a ver que aspecto tiene nuestro entorno de trabajo cuando instalamos este experto. Lo veremos desde el punto de vista de Delphi 7 y de Delphi 2007.

Este es el aspecto que tiene Delphi 7 al arrancar con este experto:

Lo primero que se aprecia es el chorro de iconos nuevos encima del editor de código:

Además nos aparece a la izquierda del editor de código algo que no tenían las versiones de Delphi 7 y anteriores, la numeración de líneas:

Pero es que si nos fijamos en el código fuente veremos que a añadido un nuevo sistema de coloración de palabras reservadas de modo que según el nivel de identación tienen un color u otro:

De un solo vistazo podemos saber si todos los begin cierran correctamente con su correspondiente end. También podemos apreciar en la imagen que nos ilumina la línea actual donde estamos situados. Luego veremos más adelante como activar y desactivar todas estas opciones.

El aspecto que tiene Delphi 2007 a arrancar con este experto es el siguiente:

Aquí se produce la primera contradicción: nos aparecen los números de línea de cnPack mas los números de línea que ya lleva el IDE de Delphi:

Lo lógico sería desactivar los números de cnPack seleccionando en el menú superior cnPack -> Options:

O bien pulsando este botón en la barra de herramientas que hay encima del editor de código:

En la ventana que aparece seleccionamos el apartado Editor Enhacements:

Pulsamos el botón Settings que hay a la derecha y aparecerá esta otra ventana:

Seleccionamos la pestaña Line Number / Toolbar y desactivamos la opción Show Line Number in Editor:

Pulsamos Ok en esta ventana y en la anterior y ya tenemos Delphi 2007 listo para trabajar:

Aunque todavía hay dos cosas que por lo menos a mí nunca me han gustado en las versiones modernas de Delphi: una es el resaltar la línea donde estamos situados y otra es que me ilumine los paréntesis donde tengo situado el cursor (los programadores no somos tan cegatos):

Para quitar ambos volvemos a abrir la ventana de opciones y seleccionamos Source Hightlight Enhacements:

Pulsamos el botón Settings y desactivamos estas dos opciones:

Hay otro tipo de iluminación que tiene que enciende las palabras begin y end cuando nos situamos sobre una de ellas:

Esta no merece la pena quitarla porque viene muy bien para saber que end cierra cada begin. De todas formas, si queréis quitarla se encuentra en la ventana de opciones que hemos visto anteriormente:


OTROS CAMBIOS QUE SE APRECIAN EN EL IDE

Si nos ponemos a modificar un formulario veremos que en la esquina superior izquierda de cada componente veremos su número de orden de tabulación:

Esto viene muy bien para detectar fallos en la tabulación sin tener que sacar la lista de componentes:

También veremos que se han añadido checkbox a las propiedades booleanas del inspector de objetos:

Si buscamos una unidad pulsando la combinación de teclas CTRL + F12 o pulsamos este botón:

Veremos como cambia la lista de unidades:

Incorpora en la parte de arriba un buscador que nada más comenzar a teclear nos filtrará las unidades que contienen dicha cadena de texto:

Lo mismo sucede si buscamos un formulario pulsando SHIFT + F12:

En el próximo artículo entraré mas en detalle entrando en cada una de las características que añade a Delphi y recomendaré aquellas de deberíamos quitar porque son más una molestia que una ayuda.

Pruebas realizadas en Delphi 7 y Delphi 2007.

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.

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.

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