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