sábado, 3 de noviembre de 2012

Ejemplo #3: Invasores del espacio.

En este tercer ejemplo voy a mostrar cómo hacer un juego en el estilo de Space Invaders.

Creado en 1978 por Tomohiro Nishikado para Taito, el concepto del juego es simple: una nave controlada por el jugador se enfrenta a una flota de naves enemigas que se mueven juntas por la pantalla para esquivar los disparos y a su vez atacar al jugador.

Para resolver este programa vamos a utilizar nuevamente el lenguaje C.
No haremos uso de librerias graficas asi que todas las naves y los disparos seran representados por distintos caracteres.

Consideraciones generales

En principio se nos plantean dos problemas principales:

El primero es el movimiento de las naves enemigas. En esta versión del juego vamos a crear una flota de 35 naves, ordenadas en 7 columnas de 5 naves cada una.

Las naves serán representadas por el carácter ' * ' y serán almacenadas en una matriz de dos dimensiones de 7x5.

Les vamos a asignar una posición inicial. Estamos utilizando una pantalla de 80x24 así que en el eje horizontal las naves enemigas estarán en las columnas 20, 30, 40, 50, y 60 y sobre el eje vertical estarán en las filas 2, 4, 6, 8, 10. Tenemos que tener en cuenta esto para entender los valores de los bucles que imprimen las naves en pantalla.

Para poder solucionar el movimiento le vamos a pasar a la función que imprime la flota enemiga en pantalla dos valores que pueden ser 0 o 1, y se los vamos a sumar a la posición sobre los ejes X e Y. De esta forma tenemos 4 posibles combinaciones: 0-0, 1-0, 1-1, 0-1 que nos permiten realizar una vuelta completa sumando 0 o 1 a X e Y para modificar la posición de las naves.

Esto es fácilmente modificable para realizar otros movimientos pero por ahora vamos a quedarnos con una vuelta simple.

El segundo problema que tenemos que resolver es como vamos a hacer el seguimiento de los rayos disparados por las naves enemigas.

Por empezar en cada vuelta completa de las naves vamos a elegir aleatoriamente una que va a realizar un disparo. La selección también podría ser dependiendo de la zona en la que se encuentre la nave del jugador, pero lo vamos a hacer aleatorio para restarle algo de dificultad al juego.

La nave seleccionada en cada vuelta para atacar va a ser almacenada en un vector de acuerdo a la columna a la que pertenece, en la posición que le corresponde de acuerdo con su número. En la vuelta siguiente se realiza una comprobación a través del vector para saber que nave realizó un disparo, en ese caso llamamos a una función para inicializarlo. Una vez inicializado apagamos su valor en el vector de inicialización y le asignamos un valor en otro vector que guarda los rayos disparados en curso. Luego se realiza una comprobación que recorre este último vector y por cada rayo en curso llama a una función que lo imprime en pantalla y actualiza su posición.

Hemos resuelto así los dos mayores problemas que nos presenta el juego.


Análisis del programa

A continuación presento el código completo y luego pasare a explicar cada función:


En principio nos encontramos nuevamente con la función sleep() que ya ha sido explicada muy en detalle en el Ejemplo #1 así que no volveré a explicarla aquí.


Vemos también una función getrand(). Esta función contiene una fórmula para producir un número aleatorio entre 0 y 6. Para utilizarla debemos incluir la linea srand(time(0)) al comienzo del programa ya que para generar números aleatorios utilizará la hora del sistema.




Otra pequeña función que he incluido en este ejemplo es gotoxy(). Hasta ahora veníamos utilizando la función gotoxy que se encuentra en conio.h. Creamos aquí nuestra propia gotoxy() utilizando la librería windows.h. También hemos reemplazado los llamados a clrscr() por system(“cls”). Esto hace nuestro código un poco menos orientado a Borland y mas compatible en otros compiladores. Seguiremos utilizando igualmente la función kbhit() de conio.h.



Nos encontramos nuevamente con la función leoTecla() que ya ha sido utilizada en el Ejemplo #1. Vemos que ha sido un poco modificada para adaptarse a nuestras necesidades en este juego. Observemos el case DISPARA. Si el jugador pulsa la barra de espacio (representada por la constante DISPARA) comprueba si hay un rayo del jugador en curso. Si lo hay ignora la llamada. Si no hay un rayo en curso activa uno nuevo asignándole el valor 1 a rayoON y guardando la posición inicial de acuerdo con la posición del jugador.



Las funciones menuPrincipal() y menuFin() son bastante sencillas. La primera es llamada al comienzo de un nuevo juego. Según el nivel de dificultad seleccionado almacenará 0 (difícil), 1 (medio) o 2 (fácil) en la variable NUEVOATAQUE para tomar como valor de referencia. Esto hará que los disparos enemigos se habiliten cada 0, 1 o 2 vueltas.


menuFin() es llamada luego de finalizado el juego para saber si se desea continuar jugando:



La función inicializoArrays() es llamada al comenzar un juego nuevo. Inicializa en 0 los arrays que guaran la cantidad de rayos enemigos disparados y sus respectivos estados y posiciones.



inicializoNaves() lo que hace es inicializar la matriz que representa las naves enemigas asignándoles el carácter ' * ' con el que serán representadas en pantalla.




El rayo del jugador es controlado por dos funciones:
La primera, comprueboRayo() es llamada en cada vuelta del loop principal. Lo que hace es comprobar si hay un rayo disparado, y en tal caso llama a rayo() para imprimirlo en pantalla y luego actualiza su posición.


dibujoJugador(int) recibe como parámetro la posición del jugador sobre el eje X. El primer bucle for dibuja la nave del jugador, representada por 3 caracteres del valor almacenado en x.
El segundo for comprueba si la nave del jugador fue alcanzada por alguno de los rayos enemigos. En ese caso asigna el valor 1 a la variable GameOver para terminar el juego.


A continuación vamos a ver las funciones mas complejas que son las que tratan con los problemas expuestos en la presentación.

La función void escuadronVuelta(int, int) recibe como parámetros dos valores, que van a ser 1 o 0, para representar las 4 posibles posiciones que forman una vuelta completa de la flota enemiga mediante las combinaciones 0-0, 1-0, 1-1, 0-1.

Utilizaremos 2 bucles for anidados para imprimir las naves en pantalla, con sus valores modificados para que nos sirvan como referencias a las posiciones que necesitamos alcanzar.

Como queremos que las naves estén separadas cada 10 espacios horizontales y 2 espacios verticales utilizamos los valores de ambos for para alcanzar las posiciones deseadas e imprimimos la matriz en pantalla por orden de filas.

La llamada a printf imprime el valor de la matriz correspondiente compensando con los valores de x e i.



Las próximas dos funciones controlan los rayos enemigos:

void disparaEnemigo(int) toma como parámetro el numero de nave enemiga que tenga un disparo pendiente de ser inicializado.

Comienza la función comprobando si la nave que esta primera en la columna seleccionada (la que esta mas arriba en la matriz) existe o si fue eliminada, para poder realizar el ataque. Si esta condición no se cumple, en la próxima vuelta del for intentará con la que esta detrás.

Si se cumple, entonces rayoEnemX[] y rayoEnemY[] contienen las posiciones de todos los rayos enemigos. Les asignamos la posición del rayo nuevo de acuerdo a la posición de navequeAtaca en la matriz. Luego imprimo en pantalla el primer rayo.

A continuación actualizo el valor de proxAtaque para llevar la cuenta de vueltas hasta el próximo ataque.

Ahora que el rayo ha sido inicializado el seguimiento será guardado en el vector rayoDisparado[] asignandole el valor 1 en el lugar que le corresponde y lo apago en rayoEnemigoON[] para que no vuelva a ser inicializado hasta que esa nave no dispare nuevamente.



La próxima función es void disparo(int), que actualiza la posición del disparo enemigo e imprime el disparo en pantalla en su nueva posición. Si el rayo alcanzo el borde de la pantalla lo elimina asignandole el valor 0 correspondiente a la posición del número de nave que recibe como parámetro en el vector rayoDisparado[] que almacena cuales son las naves enemigas que tienen rayos disparados activos.



Ahora hemos llegado a la función principal: void naves(). Esta función es llamada desde el loop del juego en el main(). Contiene a su vez un bucle de 4 vueltas que forman el ciclo completo de movimientos de las naves enemigas.

Comienza comprobando que la nave seleccionada por getrand() para atacar no tenga un disparo activo (si es la primer vuelta del juego no realiza ataque por que aun no se ha seleccionado ninguna) y que proxAtaque haya alcanzado el valor de NUEVOATAQUE, en ese caso asigna un 1 en su posición correspondiente, de acuerdo a la columna de la nave que ataca, en rayoEnemigoON[] permitiendo así que el rayo sea inicializado.

La primera serie de instrucciones del bucle limpia la pantalla e imprime las naves en la posición correspondiente llamando a escuadronVuelta(), pasando como parámetro dos ints(0 o 1) para generar las 4 posiciones posibles.

La llamada a comprueboRayo() es para comprobar si hay un rayo disparado por el jugador, en tal caso llama a rayo() para dibujarlo y luego actualiza su posición.

En el for siguiente primero se comprueba si se activó un rayo enemigo y en ese caso se llama a disparaEnemigo() para inicializarlo. Luego recorremos las naves que tienen disparos activos en curso llamando a disparo() para mostrarlos en pantalla y actualizar su posición.

Las llamadas a leoTecla() y dibujoJugador(int) realizan la lectura del buffer de teclado e imprimen en pantalla la nave del jugador respectivamente.

muestroScore() solo es responsable de mostrar en pantalla la puntuación.

En la comprobación siguiente, si GameOver==1 entonces salgo del loop antes que termine la vuelta completa de las naves enemigas.

Finalmente producimos la demora necesaria con sleep() y le asignamos a navequeAtaca un numero de nave (de columna en realidad) aleatorio seleccionado por getrand().



Para terminar nuestro análisis vemos la función main().

Toda la función esta encerrada en un bucle que nos permite que el juego continúe si el jugador selecciona la opción al terminar.

En principio seteamos el nivel de dificultad de acuerdo al valor devuelto por la llamada a menuPrincipal(). NUEVOATAQUE toma el valor de nivelDeDif. Esto es simbólico ya que podríamos asignarlo directo a NUEVOATAQUE pero utilizando una variable mas el código se lee con mas facilidad.

Llamamos luego a las funciones que inicializan las naves y los arrays.

Después de inicializar GameOver en 0 y llamar a srand() comienza el loop del juego que se repetirá mientras GameOver se mantenga en 0.

Dentro del loop llamo a naves() y al salir de la función se incrementa el cuenta vueltas proxAtaque o se le asigna 0 si ya alcanzó a NUEVOATAQUE.

Saliendo del loop del juego se permite al jugador elegir si desea seguir jugando, en tal caso resetea el score y el nivel de dificultad y vuelve a comenzar el ciclo principal, caso contrario termina el juego.



Finalizamos de esta manera la revisión del código de Invasores del espacio.

He optado en este ejemplo por un estilo de código un poco mas extenso pero que me permita realizar modificaciones y expandir la jugabilidad con mas facilidad.




Link para descargar el código:

https://dl.dropbox.com/u/103165598/InvasoresDelEspacio.c