martes, 6 de noviembre de 2012

Ejemplo #4: Snake en JAVA.

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