Archive for the 'Proyectos' Category

Eileen, programando el tetris

Sunday, October 15th, 2006

Esta semana me he centrado en Eileen, de cara a dejarla más o menos terminada. Debido a la cantidad de curro por hacer en otros proyectos, me temo que se quedará en este estado durante un tiempecillo. (Lo lamento por Rosa y compañía, viciad@s de tales juegos).

 

Una de cambios

En el post anterior, comentaba que me decidí por usar un sprite por cada pieza. Bien, debido a problemillas varios con el giro de las piezas, opté por volver al planteamiento de formar las piezas a través de cuadritos. Con ello conseguí formar cualquier pieza (antes necesitaba que la pieza comenzara por un cuadro arriba a la derecha que me servía como guía) y tener más control en el giro. Además, a la hora de quitar las líneas no tengo que hacer ninguna conversión rara, simplemente quito las líneas de Matrix (los elementos de la fila que desaparece se resetean a 0) y muevo progresivamente todas las líneas. (El progresivamente es un decir, lo hago todo de un plumazo… pero la idea es que en la 1.0 haya una especie de animación por cada línea que desaparece).

 

Niveles, puntos y game over

Una vez que el esqueleto del programa quedaba operativo.. había que implementar detalles tales como los niveles o la puntuación.

Para el control de niveles me creé otra matriz, declarada como int Levels[2][2];. El 2 hace referencia al número de niveles y, el hecho de su bidimensionalidad se debe a que en cada fila guardo por un lado el número de piezas necesarias para cambiar de nivel (tras generar ese número de piezas, se cambia de nivel) y por otro lado la velocidad de bajada de las piezas. Teniendo una función que setee los valores, en el game loop sólo queda tomar esos valores donde sea necesario.

Para la carga de niveles, utilizo un switch o interruptor… si al entrar en el game loop vale false, cargo el nivel según una variable que me indica el nivel actual; si vale true, no cargo nada.. ;)

En cuanto a la puntuación y el número de piezas generadas, son simples contadores.

Por último, el gamer over sólo consiste en comprobar si la función que valida el movimiento hacia abajo devuelve true o false. Si la pieza no puede moverse hacia abajo (tras ser generada) es que el juego ha terminado, mostrando game over durante unos segundos y volviendo al menú.

 

Siguiente pieza

Otro de los detalles a implementar consistía en mostrar las piezas siguientes. Para ello, de nuevo uso una matriz, esta vez de dimensiones de 4×4 (número máximo de cuadraditos que tiene una pieza). En cada vuelta al game loop, actualizo esa matriz en función de la siguiente pieza a generar, que previamente guardo en una variable. Luego es mostrar el contenido de esa matriz y dibujar de forma pertinente.

 

El menú

Para el menú necesité ampliar Nessi con una clase que no tenía pensada, pero que sin duda ha sido y será muy útil. Nessi_XT_Menu carga los sprites o botones del menú, teniendo soporte para dos estados, el de seleccionado y en reposo. Con funciones miembro, se lleva el control de la opción seleccionada, el paso de una a otra… así que desde el programa sólo hay que cargar los sprites y poquillo más. :D

 

Versión 0.80, en fase beta

La versión subida tiene algunas cosillas que no me ha dado tiempo a implementar i seguro que algún que otro bug no detectado. (El coding express es lo que tiene… je,je,je..). Por ejemplo, las piezas no son generadas de forma aleatoria (o medianamente aleatoria), hay pocas piezas (sólo 5) y como he comentado, sólo dos niveles, muy facilitos. Pero bueno, para hacerse una idea de lo que en un futuro será… ;) :) Está por la sección de software… con un programa llamado installer2go, he generado un instalador, que siempre igual se agradece. ;) :)

 

Versión 0.83, varias mejoras…

Por petición de una de esas amigas aficionada a los puzzles, a noche de 17 de octubre he añadido varias mejoras. En primer lugar he solucionado un bug al término de todos los niveles, que se me había olvidado controlar. En relación a ellos, he ampliado los niveles a 4, aunque sigue habiendo sólo dos mapas (que se van alternando, pero con la velocidad en aumento). Por último, he mejorado la generación aleatoria de piezas… ya sí se puede decir que es aleatoria (los entendidos sabrán que no es del todo cierto, pero el juego generará piezas diferentes en cada ejecución. ;) :)

Eileen, comenzando…

Tuesday, October 3rd, 2006

Este fin de semana he estado ampliando y mejorando a Nessi. Para poner a prueba esas nuevas funcionalidades y con cierta mosca detrás de la oreja (nunca he hecho un tetris), he comenzado Eileen. A parte de los conceptos que todo juego ha de tener, como el de game loop, timing… para este juego tenía que pensar cómo gestionar las piezas y el tablero de juego (el rectángulo que va conteniendo las piezas que se van generando…). Bien…

La matriz Matrix

Pensando un poco sobre ello… una ruta a tomar podría ser basarlo en coordenadas y colisiones de los sprites (o piezas). Sin profundizar demasiado, me viene a la mente alguna de las limitaciones de tal planteamiento… como por ejemplo la eliminación de una fila tras hacer línea. Igual se podría hacer algún tipo de cálculo para solucionar cosas así, pero teniendo la experiencia exitosa de una matriz (en el buscaminas, el ajedrez y otros programillas que tengo por ahí con el interface más que simplón…) por debajo que controle todo… me decanté por ella.

Matrix quedó definida con 20 filas y 14 columnas… (el espacio del tablero dibujado en photoshop entre 25, tamaño del cuadro base). A través de una función de Reset, todos sus elementos son inicializados a cero, código que he tomado como marca de que esa casilla está vacía.

Las piezas

Otro aspecto importante, quizás de lo más importante, son las piezas. Antes he hablado del tamaño del cuadro base. En el tetris las piezas se desplazan por una especie de casillero con celdas (que no se ven, pero cada cuadro que forma una pieza, al desplazarse, se mueve de celda a celda). A donde quiero llegar es a cómo estarán hechas las piezas. Una pieza que sólo ocupe un cuadro o celda en cada movimiento (llamémosla A), no tiene complicación. Pero como es obvio, un tetris de sólo esa pieza quedaría muy pobre y poco jugable. Si meto una pieza que ocupe dos cuadros horizontales.. los problemas surgen. Una de las ideas era formar las piezas por unión de sprites únicos (la citada A): generar el tipo de pieza y en funcion de ello dibujar en pantalla la forma correspondiente, actualizando las posiciones de todos los A de la pieza y de todas las piezas. Mi principal problema con esto era gráfico. Me gustaba que las piezas tuvieran un borde alrededor, así que uniendo A con otra A, en el medio de la unión, tendría un borde algo feote. Como solución a esto podría haberme currado la pieza A sin borde una vez en cada lado… pero decidí optar por otra forma de hacerlo.

Cada pieza es un gráfico. En total, de momento, hay 10 piezas disponibles, que se irán generando con un random y moviéndose por Matrix. A tales efectos, cada pieza irá actualizando los elementos de Matrix que ocupe / libere con cada movimiento. Es decir, la pieza A comenzará en el elemento 0,0. Un ciclo despues… si el usuario no mueve hacia los lados, ocupará la posición 1,0. La manera de indicar ese ocupamiento es mediante números. (El número de pieza (1,2,3… hasta 10)) Para entender mejor a lo que me refiero… mira esta captura. En ella se ve Matrix y todos sus valores. Los números negativos indican que esa posición está ocupada por ese pieza. (-2, por la 2, -4, por la 4…).

Dibujando las piezas

Como comentaba en algún párrafo anterior, Eileen se basa en Matrix. A través del valor de sus elementos, el procedimiento void DrawMatrix(int Matrix[20][14]); dibuja el juego. En una instancia de Nessi_Sprite llamada piece, cargo todas las piezas, con el orden de piezas ya comentado. (la pieza uno la cargo primera, la pieza 2, segunda…). Hecho esto con dos bucles recorro cada elemento de la matriz. Si el elemento es mayor que cero, dibujo la pieza, seteando el frame activo a Matrix[j][k]-1. (El número de frames de un sprite comienza en cero).

/* Dibuja la matrix en pantalla */
void DrawMatrix(int Matrix[20][14])
{
int j,k;

int inicio_row=88;
int inicio_col=15;

Nessi_Sprite piece(9);
piece.AddFrame(”graphics/piece01.png”);
piece.AddFrame(”graphics/piece02.png”);
piece.AddFrame(”graphics/piece03.png”);
piece.AddFrame(”graphics/piece04.png”);
piece.AddFrame(”graphics/piece05.png”);
piece.AddFrame(”graphics/piece06.png”);
piece.AddFrame(”graphics/piece07.png”);
piece.AddFrame(”graphics/piece08.png”);
piece.AddFrame(”graphics/piece09.png”);

for(j=0;j<20;j++)
{
for (k=0;k<14;k++)
{
if (Matrix[j][k]>0)
{
piece.SetCurrentFrame(Matrix[j][k]-1);
piece.SetX(inicio_col+(25*k));
piece.SetY(inicio_row+(25*j));
piece.Draw();
}
}
}
}

Las dos variables int de inicio, es la referencia del width y del height en donde comienza el tablero de juego y a partir del cual basarse en el dibujado de piezas. A pesar de que cada pieza que ocupa varias celdas es un gráfico, a efectos de Matrix cuenta como si fueran varios A, pero en lugar de tomar el valor 1 para todos los cuadros, el primer cuadro de arriba a la izquierda vale el número de pieza y, el resto, el número de pieza en negativo. De ahí el if del segundo bucle. Si por ejemplo se encuentra un 4, con el setframe sabe que ha de dibujar la pieza 4 (la que tiene índice 3 en el array de frames) en la posición x e y calculada.

En la galería he subido varias dos caps del juego y una cap de todas las piezas, para que se entienda mejor la filosofía utilizada. Observando las piezas, es curioso que todas ellas, las disponibles en el juego, tiene el citado cuadrado arriba izquierdo dibujado. Esto es así por el motivo explicado antes: para el dibujado tengo que tener una referencia que me indique qué pieza es. Si el cuadro está vacío… tendría que indicar un cero en lugar del número de pieza, perdiendo esa referencia. Ya se me ocurren algunas cosillas como solución… que para no hacer un post kilométrico comentaré en otra ocasión. ;) :)

Por otro lado, comentar que para el cambio de sentido, como se verá en la cap de piezas, hago un cambio de sprite. Es decir, si tengo una pieza 6… y su pieza girada es, por ejemplo, la número 15… sólo hay que cambiar el id de la pieza actual ajustando el valor de los elementos de Matrix afectados.

Validaciones

Para que el juego de tetris sea tal, es necesario implementar un sistema de validaciones. Que la pieza no se salga del tablero por ninguno de los lados, que una pieza no ocupe una posición o alguna de las posiciones que esté ocupando otra pieza… con Matrix y los valores de sus elementos, se comprueba todo ello según la pieza actual. (básicamente, si hay cero, la celda está libre y se permite el movimiento).

Haciendo línea

Hay que tener en cuenta que cuando una línea esté totalmente llena, hay que borrar esa línea y bajar las superiores a ella. Esto lo logré en tres pasos…

  1. Convierto todas las celdas ocupadas a 1, menos las de la fila que tiene la linea.
  2. Seteo a cero las celdas de la fila que tiene línea.
  3. Muevo progresivamente las celdas hacia abajo. (Matrix[j+1][k]=Matrix[j][k]; ) Para que sea eficiente, una vez una fila es toda de ceros, dejo de mover hacia abajo.

Puntos, velocidad….

Ahora queda llevar el control de puntos, de la velocidad del juego según el nivel… y todos los detalles deseados. Quizá sea la parte menos complicada… así que tampoco entraré en detalles… ;) Todavía me quedan cosillas de esas y algún bug que otro por resolver y mejorar… así que tardaré algo má en publicar el .exe… :roll:

Nessi: modelo singleton

Saturday, September 2nd, 2006

En el último post sobre Nessi, comentaba que había decidido ir montando Nessi sin usar el modelo singleton, sino a base de unas instancias globales, por resultar más cómoda su llamada. Esto traía, a parte del que el usuario podría declarar instancias sin ningún problema, otra serie de problemas por los que he tenido que volver al planteamiento inicial del modelo singleton, para aquellas clases que han de ser únicas y accesibles desde cualquier parte del programa. Y eso de singleton, qué es?

La wikipedia (como de costumbre, je,je,je…), en este artículo, lo explica muy bien. Como reza el enlace, es una forma de lograr que sólo haya una instancia de una determinada clase en ejecución. Para lograrlo.. hay que meter en constructor de la clase dentro de la parte privada, para que sea la propia clase desde dentro quién cree la instancia y, si ya está creada, devuelva su dirección. El proceso encargado de realizar esta tarea será otra función, esta vez pública, en el caso de Nessi, static Nessi* GetNessi();. Para acceder a sus miembros… se hace algo como Nessi::GetNessi()->miembro. Para evitar escribir tanto, en cada función miembro de las otras clases que hacen uno de ella o en el propio programa principal, uso variables locales.

Nessi *nessi_local;
nessi_local=Nessi::GetNessi();

Y luego ya trabajo con nessi_local, haciendo referencia del mismo modo que antes, nessi_local->miembro;.

Con el cambio de filosofía, me sigo ahorrando el paso de parámetros (el miembro screen de Nessi, por ejemplo, es accesible desde cualquier punto del programa y de la propia librería, al igual que el sistema de log, el de tiempo, el de efectos especiales….), siendo cómodo y teniendo más control sobre el asunto. ;) Así que, con un nuevo camino abierto, queda seguir migrando / implementando las clases y funciones… La documentación estará basada en la que genera un programa llamado doxygen… (google :)), sino recuerdo mal open source. Genera HTML y varios formatos más en modo muy guapete (organizado y vistoso), muy recomendado.. :D

pd.- Hasta después del deadline del proyecto con UC, no tenía pensando seguir con Nessi.. pero el otro día tuve un ratillo muerto…. así que me puse a probar, funcionó… y migré lo básico al nuevo modelo. Para la semana que viene, publicaré una pequeña (realmente pequeña) demo… un par de fades, algún efecto de sonido y poco más… todavía queda bastante curro para sacar una versión estable… pero bueno… coincidiendo con el rule de esa demo a los compañeros de curro….

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

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.