Cavebola / Close To Metal

Pantalla Inicio Nivel 1 Nivel 2 Pantalla Game Over
Cavebola

Descargar Juego Descargar Código Fuente

Historia

Mi nombre es Roguelio y soy un científico español que estaba en busca de un tratamiento para el virus del Ebola. Después de unos días en África, por fin encontraba una cura efectiva. Sin embargo, de vuelta a España me caí en una enorme cueva y he perdido el antídoto. Los componentes de la cura se han perdido en la inmensidad de esta cueva interminable, debo encontrarlos… soy la única esperanza para España y para Excalibur…

Cómo jugar

El personaje (Roguelio) se puede desplazar por todo el juego usando 4 teclas:
– MOVER ARRIBA : FLECHA ARRIBA
– MOVER ABAJO : FLECHA ABAJO
– MOVER IZQUIERDA : FLECHA IZQUIERDA
– MOVER DERECHA : FLECHA DERECHA
Para realizar un ataque simplemente has de moverte hacia el enemigo.

Existen tres pociones verdes (partes del tratamiento para el Ebola) dispuestas en cada nivel. Recolectar estas pociones es un requisito necesario para poder subir por la escalera y pasar así al siguiente nivel. Estas pociones tienen el poder de curar a Roguelio un poco, pero en cuanto cargues las pociones Roguelio se moverá más despacio debido al peso. Existe también un objeto especial en ciertos niveles que incrementa los niveles de defensa del personaje.

Existen tres diferentes enemigos en el juego:

– ESQUELETO: se trata de un esqueleto blanco que se mueve a la misma velocidad que Roguelio, aunque a veces por eso de ser solo hueso se puede llegar a tropezar. Este enemigo patrulla por algunas áreas del mapa. Ataca cuerpo a cuerpo y nunca se echará atrás en un enfrentamiento. Si consigues despitarlo, patrullará por la zona donde te perdió para buscarte.

– MURCIELAGO: consiste en un murciélago negro que se mueve incluso entre las paredes, pues es muy escurridizo, y es capaz de recuperar vida mientras cuelga de las mismas. Pueden oírte si te acercas lo suficiente, en cuyo caso se dirigirán hacia la fuente del sonido y a medida que se acercan serán más precisos en sus movimiento hasta ir directos hacia Roguelio. Este enemigo es asustadizo y si lo golpeas huirá.

– TIRA-MOCOS: este es un enemigo especial. Tiene el aspecto de un zombie verde el cual no se moverá pero mirará en todas direcciones para buscarte. Ten cuidado porque lanza unas bolas verdes de mocos.

Adelante. Intenta pasar tantos niveles como puedas, pero no te confíes: el nivel de dificultad irá aumentando hasta convertirse en un infierno.

Idea y desarrollo

El nacimiento de Cavebola

Juego Cavebola

Pantalla con el menu del juego

Cavebola nace principalmente porque notamos la ausencia de juegos de rogue-like para Amstrad CPC. Además, observamos que este género nos permitiría cumplir con todos los requisitos de la asignatura en cuanto a IA e incluso diseñar e implementar ideas llamativas que se nos fueron ocurriendo durante tormentas de ideas como mapas aleatorios, scroll…

El nombre surge inicialmente del español Cuébola, en el momento de diseñar el juego el virus ébola tuvo un episodio que despertó sentimientos apocalípticos en la España inculta, la historia entonces estaba clara: reivindicar el papel científico que juega España en la actualidad y a la vez realizar una sátira a la enfermedad. Para darle un poco de carácter internacional al nombre vimos que Cavebola encajaba perfectamente.

El nombre del personaje no podía ser otro que ROGuELIO, muy español.

Pasos de elaboración

En primer lugar realizamos una sesión de brainstorming para decidir la idea principal del videojuego.

Seguidamente establecimos los frentes a atacar basándonos principalmente en los criterios de evaluación de la asignatura, decidimos darle mucha importancia tanto a la IA como al pathfinding.

Cada miembro del equipo fue asignado a una tarea específica e independiente, realizamos además una reunión todas las semanas para poner en común lo aprendido, los progresos y decidir el trabajo a realizar para la semana siguiente. Además, cada semana fuimos probando el juego repetidas veces en un Amstrad físico.

En las primeras semanas realizamos unas pruebas sencillas para comprobar cómo dibujar y reproducir sonidos tanto con firmware como sin él, así como instalar el toolchain para desarrollar en C. Las dos primeras semanas fueron por lo tanto dedicadas principalmente a leer tutoriales, aprender el funcionamiento del sistema y a realizar pruebas preliminares.

La tercera semana la dedicamos a crear lo que podríamos llamar el motor del juego. A su finalización ya disponíamos del dibujado del mapa y scroll por habitaciones, también de capacidad de movimiento para el personaje y detección de colisiones, así como un sistema de sonido para música de fondo sin efectos, generación básica del mapa sin optimizaciones y una IA básica que simplemente perseguía al jugador.

La cuarta semana la dedicamos a implementar el sistema de combate y diseñar la IA, así como mejorar el sistema de sonido para introducir efectos, pulir el sistema de generación de mapas para mejorar su eficiencia espacial y su calidad así como mejorar el pintado de sprites para añadir máscaras y preprocesado del mapa.

La última y quinta semana fue dedicada principalmente a implementar la máquina de estados para la IA, integrar el sistema de pathfinding con los enemigos y pulir aspectos de jugabilidad, interfaz con el jugador, menús, cargador de imagen y preparación del despliegue del juego tanto en formato CDT como DSK.

Los últimos minutos fueron dedicados a corregir bugs…

Tecnologías y Problemas

Gráficos

Sprites del juego

Sprites del juego

Partimos en un principio de los tutoriales de AmstradESP para generar gráficos en BASIC copiando en memoria mediante el uso de POKE los valores de los píxeles. Estos primeros pasos nos permitieron aprender la organización de la memoria de vídeo así como la disposición de los bits de los píxeles dependiendo del modo de vídeo utilizado 0, 1 ó 2. Gracias a ellos aprendimos el comienzo de la memoria de vídeo en 0xC000 y los saltos de 0x0800 que había que dar para pintar en la siguiente fila de píxeles verdadera en pantalla y no física en memoria.

Dado que queríamos realizar nuestro juego en C/ASM echamos un vistazo a los tutoriales de CPCMania que suponen una gran introducción a los gráficos en Amstrad CPC. En ellos aseguramos nuestros conceptos sobre la organización de la memoria de vídeo y de los píxeles a la vez que nos dimos cuenta de que no sabíamos tanto como creíamos pues en un principio no fuimos capaces de crear una función para dibujar un sprite de tamaño aleatorio en pantalla.

Uno de los problemas que nos encontramos durante estos tutoriales fue la extraña gestión del paso de parámetros a funciones que realizaban. Básicamente, existen dos formas de extraer parámetros de la pila de llamada: utilizar el registro índice IX o utilizar las instrucciones pop y push propias de la pila. El SDCC en sus últimas versiones omitía una serie de instrucciones necesarias para hacer que el registro IX apuntara directamente al SP (stack pointer, pila de llamada) por lo que los tutoriales que empleaban el registro IX para extraer los argumentos de la llamada fallaban miserablemente. La solución a este problema fue o bien emplear las instrucciones pop/push o bien añadir el flag –oldralloc a la línea de compilación para hacer que SDCC cargara correctamente el SP en IX.

Gracias los tutoriales de Mochilote conseguimos realizar todo aquello que íbamos a necesitar para nuestro juego de cara a los gráficos: dibujar sprites, moverlos, limpiar la pantalla, aplicar transparencias, etc. Sin embargo, nos dimos cuenta de que el código de los tutoriales estaba lejos de ser óptimo dado que era muy genérico. Por ello, decidimos que, dado que en nuestro juego emplearíamos sprites de 8×8 píxeles (4×8 bytes, la decisión obedece a que al no usar más de 8 píxeles de alto, no necesitamos hacer cálculos de direcciones de memoria complicados al salir fuera del espacio de vídeo; únicamente tendremos que incrementar en uno por cada byte de ancho y una vez alcanzado el ancho incrementar 0x0800 para saltar a la siguiente fila desde la posición original de la anterior.) y que podríamos codificar una función para pintar sprites de 4×8 bytes sin ningún tipo de comprobaciones adicionales. Para ello empleamos la instrucción LDI para cargar bytes a memoria ( (DE)

Posteriormente decidimos aplicar esta rutina de dibujado para pintar un mapa de tiles completo en Modo 0 con tiles de 8×8 píxeles (un mapa de 20×20 tiles con un espacio de 40 píxeles de alto y 160 de ancho en la parte baja de la pantalla). En nuestro juego queríamos conseguir un efecto de scroll continuo por lo que en cada frame debíamos redibujar el mapa por completo. Los resultados nos desanimaron un poco, nuestra rutina era eficiente pero no lo suficiente como para evitar que se viera un parpadeo molesto en la pantalla al redibujarla por completo.

Al comentar este problema con Fran, nos planteó la posibilidad de que el parpadeo fuera debido a que estábamos dibujando sin sincronizarnos con la señal VSYNC del monitor. Consultamos la implementación de la función de espera a la señal VSYNC en la CPCRSLIB y la implementamos en nuestro juego. El parpadeo se vio reducido en cierta manera, pero seguía observándose claramente que estábamos redibujando la pantalla entera; aunque este aspecto fuera meramente estético ya que el tiempo de redibujado era suficientemente pequeño como para que el juego fuera perfectamente jugable, decidimos descartar el scroll continuo porque no nos gustaba el aspecto.

Como alternativa planteamos la posibilidad de realizar un scroll por habitaciones al estilo del Zelda original de la NES. En esta aproximación, el mapa se divide en fragmentos del tamaño de la pantalla; cada fragmento se mantiene fijo mientras el jugador se encuentre en él (de esta forma se mueve el jugador y no el mapa, al contrario que en el scroll continuo en el que el jugador permanece centrado en pantalla), cuando el jugador sobrepasa uno de los bordes se redibuja la pantalla para mostrar el fragmento de mapa correspondiente. Así pues, buscamos formas sencillas de implementar un mapa de tiles.

La primera solución con la que dimos fue la CPCRSLIB de Artaburu, que posee una serie de funciones para utilizar y dibujar un mapa de tiles. Su aproximación es bastante eficiente puesto que se mantiene una lista de tiles «tocadas», es decir, tiles que se marcan para redibujar porque en el frame anterior algún otro elemento del juego las ocultaba; de esta forma únicamente se dibujan las tiles tocadas mientras el resto del mapa permanece como en el frame anterior. Esta idea permite ahorrar mucho tiempo de dibujado. Para poder usar la librería instalamos la CPC-DEV-TOOL-CHAIN siguiendo el tutorial de Fran. Cabe destacar que usarla en Linux tiene una instalación y un proceso mucho más sencillo; en Windows nos encontramos con el problema de haber instalado el compilador SDCC previamente por lo que la CPC-DEV-TOOL-CHAIN no se configuraba correctamente, la solución pasaba por desinstalar las herramientas de compilación que hubiéramos instalado
previamente e instalar la tool chain de forma limpia.

Desarrollamos una serie de funciones que modificaban el dibujado del mapa de la CPCRSLIB para implementar el scroll tipo Zelda. Al final queríamos implementar muchas más cosas que las que nos ofrecía la CPCRSLIB por lo que a sugerencia de Fran creamos nuestra propia implementación de mapa de tiles con marcado de tiles basándonos en la implementación del juego SaveTheHumor de FremosSoldiers. Esta implementación propia nos permitió mucha más libertad y ligereza sin funciones inútiles para nuestro juego. Sin embargo, el dibujado de las habitaciones seguía siendo un poco lento: estábamos utilizando la función de CPCRSLIB PutSp genérica. Sustituimos esta rutina de pintado de sprites por la nuestra optimizada para sprites de 8×8 y obtuvimos un rendimiento mucho mayor, haciendo mucho más suave la transición entre habitaciones. También probamos la implementación de Fran publicada en la CPCTelera, las dos opciones tardaban prácticamente lo mismo en dibujar el mapa completo, al menos a ojo desnudo.

Una vez teníamos todo el sistema gráfico establecido, queríamos cambiar la paleta de colores para lograr gráficos más interesantes. Nos encontramos con un problema al utilizar la función cpc_SetInk para ello. En resumen, debíamos utilizar la función para indicar cada color de la paleta individualmente ANTES de deshabilitar el firmware mediante cpc_DisableFirmware y ANTES de seleccionar el modo de pantalla con cpc_SetMode. En otro caso, obteníamos resultados extraños en los colores.

Generación y almacenamiento de mapas

Uno de los mapas del juego en su versión 1.0

Uno de los mapas del juego en su versión 1.0

Inicialmente se llevó a cabo en C un algoritmo iterativo de generación de mapas tipo cueva. Se basaba en inicializar una matriz (en realidad un array de una dimensión) de enteros en la que cada posición tiene un 45% de posibilidades de ser 1 (muro) y el resto de ser 0 (suelo por el que se puede caminar). Para ello utilizamos la función rand() de C. Tras la inicialización aleatoria, se realiza un refinamiento que consiste en mantener una casilla como muro si al menos 4 de las que lo rodean son también muros, y convertir a muro un suelo rodeado de 5 o más muros. Para favorecer que no se generen espacios muy abiertos, se añadió una condición que creaba un muro si un suelo esta rodeado de un solo muro a una distancia de dos casillas. Se realizan 4 iteraciones con estas condiciones recorriendo el mapa, y otras tres sin la última condición (la de crear muros en zonas abiertas), con el fin de conseguir un refinamiento mayor. Los resultados eran bastante realistas.

Al pasarlo al CPC descubrimos que el tiempo que tardaba en generar un mapa era inviable (alrededor de 1 minuto un mapa de 60×60). El siguiente paso fue capar el algoritmo para que nos proporcionara resultados válidos en un menor tiempo. Esto se consiguió en primera instancia reduciendo al mínimo el número de iteraciones (y por tanto, el número de veces que se recorre todo el mapa) conservando unos mapas decentes. Sorprendentemente, con dos iteraciones (una con creación de muros en espacios abiertos y otra sin). Se consiguió un tiempo de 20 segundos para un mapa de 40×40 y de 40 segundos para uno de 60×60. Los resultados eran viables, pero con mayor frecuencia se obtenían zonas abiertas y zonas inconexas.

Paralelamente al intento de mejora temporal, nos dimos cuenta de que, puesto que nuestro mapa solo consta de dos estados (muro o suelo), y dado que la máquina que estamos programando no anda sobrada de memoria, pensamos en que podríamos ocupar una octava parte del espacio que ocupaba el mapa si en lugar de almacenar cada 1 y 0 del mapa en un entero (1 byte) lo hacíamos en 1 bit. De esta forma creamos unas funciones que nos permitía acceder a los datos almacenados en un array a nivel de bit. Éstas juegan con la operación AND para descubrir si un bit en concreto está a 1 o a 0, y en sumar o restar potencias de 2 para ponerlos al valor que queramos.

Un análisis más profundo del algoritmo reveló que aproximadamente la mitad del tiempo del algoritmo se invertía en la inicialización aleatoria del mapa (ejecutamos el algoritmo con una inicialización fija a 1, tardando 20 segundos en un mapa de 60×60), por lo que la función que nos proporciona el número aleatorio, que seguía siendo la nativa de C, fue lo siguiente en el punto de mira. Encontramos una función en la librería de la CPCrslib llamada cpc_Random() que reducía la ejecución de 40 segundos a 30 segundos. No obstante, puesto que las características que requerimos en número aleatorio no son demasiado exigentes, decidimos que crear una función simple en ensamblador que nos acercara lo máximo al tiempo del algoritmo conseguido con la inicialización fija.

La función pseudo-aleatoria implementada incialmente era una basada en la forma x[i+1] = 5*x[i]+1 mod 256. El primer elemento es una semilla que inicializaremos mediante la anteriormente nombrada CPC_Random() (hay que comprobar si saca la misma secuencia siempre o no).

Se ha comprobado que cpc_Random() de la librería cpcrslib no devuelve siempre la secuencia de números (tiene alguna forma de obtener el primer valor de la secuencia de forma cambiante). Este hecho, sumado a que nuestra función en ensamblador, aunque rápida y simple, siempre sacaba la misma secuencia de números, y que además introducía patrones en los mapeados (suponemos que al hacerse circular la secuencia), nos hizo decantarnos por utilizar cpc_Random() en las partes del código donde necesitáramos aleatoriedad.

Tras crear una función que aleatoriamente devolvía una posición en el mapa (que utilizaremos para ubicar casi la totalidad de elementos en el mapa), observamos que frecuentemente tendían a aparecer varios elementos juntos, de manera que introdujimos otra función que calculaba una posición aleatoria a partir de otra dadas unas cotas de máxima y mínima distancia. Esta función nos permitía asegurarnos de que elementos que si aparecen juntos empeoran la experiencia del juego, apareciesen alejados entre sí.

Adicionalmente se han ubicado ciertos elementos de manera «fija», como pueden ser los enemigos esqueletos, que siempre aparecen junto a los fragmentos que hay que recoger, patrullando a su alrededor; o como pueden ser los murciélagos, que si bien su posición es aleatoria se ha querido favorecer el hecho de que aparezca uno por ventana (nueve ventanas de 20×20 en el mapa de 60×60).

Uno de los mapas del juego en su versión 2.0

Uno de los mapas del juego en su versión 2.0

Puesto que la ubicación de los elementos del juego es aleatoria, se hacía necesario asegurar que todo el mapa era conexo, es decir, que se podía acceder a todas las zonas del mismo. El algoritmo de generación, sin embargo, no asegura que no se vayan a quedar pequeñas zonas inconexas alrededor del área principal. Así, decidimos hacer un procesado del mapa anterior al posicionamiento de los elementos que nos asegurase que el mapa se compone de una única gran zona conexa. Este proceso se incluye en una función que escoge un punto cercano al centro del mapa, donde muy pocas veces tendremos una pequeña zona inconexa del resto, y la marca en una matriz auxiliar como conexa. A partir de ahí empezamos a marcar conexas las tiles que son suelo en el mapa y tienen alguna otra alrededor que también lo es. Recorremos el mapa en varias direcciones para asegurar que todo el espacio conexo ha sido marcado, y se procede a eliminar las partes no conexas del mapa. Esta función también devuelve el número de tiles que son suelo.

Otro aspecto clave de la generación de mapas es el tiempo que lleva este proceso. Con un mapa de 60×60 nos puede llevar 40 segundos fácil, por lo que se nos ocurrió que podíamos aprovechar la matriz auxiliar de la función anterior para ir calculando el mapa siguiente en el tiempo libre que queda en cada iteración del bucle principal del juego, aprovechando así mucho más el tiempo de cálculo. Tendremos una variable global que nos indicará el paso de la generación en el que nos hemos quedado y otras que nos dirán la posición donde nos quedamos. Le podremos decir también cuantos tiles procesar para así ajustar el tiempo que requiere por iteración del bucle principal. Si el número de tiles a procesar es 0, la función comlpentará lo que falte del proceso.

Inicialmente intentamos implementar esta funcionalidad con punteros, pero se nos liaban mucho algunos aspectos con los tipos y decidimos utilizar variables globales que, aunque no es lo óptimo, nos aportarían la flexibilidad que necesitamos. Con este método de generación de mapas, cuanto más tiempo aguntemos vivos en el mapa actual, menos tiempo habrá que esperar para la carga del siguiente, llegando a tardar sólo 15 segundos si finalizamos el nivel con el siguiente mapa ya cargado (quedando por tanto solamente el procesamiento de la conectividad y el posicionamiento de los distintos elementos).

Sonido

El sonido empezó siendo tratado siguiendo el tutorial publicado en CPCMANIA en el que se reproducen sonidos utilizando las instrucciones básicas de BASIC como son SOUND para reproducir notas por separado, y ENV junto a ENT para crear envolventes de volumen y tono para obtener mejores sonidos con SOUND. Como material de apoyo hemos utilizado el libro «Música y sonidos con Amstrad» de Jeremy Vine donde se explica de forma más detallada el uso de estas instrucciones, a parte de tener un buen apéndice con notas y frecuencias para reproducir. Con él, pudimos componer una primera versión de la que es la melodía de exploración principal en el juego para lo que se utilizó la formula que convertía números de tono en frecuencias dada en el libro.

Tras esto, pasamos a utilizar el firmware con llamadas desde ensamblador al escribir código para reproducir sonidos en C, incluyendo nuestra melodía. Sin embargo, como decidimos utilizar la cpc-dev-tool-chain y deshabilitar el firmware no podíamos utilizar estas instrucciones de CALL CODIGO. Así pues, tuvimos que instruirnos en el uso de la CPCRSLIB en cuanto al sonido también. Para ello, vimos en la documentación sobre la librería publicada en AmstradESP que el sonido se encuentra en la librería CPCWYZLIB incluida y basándose en el reproductor de sonido codificado por WYZ. También vimos por primera vez una referencia al WYZTracker de Augusto Ruiz que en seguida empezamos a utilizar para componer de forma muy sencilla.

En la documentación publicada en AmstradESP realmente no aprendimos mucho más que la forma de llamar a las funciones pero no de qué se tratan sus propios parámetros como el de cpc_WyzInitPlayer o cpc_WyzStartEffect. Para poder aprender mejor sobre el uso de esta librería nos dirigimos a los ejemplos que acompañan la descarga de la CPCRSLIB. Viendo los ejemplo intentamos utilizar la librería junto a los datos que obteníamos de componer con el WYZTracker, pero no conseguimos reproducir nada de lo nuestro aunque los ejemplos sí conseguíamos compilarlos.

Fijándonos en ejemplos de otros videosjuegos como SaveTheHumor de FremosSoldiers pudimos empezar a utilizar la librería pues nuestro problema era que no sabíamos dónde se encontraban los datos de las frecuencias utilizadas en una melodía para cargarla. Es decir, nos faltaban los datos a utilizar en SONG TABLE. Estos se encuentran en el archivo .mus que general el WYZTracker y que abríendolo encontramos los números hexadecimales que representan estas frecuencias de las notas musicales. Simplemente cargándolas directamente desde ensamblador usando la instrucción .DW pudimos cargarlas. Sin embargo…no había sonido.

El problema resultó ser que al parecer una melodía que solo utilice un canal al componerla en el WYZTracker no suena. Pero si utilizas más de uno, al cargar sus datos en nuestro programa estaba ya pasaba a sonar sin ningún problema. Así que una vez cambiamos la melodía para que empleara varios canales – y además completarla para hacerla más compleja y emocionante – pudimos cargarla y obtener sonido al ejecutar nuestro juego.

El siguiente paso ha sido cambiar toda la carga de los sonidos y melodías para usar nuestro propio código porque la carga directa en ensamblador y la llamada a las funciones de la CPCRSLIB se hacían con el código tal y como estaba en SaveTheHumor – hicimos esto para comprobar si nuestro problema era el código de carga y reproducción o el código de la música. Pasamos a declarar vectores de Int que son las estructuras que almacen las tablas necesarias para reproducir una melodía y cargar los datos desde el código ensamblador directamente. Todo esto lo hicimos fijándonos en cómo se carga la música en los ejemplos que vienen con la CPCRSLIB y funcionó perfectamente. Como dijimos con anterioridad, al principio ya lo probamos pero nuestro error entonces era que no teníamos cargados los datos de las notas musicales. Ahora ya lo teníamos todo bien.

Una vez teníamos melodías compuestas y cargadas con facilidad, nos pusimos con los efectos. La carga de los efectos va directamente en ensamblador y siguiendo lo indicado en el manual de AmstradESP indicado con anterioridad pudimos ver cómo se hacia esto. Para probar intentamos cargar una nota que según vimos en el WYZTracker tenía la frecuencia 191 (Mi). La cuestión está en que se deben cargar dos bytes siendo el primero y medio del segundo la codificación de esta nota en binario. Pero hay un pequeño detalle y es que se ha de codificar en Little Endian así que el bit menor está a la izquierda del todo y no a medio del segundo byte como pensábamos al principio. Para cuando nos dimos cuenta de esto pasó algo de tiempo. Una vez codificado correctamente, no tuvimos más dificultad para conseguir reproducir un sonido FX en nuestro juego.

Las melodías que se han compuesto para el juego, así como los FXs, han sido todos compuestos primero con una guitarra e improvisando. Son totalmente originales. Una vez teníamos la melodía en la guitarra, las hemos pasado al WYZTracker. Los FXs han sido un poco más complicados pero se han guiado por la misma idea de primero componer una idea con dicho instrumento.

Pathfinding

Desde un principio sabíamos que el pathfinding no será un tema fácil de abordar, en primer lugar por las limitaciones hardware de la máquina y luego por la naturaleza de la mayoría de los algoritmos de pathfinding existentes. Empezamos investigando los diferentes algoritmos utilizados actualmente en el proceso de pathfinding, siempre mirando el lado de la cantidad de memoria que necesita cada uno y de la rápidez en encontrar un camino. Observamos que la mayoría de ellos necesitan una estructura como por ejemplo, una lista, donde guardar los nodos y mantener un orden en función del valor obtenido por una heurística, generalmente una distancia como la distancia euclídea, manhattan o chebyshev. Eso en un Amstrad CPC 464 puede ser una buena carga. Con esta limitación, el abanico de algoritmos de pathfinding se ha reducido mucho. Sin embargo, nos encontramos con un algoritmo que no conocíamos llamado Sample Algorithm, un algoritmo utilizado en mapas basados en tiles. El algoritmo se basa en visitar las cuatro tiles adyacentes (sin permitir diagonales). Cada movimiento a una casilla adyacente tiene un coste de 1. El algoritmo empieza por la tile destino (coste 0) y va inspeccionando las celdas adyacentes almacenándolas en una lista con su respectivo coste acumulado (coste tile origen + 1). El siguiente paso será mirar si las tiles visitadas son un muro, o existe una tile con las mismas coordenadas pero con un coste acumulado menor. En ese caso procedemos a eliminar esas tiles. En el momento en el que encontramos la tile que buscamos, paramos la búsqueda y procedemos a recorrer la lista de tiles quedandonos con las celdas adyacentes de menor coste. De esta forma obtendremos el camino que buscamos.

Este algoritmo fue el comienzo para nuestra implementación de pathfinding. Al principio implementamos el algoritmo tal cual pero con algunas diferencias (mejoras en nuestro caso). La primera de ellas fue añadir la condición de inspeccionar solo las tiles que no son paredes. De esta forma nos ahorraríamos el hecho de luego mirar si hemos añadido una tile que es una pared. Por otra parte, añadimos una matriz auxiliar del mismo tamaño que el mapa de tiles donde vamos guardando el coste de cada celda que inspeccionamos. De esta forma solo visitaremos las celdas que tienen un valor 0, es decir, que aún no han sido visitadas, ya que al contrario tendrían un valor diferente de 0. Esta fue otra de las mejoras de nuestro algoritmo. Al terminar la implementación observamos que el algoritmo era bastante rápido y dentro de lo que cabe no utilizaba mucha memoria, no obstante tenía fallos de implementación y al mismo tiempo de aplicación ya que no orientabamos el pathfinding de la forma adecuada. En cuanto a la implementación cometimos el fallo de utilizar memoria dinámica para la lista de tiles. Al tener la memoria tan justa tenemos obligatoriamente que limitar la memoria de cada función de nuestro juego y saber en todo momento cuanta memoria utiliza cada parte de nuestro código. Por otra parte, estabamos haciendo uso de una lista enlazada, cosa inecesaria ya que con la matriz auxiliar estabamos guardando el orden de las tiles.

Por otra parte, nosotros ibamos a utilizar el pathfinding en la persecución de los enemigos. Gran error de concepto ya que la aplicación del pathfinding tiene que ser totalmente diferente. Además, con el pathfinding no se puede crear una situación de persecución adecuada, existiendo diferentes alternativas mucho más rápidas. Ahora que ya tenemos la base de nuestra implementación empezamos a hacer pruebas. Observamos que el algoritmo funcionaba muy bien en mapas estilo laberinto, con muchos obstáculos. No obsante en un mapa vacío por completo y bastante grande el algoritmo inspeccionaba gran cantidad de tiles que no levaban a la solución. Esto nos hizo pensar, ya que nuestros mapas generados tenían zonas sin obstáculos que serían un cuello de botella para el algoritmo. Dado esto, tuvimos que implementar una posible heurística para aplicarla al algoritmo y así ir más «directos» hacia la solución. No obstante, esto también implica ciertas desventajas. Una heuristíca no tiene por que ser la mejor en todas las situaciones y al generar los mapas aleatoriamente podemos enfrentarnos a un alto abanico de posibilidades. Dado esto, tuvimos que implementar una heurística que se comportase bien en muchas de las situaciones sin discriminar ningúna de ellas. Esta ha sido la parte más difícil de la implementación ya que encontrar una heurística equilibrada es muy complicado. Para la heurística hemos utilizado la distancia de Manhattan. No obstante, en muchas ocasiones puede haber más de una casilla con igual valor de distancia hacia la solución. Ahí es donde tenemos que tomar la decisión de seguir por un camino u por otro. Una primera aproximación fue basarnos en «de dónde venimos, dónde estamos y hacía dónde deberíamos de dirigirnos». Por ejemplo, si estamos en la casilla (10,10) y el destino es la casilla (20,20) sabríamos que debemos dirigirnos hacia la derecha y hacía abajo, no obstante, esto no tiene por que ser así ya que nos puede llevar por un camino sin salida. Aquí nos encontramos con dos problemas, el primero de ellos el camino sin salida y luego la preferencia de ir hacia la derecha y hacia abajo. Para los caminos sin salida hemos pensado en una solución basada en backtracking. Un camino sin salida es cuando nos encontramos con una pared y las demás celdas adyacentes, o son paredes o son celdas que ya visitamos. En este caso retrocedemos por el camino por el cual veníamos hasta encontrar una salida o celda sin visitar. El segundo problema es que si la heurística nos dirige hacia un camino equivocado, tendríamos que hacer mucho backtracking perdiendo mucho tiempo.

El problema de los caminos sin salida lo tenemos ya solucionado. Falta decidir de alguna forma por que camino seguir en el caso de que haya dos o más celdas adyacentes con la misma distancia hasta la solución. Probamos muchas alternativas, pero ningúna de ellas se comportaba bien en todos los casos. En unas situaciones funcionaba muy bien pero en otras muy mal o no funcionaba. Fuimos descartando posibilidades hasta llegar a una solución más o menos equitativa para todos los casos. La solución fue escoger una celda sin criterio en el caso de que haya dos o más igual de prometedoras pero limitar su movimiento por las demás celdas adyacentes no elegidas. Si el hecho de limitar el movimiento nos hace dirigirnos hacia una solución equivocada, backtracking se encargará de sacarnos de esa situación. No obstante, observamos que en el caso de nuestros mapas, esta era la alternativa que mejor funcionaba. Al fin y al cabo la heurística es un tema de experimentación que funcionará mejor en unos mapas y peor en otros. Con esto ya tendríamos el funcionamiento de nuestro pathfinding definido e implementado. En este punto es cuando empezamos a mejorar la implementación y hacerla más eficiente. Procuramos utilizar char en lugar de int para ahorrar memoría. Al principio utilizabamos un struct que definía una celda con dos componentes, x e y. Para almacenar esas celdas creabamos un array de structs. No obstante, definiendo un patrón para añadir las coordenadas al array podemos apañarnos con un simple array de chars organizado de la siguiente forma: array[0] = x1 ; array[1] = y1 ; array[2] = x2 ; array[3] = y2… Ahí reducimos el tamaño del array considerablemente (/4). Por otra parte la matriz auxiliar que antes era bidimensional y de int, ahora es unidimensional y char. Además de que al utilizar solo valores de 0 y 1 creamos dos funciones que almacenarían el valor de cada celda en un bit de cada char. De esta forma en cada char almacenaríamos 8 celdas. Así reducimos el tamaño de la matriz (/8). Estas han sido las mejoras de implementación realizadas.

Vamos ahora a hablar un poco de los resultados obtenidos con el pathfinding. Nuestro algoritmo no devuelve el camino óptimo hacia la solución, entendiendo por camino óptimo, el más corto. Llegamos a la conclusión de que no nos hace falta eso, además de que en un mapa random eso es complicado de obtener y costoso. El pathfinding lo utilizaremos para marcar rutas a los enemigos para patrullar, por lo que las rutas no tienen por que ser las más cortas. El requisito es que siempre lleguen al destino indicado, no obstante, el camino si es un poco más largo no importa. En la experimentación obesrvamos que los caminos no son mucho más largos que el óptimo en mapas complejos, pero si serán los óptimos en situaciones con pocos obstáculos y mapas menos complicados.

IA / ENGAGE

Respecto a la IA de nuestros nuestros enemigos partimos de un concepto muy básico, la máquina de estados. Para ello, las primeras pruebas que comenzamos a realizar fueron simulando un entorno que pudiese correr en la terminal ya que aun no disponíamos de todo lo necesario para poderlas hacer en un entorno real en el Amstrad. En estas primeras pruebas decidimos que los enemigos tendrían 3 estados posibles (quieto, persiguiendo y moviendo). Para empezar a probar todo, se implementó un movimiento aleatorio de los enemigos que más tarde se decidió descartar para darle un comportamiento más real a los enemigos, como veremos más adelante. Lo primero en realizarse después de tener todo esto preparado fue implementar los cambios de estados de nuestros enemigos, de esta manera, cuando en nuestra simulación te acercabas a un enemigo más de X casillas tanto en el eje X como en el eje Y, éste te veía y cambiaba su estado a persiguiendo (todavía no era capaz de perseguirte). En este momento fue cuando nos dimos cuenta de que tal vez nuestra implementación de la maquina de estados con la que el enemigo actuaría era demasiado simple y que carecíamos de un diseño previo de la IA.

Aquí comenzó la redefinición de nuestra IA. Para empezar establecimos qué enemigos posibles íbamos a tener en nuestro juego (esqueleto, murciélago, zombie), qué estados o estrategias iban a poder adoptar y qué acciones iban a poder llevar a cabo. Así pues, definimos:

– Estrategias: PATROL, ENGAGE y FLEE.
– Acciones: MOVE_UP, MOVE_DOWN, MOVE_LEFT, MOVE_RIGHT, ATTACK_UP, ATTACK_DOWN, ATTACK_LEFT, ATTACK_RIGHT y STAND_GROUD.

La estrategia PATROL, es la estrategia en la que el enemigo se encuentra en busca de nuestro personaje, como es de suponer cada tipo de enemigo tiene su propia manera de llevar a cabo dicha estrategia. Se puede producir una transición de PATROL a ENGAGE si el enemigo se da cuenta de su presencia. Los enemigos tienen diferentes capacidades sensoriales las cuales han sido implementadas en 2 módulos: CheckSight() y CheckHear() que más adelante los explicaremos.

FLEE es la estrategia cuyo objetivo principal es huir de nuestro personaje y por último, se puede producir un cambio de FLEE a PATROL en el caso de los murciélagos.

ENGAGE es la estrategia en la cual el enemigo intenta atacar a nuestro personaje, los enemigos cambiarán de ENGAGE en el caso que dejen de percibirnos.
Las acciones que podemos llevar acabo tienen un nombre bastante descriptivo por lo que no entraremos en su explicación.

De este modo, en nuestro flujo de programa durante la ejecución de nuestro juego los enemigos primero ejecutan un módulo para determinar que estrategia van a llevar a cabo puesto que la estrategia les limitara las acciones y una vez determinada la estrategia otro módulo se encargará de ejecutar la acción que el enemigo va a hacer en ese turno.

A continuación explicaremos el funcionamiento de cada enemigo dependiendo de las estrategias:

– Esqueleto

PATROL: el esqueleto patrulla entorno a un path que hemos determinado con nuestro algoritmo de pathfinding. Durante la patrulla los esqueletos pueden oír y ver a nuestro personaje con los módulos que hemos citado anteriormente, si esto se produce la estrategia del enemigo cambiará a ENGAGE.

ENGAGE: cuando un esqueleto se encuentra en dicha estrategia tiene dos posibles opciones que puede tomar: atacar o perseguirte, las cuales depende de la distancia a la que te encuentres de él. Los esqueletos son enemigos cuerpos a cuerpo por lo que tendrán que estar totalmente al lado del personaje para poder atacar lo cual comprobamos con un módulo. Por lo tanto, si el esqueleto no se encuentra en un rango en el que pueda atacarte éste se dispondrá a perseguirte el cual lo hará ejecutando un modulo muy simple en el que determina la siguiente acción de deberá tomar basándose en si el personaje se encuentra más arriba o más abajo, a la derecha o a la izquierda de el esqueleto. Para hacer más natural el movimiento y que el enemigo no se desplazase en lineas rectas pusimos una condición para que los movimiento verticales y horizontales se turnaran ((e->x + e->y) % 2 == 0). Simplemente tiene en cuenta si la suma de las coordenadas de la posición del esqueleto son impares o pares para llevar a cabo un movimiento horizontal o vertical comprobando posteriormente si es posible dicho movimiento, es decir, si no tiene una pared o un enemigo en esa dirección. Más tarde explicaremos unas modificaciones que llevamos a cabo en la condición y el algoritmo para que los enemigos pudieran rodearte.

Para añadirle algo más de inteligencia a estos movimientos antes de realizar la comprobación para darle prioridad al movimiento o horizontal ejecutamos un modulo que nos da como resultado si el esqueleto esta acorralando contra una pared a nuestro personaje y debe llevar a cabo una serie de movimiento para que este tenga que enfrentarse a él si quiere huir de ese lugar. Este módulo comprueba si el personaje tiene una pared en alguno de sus posibles lados y el esqueleto se encuentra en una de sus diagonales contrarias (x+-1,y) o (x, y+-1) al lado en el que tiene bloqueado sus movimientos.

Por último vamos a explicar, el último modulo que puede modificar el comportamiento del engage del esqueleto. Este módulo es el encargado de que si un enemigo tiene a otro enemigo delante, éste lo rodee para buscar otro flanco en el cual atacarte. El funcionamiento es simple, si un enemigo se encuentra en la posición que el esqueleto iba a adoptar en su siguiente movimiento, el módulo devuelve que hay un enemigo bloqueado el camino por lo que el esqueleto no tomará esa dirección y buscará otra posible. Debido anuestra priorización de dirección (que hemos explicado anteriormente) el enemigo puede tomar una acción y que la deshaga puesto que en esa coordenada este con prioridad un movimiento con la dirección contraria a la que tomó anteriormente. Para solucionar este problema decidimos añadir un atributo al enemigo que nos dijera si el esqueleto tenía el camino bloqueado e invertir la priorización de los movimientos, es decir, antes se decidía según ((e->x + e->y) % 2 == 0) dando prioridad a las casillas cuya suma era par y lo cambiamos a la siguiente manera ((e->x + e->y) % 2 == e->blocked) por lo que si el enemigo se viera bloqueado por otro enemigo, cambiaría su manera de dar prioridad a los movimientos para poder esquivarlo.

FLEE: los esqueletos son valientes guerreros por lo que luchan hasta la muerte, nunca huyen.

– Murciélago

PATROL: el murciélago es un animal que le gusta estar colgando sin más en la cueva así que de hecho no sigue ninguna ruta ni patrulla. Sólamente está en su punto inicial fijo descansando. Además, se guían por su excelente sentido auditivo así que si oyen pasos empezarán a moverse hacia el origen de estos dispuestos para atacar. No obstante, si no están al 100% de su vida no se van a mover, así que se quedarán recuperando salud.

ENGAGE: una vez el murciélago ha oido los pasos del enemigo y está al 100% de su salud, se moverá en dirección del sonido para darle caza mientras pueda oirlo. Por este lado funciona igual que el esqueleto así que se acercará para poder atacarte cuerpo a cuerpo aunque con alguna pequeña diferencia: los murciélagos son muy hábiles moviéndose por la cueva así que son capaces de encontrar pequeños pasajes entre las paredes por lo que se van a mover directos sin esquivar obstáculos aunque como se guían por su audición, no son capaces de moverse directamente hacia el enemigo mientras estén lejos. A medida que se acercan a la fuente del sonido podrán localizarlo mejor. Esta funcionalidad se ha implementado de forma que para el murciélago la localización del jugador es difusa sumándole números aleatorios según la distancia a la que se encuentre. A mayor distancia, mayores números le sumamos. Sin embargo, cuando se encuentra por debajo de cierta cota, va directo. Al igual que los esqueletos, el murciélago también intenta acorralarte si estás contra una pared. El murciélago te atacará mientras esté al 100% de su vida pero si le das un golpe, huirá. Para ello ponemos la direction del player como direction del murciélago y pasamos a FLEE.

FLEE: el murciélago pasará a este modo si su vida baja del 100% y lo que hará es moverse en dirección contraria a la del personaje. Se moverá en esta dirección hasta que llegue a una tile que sea pared y pasar a PATROL, siendo esto comprobado en este estado de la estrategia. Como los mapas están rodeados de pared y también hay muros por dentro, en cualquier dirección podremos encontrar muro. Sin embargo, para evitar que se quede en muros muy finos, no se le cambia su ACTION al detectar que estamos en muro para que así, al pasar a PATROL aún le quede un paso más en esta dirección. De esta forma, el murciélago quedará más resguardado dentro del muro y no en los bordes (aunque no puede ser atacado de todas formas). Una situación que se puede dar es que el murciélago se vaya del mapa, pero esto está hecho con intención: el player no debe olvidar por dónde se le fue un murciélago porque en cuanto recupere vida y te vuelva a oir, volverá al mapa.

-Zombie (Tiramocos)

La capacidad principal de este enemigo es lanzar (como su propio nombre indica) mocos. Tenemos que especificar que es el único enemigo al que no le afecta la reducción de armadura del personaje a su daño. Su comportamiento es muy simple:

PATROL: el zombie esta quieto en una posición en la cual va haciendo guardia mirando hacia cada una de sus posiciones (arriba, abajo, derecha, izquierda) mediante el sentido de la vista mientras este en su estado de PATROL. El Tiramocos, sólo te puede ver en el caso de que tu estés en su rango de visión, si estás a su espalda no será capaz de detectarte a menos de que le ataques, entonces se dará la vuelta y comenzará a atacarte.

ENGAGE: En el caso de que te haya visto antes de que tu hayas podido acercarte a él este comenzará a tirar sus mocos que irán dirigidos a una posición aproximada a la tuya que es calculada mediante un módulo que según su dirección divida el área en cuadrantes y le da una dirección y velocidad al proyectil. El proyectil o moco tiene cierto rango hasta el que se desplazará siguiendo la misma dirección que la inicial hasta que te golpee o desaparezca. El zombie sólo es capaz de lanzar un moco a la vez por lo que cuando acabe de lanzar un moco es buen momento para atacarle sin peligro.

FLEE: las capacidades del zombie no le permiten huir, ¡aprovecha para destrozarlo!.

Sistema de Combate

Hemos dotado al juego de un sistema de combate y daños para intentar darle el mayor realismo posible y añadirle un poco de azar para que sea más imprevisible y divertido.

RANGOS DE DAÑO: tanto el personaje como el enemigo tienen de dos atributos que le dan al sujeto un rango de ataque en el cual cada vez que ataquen harán un daño aleatorio perteneciente a ese rango. Así pues, si un enemigo tiene de daño mínimo 2 y de daño máximo 5 puede asestar golpes de 2,3,4,5 o unidad de daño. Este daño lo mas probable es que sea reducido por la armadura que tenga el objetivo de ese golpe como explicaremos más adelante. El tiramocos siempre provoca la misma cantidad de daño.

CRÍTICOS: al igual que el daño los críticos depende del azar, cada sujeto, tanto el jugador como los enemigos tienen un porcentaje de críticos (menos el tiramocos) el cual puede provocar la duplicación del daño que se va a producir en ese ataque. Es decir, si el personaje tiene un 5% de crítico tiene esa posibilidad de que al asestar ese golpe, la cantidad de daño que se va a producir se duplique.

ARMADURA: la armadura es un sistema de reducción de daño que se calcula mediante una formula, en este caso no influye el azar o la probabilidad. Esta propiedad pertenece a todos los enemigos, la reducción se aplica al daño que hemos calculado en mediante las dos propiedades anteriores, es decir, se calcula el daño, se duplica en el caso de que sea crítico y entonces aplicamos la formula de la reducción, que será un multiplicador que aplicaremos al daño. Nos hemos inspirado en el juego de League Of Leguends para hacer este sistema, la formula es la siguiente:

Multiplicador de daño = 100 / 100 + Armadura SI Armadura >= 0
Multiplicador de daño = 2 – 100 / 100 – Armadura SI en otro caso

Sprites

Para poder realizar todos los sprites del juego, se ha utilizado la herramienta RGAS (Retro Game Asset Studio). Gracias a esta herramienta hemos podido exportart todos los sprites que hemos ido diseñando, a nuestro juego. Es capaz de exportar al lenguaje CPCRSLIB Line Assembler, es decir, el lenguaje ensamblador que la librería CPCRSLIB es capaz de comprender a la hora de representar nuestros sprites.

Todos los sprites que se han realizado han tenido un tamaño de 8×8, excepto la barra de información del personaje, el título y gráfico (personaje con esqueleto) del menú. Todos estos sprites han sido el resultado de un proceso de adaptación al mundo en el que nuestro personaje va a interactuando, es decir, una cueva. A partir de este dato, todos los sprites tienen elementos relacionados con este paraje. En primer lugar, los colores tienen tonos más oscuros y, en segundo lugar, los enemigos que aparecen en la cueva son los más característicos de lugares oscuros, abandonados y peligrosos, como: murciélago, esqueleto y zombi.

Durante el proceso de diseño de estos sprites, nos hemos ido encontrando con una gran serie de problemas.

* El primero y principal, la paleta. Ya sabemos que de la paleta de colores del AMSTRAD CPC, únicamente podemos utilizar 16 colores, es decir, muy pocos si se quiere llegar a una mayor realidad y efectos en nuestros sprites. Aún con 16 colores, podrían realizarse sprites muy originales y, a la vez, reconocibles (hay veces que se realizan sprites de 8×8 y nadie sabe que es lo que representa, excepto el artista). El problema es que los personajes que nos ofrece la paleta son muy poco significativos con el ambiente oscuro y tenebroso de una cueva. Percibimos la falta del color marrón y de diferentes tonos de grises, los cuales nos hubiesen mejorado, en gran medida, la calidad de nuestros sprites y elementos visuales in-game. Además, el tener un número tan reducido de colores y tan poca diversidad de éstos, nos conduce a un efecto muy poco agradable en el juego y que se aprecia con mucha facilidad. Esto es que si tenemos un color de fondo determinado, entonces en nuestro sprite no debemos utilizar este mismo color ya que con el fondo, parecería que algunos elementos del sprite han desaparecido, pero no es así. Para ello, nos hemos tenido que ajustar a los colores disponibles y a veces recurrir a colores no cercanos a la realidad para algunos de ellos, pero que permitan al usuario final del juego, apreciar los sprites y diferenciar que representa cada uno de ellos. Por ejemplo, disponemos en el juego de un sprite de esqueleto, para ello hemos utilizado colores como el blanco, gris y amarillo oscuro. Por ello, no podemos disponer de un fondo blanco o gris, ya que el detalle de nuestro esqueleto se perdería en el fondo, sin poder apreciarlo lo más mínimo.

* Otro gran problema es la memoria. Los sprites tienen un impacto bastante considerable en la memoria de nuestro juego. Durante el diseño de nuestro juego, hemos tenido que debatir en numerosas ocasiones sobre si añadir determinados sprites o no. El no introducir nuevos sprites puede ayudarnos a dejarnos algo de memoria para introducir nuevas funcionalidaes en el juego. Es decir, hay que equilibrar la balanza, no podemos esperar un juego que incluya todo tipo de funcionalidades y gráficos por todas partes sin que nos quedemos sin memoria para poder cargarlo en la máquina. Para ello, hemos establecido un equilibrio y actualmente hemos tenido que olvidarnos de algunos sprites para poder finalizar el juego en cuanto a funcionalidades. Ganamos funcionalidad pero perdemos una parte de gráficos del juego, esto hace que nuestro juego sea menos atractivo, por lo que tenemos que ganarnos al usuario final con la jugabilidad. Este debate es problema del equipo y es el mismo el que tiene que decidir que es lo que quiere priorizar ya que los dos elementos en grandes cantidades, es muy complicado de mantener.

En cuanto a los sprites de la cueva en si, hemos utilizado 3 tipos diferentes de sprites: suelo, pared y transición suelo-pared. Muchos juegos utilizan los dos primeros tipos de sprites y pueden obtener resultados interesantes pero con este último tipo de sprite, podemos añadir un efecto de profundidad al juego. El problema de perdida de realidad y claridad en los sprites, lo ganamos con este sprite, ya que puede ser que el sprite de suelo y pared únicamente no sean lo que esperamos, pero incluyendo este último podemos ganar un realismo mucho mayor. Para poder realizar este sprite, hemos realizado una mezcla de colores y estilos entre el sprite del suelo y el de la pared.

Otro elemento que hemos añadido a nuestra colección de sprites son las orientaciones. Gracias a ellas hemos obtenido unos resultados muy buenos en cuanto a movimiento de los personajes a través del mapa. De esta forma, podemos conocer en todo momento si un enemigo nos está observando o no.

Se han incluido, además, sprites de los personajes muertos, para dotar a los combates de mayor realismo. Una vez el personaje principal o los enemigos mueren, aparecerá el sprite de muerte correspondiente a cada uno de los personajes. Además, los hemos realizado de forma que los personajes, al morir, parece que caigan al suelo. Para el personaje principal hemos añadido sangre, para el esqueleto se han esparcido los huesos por el suelo, para el zombie se ha añadido sangre también y, por último, para el murcielago se le ha dado la vuelta para que la animación sea lo más realista posible.

Optimizaciones

Utilizar memset para inicializar cualquier matriz. Es mucho más rápido que cualquier otro método de inicialización.

Comprimir construcciones if/else de comportamiento redudante pero con modificaciones entre ramas factorizando dichas modificaciones en arrays externos y dejándolas en función de un K según la rama para después realizar un bucle sobre el código de una única rama iterando sobre K, realizado por ejemplo con PlayerAction() en main.c.

Anécdotas del desarrollo

Sprites del juego

Mapa con forma de «Italia»

En un principio el juego iba a ser roguelike por turnos, un bug eliminó por error la restricción de turnos y descubrimos que el juego era mucho más divertido sin ella por lo que finalmente aportamos nuestra «visión» al género roguelike en vivo.

Nuestro sistema de generación aleatoria de mapas creó de forma fortuita una sección de mapa que era exactamente igual que Italia.

Minutos antes de la entrega nos dimos cuenta de que había un bug al dibujar la pantalla de carga (sprites de la propia pantalla se quedaban luego en el juego), como quedaba poco tiempo y era realmente complicado pasar de nivel tuvimos que desactivar el daño de los enemigos para poder pasar el nivel y ver que habíamos solucionado el bug. Entregamos el DSK con el daño de los enemigos desactivado… El CDT iba bien…

¿Qué aprendimos?

Durante el desarrollo de Cavebola hemos podido adquirir un abanico de conocimientos que no sólo se han centrado en construir el videojuego. Por un lado, hemos aprendido sobre un lenguaje de programación que muchos de nosotros nunca habíamos tocado, como es BASIC. Junto con BASIC hemos podido tener una primera toma de contacto con el CPC, que ha significado un gran reto para nosotros.

El desarrollo en una máquina tan limitada, para nuestras mentes más «modernas», como es el CPC AMSTRAD parecía el mayor problema para nosotros al principio. Pero realmente ha significado una gran fuente de aprendizaje, ya que nos ha dado la oportunidad de ampliar nuestros conocimientos en campos como el manejo de la memoria. En general, las restricciones de hardware han provocado que hayamos tenido que pensar más allá al desarrollar Cavebola pensando en ser eficientes.

Así pues, no sólo nuestro conocimiento sobre hardware se ha visto mejorado, puesto que nuetras habilidades de programación se han visto enriquecidas con este proceso de aprendizaje del funcionamiento de la CPCRSLIB. Pudimos uitlizar esta librería gracias al estudio de la misma, algo que hasta el momento no solíamos hacer. Por ello hemos aprendido a encarar la utilización de una librería externa, puesto que pudimos ser críticos en la corrección de la misma evitando confiar a ciegas en ella.

Por otra parte, para el desarrollo de la IA hemos tenido que aprender no sólo sobre modelos de algoritmos sino que hemos aprendido a diseñar una IA. Esta parte ha sido de las más enriquecedoras ya que ha supuesto un reto dejar de pensar en programar o implementar un algoritmo, sino que debíamos adaptar el modelo a nuestro diseño y no al revés. El desarrollo de la IA nos ha aportado una visión de cómo se construye una IA en un videojuego: no se adapta el comportamiento de un personaje al modelo o algoritmo utilizado; el comportamiento del personaje manda sobre la implementación.

Por último, nunca habíamos tenido contacto con el desarrollo de videojuegos y este proyecto nos ha aportado un conocimiento sobre este proceso. Hemos podido entender no sólo la parte más tecnologíca de implementación, sino que también hemos tenido contacto con partes más artísticas como el diseño del arte utilizado, así como del sonido o más importante aún: el desarrollo de una idea o el concepto del propio videojuego.

CREDITOS

  • The development team would like to thank the following people which somehow helped to carry out this game by giving credit to their work.
  • We would like to thank Óscar «Mochilote» Sánchez for his programming tutorials on CPCmania.com which were quite useful during the development process. http://www.cpcmania.com/
  • The CPCRSLIB developed by Raúl «Artaburu» Simarro has been widely used all game long, mostly on sprite drawing routines and some other auxiliar ones. http://www.amstrad.es/programacion/cpcrslib_sp.html
  • The tile map implementation is based on the used in the game «Save the Humor», developed by the Fremos Soldiers: Fran «Ronaldo» Gallego and «Pote». http://fremos.cheesetea.com
  • The map scrolling technique is influenced by the one used on the original Zelda for GameBoy in which a room-like approach is taken.
  • The README layout is inspired by the one used in the game «Save the Humor» by «Ronaldo» and «Pote».
  • The WYZPlayer developed by José Vicente Masó and included in the CPCWYZLIB that has been used to reproduce music and FXs. https://sites.google.com/site/wyzplayer/
  • The CPCRSLIB music examples that helped us to learn how sound reproduction worked using the CPCWYZLIB.
  • The WYZTracker developed by Augusto Ruiz, from Retroworks, used in order to compose the music played in our game. http://programbytes48k.wordpress.com/2013/02/27/wyztracker-de-augusto-ruiz/
  • The technique used for generating random maps is a celular automata known also as the «4-5» rule. We knew about that through a wiki page explaining it, http://www.roguebasin.com
  • The Fast-Drawing sprite routine that we are using for putting 4×8 aligned sprites to video RAM is a function from the CPCTelera by Fran Gallego. https://github.com/lronaldo/cpctelera
  • The function used by the enemies to determine the line of sight is an implementation of the Bresenham’s algorithm by RosettaCode . http://rosettacode.org/wiki/Bitmap/Bresenham’s_line_algorithm
  • Keyboard reading is performed by using the CPCRSLIB methods. https://code.google.com/p/cpcrslib/
  • The sprites have been built using RGAS from CPCWiki. It has been a really useful tool to design our sprites. http://www.cpcwiki.eu/index.php/Retro_Game_Asset_Studio
  • Thanks to Oryx for offering the set of sprites «LOFI ROGUELIKE» which inspired us when designing ours. http://realmofthemadgod.wikia.com/wiki/File:Lofi_Roguelike_sprites.png
  • The red border indicator is inspired in the game Void Hawk.
  • The CDT tape with the image preloader has been generated using the tools CPCTapeXP and samp2cdt, both can be found at http://cpcmania.org. Thanks to «Ronaldo» for his explanations.
  • Special thanks to Fran «Ronaldo» Gallego.

1 comentario on “Cavebola / Close To Metal”

  1. Pingback: Especial – Retro Santo Domingo con Fran de Pensando como Pollos | ¿Cómo se hace un videojuego?

Deja tu comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.