Capítulo 2 Desarrollo de un videojuego 2D en Java

Anuncio
Capítulo 2
Desarrollo de un videojuego
2D en Java
El siguiente capítulo forma parte del material docente de la asignatura
de Inteligencia Artificial para las carreras de Ingeniería Civil en Informática
y de Ejecución en Computación e Informática de la Universidad del BíoBío. Su objetivo es explicar, paso a paso, el desarrollo de un videojuego 2D
mediante el lenguaje de programación Java (sin emplear ninguna librería
especializada para el desarrollo de videojuegos, ni motor gráfico, ni de inteligencia artificial). Posteriormente las técnicas de búsqueda IA vistas en las
clases teóricas se incorporaran con el objetivo de dotar a los personajes de
un comportamiento autónomo e inteligente. Entendiendo inteligencia cómo
la capacidad de adaptación de una entidad en un mundo cerrado en función
de cierta información previa y cambiante. El código fuente que se presenta es
de programación libre y personal, no sigue los estándares de programación
orientada objetos, todos los atributos son públicos para conseguir un prototipado más directo. El objetivo es que éste sea lo más sencillo posible de
entender para un programador no experto. Un programador experto podrá
mejorarlo con facilidad y adaptarlo a los patrones de la POO y a los estándares requeridos cuando se trabaja en proyectos de varios programadores. No
se sigue ningún patrón de diseño de desarrollo software, lo que no significa
que no se utilice programación de calidad.
2.1.
Construcción de un escenario 2D basado en
celdas.
Cuando observamos un escenario virtual 2D -dos dimensiones- (ver Figura 2.1) podemos identificar muchos elementos visuales: obstáculos, personajes secundarios, premios, vidas, tiempo, puntos acumulados, etc. Un
escenario virtual tiene diferentes capas en las cuales aparecen estos elementos y cada uno de éstos se sitúan en una posición determinada. Dicha posición puede ser fija o variar, es decir, habrá objetos sin movimiento (barreras,
premios, caminos) y con movimiento (personaje, adversarios, coches, etc).
7
Figura 2.1: Ejemplo de un Escenario o Entorno 2D de un vídeo juego comercial
Una forma de diseñar este escenario consiste en dividirlo en celdas. Una
celda (ver Figura 2.2) es un elemento atómico, representado habitualmente
por un cuadrado o un rectángulo, que tendrá una determinada posición y
unas dimensiones concretas. El objetivo principal de nuestra inteligencia
artificial será que algunas de esas celdas, las que representan un personaje
o un adversario, se muevan de forma automática con algún tipo de objetivo.
Si nos pidieran dibujar el escenario anterior, la primera opción sería usar
lápices de colores y papel, como lo hacíamos en el colegio cuando eramos pequeños. En el lenguaje de programación Java esto equivale al objeto Canvas
(papel o lienzo) y al objeto Graphics (lápiz y lapices de colores). Por ejemplo,
si quisiéramos pintar un circulo, tomaríamos papel, lápiz y pintaríamos un
circulo en cualquier posición del papel. Para hacer esto en Java, debemos
crear un objeto Canvas (zona de dibujo), y usar el objeto Graphics (nuestro
lápiz) haciendo uso de los métodos de los que esta clase dispone para dibujar
cualquier figura geométrica en 2D (para más detalle ver la API de Java). La
única diferencia entre el método manual y el computacional es que la pantalla está dividida en píxeles, por tanto tenemos que indicar al sistema dónde
queremos dibujar el círculo y cuáles serán sus dimensiones.
El objetivo de esta sesión es mostrar, de forma práctica, los recursos disponibles en Java para construir escenarios en dos dimensiones. Inicialmente
partiremos de un plano vacío constituido por un conjunto de celdas. Nuestro objetivo es conseguir una ventana dividida en celdas o cuadrantes. En
cada cuadrante se podrá, posteriormente, colocar cualquier elemento: un
personaje, un obstáculo, una moneda, una trampa, etc, representado por
una imagen representativa de tal elemento (el tamaño de las celdas y de las
imágenes será el mismo con el objetivo de facilitar la programación de los
algoritmos de búsqueda).
El primer paso a realizar consiste en ser capaces de pintar Celdas. Serán nuestras unidades atómicas, indivisibles. Para pintar una celda haremos uso de la clase Celda que implementará la funcionalidad para dibujar
rectángulos de unas determinadas dimensiones almacenadas en la interfaz
Constantes.
public interface Constantes {
public
public
public
public
public
final
final
final
final
final
int
int
int
int
int
SIZE_CELL=16;
NUMBER_CELL_WIDTH=150;
NUMBER_CELL_HEIGHT=50;
SIZE_WIDTH=SIZE_CELL*NUMBER_CELL_WIDTH;
SIZE_HEIGHT=SIZE_CELL*NUMBER_CELL_HEIGHT;
}
La interfaz Constantes la utilizamos como archivos e configuración, es
decir, para definir e inicializar variables que se utilizaran en más de una
clase y que requerimos sean visibles para ellas al inicio de la ejecución. Por
ahora: i) anchura y altura de las celdas (SIZE_CELL); ii) número de celdas a lo
largo y a lo ancho (NUMBER_CELL_WIDTH,NUMBER_CELL_HEIGHT); iii) anchura
y altura del mundo virtual, esto, tamaño de una celda por el número de
celdas a lo ancho y a lo alto, respectivamente. Es conveniente, como primera
decisión diseño, decidir que el tamaño de las celdas sea igual al tamaño
de las imágenes que usaremos para crear nuestro mundo virtual (esto lo
veremos en capítulos posteriores).
import java.awt.Graphics;
import javax.swing.JComponent;
public class Celda extends JComponent implements Constantes {
//atributos
public int coordenadaX;
public int coordenadaY;
//constructor, inicializa los atributos
public Celda(int x,int y) {
this.coordenadaX=x;
this.coordenadaY=y;
}
//metodo de JComponent para pintar en un contexto grafico
@Override
public void paintComponent(Graphics g) {
g.drawRect(coordenadaX,coordenadaY,SIZE_CELL,SIZE_CELL);
}
}
La función g.drawRect(X,Y,Ancho,Largo); dibuja un rectángulo en la
posición x e y, de ancho Ancho y de largo Largo. La clase Laberinto se encarga de crear el esqueleto del futuro laberinto. Celdas que representan las
posiciones donde los personajes y objetos podrán estar en un momento dado. Por tanto hará uso de la clase Celda creado un matriz de N por M . Igual
que la clase Celda, ésta cuenta con un método paintComponent encargado
de pintar en el Lienzo el esqueleto basado en Celdas.
import java.awt.Graphics;
import javax.swing.JComponent;
public class Laberinto extends JComponent
implements Constantes {
public Celda[][] celdas;
public Laberinto() {
celdas=new Celda[NUMBER_CELL_WIDTH][NUMBER_CELL_HEIGHT];
//inicializar el array de celdas
for(int i=0; i < NUMBER_CELL_WIDTH; i++)
for ( int j=0 ; j < NUMBER_CELL_HEIGHT ; j++)
celdas[i][j]=new Celda(i+(i*SIZE_CELL),j+(j*SIZE_CELL));
}
@Override
public void paintComponent(Graphics g) {
for(int i=0; i < NUMBER_CELL_WIDTH ; i++)
for ( int j=0 ; j < NUMBER_CELL_HEIGHT; j++)
celdas[i][j].paintComponent(g);
}
}
La clase Lienzo hará la funcional de papel, es decir, un objeto donde podremos dibujar objetos 2D. Para ello es necesario heredar de la clase Canvas
que cuenta con la funcionalidad necesaria para ello. Una vez elegido el color
de fondo, indicaremos su tamaño. Cuando un objeto Canvas se instancia y
se añade a un contenedor (ver página siguiente) se llamará al método paint
encargado de pintar los objetos indicados en él.
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Graphics;
public class Lienzo extends Canvas {
public Laberinto laberinto;
public Lienzo(){
laberinto=new Laberinto();
this.setBackground(Color.orange);
}
//metodo llamada la primera que se pinta
@Override
public void paint(Graphics g) {
laberinto.paintComponent(g);
}
}
Por último, tendríamos las dos últimas clases: la clase VentanaPrincipal
para visualizar el lienzo sobre un JFrame y la clase Main que contiene el método main(). La clase VentanaPrincipal hereda de JFrame, adquiriendo de
esta forma todas las funcionales para dibujar una ventana (implementadas
en JFrame y sus clases padre). Posteriormente, ajustaremos el tamaño de la
ventana al tamaño de la pantalla del dispositivo en el cual estemos ejecutando la aplicación. Para ello, hacemos uso de la función getScreenSize() de
super.getToolkit(), dicha función devuelve la dimensión de la pantalla
que asignaremos a un objeto del tipo Dimension llamado sizeScreen
import java.awt.Dimension;
import javax.swing.JFrame;
public class VentanaPrincipal extends JFrame {
//nuestra clase se compone de un lienzo de dibujo (herada de canvas)
public Lienzo lienzo;
public Dimension sizeScreen;
//constructor
public VentanaPrincipal() {
lienzo=new Lienzo();
sizeScreen=super.getToolkit().getScreenSize();
this.getContentPane().add(lienzo);
this.setSize(sizeScreen.width,sizeScreen.height);
}
}
import javax.swing.JFrame;
public class Main {
public static void main (String[]args) {
VentanaPrincipal vp=new VentanaPrincipal();
vp.setVisible(true);
vp.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
}
Con este pequeño conjunto de clases tendríamos implementado el escenario 2D basado en celdas que pretendíamos al inicio.
Figura 2.2: Plano vacío del escenario donde iremos colocando las entidades
de nuestro mundo virtual
Capítulo 3
Dando vida al escenario
El objetivo de este capitulo es mostrar al alumno cómo hacer que el plano
2D vacío comience a ser interactivo, es decir, reaccione a los eventos de entrada (habitualmente de ratón o teclado). La interacción de las aplicaciones
Java 2D se consigue a través de la captura de eventos. La terminología en
Java es “escuchadores” de eventos (listeners). Eventos de entrada/salida o
eventos que nosotros creamos para que un determinado elemento haga una
determinada acción en un instante concreto.
La clase Constantes permanece intacta.
En la clase Celda debemos incorporar un nuevo atributo que denominaremos celdaSeleccionada , inicializada a false. Esto quiere decir que
inicialmente ninguna celda está seleccionada. Para ello, extendemos el método paintComponent con el objetivo de distinguir entre estos dos nuevos
estados: i) celda seleccionada; ii) celda no seleccionada.
@Override
public void paintComponent(Graphics g) {
g.drawRect(coordenadaX,coordenadaY,SIZE_CELL,SIZE_CELL);
if ( celdaSeleccionada )
g.fillRect(coordenadaX,coordenadaY,SIZE_CELL,SIZE_CELL);
}
Una vez que hemos extendido este método debemos incorporar el método
comprobarSiCeldaSeleccionada(int clickX,int clickY) con el objetivo de identificar la celda que se ha seleccionado. Para ello construimos un
área igual a las coordenadas obtenidas en el evento del ratón. A estas coordenadas le sumamos la anchura y la altura de cada celda. Esto lo hacemos
empleando la clase Rectangle que nos permite saber si un punto está incluido en un rectángulo. Si el punto está incluido en el rectángulo entonces
sabremos que la celda actual se ha seleccionado y debemos modificar el atributo celdaSeleccionada con el valor true.
public void comprobarSiCeldaSeleccionada(int clickX,int clickY) {
13
Rectangle rectanguloCelda=new Rectangle(coordenadaX,coordenadaY,
SIZE_CELL,SIZE_CELL);
if ( rectanguloCelda.contains(new Point(clickX,clickY)) )
celdaSeleccionada = !celdaSeleccionada;
}
La clase Celda por tanto quedaría como se muestra a continuación:
import
import
import
import
java.awt.Graphics;
java.awt.Point;
java.awt.Rectangle;
javax.swing.JComponent;
public class Celda extends JComponent implements Constantes {
//posicion x e y de la Celda, no cambia durante la ejecucion
public int coordenadaX;
public int coordenadaY;
//variable que indica que una celda fue seleccionada
public boolean celdaSeleccionada;//estado de la celda
//constructor
public Celda(int x,int y) {
this.coordenadaX=x;
this.coordenadaY=y;
this.celdaSeleccionada=false;
}
//metodo para dibujar celda, hace uso de drawRect
//metodo para dibujar celda, hace uso de drawRect
@Override
public void paintComponent(Graphics g) {
g.drawRect(coordenadaX,coordenadaY,SIZE_CELL,SIZE_CELL);
if ( celdaSeleccionada ) {
g.fillRect(coordenadaX,coordenadaY,SIZE_CELL,SIZE_CELL);
}
}
//si el click esta sobre la celda
public void comprobarSiCeldaSeleccionada(int clickX,int clickY) {
Rectangle rectanguloCelda=n
ew Rectangle(coordenadaX,coordenadaY,SIZE_CELL,SIZE_CELL);
if ( rectanguloCelda.contains(new Point(clickX,clickY)) ) {
celdaSeleccionada = !celdaSeleccionada;
}
}
}
La clase Laberinto quedaría con el mismo código. La clase Lienzo se
extenderá para dar soporte a los eventos. Los dos eventos que vamos a considerar son:
1. Activar celda mediante eventos de ratón.
2. Desplazar celda (arriba, abajo, derecha, izquierda) mediante eventos de
teclado.
3.1.
Eventos de ratón
Note que el evento de ratón al final no se utilizará a no ser que lo mantengamos para colocar determinados objetos, es decir, que el usuario pueda diseñar sus propios escenarios. EL primer paso consiste en añadir un
escuchador a la clase Lienzo mediante el método addMouseListener cuyo argumento es un objeto MouseAdapter que posee un método que implementa el método mouseClicked encargado de implementar la funcionalidad de capturar un evento de ratón. Cuando sobrescribimos ese método
lo que hacemos es implementar la nueva funcionalidad al hacer click, en
este caso deberemos identificar la celda que se ha pulsado comprobando
que la coordenada X e Y del ratón pertenece alguna de las celdas previamente creadas. Esto lo realizaremos con un método privado denominado
activarCelda(MouseEvent evt).
La clase Lienzo quedaría de la siguiente forma.
import
import
import
import
import
java.awt.Canvas;
java.awt.Color;
java.awt.Graphics;
java.awt.event.MouseAdapter;
java.awt.event.MouseEvent;
public class Lienzo extends Canvas implements Constantes{
//para pintar el lienzo
public Laberinto laberinto;
public Lienzo(){
laberinto=new Laberinto();
//color de fondo
this.setBackground(Color.orange);
//añadimos el escuchador
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent evt) {
activarCelda(evt);
repaint();
}
});
}
//metodo llamada la primera vez que se pinta
@Override
public void paint(Graphics g) {
laberinto.paintComponent(g);
}
private void activarCelda(MouseEvent evt) {
for(int i=0; i < NUMBER_CELL_WIDTH ; i++)
for ( int j=0 ; j < NUMBER_CELL_HEIGHT; j++)
laberinto.celdas[i][j].
comprobarSiCeldaSeleccionada(evt.getX(),evt.getY());
}
}
Las clases VentanaPrincipal y Main quedarían igual.
3.2.
Eventos de teclado
En esta parte del documento vamos a explicar como podemos capturar
los eventos de teclado para mover, por ejemplo, una celda. Solamente las
clases Laberinto.java y Lienzo.java van a sufrir cambios. El primer paso,
similar a lo que hacíamos en el caso de los eventos de ratón, debemos añadir
un escuchador de eventos de teclado. Nos situamos en la clase Lienzo.java
y a continuación del código para capturar eventos de ratón colocamos el
siguiente código:
//escuchador eventos de teclado
addKeyListener(new java.awt.event.KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
laberinto.moverCelda(e);
repaint();
}
});
Este código nos proporciona la funcionalidad para añadir un nuevo escuchador de eventos de teclado haciendo uso del método addKeyListener que
permite crear un objeto del tipo KeyAdapter. Este objeto posee un método
keyPressed que proporciona la funcionalidad para identificar la tecla que se
pulsó. Se puede sobre escribir e incorporar nuestra propia funcionalidad. Lo
que haremos será llamar a un método moverCelda donde incorporaremos
las acciones en función de la tecla pulsada. Después de haber pulsado una
tecla, posiblemente, el escenario habrá sufrido alguna modificación, por ello
la llamada al método repaint() que volverá a dibujar el escenario con los
cambios que se hayan podido producir tras los eventos.
El segundo paso consiste en elegir la celda qué queremos mover. Por simplicidad aquí se ha elegido la primera celda, es decir, celda cuya coordenada
X es igual a cero, y cuya coordenada Y es igual a cero. Creamos por tanto
una Celda llamada public Celda celdaMovimiento; que inicializaremos
en el constructor con las coordenadas cero, cero:
celdaMovimiento=new Celda(0,0);
, a continuación debemos emplear los índices de esta celda para indicar que
ha sido seleccionada para que aparezca en negro y podamos moverla:
celdas[celdaMovimiento.x][celdaMovimiento.y].celdaSeleccionada=true;
Figura 3.1: Celda seleccionada para moverse por el escenario: arriba, abajo,
izquierda o derecha
Aquí implementamos el método chequear tecla que recibe como argumento un objeto del tipo KeyEvent. Dicho objeto posee un método denominado
getKeyCode(). Para saber cuáles son los códigos que corresponden a las
flechas de las teclas arriba, abajo, izquierda, derecha; podemos acceder a la
clase KeyEvent desde NetBeans situando el ratón sobre el nombre, pulsando
control y haciendo click sobre él. Esta clase posee unas constantes junto a
sus códigos:
/**
* Constant for the non-numpad <b>left</b> arrow key.
* @see #VK_KP_LEFT
*/
public static final int VK_LEFT
= 0x25;
/**
* Constant for the non-numpad <b>up</b> arrow key.
* @see #VK_KP_UP
*/
public static final int VK_UP
= 0x26;
/**
* Constant for the non-numpad <b>right</b> arrow key.
* @see #VK_KP_RIGHT
*/
public static final int VK_RIGHT
= 0x27;
/**
* Constant for the non-numpad <b>down</b> arrow key.
* @see #VK_KP_DOWN
*/
public static final int VK_DOWN
= 0x28;
Por tanto, el método moverCelda queda como sigue:
public void moverCelda( KeyEvent evento ) {
switch( evento.getKeyCode() ) {
case KeyEvent.VK_UP:
System.out.println("Mover arriba");
moverCeldaArriba();
break;
case KeyEvent.VK_DOWN:
System.out.println("Mover abajo");
moverCeldaAbajo();
break;
case KeyEvent.VK_LEFT:
System.out.println("Mover izquierda");
moverCeldaIzquierda();
break;
case KeyEvent.VK_RIGHT:
System.out.println("Mover derecha");
moverCeldaDerecha();
break;
}
}
A continuación implementamos el método moverCeldaArriba. Como tarea se deja la implementación de los otros tres métodos restantes al lector. El
resultado debería ser similar a la Figura 3.1 donde la celda que aparece en
la posición cero, cero; se puede mover por el escenario pulsando las flechas:
arriba, abajo, izquierda, derecha.
private void moverCeldaArriba(){
if (celdaMovimiento.y > 0 ) {
celdas[celdaMovimiento.x][celdaMovimiento.y].
celdaSeleccionada=false;
celdaMovimiento.y=celdaMovimiento.y-1;
celdas[celdaMovimiento.x][celdaMovimiento.y].
celdaSeleccionada=true;
}
}
Descargar