Voy a demostrar en este ejemplo cómo
realizar un juego en el estilo de Snake, y para esto voy a
utilizar el lenguaje JAVA.
El juego Snake,
o Worm según al
título de la primera versión, fue escrito en 1978 por Peter Trefonas
para la Tandy TRS-80 y publicado en una revista.
En las primeras
épocas de las computadoras personales era muy frecuente que los
videojuegos se vendieran en formato de revistas con el código del
juego impreso para que el usuario lo copie en su computadora y lo
ejecute.
Desde
ese entonces se han realizado muchísimas versiones del juego. De
entre las mas populares recordamos el NIBBLES
para Qbasic y el RattlerRace
que formaba parte del Microsoft Entertainment Pack para Windows 3.1.
El
Snake alcanzó una
segunda ola de popularidad cuando fue introducido a fines de los '90
como el juego que venía de fabrica en los celulares Nokia.
Consideraciones
generales
En esta versión vamos a encontrar algunas de las funciones que ya
hemos utilizado para el Ejemplo #2 en el cual realizamos un PONG en
Java.
Por
otra parte, el enfoque sera algo distinto ya que utilizaremos también
una de las características mas importantes del lenguaje orientado a
objetos que es la encapsulación.
Esto significa que a los datos de una clase solo se podrá acceder
mediante determinados puntos de entrada y todo lo demás sera
invisible desde afuera de esa clase.
La
encapsulación,
junto con el polimorfismo
y la herencia
de clases
forman las bases de lo que se conoce como programación orientada a
objetos (OOP).
En
Java estamos constantemente utilizando polimorfismo
y herencia
de clases
al llamar o sobrescribir métodos de otras clases, extender clases
para crear ventanas, implementar interfaces, o crear objetos
utilizando constructores de distintos tipos para un mismo objeto.
En este ejemplo vamos a utilizar 3 clases:
La
clase JuegoSnakeJava es la clase principal, la que contiene los loops
del juego y desde donde instanciamos los otros objetos.
La clase Snake es la que contiene todos los campos y métodos
relacionados con el comportamiento de la serpiente.
La clase Frutita que sirve para crear y controlar las frutas que
serán el alimento de nuestra serpiente.
El principal problema que nos encontramos en este juego es el
movimiento de la serpiente.
La
solución es en realidad bastante sencilla. Vamos a representar el
cuerpo de la serpiente mediante un array,
o mejor dicho un ArrayList
que es uno de los tipos de listas que nos ofrece el lenguaje Java. La
ventaja del ArrayList
sobre un array tradicional es que no tenemos que definir un numero de
elementos al declararlo, es decir que podemos ir agregando elementos
y el tamaño de la lista se redefine dinámicamente. El ArrayList
funciona en este sentido como las List
de C#.
Dentro
del ArrayList
vamos a almacenar objetos de tipo Point
que son muy convenientes ya que representan un punto con coordenadas
X e Y que es justo lo que necesitamos para representar cada parte del
cuerpo de la serpiente. Cada vez que la serpiente coma una fruta
haremos crecer el cuerpo agregando un punto a la lista.
Para
resolver el problema del movimiento utilizaremos un bucle for
para ir recorriendo todos los puntos de la lista de atrás para
adelante, copiando cada punto a la posición del que esta delante en
la lista hasta llegar al elemento 1 que se copiará donde estaba la
cabeza (que es el punto 0 en la lista) y la posición del punto 0
será actualizada entonces por fuera del bucle.
Para la parte de las frutas necesitaremos crear puntos dispuestos
aleatoriamente por la pantalla. Esto no debería ser un problema.
Finalmente vamos a tener que realizar la detección de colisión
necesaria para saber cuando la serpiente ha comido alguna fruta o
cuando ha alcanzado alguna parte de su propio cuerpo. Para esta parte
vamos a necesitar hacer comparaciones entre las posiciones de los
distintos puntos, de la fruta y de las partes del cuerpo de la
serpiente.
Ahora que sabemos como resolver los problemas del juego vamos a ver
el código.
Análisis del
código
Comenzaré
con la clase Snake.
Veamos el código completo de la clase:
Primero
declaro un ArrayList
de objetos de tipo Point
que va a tener el nombre largo.
Alli almacenaremos todas las partes del cuerpo de la serpiente. Luego
las variables snakeX
y snakeY
que me servirán para controlar y mantener actualizada la posición.
Todos
los campos son de tipo privado. Solo necesitaremos acceder a largo
desde afuera de la clase y para eso haremos un método get.
En el constructor voy a agregar el primer punto ubicado en el centro
de la pantalla (20, 15) para comenzar. En breve explicaré por que
20, 15 es el centro de la pantalla.
Después del constructor vemos un metodo getLargo().
Esta función nos servirá para poder acceder al campo largo
desde afuera de la clase.
La
siguiente función dibujoSnake(Graphics)
hace uso de un bucle for
que recorre la lista y muestra en pantalla cada uno de los puntos que
forman el cuerpo de la serpiente utilizando el objeto de gráficos g
que recibe como parámetro.
El
punto p
representa la posición del punto que estamos mostrando en cada
vuelta. Cada punto tiene una dimensión de 20x20 y multiplicamos
también sus coordenadas por 20 al mostrarlo para subdividir la
pantalla. De esta manera las dimensiones de nuestro campo de juego
serán de 40x30, es decir de 800x600 / 20.
La
función muevoSnake()
contiene la lógica necesaria para mover la serpiente.
El
for
recorre todos los puntos que forman la cola de la serpiente
comenzando desde el último y moviendo cada punto a la posición del
que le antecede en la lista. Tenemos en cuenta que largo.size()
nos devuelve la cantidad de elementos de la lista completa y como la
lista comienza en 0 al igual que un array común entonces
n=largo.size()-1.
Dejamos el punto 0 (la cabeza) fuera del for
y actualizamos su posición sumando los valores de snakeX
y snakeY
que moverán el punto hacia una u otra dirección según sean
positivos o negativos.
Creamos
ahora una función que agrega un punto a largo
a la que llamaremos cada vez que necesitamos hacer crecer la cola de
la serpiente.
La
ultima función de la clase Snake es void
direccion(string).
Cuando llamamos a esta función le pasamos el sentido en el que nos
queremos mover y lo almacenamos en el string d.
El switch evalúa el valor de d
y asigna los valores correspondientes en cada caso. Observamos que si
un valor está en 0 significa que la función de movimiento le sumará
0 al movimiento en ese sentido, es decir que no se moverá. Para
mover entonces el punto hacia un lado le tenemos que asignar el valor
1, o -1 para moverlo hacia el lado contrario ya que la función que
lo mueva le estará restando valor a la posición del punto al
sumarle un número negativo.
Pasamos
a la clase Frutita.
Veamos el código:
La
fruta va a ser representada por un campo de tipo Point
llamado frutita.
Declaramos también un objeto de tipo Random
para generar los números aleatorios que necesitamos para determinar
la posición de la fruta.
Una vez declarados el constructor va a instanciar los objetos.
La
función nuevaFrutita()
le asigna a frutita.x
y un valor aleatorio entre 0 y 39 y a frutita.y
un valor entre 1 y 29. De esta manera la fruta se traslada hacia una
nueva posición creando la ilusión de una nueva fruta.
La próxima función es dibujoFrutita(Graphics).
Imprime en la posición (frutita.x,
frutita.y)
un circulo de 20x20. Es necesario multiplicar los valores de
frutita.x
y frutita.y
por 20 para poder trasladarlos a nuestro campo de juego.
Finalmente
creamos la función getFrutita()
para poder acceder al valor del punto frutita
desde afuera de la clase.
Ahora
estamos en condiciones de pasar a la clase principal: JuegoSnakeJava.
A continuación presento el código completo y luego explicaré en
detalle cada función:
Al igual que en el Ejemplo #2, vemos que la clase JuegoSnakeJava
extiende de Jframe para crear la ventana del juego y que implementa
la interfaz KeyListener para controlar los eventos relacionados con
el teclado.
Instanciamos
un objeto JuegoSnakeJava en el punto de entrada main.
A continuación definimos las propiedades de la ventana en el
constructor:
Luego
de definir las propiedades vemos que en la siguiente linea:
this.createBufferStrategy(2)
creamos un buffer doble para poder redibujar la pantalla sin cortes.
Para
poder utilizar la interfaz KeyListener tenemos que agregar también
la siguiente linea en el constructor: this.addKeyListener(this)
.
Llamamos
ahora a inicializoObjetos()
para crear los objetos Snake y Frutita y una vez que han sido creados
podemos comenzar el juego.
Vamos
a hacer a continuación un loop
infinito que en cada vuelta va a llamar a juego()
y desde allí vamos a manejar toda la lógica, y luego llamamos a
sleep(),
que es exactamente igual a la función que utilizamos en el PONG,
para producir la demora necesaria entre una vuelta y otra.
Vemos
la función juego():
Primero
vemos que llamamos al método muevoSnake()
de la clase Snake
para mantener siempre actualizada la posición de todas las partes
del cuerpo de la serpiente.
Luego
llamamos a chequearColision().
Desde ahí vamos a controlar si la serpiente alcanzo una fruta, una
pared, o su propio cuerpo.
Finalmente
con dibujoPantalla()
ponemos en pantalla todo lo que esta ocurriendo.
Vamos
a ver como funciona chequearColision():
Comprobamos
primero si la posición de la cabeza de la serpiente ha alcanzado la
posición de la fruta. En tal caso llamo a nuevaFrutita()
para crear una fruta nueva y a crecerColaSnake()
para incrementar el tamaño de la serpiente.
El
segundo bloque if
comprueba si la serpiente ha atravesado alguno de los limites de la
pantalla. En tal caso inicio un nuevo juego. Recordemos que la
pantalla es de 800x600 pero la hemos subdividido en bloques de 20x20
de manera que manejamos un campo de juego de 40x30.
El
siguiente bucle for
pasa por todos los puntos que forman la cola de la serpiente
(omitiendo el punto 0 que es la cabeza) y comprueba si la posición
de la cabeza de la serpiente es igual a la de alguna de sus partes,
en tal caso detecta una colisión y comienza el juego nuevamente.
La
función dibujoPantalla()
es idéntica a la que utilizamos anteriormente en el ejemplo del
PONG. Aquí vamos a inicializar los objetos gráficos para poder
usarlos con las otras funciones.
Inicializamos
primero los objetos g
y bf.
Luego
vamos a realizar todas las instrucciones dentro de un try
por si el programa arroja alguna excepción.
A
continuación le asignamos a g
el valor devuelto por bf.getDrawGraphics().
De esta manera obtenemos un objeto de tipo GRAPHICS2D para poder
utilizar el buffer doble.
Ahora
llamamos entonces a las funciones que imprimen en pantalla los
puntos, la serpiente, y la fruta, pasándoles como parámetro siempre
el objeto de gráficos g.
Una
vez que hemos llamado a las funciones anteriores y ya no necesitamos
el objeto g
nos deshacemos de él mediante g.dispose().
Para
terminar mostramos todo en pantalla con bf.show()
y sincronizamos con el refresh
rate
de la pantalla con sync().
muestroPuntos()
utiliza al ser llamada desde dibujoPantalla()
el objeto de gráficos g
para mostrar en pantalla la puntuación.
Pasamos
a sleep().
Esta función es exactamente igual a la utilizada en el Ejemplo #2
pero vamos a repasarla:
Primero
creamos un punto de referencia con la suma de
System.currentTimeMillis() y el tiempo de demora y lo almacenamos en
goal.
Luego comparamos el valor de System.currentTimeMillis() produciendo
una demora hasta que este alcance a goal.
Para concluir observamos que hemos tenido que sobrescribir 3
métodos. Esto es necesario para poder utilizar la interfaz
KeyListener. Como sólo vamos a hacer uso del primero de los tres,
los otros dos métodos los dejamos vacíos.
Guardamos
en tecla
el valor del código de tecla devuelto por e.getKeyCode();
El
switch que le sigue llama al método direccion(string)
de la clase Snake
pasándole el parámetro correspondiente de acuerdo con el valor de
tecla, o llama a System.exit(0);
y finaliza la ejecución del programa si presionamos la tecla 'e'. Al
pasarle 0 como parámetro a System.exit();
estamos indicando que la ejecución ha finalizado sin errores.
Hemos llegado así al fin de esta exposición del código. He tratado
de mantener un estilo de programación acorde al paradigma de objetos
y al lenguaje que utilizamos para realizar el juego.
Muchas de las ventajas de la programación orientada a objetos pueden
apreciarse mejor en sistemas y aplicaciones de mayores dimensiones,
pero aún en un ejemplo pequeño como éste vemos cuanto ganamos en
organización y claridad del código.
Link para descargar el código:
clases:
https://dl.dropbox.com/u/103165598/snakeJava-src.rar
SnakeJava.JAR:
https://dl.dropbox.com/u/103165598/SnakeJava.jar
No hay comentarios:
Publicar un comentario