Archive for July, 2006

Map Scrolling y niveles

Wednesday, July 19th, 2006

Días antes de parón obligado por temas campamentiles y con ritmo frenético de contrareloj, Nessi va cogiendo colorcillo, con sus ya cercanas 2000 líneas de código. Uno de los temas que más problemillas me ha estado dando estos días ha sido el scrolling de los mapas. Tras realizar las mil y una pruebas y de seguir tres filosofías distintas a través de 3 versiones diferentes de la función miembro de la clase MTile void Draw(SDL_Surface *srf);, por fin di con una solución efectiva. Básicamente, he seguido la técnica que se sugiere en el libro comentado en posts anteriores sobre SDL, adaptándola a Nessi y al formato propio de mapas. De forma paralela, he dejado habilitada otra función miembro que hace lo análogo, pero con los archivos .map generados por un editor de mapas llamado Mappy. De este editor no me convencen los formatos gráficos aceptados (sólo .bmp y .png), así como su configuración a la hora de generar el fichero binario donde guarda la info del mapa. No obstante, viene de perlas para mapear un escenario complejo, así que hasta que Nessi tenga su propio editor de mapas, utilizaré este programa para fases complicadas de mapear con los ficheros de texto que maneja el engine.

Esta función Draw, al igual que todas las que llevan ese mismo nombre o preposición, se encarga de dibujar en pantalla ciertos elementos, en este caso, todos los frames / tiles que componen una fase o nivel. Para ello, se sirve sólo de un parámetro, la superficie en donde dibujar o realizar su misión. Como comentaba antes, costó bastante lograr el efecto deseado, a pesar de que en sí la función no es larga ni tiene mucho código. Pero hay que pillar bien el concepto…. En primer lugar, añadí el miembro scroll a la clase MTile. Esta variable controla el progreso de cada frame, es decir, en un principio logré que el mapa hiciera scrolling, pero tile por tile, dando una sensación de saltos entre cada vuelta al game loop poco vistosa. Para evitar esto, la variable scroll controla que cada frame se vaya mostrando progresivamente, sin salto alguno de tile en tile. En cada llamada, aumenta en uno, (scroll++), siempre y cuando no sea mayor al tamaño de cada tile (height o altura). Si es mayor, scroll se resetea a cero, para que cuente de nuevo en el siguiente tile.

Por otro lado, hay que tener en cuenta que el mapa está cargado en la matriz miembro llamada ArrayTiles. En ella, los elementos están puestos según se fueron leyendo: los primeros corresponden a los primeros números del fichero y los últimos a los últimos leidos del fichero. (Es decir, de arriba a abajo). La nave protagonista dará la impresión de que se mueve hacia arriba (de abajo a arriba), por lo que se debe comenzar a dibujar el mapa desde las últimas posiciones del array e ir restado a un contador el número de tiles por fila, hasta llegar a la primera fila y con ello al fin del mapa. Puesto que en cada llamada ha de conservarse ese contador, añadí otro miembro a la clase para tal fin, index. Al tratarse de tiles de 40×40, en la pantalla caben 20 filas por 15 columnas, debiendo hacer en cada vuelta que scroll>=40, index-=20; y el citado scroll=0; Para que luego no se nos pase de rango, dejaremos de restar a index cuando lleguemos al principio de la matriz. (Recorremos la matriz de 20 en 20, desde el final al principio).

Y todo esto, para qué? El último paso es dibujar en pantalla lo calculado anteriormente, aquella parte del mapa que ha de ser dibujada. Siendo un mapa de dos dimensiones, está claro que se tratará de dos bucles for anidados. Calcular la posición x de cada tile, es simple, x*40, dentro del bucle for(x=0;x<20;x++) (el anidado). En cuanto a la y, habrá que hacer (y-1)*size_frame+scroll;. Con ello, tenemos la coordenada Y del tile a dibujar, con el scroll que le corresponde. Por último, el índice lo obtenemos de indice=index+(y*20+x); (El elemento del array de tiles, ArrayTiles, que se debe dibujar). Dicho en código, quedó como:

for(y=0;y<16;y++)
{
for(x=0;x<20;x++)
{
rect.x=x*size_frame;
rect.y=(y-1)*size_frame+scroll;
indice=index+(y*20+x);
SDL_BlitSurface(ArrayTiles[indice].frame,NULL,srf,&rect);
}
}

obteniendo un efecto de scroll suave. La idea es que haya dos tipos de niveles: uno por tiempo y otro objetivo de fin de fase. El primero, será un mapa infinito (finito, pero que se repite x veces o durante x tiempo) y el segundo será un mapa finito, una vez te termine el mapa, se quedará en ese lugar hasta cumplir el objetivo final, normalmente destruir un enemigo final. Por este motivo, tendré que retocar esta función Draw con algún código extra, que me controle cómo actuar en cada caso.

Además, estos días he realizado la estructura básica del sistema de niveles, una vez se pulsa en nuevo juego, el engine va generando los niveles… carga de nivel uno, nivel uno se genera hasta el fin de mapa, carga del nivel dos… y así sucesivamente… Para controlar todo esto y con miras ya a realizar un engine (o sentar sus bases) standar e independiente, creé una nueva clase llamada Nessi_Eng, donde se guardan todos los datos relativos al juego, como la resolución de pantalla, las funciones que inicializan modos gráficos, que vuelcan el buffer a la pantalla… y el nivel en el que se está en cada momento, así como un flag para indicar si el nivel ha sido o no ha sido cargado. Ya en el game loop, se comprueba con if(!Nessi_SEng.SayLevelLoaded()), esto último, cargando el nivel que toque en caso de no estar cargado. Después, se pregunta con una nueva función miembro si el mapa ha terminado o no, y en función de eso, sigue normalmente o resetea el flag de nivel a false (nuevo nivel no cargado) y a nivel actual a nivel actual++; Si estamos en la última fase, a su término, se regresa al menú.

Los siguientes pasos con los que ya estoy metido, son el gestor de balas y disparos y los enemigos, de momento con IA muy muy básica.

Tiles y mapeado

Wednesday, July 19th, 2006

En el post anterior, comentaba la existencia de problemillas varios con el sistema de carga de mapas de Nessi Engine. La principal traba es que al ser un fichero de texto (para facilitar la edición del mapa a mano, desde un bloc de notas mismamente) hay que leerlo como tal. Una primera opción es leer caracter a caracter, quedándome sólo con los números (que han de ser de una cifra). La función standar get(), hace esa función, pero devuelve ese char como unsigned char convertido a integer, es decir, el código ascii del caracter leido. (Si lee un cero, por ejemplo, file_map.get() devuelve un 48 ). Jugando con el código ascii y restando 48 al número sacado, la conversión al número leido estaba hecha. (por ejemplo, 48-48=0, lo que habíamos leido). Hasta ahí bien, pero eso da sólo 10 combinaciones posibles o, mejor dicho, sólo puedo tener en el array de recursos 10 frames, para poder referenciarlos a todos (su índice, del 0 al 9, pues para que lo anterior funcione en el fichero solo puede haber números de una cifra). Esto se queda corto, 10 frames para confeccionar mapas interesantes es bastante escaso. Intentando tirar del hilo, también podría utilizar las letras, y nuevamente hacer esa conversión a un número restando al ascii. Con varios ifs controlaría en que rango está, y en función de eso, restaría un número al otro. (de 48 a 57, restaría 48 y de 65 (A) a 90 (Z), restaría 55). Si además se incluyen las minúsculas, tendría un mayor número de recursos posibles a referenciar. (Ahora en el fichero, no sólo habría numeros, sino letras que la función SetLevel se encargaría de pasar a números válidos). Pero a pesar de todo, sigue resultando algo escaso, con unos 50 o 60 recursos a tener como máximo. Igual puede que este sistema nunca se me quede corto, pero nunca me ha gustado tener este tipo de limitaciones, así que toca cambiar de estrategia o filosofía.

Algo claro es que el fichero ha de seguir un patrón, tanto para que siga siendo fácilmente editable como para que la función que lea el fichero pueda interpretarlo sin demasiados problemas. Tener por ejemplo 1 45 234 en el fichero, no sería apropiado para este planteamiento. Sin embargo, un buen formato sería 000.000.000…. es decir, grupos de 3 caracteres (numéricos) y un punto de separación entre grupos. Este punto sólo sirve para facilitar esa edición, viendo de forma clara la separación entre grupos. El fichero, además, conserva las 20 columnas (cada columna ahora es cada grupo) y nos da un abanico de hasta 1000 recursos a referenciar, desde el 000 (que sería el 0) hasta el 999. El flamante fichero ya está definido, cumpliendo la premisa de seguir un mismo patrón. Ahora toca implementar la función que lo lea…

La función getline, presente en File input stream class o ifstream, lee una línea. Ahora, leer caracter a caracter no interesa, optando por ir leyendo línea a línea, tratándolas según son leidas. Por tanto, codificando file_map.getline(linea,81);, se obtiene toda una línea ( 000.000.000…..) y se almacena en la variable linea. Con un bucle for, se lee por partes esa línea sacada… for(j=0;j<80;j+=4). Incremento la i en 4, pues es el tamaño de los grupos. Dentro del bucle, se trata caracter a caracter cada grupo, eliminando los ceros a la izquierda, a través de dos ifs. Si linea[j]=0, significa que prescindiremos de ese dígito, fijandonos en el siguiente, linea[j+1]. Si éste también es cero, sólo nos quedaremos con la última cifra del grupo, linea[j+2]. Pero sigue siendo necesario tener un int, para el índice del array de recursos. La función atoi(), presente en stdlib.h, convierte una cadena de caracteres en una variable de tipo integer. Si se logra tener una cadena que contenga el número en un formato adecuado para ser convertido, tendremos solucionado el problema. Es obvio que si los grupos son de 3 digitos, el número mayor después de quitar los ceros a la izquierda, será de 3 digitos. Teniendo un array como char catet[4];, podremos almacenarlo en forma de cadena. (3 posiciones para los digitos y otra para el fin de cadena o \0). Por ejemplo, en el caso que un grupo sea como 005

catet[0]=linea[j+2]; // Paso el último digito del grupo
catet[1]=’\0′; // Añado el final de cadena
num=atoi(catet); // Convierto a integer

Y una vez generado num para todos los casos (número de una, dos o tres cifras), se indica el frame a cargar en objeto MTile del nivel (st), st.AddFrame(maps.ArrayTiles[num].name);. Si en el fichero hay un 012, num valdrá 12, seleccionandose del array de recursos el elemento con índice 12. Si vale 000, num vale 0 y se selecciona el primer elemento, con índice cero, del susodicho array de recursos. La función donde se hace todo esto es bool SetLevel(MTile &st, MTile maps, char *level), que devuelve true si todo fue ok, o false en caso contrario. (comentada en el anterior post, como void).

Lo siguiente será implementar su scrolling… :P