Simple ejemplo del juego de la serpiente con Java – Parte 2

En el articulo anterior llegamos al punto donde poder mover la cabeza de la víbora y «comer» los huevos que iban apareciendo aleatoriamente en la pantalla; quedo pendiente que crezca con cada huevo que comía y que al colisionar contra si misma el juego termine.

Usaremos el siguiente razonamiento: cada vez que la vibora coma un huevo se debe agregar un nuevo punto que representa su crecimiento; en algún momento sera una lista de puntos. Es por ello que utilizaremos el objeto ArrayList que agrupara los puntos de la vibora, comenzaremos solo con uno, agregaremos de manera global una variable llamada listaPosiciones:

ArrayList<Point> listaPosiciones = new ArrayList<Point>();

En el metodo startGame() agregaremos las siguientes lineas:

listaPosiciones = new ArrayList<Point>();
listaPosiciones.add(snake);

Será necesario modificar el metodo paintComponent(Graphics g) para que dibuje la vibora acorde a la lista; de paso aprovechamos para incorporar algunas lineas relacionadas a cuando se termina el juego, el metodo queda de la siguiente manera:

public void paintComponent(Graphics g) {
    super.paintComponent(g);

    if(gameOver) {
        g.setColor(new Color(0,0,0));
    } else {
        g.setColor(new Color(255,255,255));
    }
    g.fillRect(0,0, width, height);
    g.setColor(new Color(0,0,255));

    if(listaPosiciones.size() > 0) {
        for(int i=0;i<listaPosiciones.size();i++) {
            Point p = (Point)listaPosiciones.get(i);
            g.fillRect(p.x,p.y,widthPoint,heightPoint);
        }
    }

    g.setColor(new Color(255,0,0));
    g.fillRect(comida.x,comida.y,widthPoint,heightPoint);    
    
    if(gameOver) {
        g.setFont(new Font("TimesRoman", Font.BOLD, 40));
        g.drawString("GAME OVER", 300, 200);
        g.drawString("SCORE "+(listaPosiciones.size()-1), 300, 240);

        g.setFont(new Font("TimesRoman", Font.BOLD, 20));
        g.drawString("N to Start New Game", 100, 320);
        g.drawString("ESC to Exit", 100, 340);
    }

}

Son dos los cambios realizados:
1. El primer consiste en recorrer la lista e ir dibujando los puntos
2. El segundo muestra la leyenda cuando se termina el juego junto con el puntaje y las posibles opciones para continuar.

Ahora solo nos queda modificar el método actualizar() para que cambie las posiciones de los elementos de la lista de manera que se genere cierta continuidad con los puntos; y tambien detecte si existe colision, el metodo queda de la siguiente manera:

public void actualizar() {

    listaPosiciones.add(0,new Point(snake.x,snake.y));
    listaPosiciones.remove(listaPosiciones.size()-1);

    for (int i=1;i<listaPosiciones.size();i++) {
        Point point = listaPosiciones.get(i);
        if(snake.x == point.x && snake.y  == point.y) {
            gameOver = true;
        }
    }

    if((snake.x > (comida.x-10) && snake.x < (comida.x+10)) && (snake.y > (comida.y-10) && snake.y < (comida.y+10))) {
        listaPosiciones.add(0,new Point(snake.x,snake.y));
        System.out.println(listaPosiciones.size());
        generarComida();
    }
    imagenSnake.repaint();

}

Con este ultimo cambio el juego esta completo. Es muy sencillo, es una forma ideal de aprender a utilizar listas en Java.

El link al repositorio en github lo pueden encontrar aqui.

Simple ejemplo del juego de la serpiente con Java

Cada tanto me gusta salirme del tipo de aplicaciones con las que trabajo y armar algún juego sencillo o programa simplemente para ver hasta donde llego en una tarde. Ayer escribi una versión sencilla y simple del juego de la vibora (Hola Nokia!) en Java para ver como me salia.

Por supuesto no tiene gráficos elaborados, solo son rectángulos de diferente color; pero creo que lo interesante son algunas cuestiones relacionadas a como manejar los gráficos, los eventos del teclado y el ritmo del juego.

Para atacar el programa vamos a dividirlo en partes.

Estructura del juego

  • El juego tiene una clase que se utiliza para controlar los movimientos de la víbora llamada Teclas que es una extensión de KeyAdapter.
  • Una clase llamada ImagenSnake extensión de JPanel donde dibujamos la gráfica del juego.
  • Una clase llamada Momento , que es una extensión de un Thread que corre indefinidamente y maneja el ciclo de refresco del juego.
  • La clase principal donde llevamos el control de la posición de la víbora, la comida o el huevo (depende la versión del juego que conozcan) y otros parámetros necesarios.

A diferencia de un programa tradicional donde tenemos un comienzo, procesamos datos y tenemos una salida

Aqui el ciclo del programa estará basado en ciclos determinados por un hilo de ejecución (el Thread Momento) que modificara variables del juego. Eventualmente podemos hacer que la frecuencia con la que se dan los ciclos sea mas corta o mas larga cambiando la variable frequency.

En particular en este juego haremos uso Graphics para dibujar la vibora , la comida/huevos y cualquier otro contenido que tenga el juego.

Recomiendo que se lea el código final del juego, solo son 230 lineas; e intente que sea lo más fácil de leer posible, el link al archivo es el siguiente:

https://github.com/gsampallo/Snake/blob/master/src/gsampallo/Snake.java

Entorno y control del teclado.

Utilizaremos una ventana para mostrar el juego a la misma le quitaremos el marco y el titulo; tendra un tamaño de 640×480 pixeles y centraremos la ventana.

Definimos dos variables width y height para el ancho y alto de la ventana.

import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.*;
import java.awt.Toolkit;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Font;

import javax.swing.JFrame;
import javax.swing.JPanel;

import java.util.ArrayList;
import java.util.Random;

public class Snake extends JFrame {

    public Snake() {
		setTitle("Snake");

		setSize(width,height);

		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		JFrame.setDefaultLookAndFeelDecorated(false);
		setUndecorated(true);
		Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
		this.setLocation(dim.width/2-this.getSize().width/2, dim.height/2-this.getSize().height/2);

        setVisible(true);

    }

	public static void main(String[] args) {
		Snake snake1 = new Snake();
	}
}

Si ejecutamos el código anterior, solo obtendremos una ventana en blanco sin ningún tipo de contenido.

Agregaremos una sencilla clase que realizara las acciones segun que tecla presionemos, para ello creamos una clase interna llamada Teclas que extiende de KeyAdapter (una clase de Java que nos facilita los eventos del teclado).

	public class Teclas extends java.awt.event.KeyAdapter {
		@Override
		public void keyPressed(KeyEvent e) {
            if(e.getKeyCode() == KeyEvent.VK_ESCAPE) {
                System.exit(0);
            }
        }
    }

Será necesario instanciar la clase y agregarla para que atienda los eventos de las teclas en la clase principal, lo hacemos incorporando la siguiente linea en el constructora de Snake:

this.addKeyListener(new Teclas());

Mediante el método getKeyCode() de KeyEvent, el evento que recibimos al presionar una tecla, determinamos que tecla fue presionado, luego es cuestión de comparar contra el código de la tecla que deseamos que dispare el evento. En el ejemplo anterior al presionar la tecla ESC (cuyo codigo es KeyEvent.VK_ESCAPE) salimos del juego.

Crearemos una segunda clase llamada ImagenSnake que extiende de JPanel, que contendrá la gráfica del juego, sobrescribiremos su método paintComponent(Graphics g) de manera que grafique lo que deseamos; por lo pronto solo nos interesa dibuja un punto.

Existe una clase llamada Point que podemos utilizar para ubicar un punto en el planto; creamos una instancia de ella a nivel global dentro de la clase Snake, le pasamos al constructor las coordenadas de x e y.

Point snake;

    public class ImagenSnake extends JPanel {
        public void paintComponent(Graphics g) {
            super.paintComponent(g);
            g.setColor(new Color(0,0,255));
            g.fillRect(snake.x,snake.y,widthPoint,heightPoint);
        }
    }

En el constructor de Snake agregamos las siguientes lineas:

        snake = new Point(320,240); //ubicamos el punto en el centro de la pantalla

        imagenSnake = new ImagenSnake();

        this.getContentPane().add(imagenSnake);

Esto permite instanciar el panel que contendrá los gráficos y agregarlo al contenedor de la ventana. Al ejecutarlo tendremos lo siguiente:

El código hasta este punto pueden descargarlo de acá.

Hasta aquí tenemos listo el espacio donde vamos a dibujar el juego y preparada la clase que detecta los eventos del teclado.

It’s Alive: movemos el punto.

Para mover el punto azul de nuestra pantalla, que sea la cabeza de la víbora, primero debemos incorporar algunas lineas a la clase Teclas, para detectar cuando presionamos cada una de las flechas, modificamos de modo que quede de la siguiente manera:

	public class Teclas extends java.awt.event.KeyAdapter {
		@Override
		public void keyPressed(KeyEvent e) {
			if(e.getKeyCode() == KeyEvent.VK_ESCAPE) {
				System.exit(0);
			} else if(e.getKeyCode() == KeyEvent.VK_RIGHT) {
				if(direccion != "LEFT") {
                    direccion = "RIGHT";
				}
			} else if(e.getKeyCode() == KeyEvent.VK_LEFT) {
				if(direccion != "RIGHT") {
                    direccion = "LEFT";
				}
			} else if(e.getKeyCode() == KeyEvent.VK_UP) {
				if(direccion != "DOWN") {
                    direccion = "UP";
				}
			} else if(e.getKeyCode() == KeyEvent.VK_DOWN) {
				if(direccion != "UP") {
                    direccion = "DOWN";
				}								
			}
		}

	}

De manera global sera necesario definir una variable del tipo String llamada dirección; la cual contendrá la dirección sobre la que se mueve la víbora, nunca podrá dar marcha atrás, por lo que el sentido solo deberá cambiarse si es una dirección diferente a la opuesta actual; es decir si la dirección actual es RIGHT no podrá cambiarse a LEFT, solo podra modificarse por UP o DOWN. Esto evitara que colisione contra ella misma.

Adicionalmente tendremos que agregar otra clase que se ocupara de llevar el ritmo del juego; básicamente su tarea es cada cierta cantidad de tiempo (milisegundos) actualizar las variables del juego para generar el movimiento.

Antes de incorporar las clases, agregamos las siguientes variables a nivel global en Snake:

int widthPoint = 10;
int heightPoint = 10;

String direccion = "RIGHT";
long frequency = 50;

widthPoint y heightPoint nos daran la cantidad de pixeles que queres que ocupe nuestro rectangulo, asi lo podemos modificar desde un solo lugar.

direccion como habíamos comentado antes, le indica al juego hacia donde se dirige la víbora en la pantalla.

frequency establece cada cuanto sera el refresco.

Finalmente agregamos la clase Momento que contendrá lo siguiente:

	public class Momento extends Thread {
		
		private long last = 0;
		
		public Momento() {
			
		}

		public void run() {
			while(true) {
				if((java.lang.System.currentTimeMillis() - last) > frequency) {
					if(!gameOver) {

                        if(direccion == "RIGHT") {
                            snake.x = snake.x + widthPoint;
                            if(snake.x > width) {
                                snake.x = 0;
                            }
                        } else if(direccion == "LEFT") {
                            snake.x = snake.x - widthPoint;
                            if(snake.x < 0) {
                                snake.x = width - widthPoint;
                            }                        
                        } else if(direccion == "UP") {
                            snake.y = snake.y - heightPoint;
                            if(snake.y < 0) {
                                snake.y = height;
                            }                        
                        } else if(direccion == "DOWN") {
                            snake.y = snake.y + heightPoint;
                            if(snake.y > height) {
                                snake.y = 0;
                            }                        
                        }
                    }
                    actualizar();
					
					last = java.lang.System.currentTimeMillis();
				}
			}
		}
	}

Extiende de Thread puesto que necesitamos que se ejecute en un hilo paralelo al de la clase principal. Dentro del metodo run() se ejecuta un bucle permanente donde evalúa si a transcurrido la cantidad de milisegundos indicados en la variable frequency; de se así determina la dirección de la víbora y desplaza el punto tantos pixeles como lo hayamos definido en heightPoint/widthPoint. Por ultimo llama al método actualizar() (aún no lo escribimos) que refrescara las variables y actualizara la pantalla.

Dentro de la clase Snake agregamos el metodo actualizar() de la siguiente manera:

    public void actualizar() {
        imagenSnake.repaint();
    }

Por el momento solo actualiza la gráfica; pero sera en este método donde ira parte de la lógica del juego, se detectara si la víbora alcanzo la comida/huevo o si colisiono contra ella misma.

Por ultimo debemos instanciar la clase Momento y ejecutarla, lo hacemos agregando estas lineas al constructor de Snake:

Momento momento = new Momento();
Thread trid = new Thread(momento);
trid.start();

Es posible descargar el código del juego hasta este punto desde aquí.

En este punto al ejecutar el programa obtendremos un punto que se desplaza en la pantalla según la tecla que presionemos.

¿Que hay de comer?

Agregaremos algunas lineas para que figure en el plano la comida de la víbora; para ello incorporaremos una variable global del tipo Point llamada comida y un método llamado generarComida(), será de la siguiente manera:

Point comida;
public void generarComida() {
    Random rnd = new Random();
    
    comida.x = (rnd.nextInt(width)) + 5;
    if((comida.x % 5) > 0) {
        comida.x = comida.x - (comida.x % 5);
    }

    if(comida.x < 5) {
        comida.x = comida.x + 10;
    }
    if(comida.x > width) {
        comida.x = comida.x - 10;
    }

    comida.y = (rnd.nextInt(height)) + 5;
    if((comida.y % 5) > 0) {
        comida.y = comida.y - (comida.y % 5);
    }	

    if(comida.y > height) {
        comida.y = comida.y - 10;
    }
    if(comida.y < 0) {
        comida.y = comida.y + 10;
    }

}

Utilizamos la clase Random para generar un entero aleatorio entre 0 y el ancho de la pantalla; luego verificamos que sea múltiplo de 5, de modo que este en la misma rejilla que nuestra víbora; y por ultimo si la posición esta fuera de los margenes de la pantalla la ajustamos.

Modificamos el método actualizar para que cuando el punto de snake este dentro del rango de la comida desaparezca y se genere una nueva comida en otro lado, agregamos las siguientes lineas en actualizar():

public void actualizar() {
    if((snake.x > (comida.x-10) && snake.x < (comida.x+10)) && (snake.y > (comida.y-10) && snake.y < (comida.y+10))) {
        generarComida();
    }        
    imagenSnake.repaint();

}

Adicionalmente también debemos modificar el método paintComponent(Graphics g) de ImagenSnake para dibujar la comida/huevo, agregamos la siguiente linea al final:

g.setColor(new Color(255,0,0));
g.fillRect(comida.x,comida.y,widthPoint,heightPoint);   

Crearemos un nuevo método dentro de Snake llamado startGame() inicializara comida y otras vbles mas. Este método será invocado desde el contructor de Snake.

public void startGame() {
     comida = new Point(200,100);	
     snake = new Point(320,240);        
}

En este punto si ejecutamos el programa, nos va a mostrar una ventana con el punto azul que es la cabeza de nuestra víbora y un punto rojo que es la comida; al llegar a la comida desaparece y aparece en otra ubicación en la pantalla.

Se puede descargar el código hasta este punto aquí.

Solo queda algo de lógica y el juego estará completo, para que no se extienda mucho lo haré en un siguiente articulo, de todas formas más arriba se encuentra el link al repositorio con el juego completo.

La 2da parte pueden encontrarla aqui.