Desarrollo de Videojuegos 2D en Java con técnicas de Inteligencia

Anuncio
Departamento de Sistemas de Información
Desarrollo de Videojuegos 2D en Java con técnicas de
Inteligencia Artificial
Libro de texto creado para la asignatura
Escrito por:
Dr. Clemente Rubio Manzano
Prologo por Michio Kaku.
Algunas personas piensan que la inteligencia es el gran logro de la evolución. Pues bien, si esto es así, entonces deberían haber existido muchas
más especies inteligentes sobre el planeta Tierra. Pero, por lo que sabemos,
somos los únicos. Los dinosaurios, que estuvieron en el planeta durante más
de doscientos millones de años, no llegaron a ser inteligentes.
Los humanos modernos, han estado en la Tierra aproximadamente ciento
de miles de años, una pequeña fracción de la edad de la Tierra, que se estima
en 4.5 billones de años. Por tanto podríamos sacar una primero conclusión,
la inteligencia no es realmente necesaria para la vida. La madre naturaleza
se las ha ingeniado bastante bien sin criaturas inteligentes.
Pero, ¿Cómo nos hicimos inteligentes? ¿Qué nos diferencia de los animales? Hay tres ingredientes básicos. Uno es el pulgar oponible que nos permite
manipular el entorno. Así que, este es uno de los ingredientes secretos, ser
capaces de cambiar el mundo que nos rodea. El segundo ingrediente es la
visión. Pero no cualquier visión, una visión de depredador. Tenemos ojos en
la frontal de nuestra cabeza, no a los lados y ¿por qué?, porque los animales
con ojos en la parte frontal son depredadores (leones, tigres, zorros). Los animales con ojos a los lados son presas y no son inteligentes, como por ejemplo
un conejo. Un depredador tiene que aprender a cazar y acechar, aprender a
predecir los movimientos de la presa, si no, no podrá comer. Si tú eres un
conejo lo único que debes hacer es correr. El tercer ingrediente es el lenguaje porque debes poder comunicarte para que tu conocimiento adquirido
durante tu vida pase de generación en generación y por lo que sabemos los
animales aunque se comunican no permiten la transmisión de conocimiento,
salvo algunas señales primitivas. Los animales no tienen cultura, en el sentido más amplio de la palabra, nosotros sí y somos capaces de transmitirla
de forma eficaz.
Así se supone que el cerebro evolucionó. Tenemos pulgar oponible, tenemos un lenguaje de entre cinco a diez mil palabras y tenemos la visión de un
depredador. Por tanto, quizás la razón por la que ninguna criatura consiguió
ser inteligente es que no cuenta con estos tres ingredientes.
Entonces, la siguiente pregunta si podemos crear seres inteligentes. ¿Podemos hacer que un chimpancé sea inteligente como ocurrió en el planeta de
los simios?. Pues lo creas o no, la respuesta podría ser que sí. Somos iguales
a ellos en un 98.5 por ciento. Por tanto, en el futuro mediante una terapia
genética podríamos crear seres inteligentes. Pero la pregunta y la controversia, por qué manipular un chimpancé para que sea más y más humano, si
ya conocemos el resultado, nosotros.
Capítulo 1
Introducción a la
Inteligencia Artificial
La Inteligencia Artificial (IA o AI de sus siglas en inglés Artificial Intelligence) es una disciplina científica relativamente nueva cuyo objetivo es el diseño
e implementación de sistemas artificiales que permitan simular la inteligencia del ser humano, y con ello estar en disposición de realizar la mayoría de
nuestras habilidades cognitivas y motoras.
Hoy día, la IA está aún lejos de conseguir este reto, no se ha podido alcanzar aún las promesas iniciales, ni los increíbles predicciones de la industria
cinematográficas. Seguimos esperando a Terminator, al niño de IA o al sistema operativo romántico de Her; si es que alguna vez llegan, porque parece
que el par Hombre-Máquina es mucho más prometedor que el par MáquinaHombre. Parece más cercano conseguir hombres mejorados tecnológica y
genéticamente que máquinas que sean tan inteligentes como los hombres.
Porque si ya tenemos una gran máquina con un súper diseño, como es el
ser humano, por qué diseñar máquinas que nos imite en todos nuestros aspectos. ¿Tendría sentido crear máquinas que sufran o sientan dolor?. Este
interesante dilema estará fuera del alcance de este texto.
Sin embargo, la IA ha conseguido algunos hitos importantes: se ha desarrollado un software capaz de jugar a los videojuegos de Atari y superar a
los humanos en puntos; existen pacientes que han renunciado a sus manos
humanas dañadas por manos biónicas, se han creado poemas por computador que han conseguido engañar a los editores de revistas. Además de los
famosos casos con gran repercusión mediática como la victoria del supercomputador de IMB frente al campeón mundial de ajedrez o los campeones
del concurso Jeopardy de EEUU. O más recientemente, la victoria de una red
neuronal creada por la empresa DeepMind, propiedad de Google, consiguió
ganar al campeón del mundo del juego GO.
Tras este pequeño paréntesis nos toca hablar de la IA como asignatura
de Universidad, que es el tema que nos ocupa. La IA es una asignatura multidisciplinar, que podría cubrir una carrera en si misma, pues abarca áreas
tan diversas como la robótica o la biomecánica, las ciencias de la computación, la algorítmica, el análisis numérico. Pasando por las Ciencias de la
3
Cognición, hasta llegar a la filosofía o la antropología.
Si existiera una llamada Ingeniero en Inteligencia Artificial, algunos de los
ramos junto a sus profesores deberían ser:
1. Lógica y sentido común (John McCarthy).
2. Ciencias de la cognición e IA (Marvin Misky).
3. Resolución de problemas y Planificación (Nils J. Nilsson)
4. Computación con percepciones (Lofti Zadeh)
5. Probabilidad y Causalidad (Judea Pearl)
6. Computación Lingüística (Noam Chomsky)
7. Ingeniería del Conocimiento (Edward Feigenbaum)
Nosotros como ingenieros informáticos podríamos estar interesados en
cualquiera de ellas, pero en un curso de Introducción a la IA y por ser la
programación una de las competencias fundamentales de egreso, debemos
centrar nuestra atención en las técnicas de programación que permitan crear
programas informáticos que muestren un comportamiento autónomo e inteligente, es decir, crearemos sistemas software inteligentes. Y esto lo realizaremos desde una perspectiva novedosa, seremos los creadores de nuestro
propio mundo, dotaremos de movimiento a nuestras entidades y las programaremos para que realicen las funciones deseadas. La técnica con la que
trabajaremos a lo largo del curso será la Resolución de Problemas y la Planificación de rutas.
1.1.
Nuestro concepto de inteligencia
La inteligencia es un concepto con mucha controversia y sin una definición consensuada clara. Para definirla con cierto rigor y claridad necesitamos
acotarla. Para ello, vamos a crear un mundo artificial (virtual), lo más simple
o complejo que queramos, con los medios que dispongamos -y el tiempo, por
supuesto, con el que contemos, 4 meses en nuestro caso-. En este mundo
virtual habrá entidades, actores, con movimiento, sin movimiento que interactúan unos con otros.
Un humano podría interactuar con ese mundo virtual y se le podría encomendar una tarea o problema, capturar recompensas distribuidas por un
espacio 2D, por ejemplo. Éste sería capaz de resolverlo, con algo de entrenamiento, llegando incluso a obtener gran habilidad en ese proceso con el
transcurso del tiempo. La inteligencia es, en este contexto, la habilidad del
humano para capturar de la mejor forma posible las recompensas distribuidas por el escenario. Si las recompensas cambian de posición o, incluso, si
surgen nuevos elementos, un humano, tú mismo, sería capaz de adaptarse
a ese nuevo escenario, cambiar su comportamiento y volver a resolver el problema de la misma forma. La adaptación es, por tanto, como se ha determinado en la literatura especializada, unas de las cualidades más importantes
de los humanos y de su inteligencia, y continua, hoy día, siendo la principal
limitación de la IA (conocido desde sus inicios como FRAME PROBLEM). Hoy
día, ningún sistema que se haya sido creado por el hombre ha sido capaz de
adaptarse de igual forma que un humano, porque cuando crea un robot para patinar difícilmente éste podrá escalar una montaña, requerirá un nuevo
diseño, no solo cambiar su programación, también cambiar sus piezas, su
aparato locomotor artificial.
Por lo tanto, acotando, la IA será para nosotros la creación de un entorno
virtual y de unos agentes software que permitan simular a un humano, es decir, sustituirlo para la resolución de problemas en este entorno. Por ejemplo,
en el caso que nos ocupa la capacidad de recoger las recompensas distribuidas por el mundo virtual sin ser capturados por adversarios y esquivando los
obstáculos. Por tanto el concepto de agente será fundamental para nosotros.
1.2.
Diseño basado en agentes
El diseño basado en agentes hace uso de un conjunto de algoritmos de
persecución y escape, búsqueda de rutas, planificación, toma de decisión
para poder implementarlos en un entorno virtual. Los agentes se relacionan
directamente con el concepto de NPC (Non-player character), por tanto, en
este sentido, los videojuegos y los entornos virtuales se pueden considerar
sistemas multiagente. Por tanto, nuestro objetivo es estudiar cómo usar estos
agentes para que puedan emplearse en la construcción de entornos virtuales.
Existen, al menos, dos metodologías: diseño de agentes basados en comportamiento y diseño de agentes basados en objetivos.
Un agente es una entidad que recibe como entrada percepciones y devuelve acciones. Desde el punto de vista de la implementación un agente es
un programa que se instala sobre un entorno o arquitectura (mundo virtual). Los entornos con los que vamos a trabajar son entornos totalmente
observables, deterministas y discretos.
Los agentes se pueden programar en función de sus comportamientos
(e.g. atacar, huir). Para realizar una programación basada en comportamientos se deben seguir los siguientes pasos:
1. Estudiar el entorno. Definirlo formalmente mediante parámetros adecuados.
2. Analizar qué es lo qué debe ser capaz de hacer el agente (e.g. moverse,
esquivar obstáculos y oponentes, capturar recompensas.
3. Estudiar cuáles son los comportamientos diseñados y cuándo ocurren
en función del entorno
4. Asociar a cada comportamiento una funcionalidad. Establecer qué es lo
que debe ocurrir como acción asociada a cada comportamiento
Las técnicas habituales de implementación son las Máquinas de Estados
Finitas para implementar las transiciones de comportamiento y los algoritmos de búsqueda para implementar su funcionalidad asociada. Por ejemplo, en el caso del comecocos podríamos tener 3 comportamientos para los
fantasmas: inactivo, persiguiendo y huyendo. Se pasa del estado inactivo al
persiguiendo cuando han pasado dos segundos, del persiguiendo al huyendo
si el comecocos ha comido una pastilla, del huyendo al persiguiendo cuando han pasado diez segundos, de huyendo al inactivo cuando el comemos
está comiendo. Además deberíamos asociar dos funciones a cada comportamiento. la función huir() que deberá implementar la funcionalidad de este
comportamiento y la función perseguir() que implementará la funcionalidad del comportamiento persiguiendo.
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. El objetivo es que éste
sea lo más sencillo posible de entender para un programador no experto. 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 del Escenario 2D. Plano basado en Celdas
Si tomásemos como ejemplo cualquier entorno virtual en dos dimensiones 2D (ver Figura 2.1) podríamos identificar muchos elementos: obstáculos,
personajes, premios, vidas, tiempo, puntos acumulados, etc. Un entorno virtual tiene diferentes capas en las cuales aparecen estos elementos. Cada
elemento se sitúa en una posición determinada que podrá mantenerse fija
o variar. Es decir, habrá objetos inmóviles y objetos móviles. Como objetos
inmóviles destacamos obstáculos, barreras, premios, camino, etc. Como objetos móviles podríamos nombrar al personaje o los adversarios. Una posible
forma de diseñar tal universo es dividir una determinada área (en cuadrados
o rectángulos, por ejemplo ver Figura 2.2) con unas determinadas dimensio7
Figura 2.1: Ejemplo de un Escenario o Entorno 2D de un vídeo juego comercial
nes en la cual un objeto (móvil o inmóvil) podrá estar. Nuestro objetivo será
que esos objetos que se mueven parezcan inteligentes.
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 decir dónde queremos
dibujar el círculo y cuáles serán sus dimensiones.
El objetivo de esta sesión es mostrar los recursos disponibles en el lenguaje Java para construir un escenario 2D. Un escenario siempre parte de
un plano vacío de celdas que consiste en un conjunto de celdas sobre la cual
iremos colocando los elementos, nos centramos en este aspecto. No entramos, por ahora, en aspectos gráficos más avanzados. Queremos conseguir
una ventana dividida en celdas o cuadrantes. En cada cuadrante se puede
colocar cualquier elemento: un personaje, un obstáculo, una moneda, una
trampa, etc, tal y como lo habíamos especificado anteriormente.
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.
/* interfaz constantes para variables de configuracion globales */
public interface Constantes {
public
public
public
public
final
final
final
final
int
int
int
int
anchuraCelda=64;
alturaCelda=64;
anchuraMundoVirtual=5;
alturaMundoVirtual=3;
}
La interfaz Constantes la utilizamos para definir parámetros de configuración, anchura y altura del mundo virtual, anchura y altura de las celdas, es
decir, variables que se utilizaran en más de una clase y que requerimos sean
visibles para ellas. Es conveniente como primera decisión diseño pensar que
el tamaño de las celdas sea igual al tamaño de las imágenes que usaremos
para crear nuestro mundo virtual.
/* paquetes que utilizaremos */
import java.awt.Graphics;
import javax.swing.JComponent;
/* clase Celda que era de JComponent e implementa Constantes */
public class Celda extends JComponent implements Constantes {
//posicion x e y de la Celda
public int x;
public int y;
//constructor
public Celda(int x,int y) {
this.x=x;
this.y=y;
}
//metodo para dibujar celda, hace uso de drawRect
@Override
public void paintComponent(Graphics g) {
g.drawRect(x,y,anchuraCelda,alturaCelda);
}
}
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 int anchuraLaberinto,alturaLaberinto;//dimensiones del laberinto
public Celda[][] celdas;//las casillas n x m
public Laberinto() {
celdas=new Celda[anchuraMundoVirtual][alturaMundoVirtual];
//inicializar el array de celdas
for(int i=0; i < anchuraMundoVirtual; i++)
for ( int j=0 ; j < alturaMundoVirtual ; j++)
celdas[i][j]=new Celda(i+(i*anchuraCelda),
j+(j*alturaCelda));
//ancho y largo del laberinto
this.anchuraLaberinto=anchuraMundoVirtual*anchuraCelda;
this.alturaLaberinto=alturaMundoVirtual*alturaCelda;
this.setSize(anchuraLaberinto,alturaLaberinto);
}
@Override
public void paintComponent(Graphics g) {
for(int i=0; i < anchuraMundoVirtual ; i++)
for ( int j=0 ; j < alturaMundoVirtual; 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.
/* paquetes que utilizaremos */
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Graphics;
/* la clase Lienzo hereda de Canvas */
public class Lienzo extends Canvas {
//para pintar el lienzo
public Laberinto laberinto;
public Lienzo(){
laberinto=new Laberinto();
//color de fondo
this.setBackground(Color.orange);
//dimensiones del lienzo
this.setSize(laberinto.anchuraLaberinto,laberinto.alturaLaberinto);
}
//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().
/* paquetes que utilizaremos:
-la clase JFrame nos proporciona funcionalidad para crear ventanas
-la clase BorderLayout nos proporciona funcionalidad para distribuir los
elemtnos graficos */
import java.awt.BorderLayout;
import javax.swing.JFrame;
/* clase VetanaPrincipal hereda de JFrame para obtener funcionalidad
de creacion de ventanas graficas
*/
public class VentanaPrincipal extends JFrame {
//nuestra clase se compone de un lienzo de dibujo (herada de canvas)
public Lienzo lienzo;
//constructor
public VentanaPrincipal() {
lienzo=new Lienzo();
this.getContentPane().setLayout(new BorderLayout());
this.getContentPane().add(lienzo);
this.setSize(lienzo.getWidth(),lienzo.getHeight());
}
}
/* paquetes que utilizaremos:
-la clase JFrame nos proporciona funcionalidad para crear ventanas*/
import javax.swing.JFrame;
/* clase Main */
Figura 2.2: Plano vacío del escenario donde iremos colocando las entidades
de nuestro mundo virtual
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 un plano
basado en celdas.
2.2.
Dando vida al escenario
El objetivo de esta sección 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. Esto requiere extender el método paintComponent con el objetivo de distinguir entre estos dos nuevos
estados: i) celda seleccionada; ii) celda no seleccionada.
public void paintComponent(Graphics g) {
g.drawRect(x,y,anchuraCelda,alturaCelda);
//si la celda esta seleccionada entonces doy color a la celda
if ( celdaSeleccionada ) {
g.fillRect(x,y,anchuraCelda,alturaCelda);
}
}
Una vez que hemos extendido este método debemos incorporar uno nuevo que se encargue 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 ya 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.
//si el click esta sobre la celda
public void celdaSeleccionada(int xp,int yp) {
if ( rectanguloCelda.contains(new Point(xp,yp)) ) {
if ( celdaSeleccionada ) celdaSeleccionada=false;
else celdaSeleccionada=true;
}
}
La clase Celda por tanto quedaría como se muestra a continuación:
/* Version Celda.java de la sesion 0.1 */
/* paquetes que utilizaremos */
import
import
import
import
java.awt.Graphics;
java.awt.Point;
java.awt.Rectangle;
javax.swing.JComponent;
/* clase Celda que era de JComponent e implementa Constantes */
public class Celda extends JComponent implements Constantes {
//posicion x e y de la Celda, no cambia durante la ejecucion
public int x;
public int y;
//variable que indica que una celda fue seleccionada
//puede cambiar durante la ejecucion
public boolean celdaSeleccionada;//estado de la celda
public Rectangle rectanguloCelda;
//constructor
public Celda(int x,int y) {
this.x=x;
this.y=y;
this.celdaSeleccionada=false;
rectanguloCelda=new Rectangle(x,y,anchuraCelda,alturaCelda);
}
//metodo para dibujar celda, hace uso de drawRect
@Override
public void paintComponent(Graphics g) {
g.drawRect(x,y,anchuraCelda,alturaCelda);
if ( celdaSeleccionada ) {
g.fillRect(x,y,anchuraCelda,alturaCelda);
}
}
//si el click esta sobre la celda
public void celdaSeleccionada(int xp,int yp) {
if ( rectanguloCelda.contains(new Point(xp,yp)) ) {
if ( celdaSeleccionada ) celdaSeleccionada=false;
else celdaSeleccionada=true;
}
}
}
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.
2.2.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.
/* Version Lienzo.java de la sesion 0.1 */
/* paquetes que utilizaremos */
import
import
import
import
import
java.awt.Canvas;
java.awt.Color;
java.awt.Graphics;
java.awt.event.MouseAdapter;
java.awt.event.MouseEvent;
/* la clase Lienzo hereda de Canvas */
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);
//dimensiones del lienzo
this.setSize(laberinto.anchuraLaberinto,laberinto.alturaLaberinto);
//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 < anchuraMundoVirtual; i++)
for ( int j=0 ; j < alturaMundoVirtual ; j++)
laberinto.celdas[i][j].celdaSeleccionada(evt.getX(),evt.getY());
}
}
Las clases VentanaPrincipal y Main quedarían igual.
2.2.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 2.3: 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 2.3 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;
}
}
2.3.
Eligiendo las imágenes de las entidades
El objetivo de esta sección es mostrar al alumno como se puede diseñar
los actores que van intervenir en el juego. El primer paso consiste en elegir
las imágenes que se utilizaran para representar gráficamente a las entidades.
Por simplicidad supondremos tres entidades: jugador, adversario, camino y
pared. El jugador se manejará desde el teclado por el usuario, el adversario
será un NPC, el camino será una celda donde el usuario y el adversario
puedan estar o moverse en un momento dado.
Para poder usar las imágenes en el proyecto NetBeans crearemos una
carpeta denominada imagenes/ donde copiaremos las imágenes que hayamos seleccionado. Recordar que el tamaño de las imágenes debe ser similar
al tamaño de las celdas indicadas en la interface Constantes. El proyecto
NetBeans debería quedar con la siguiente estructura.
Al finalizar esta parte de la sección deberías ser capaz de poder crear un
escenario 2D como el de la Figura 2.6. Notar que una vez hayas hecho esto
podrás añadir los elementos que quieras y diseñar los laboratorios más o
menos complejos, más o menos simples. Esto es una decisión de diseño en
la que no entramos por le momento.
Para poder realizar esto necesitamos extender la funcionalidad de las clases. En la clase Celda.java debemos implementar la carga de las imágenes.
Para poder definir y cargar imágenes haremos uso de los siguientes paquetes:
java.io.File, para poder abrir un archivo guardado en memoria secundaria.
java.io.IOException, para manejar posibles errores en la apertura
de tales archivos.
javax.imageio.ImageIO, para poder cargar archivos que sean imágenes, posee un método read() de devuelve una referencia a un objeto
del tipo BufferedImage.
java.awt.image.BufferedImage, para apuntar a una imagen.
Haciendo uso de estos paquetes crearemos los siguientes atributos:
public char tipo;
public BufferedImage jugador,obstaculo,camino, adversario;
El atributo tipo guardará un carácter sobre el tipo de celda que se define.
Para ello, debemos incorporar a la interfaz Constantes cuatro constantes,
quedando con la siguiente configuración:
Figura 2.4: Imágenes elegidas para representar las entidades
Figura 2.5: Imágenes elegidas para representar las entidades
public interface
public final
public final
public final
public final
Constantes {
int anchuraCelda=64;
int alturaCelda=64;
int anchuraMundoVirtual=10;
int alturaMundoVirtual=7;
//Para manejar los tipos de celdas
public
public
public
public
final
final
final
final
char
char
char
char
JUGADOR=’J’;
CAMINO=’V’;
OBSTACULO=’O’;
ADVERSARIO=’A’;
}
Para crear y dibujar una celda del tipo Jugador haremos uso del carácter ’J’, para el camino ’V’ (de vacía), para el obstáculo ’O’ y para el adversario usaremos ’A’. Ahora en el constructor se debe inicializar y cargar
los cuatro tipo de imágenes haciendo uso del método read() de la clase
+ImageIO. Posteriormente debemos redefinir el método paintComponent para que permita dibujar la imagen en función del tipo de celda que se haya
pasado como argumento en el constructor. Por último redefinimos el método
celdaSeleccionada para que devuelve un booleano para gestionar posteriormente su selección. Notar que el atributo celdaSeleccionada desaparece porque no lo utilizaremos más. De esta forma, la clase queda con la
siguiente implementación:
import
import
import
import
import
import
import
import
java.awt.Graphics;
java.awt.Point;
java.awt.Rectangle;
java.io.File;
java.io.IOException;
java.awt.image.BufferedImage;
javax.imageio.ImageIO;
javax.swing.JComponent;
public class Celda extends JComponent implements Constantes {
public int x;
public int y;
//nuevos atributos para manejar imagenes
public char tipo;
public BufferedImage jugador,obstaculo,camino, adversario;
//constructor
public Celda(int x,int y,char tipo) {
this.x=x;
this.y=y;
this.tipo=tipo;
try {
jugador = ImageIO.read(new File("images/jugador.png"));
obstaculo = ImageIO.read(new File("images/obstaculo.png"));
camino = ImageIO.read(new File("images/camino.jpg"));
adversario = ImageIO.read(new File("images/adversario.png"));
} catch (IOException e) {
System.out.println(e.toString());
}
}
//metodo para dibujar celda, hace uso de drawRect
@Override
public void paintComponent(Graphics g) {
switch(tipo) {
case ’J’: g.drawImage(jugador,x,y, null); break;
case ’O’: g.drawImage(obstaculo,x,y, this); break;
case ’V’: g.drawImage(camino,x,y, this); break;
case ’A’: g.drawImage(adversario,x,y, this); break;
}
}
//si el click esta sobre la celda
public boolean celdaSeleccionada(int xp,int yp) {
return rectanguloCelda.contains(new Point(xp,yp));
}
}
En la clase Laberinto.java tenemos los cambios necesarios para la inicialización de las variables ya que ahora al definir las celdas debe recibir
como tercer argumento el tipo de celda. Por tanto el constructor quedaría de
la siguiente forma:
public Laberinto() {
celdas=new Celda[anchuraMundoVirtual][alturaMundoVirtual];
//inicializar el array de celdas
for(int i=0; i < anchuraMundoVirtual; i++)
for ( int j=0 ; j < alturaMundoVirtual ; j++)
celdas[i][j]=new Celda(i+(i*anchuraCelda),
j+(j*alturaCelda),’V’);
celdaMovimiento=new Celda(0,0,’J’);
celdas[celdaMovimiento.x][celdaMovimiento.y].tipo=’J’;
//ancho y largo del laberinto
this.anchuraLaberinto=anchuraMundoVirtual*anchuraCelda;
this.alturaLaberinto=alturaMundoVirtual*alturaCelda;
this.setSize(anchuraLaberinto,alturaLaberinto);
}
Adicionalmente como el atributo celdaSeleccionada fue descartado debemos redefinir la implementación de los movimientos. Un movimiento se
simula pasando la celda actual a tipo ’V’ y la futura celda a tipo ’J’. Se pone el código de moverCeldaArriba() y se deja como ejercicio al lector la
definición del resto de métodos:
private void moverCeldaArriba(){
if (celdaMovimiento.y > 0 ) {
celdas[celdaMovimiento.x][celdaMovimiento.y].tipo=’V’;
celdaMovimiento.y=celdaMovimiento.y-1;
celdas[celdaMovimiento.x][celdaMovimiento.y].tipo=’J’;
}
}
En la clase Lienzo.java debemos modificar la implementación del método activarCelda. Ahora debemos saber cuándo se pulsó el botón derecho o
el izquierdo. Si se pulsa el botón izquierdo entonces el tipo de celda será un
adversario, si se pulsa el botón el botón derecho entonces el tipo de celda será
un obstáculo. Para conseguir esta funcionalidad debemos emplear la función
evt.getModifiers() haciendo un “and” con InputEvent.BUTTON1_MASK,
si el resultado es igual a InputEvent.BUTTON1_MASK entonces se pulsó el
botón derecho, en otro caso el izquierdo.
private void activarCelda(MouseEvent evt) {
for(int i=0; i < anchuraMundoVirtual; i++) {
for ( int j=0 ; j < alturaMundoVirtual ; j++) {
if ( laberinto.celdas[i][j].celdaSeleccionada(evt.getX(),evt.getY()) ) {
//Para saber si se pulso
if((evt.getModifiers() & InputEvent.BUTTON1_MASK) == InputEvent.BUTTON1_MA
System.out.println("Boton derecho - Poner obstaculo");
laberinto.celdas[i][j].tipo=’O’;
}else {
System.out.println("Boton izquierdo - Poner adversario");
laberinto.celdas[i][j].tipo=’A’;
}
}
}
}
}
La última modificación tiene que ver con el inicio de ventana. Si no se ha
percatado aún, al ejecutar las primeras veces la aplicación los eventos no se
consideran hasta que el usuario hace click sobre la ventana. Para solucionar
y focalizar los escuchadores sobre la ventana principal debemos hacer uso de
dos métodos lienzo.setFocusable(true); y lienzo.requestFocus();.
De esta forma la clase VentanaPrincipal.java quedaría con la siguiente
implementación.
import java.awt.BorderLayout;
import java.io.IOException;
import javax.swing.JFrame;
public class VentanaPrincipal extends JFrame {
//nuestra clase se compone de un lienzo de dibujo (herada de canvas)
public Lienzo lienzo;
//constructor
public VentanaPrincipal() {
lienzo=new Lienzo();
lienzo.setFocusable(true);
lienzo.requestFocus();
this.getContentPane().setLayout(new BorderLayout());
this.getContentPane().add(lienzo);
this.setSize(lienzo.getWidth()*2,lienzo.getHeight()*2);
}
}
Si todo es correcto usted debería poder crear el primer diseño de su mundo virtual, similar a como aparece en la Figura 2.6.
2.3.1.
Eliminando el parpadeo
Uno de los problemas más comunes cuando se desarrollan animaciones
en Java es el parpadeo (ver Figura 2.7). El parpadeo es un problema bastante
Figura 2.6: Primer diseño de nuestro mundo virtual
común en animación y se debe a dos factores: i) el fondo de la animación se
limpia por defecto con el color de fondo antes de que se llame al método
pintar; ii) los métodos de pintar en pantalla son costosos y el sistema suele
tardar más en dibujar la animación que el sistema en refrescar la pantalla.
Se pueden emplear dos técnicas para eliminar el parpadeo. La primera consiste en sobre escribir el método update(Graphics g). La segunda,
que veremos en la siguiente sección, consiste en implementar la técnicas de
double buffering o doble buffer.
Usar el método update(Graphics g) es la única forma de prevenir que el
fondo del componente se limpie justo antes de que se dibuje el componente.
Es decir, con update(Graphics g) se fuerza a que el componente se dibuje
y después se limpie el fondo.
En nuestro caso, deberemos modificar los protocolos de pintado de las
clases Celda.java, Laberinto.java y Lienzo.java, empleado tal método.
En las tres clases procederemos de la misma forma, crearemos un método update(Graphics g), llevaremos el código que aparece en los métodos
+paintComponent y paint a este método y en su lugar llamaremos al método update. Por ejemplo el código de pintado para las tres clases sería el
siguiente:
// Código de pintado de la clase Celda.java
@Override
public void update(Graphics g) {
switch(tipo) {
case ’J’: g.drawImage(jugador,x,y, this); break;
case ’O’: g.drawImage(obstaculo,x,y, this); break;
case ’V’: g.drawImage(camino,x,y, this); break;
case ’A’: g.drawImage(adversario,x,y, this); break;
}
}
Figura 2.7: La parte derecha se muestra parte del fondo sin que las celdas
aparezcan como ocurre en la parte izquierda
@Override
public void paintComponent(Graphics g) {
update(g);
}
// Código de pintado de la clase Laberinto.java
@Override
public void update(Graphics g) {
for(int i=0; i < anchuraMundoVirtual ; i++)
for ( int j=0 ; j < alturaMundoVirtual; j++)
celdas[i][j].update(g);
}
@Override
public void paintComponent(Graphics g) {
update(g);
}
//Codigo de pintado de la clase Lienzo.java
@Override
public void update(Graphics g) {
laberinto.paintComponent(g);
}
@Override
public void paint(Graphics g) {
update(g);
}
Figura 2.8: Fondo del mundo virtual
2.3.2.
Imagen de fondo
Es habitual emplear una imagen de fondo para dar más realismo al videojuego. Por ejemplo, supongamos que queremos emplear una pecera como
escenario del videojuego. Podríamos elegir una imagen que la simule (ver
Figura 2.8).
Para incorporar este fondo debemos modificar la clase Lienzo.java haciendo uso de las funciones de carga y dibujo de imágenes. Por tanto el
código de la clase Lienzo quedaría cómo sigue (solo se muestra las partes
que cambian). Note que la imagen debería tener una dimensión de ancho de
anchoCelda*anchoMundoVirtual y una altura de anchoCelda*anchoMundoVirtual
para que los movimientos de teclado alcancen a todo el fondo. Para este caso
se emplearon las siguiente definición de constantes y una imagen jpeg de
ancho 1280 y largo de 640.
public
public
public
public
final
final
final
final
int
int
int
int
anchuraCelda=64;
alturaCelda=64;
anchuraMundoVirtual=20;
alturaMundoVirtual=10;
public class Lienzo extends Canvas implements Constantes{
public Laberinto laberinto;
public Image fondo;
public Lienzo() {
laberinto=new Laberinto();
try {
fondo = ImageIO.read(new File("images/fondo.jpg"));
} catch (IOException e) {
System.out.println(e.toString());
}
[...]
}
@Override
public void update(Graphics g) {
g.drawImage(fondo,0,0, null);
laberinto.paintComponent(g);
}
[...]
}
El siguiente paso consiste en comentar la parte de la clase Celda.java
donde se gestiona las celdas de tipo ’V’.
//constructor
public Celda(int x,int y,char tipo) {
this.x=x;
this.y=y;
this.tipo=tipo;
try {
jugador = ImageIO.read(new File("images/jugador.png"));
obstaculo = ImageIO.read(new File("images/obstaculo.png"));
//camino = ImageIO.read(new File("images/camino.jpg"));
adversario = ImageIO.read(new File("images/adversario.png"));
} catch (IOException e) {
System.out.println(e.toString());
}
}
//metodo llamado cuando repaint
@Override
public void update(Graphics g) {
switch(tipo) {
case ’J’: g.drawImage(jugador,x,y, null); break;
case ’O’: g.drawImage(obstaculo,x,y, this); break;
//case ’V’: g.drawImage(camino,x,y, this); break;
case ’A’: g.drawImage(adversario,x,y, this); break;
}
}
Otra opción muy conveniente que se puede utilizar en la etapa de diseño
es mostrar las celdas por encima del fondo para monitorizar los movimientos
de las entidades (ver Figura 2.10). Para hacer esto hay que seguir dos pasos:
i) definir un color que sea transparente; ii) en el caso ’V’ cambiar el dibujado
de imagen por un rectángulo que se rellene con este color. El color de fondo
lo podemos definir en la interfaz Constantes.
import java.awt.Color;
public interface Constantes {
[...]
public final int ALFA=127;
public final Color COLORFONDO=new Color(153,217,234,ALFA);
}
La parte de código del método update de la clase Celda quedaría como
sigue:
@Override
public void update(Graphics g) {
switch(tipo) {
case ’J’: g.drawImage(jugador,x,y, null); break;
case ’O’: g.drawImage(obstaculo,x,y, this); break;
case ’V’: g.setColor(COLORFONDO);
g.fillRect(x, y,anchuraCelda,alturaCelda);
break;
case ’A’: g.drawImage(adversario,x,y, this); break;
}
}
El problema de esta solución es que se repintan muchos elementos por lo
que necesitaremos implementar el double buffering. Para implementar esta
técnica es necesario modificar el protocolo de pintado de la clase Lienzo.
Vamos a utilizar dos nuevos atributos en dicha clase. El primero será de tipo
Graphics y lo utilizaremos como buffer de contexto grafico. El segundo será
la imagen donde pintaremos el fondo del contexto.
//clase Lienzo
//para implementar el doble buffer
public Graphics graficoBuffer;
public Image imagenBuffer;
Ahora modificamos el método update(Graphics g) para implementar el
doble buffer. Primero, si el buffer no fue creado, se inicializa el buffer gráfico mediante la imagen auxiliar: creamos una nueva imagen y asignamos su
contexto gráfico al nuevo buffer. Si el buffer gráfico ya fue creado entonces
modificamos su color con el fondo del contexto gráfico actual, pintamos un
rectángulo entero y dibujamos la imagen de fondo. Entonces se llama al método update con el contexto gráfico del buffer. Después del update pintamos
la imagenBuffer (ver Figura 2.9).
Figura 2.9: Explicación del doble buffer
Override
public void update(Graphics g) {
//inicialización del buffer gráfico mediante la imagen
if(graficoBuffer==null){
imagenBuffer=createImage(this.getWidth(),this.getHeight());
graficoBuffer=imagenBuffer.getGraphics();
}
//volcamos color de fondo e imagen en el nuevo buffer grafico
graficoBuffer.setColor(getBackground());
graficoBuffer.fillRect(0,0,this.getWidth(),this.getHeight());
graficoBuffer.drawImage(fondo, 0, 0, null);
laberinto.update(graficoBuffer);
//pintamos la imagen previa
g.drawImage(imagenBuffer, 0, 0, null);
}
2.4.
2.4.1.
Funcionalidades adicionales: música y sprites
Música
Para incorporar música a nuestro proyecto Java vamos a emplear tres paquetes: javax.sound, java.net y java.io. Lo primero que debemos hacer
será seleccionar un archivo .wav y copiarlo en una carpeta \musica a la
altura de \src e \imagenes. En segundo lugar crearemos una clase llamada HiloMusica.java que heredará de la clase Thread. La clase Thread, que
veremos con más profundidad más adelante, permita al programador crear
hilos de ejecución. Un hilo de ejecución es una simplemente una porción de
código, que aparecerá en un método run(), que se puede ejecutar de forma
concurrente o paralela sobre el computador. Note que hay una pequeña diferente entre estos conceptos. Hablamos de concurrencia cuando un mismos
procesador o unidad de procesamiento se comparte entre varios hilos. Se
Figura 2.10: Mundo virtual con sobre escritura del método update e implementación doble buffer
habla de paralelismo cuando cada hilo se ejecuta en una unidad de procesamiento diferente al contar el computador con varias de estas unidades (lo
que denominada como núcleo, del inglés core).
La clase HiloMusica.java estará formada por tres atributos. Uno de tipo
Clip para manejar la canción, uno de tipo string para almacenar la ruta del
mismo y uno de tipo int para establecer el número de veces que queremos
que se repita la canción maneja por Clip.
import
import
import
import
import
import
import
import
java.io.IOException;
java.net.MalformedURLException;
java.net.URL;
javax.sound.sampled.AudioInputStream;
javax.sound.sampled.AudioSystem;
javax.sound.sampled.Clip;
javax.sound.sampled.LineUnavailableException;
javax.sound.sampled.UnsupportedAudioFileException;
public class HiloMusica extends Thread {
public Clip cancion;
public String ruta;
public int repeticiones;
public HiloMusica(String ruta,int repeteciones) {
this.ruta=ruta;
this.repeticiones=repeteciones;
}
@Override
public void run() {
try {
URL url = new URL(ruta);
AudioInputStream audioIn = AudioSystem.getAudioInputStream(url);
cancion = AudioSystem.getClip();
cancion.open(audioIn);
cancion.loop(repeticiones);
}catch(MalformedURLException murle) {
System.out.println(murle.toString());
}catch(UnsupportedAudioFileException |
IOException |
LineUnavailableException e) {
System.out.println(e.toString());
}
}//fin del método run
}
Para abrir un archivo de este tipo se debe proporcionar la ruta absoluta. Por tanto vamos a añadir a la clase Constantes la ruta global proyecto
haciendo uso de System.getProperty(‘‘user.dir’’) que nos proporciona la ruta absoluta del proyecto. Añadimos la siguiente linea de código a la
clase Constantes.java.
public final String RUTA="file:///"+System.getProperty( "user.dir" );
Por último en la clase VentanaPrincipal.java inicializamos un objeto
de tipo HiloMusica y lo lanzamos llamando al método run() que ejecuta el
código necesario para lanzar la canción.
public class VentanaPrincipal extends JFrame implements Constantes{
[...]
public HiloMusica player;
//constructor
public VentanaPrincipal() {
[...]
player=new HiloMusica(RUTA+"/music/klemenzza.wav",2);
player.run();
}
2.4.2.
Sprites
Los sprites (“duendes”) se emplean en videojuegos para crear los gráficos
jugadores y adversarios. Se utilizan para producir una simulación de movimiento, como un personaje corriendo, alguna expresión de la cara o algún
movimiento corporal (ver Figura 6.1).
Figura 2.11: Ejemplos Sprites
Para emplearlos se procede de forma simular a la carga de imágenes. Sin
embargo ahora debemos recorrer la imagen e ir cargando de forma individual cada imagén por separado. Normalmente el proceso consiste en leer
subimágenes e ir almacenándolas en un array (ver Figura 2.12)
Figura 2.12: Proceso de lectura de un sprites y su almacenamiento de sus
subimágenes en un array
Antes de nada debemos ajustar el tamaño de las celdas al tamaño de los
sprites. En este ejemplo usamos un sprites cuyas subimágenes son de 32
por 32. Debemos ir a Constantes.java y cambiar el tamaño. A continuación,
para implementar el proceso de carga de las subimágenes debemos crear tres
atributos en la clase Celda.java.
public int indexSprite;
public BufferedImage sprites[],imagenSprites;
El atributo indexSprite lo empleamos para movernos por el array sprites.
El atributo sprites almacenará las imágenes. EL atributo imagenSprites
será el que almacena la imagen de los sprites. Así, el constructor de la clase
Celda queda como sigue:
public Celda(int x,int y,char tipo) {
this.x=x;
this.y=y;
this.tipo=tipo;
indexSprite=2;
//indice que corresponde a una subimagen de frente
try {
jugador = ImageIO.read(new File("images/jugador.png"));
obstaculo = ImageIO.read(new File("images/obstaculo.png"));
//camino = ImageIO.read(new File("images/camino.jpg"));
adversario = ImageIO.read(new File("images/adversario.png"));
//gestion de sprites
//cargo la imagen de grupo de imagenes
imagenSprites = ImageIO.read(new File("images/snake.png"));
//creo una array de 4 x 3
sprites = new BufferedImage[4 * 3];
//lo recorro separando las imagenes
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 4; j++) {
sprites[(i * 4) + j] =
imagenSprites.getSubimage(i * anchuraCelda,
j * alturaCelda,
anchuraCelda, alturaCelda);
}
}
} catch (IOException e) {
System.out.println(e.toString());
}
}
Una vez cargadas las subimágenes debemos modificar el atributo indexSprite cada vez que se haga un movimiento. El procedimiento consiste en
modificar su valor en función de si se pulsa arriba, abajo, izquierda o derecha. Si se pulsa arriba indexSprite debe apuntar a una subimagen donde el
personaje aparezca de espaldas, si es abajo a una subimagen de frente, si es
izquierda a una subimagen donde el personaje aparezca de perfil izquierdo y
si se pulsa derecha, una subimagen de perfil derecho. Estas modificaciones
se realizan en la clase laberinto en los métodos de movimientos. Así, estos
métodos quedarían como sigue:
private void moverCeldaArriba(){
if (celdaMovimiento.y > 0 ) {
celdas[celdaMovimiento.x][celdaMovimiento.y].tipo=’V’;
celdaMovimiento.y=celdaMovimiento.y-1;
celdas[celdaMovimiento.x][celdaMovimiento.y].tipo=’J’;
celdas[celdaMovimiento.x][celdaMovimiento.y].indexSprite=0;
}
}
private void moverCeldaAbajo(){
if (celdaMovimiento.y+1 < alturaMundoVirtual ) {
celdas[celdaMovimiento.x][celdaMovimiento.y].tipo=’V’;
celdaMovimiento.y=celdaMovimiento.y+1;
celdas[celdaMovimiento.x][celdaMovimiento.y].tipo=’J’;
celdas[celdaMovimiento.x][celdaMovimiento.y].indexSprite=2;
}
}
private void moverCeldaIzquierda(){
if (celdaMovimiento.x > 0 ) {
celdas[celdaMovimiento.x][celdaMovimiento.y].tipo=’V’;
celdaMovimiento.x=celdaMovimiento.x-1;
celdas[celdaMovimiento.x][celdaMovimiento.y].tipo=’J’;
celdas[celdaMovimiento.x][celdaMovimiento.y].indexSprite=1;
}
}
private void moverCeldaDerecha(){
if (celdaMovimiento.x+1 < anchuraMundoVirtual ) {
celdas[celdaMovimiento.x][celdaMovimiento.y].tipo=’V’;
celdaMovimiento.x=celdaMovimiento.x+1;
celdas[celdaMovimiento.x][celdaMovimiento.y].tipo=’J’;
celdas[celdaMovimiento.x][celdaMovimiento.y].indexSprite=3;
}
}
Capítulo 3
Animación básica
3.1.
Dando vida al adversario
El objetivo de este capítulo es explicar cómo se pueden animar las entidades, esto es, proporcionales movimiento. Para nosotros animar será decirle
a la entidad que pasos debe dar. Para comenzar sólo animaremos a los adversarios y los movimientos serán de derecha a izquierda simulando así una
avance. Al llegar al final el adversario desaparecerá y de forma aleatoria aparece en el lado derecho de nuevo, así sucesivamente hasta que termine la
partida.
Esta funcionalidad puede implementarse en Java con Hilos de Ejecución.
Informalmente, y en este contexto, un hilo de ejecución será una tarea (bloque de código) que se ejecuta cada cierto tiempo y que puede hacerlo de forma
concurrente o paralela junto a otros hilos de ejecución. Estos hilos de ejecución se denominan TimerTask. Un TimerTask puede lanzarse cada X segundos. En cada lanzamiento se ejecuta el código situado en el método run().
Por tanto mi adversario deberá heredar de esta clase para poder moverse
por el escenario. La implementación de una clase Adversario.java sería la
siguiente:
import java.util.TimerTask;
public class Adversario extends TimerTask implements Constantes{
public Laberinto laberinto;
public Celda adversario;
public Adversario(Laberinto laberinto) {
this.laberinto=laberinto;
adversario=new Celda(anchuraMundoVirtual-1,
numeroAleatorio(0,alturaMundoVirtual-1),’A’);
laberinto.celdas[adversario.x][adversario.y].tipo=’A’;
}
public void moverAdversario(){
35
if (adversario.x > 0 ) {
laberinto.celdas[adversario.x][adversario.y].tipo=’V’;
adversario.x=adversario.x-1;
laberinto.celdas[adversario.x][adversario.y].tipo=’A’;
}else {
laberinto.celdas[adversario.x][adversario.y].tipo=’V’;
adversario.x=anchuraMundoVirtual-1;
adversario.y=numeroAleatorio(0,alturaMundoVirtual-1);
laberinto.celdas[adversario.x][adversario.y].tipo=’A’;
}
}
@Override
public void run() {
moverAdversario();
laberinto.lienzoPadre.repaint();
}
}
Necesitamos un método numeroAleatorio que nos de un número cualquier entre n y m.
public interface Constantes {
[...]
default int numeroAleatorio(int minimo, int maximo) {
Random random = new Random();
int numero_aleatorio = random.nextInt((maximo - minimo) + 1) + minimo;
return numero_aleatorio;
}
}
La clase Lienzo quedaría como sigue. Notar que se ha eliminado todo lo
relacionado con eventos de ratón.
public class Lienzo extends Canvas implements Constantes{
//sin cambios (resto de atributos)
//Para animacion basica
public Adversario adversario,adversario2;
public Timer lanzadorTareas;
public Lienzo() {
laberinto=new Laberinto(this);
adversario=new Adversario(laberinto);
adversario2=new Adversario(laberinto);
try {
fondo = ImageIO.read(new File("images/fondo.jpg"));
} catch (IOException e) {
System.out.println(e.toString());
}
//dimensiones del lienzo
this.setSize(laberinto.anchuraLaberinto,laberinto.alturaLaberinto);
//escuchador eventos de teclado
addKeyListener(new java.awt.event.KeyAdapter() {
@Override
public void keyPressed(KeyEvent evt) {
laberinto.moverCelda(evt);
repaint();
}
});
lanzadorTareas=new Timer();
lanzadorTareas.scheduleAtFixedRate(adversario,0,1000);
lanzadorTareas.scheduleAtFixedRate(adversario2,0,500);
}
@Override
public void update(Graphics g) {
//sin cambios
}
//metodo llamada la primera vez que se pinta
@Override
public void paint(Graphics g) {
//sin cambios
}
}
El último cambio lo debemos hacer en Laberinto, pasarle como argumento
el lienzo para que desde adversario, que se mueve por un laberinto, se pueda
llamar al repintar de lienzo.
public class Laberinto extends JComponent
implements Constantes {
public
public
public
public
int anchuraLaberinto,alturaLaberinto;//dimensiones del laberinto
Celda[][] celdas;//las casillas n x m
Celda celdaMovimiento;
Lienzo lienzoPadre;
public Laberinto(Lienzo lienzoPadre) {
this.lienzoPadre=lienzoPadre;
//resto igual
}
//resto igual
}
3.1.1.
Practicando mediante dos tareas
En la sección previa aprendimos a mover a los adversarios de derecha
a izquierda. En este mismo proyecto se pide realizar lo siguiente. Primero,
diseñar e implementar una clase Jugador.java que sea un hilo de ejecución.
Segundo, crear los métodos necesarios para mover al jugador de izquierda a
derecha. Si se produce una colisión entre el jugador y el adversario deberían
salir en sentido contrario al que iban al momento de chocar.
El primer paso consiste en construir una clase Jugador.java que herede
de TimerTask. La clase Jugador cuenta con un atributo direccion que indica
si se mueve hacia la izquierda o hacia la izquierda. Por tanto, contará con dos
métodos de movimiento moverJugadorDerecha y moverJugadorIzquierda.
El algoritmo de movimiento a la derecha es el siguiente:
1. Si al mover a la derecha no es adversario me muevo normal
2. Si al mover a la derecha hay un adversario cambio de direccion
El código de la clase Jugador queda como sigue.
import java.util.TimerTask;
public class Jugador extends TimerTask implements Constantes{
public Laberinto laberinto;
public Celda jugador;
public int direccion;//para saber hacia donde me muevo
public Jugador(Laberinto laberinto) {
this.laberinto=laberinto;
jugador=new Celda(0,numeroAleatorio(0,alturaMundoVirtual-1),’J’);
laberinto.celdas[jugador.x][jugador.y].tipo=’J’;
direccion=0;//mover derecha
}
public void moverJugadorDerecha(){
if (jugador.x < anchuraMundoVirtual-1 ) {
//si al mover a la derecha no es adversario
//me muevo normal
if ( laberinto.celdas[jugador.x+1][jugador.y].tipo!=’A’ ) {
laberinto.celdas[jugador.x][jugador.y].tipo=’V’;
jugador.x=jugador.x+1;
laberinto.celdas[jugador.x][jugador.y].tipo=’J’;
//y si no cambio de direccion a la izquierda => direccion=1
}else direccion=1;
}else {
//para re-aparecer
laberinto.celdas[jugador.x][jugador.y].tipo=’V’;
jugador.x=0;
jugador.y=numeroAleatorio(0,alturaMundoVirtual-1);
laberinto.celdas[jugador.x][jugador.y].tipo=’J’;
}
}
public void moverJugadorIzquierda(){
//si no estoy al inicio
if (jugador.x > 0 ) {
//compruebo que al moverme a la izquierda no
//este el adversario
if (laberinto.celdas[jugador.x-1][jugador.y].tipo!=’A’){
laberinto.celdas[jugador.x][jugador.y].tipo=’V’;
jugador.x=jugador.x-1;
laberinto.celdas[jugador.x][jugador.y].tipo=’J’;
//en caso contrario cambio de direccion
}else direccion=0;
}else {
laberinto.celdas[jugador.x][jugador.y].tipo=’V’;
jugador.x=anchuraMundoVirtual-1;
jugador.y=numeroAleatorio(0,alturaMundoVirtual-1);
laberinto.celdas[jugador.x][jugador.y].tipo=’J’;
}
}
@Override
public void run() {
if (direccion==0) moverJugadorDerecha();
else moverJugadorIzquierda();
laberinto.lienzoPadre.repaint();
}
}
Notar la implementación del método, ahora, si direccion es igual 0 entonces el jugador se movera a la derecha, en caso contrario se moverá a la
izquierda. La implementación de la clase Adversario sería análoga. Recuerde
que la clase Lienzo debería modicarse para lanzar al jugador.
Capítulo 4
Interfaces gráficas en Java
En este capítulo veremos una introducción al paquete Swing de Java mediante el cual se pueden construir interfaces de usuario rápida y fácilmente.
Presentaremos sus principales características y veremos detalladamente todos los conceptos que necesita conocer para poder utilizar y entender los
componentes Swing de forma efectiva.
Vamos a mostrar algunos de los componentes más utilizados de Swing y
explicaremos el concepto de Contenedor (Container), central en la lógica de
ventanas de Java.
Para ilustrarlo, usaremos el programa “Hola Mundo Gráfico” y un conversor de temperatura. La aplicación crea cuatro componentes Swing: un frame,
o ventana principal (JFrame) un panel, algunas veces llamado panel (JPanel)
un botón (JButton) una etiqueta (JLabel)
4.1.
Java Foundation Classes
¿Qué son el JFC y Swing? JFC es la abreviatura de Java Foundation
Classes, que comprende un grupo de características para ayudar a construir
interfaces gráficas de usuario (GUIs). Los componentes Swing incluyen todo tipo de elementos gráficos, desde botones hasta paneles de separación o
tablas de datos. Swing da Soporte para definir el aspecto y el comportamiento: ofrece una amplia selección de aspectos y comportamientos para cada
componente (Por ejemplo, el mismo programa puede usar el Aspecto y Comportamiento Java o el Aspecto y Comportamiento Windows). Existe además
una API de Accesibilidad permite crear interfaces para asistencia al usuario
como lectores de pantalla o interfaz Braille para obtener información desde el
interface de usuario. Por otro lado, la API 2D de Java permite a los desarrolladores incorporar fácilmente gráficos 2D de alta calidad, texto, e imágenes
en aplicaciones.
4.2.
Menu Inicial
Empleamos el menú inicial del juego YADY (http://www.youractionsdefine.you/y
41
Constantes.java
import java.awt.Dimension;
import java.awt.Toolkit;
public interface Constantes {
public
public
public
public
public
public
int FUENTE_SIZE=12;
int CELDA_SIZE=32;
int N=31;
int M=21;
Dimension SCREEN_SIZE = Toolkit.getDefaultToolkit().getScreenSize();
String RUTA_DIRECTORIO=System.getProperty( "user.dir" );
}
Lienzo.java
import java.awt.Canvas;
import java.awt.Graphics;
public class Lienzo extends Canvas
{
public Lienzo() {}
@Override
public void update(Graphics g) {}
}
PanelDeJuego.java
import java.awt.BorderLayout;
import javax.swing.JPanel;
public class PanelDeJuego extends JPanel implements Constantes {
public Lienzo lienzo;
public String nombre, email;
public boolean castellano;
public boolean toShowFeedback;
public PanelDeJuego(String nombre,String email,boolean castellano,
boolean toShowFeedback) {
this.nombre=nombre;
this.email=email;
this.castellano=castellano;
this.toShowFeedback=toShowFeedback;
setLayout(new BorderLayout());
setSize((CELDA_SIZE*M)+CELDA_SIZE,
(CELDA_SIZE*N)+CELDA_SIZE);
lienzo=new Lienzo();
lienzo.setFocusable(true);
lienzo.requestFocus();
add(lienzo);
}
}
PanelMenuInicial.java
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
import
java.awt.Color;
java.awt.Dimension;
java.awt.Font;
java.awt.Graphics;
java.awt.event.ActionEvent;
java.awt.image.BufferedImage;
java.io.File;
java.io.IOException;
java.util.logging.Level;
java.util.logging.Logger;
javax.imageio.ImageIO;
javax.swing.ImageIcon;
javax.swing.JButton;
javax.swing.JCheckBox;
javax.swing.JFrame;
javax.swing.JLabel;
javax.swing.JOptionPane;
javax.swing.JPanel;
javax.swing.JTextField;
public class PanelMenuInicial extends JPanel implements Constantes{
public
public
public
public
public
public
public
public
public
public
JButton comenzar,salir;
JCheckBox with_feedback, with_feedback2, castellano, ingles;
JLabel nombre;
JTextField campo_nombre;
JLabel email;
JTextField campo_email;
ImageIcon icono_mundo_klemenzza;
String[] tipos_letra;
JFrame ventana_principal;
boolean toShow_feedback;
public PanelMenuInicial(JFrame ventana_principal) {
this.ventana_principal=ventana_principal;
this.setName("YADY Computer Game");
this.setLayout(null);
this.setSize(SCREEN_SIZE.width,SCREEN_SIZE.height);
toShow_feedback=true;
nombre=new JLabel("N A M E : ");
nombre.setFont(new Font("Times New Roman",Font.BOLD,40));
nombre.setForeground(Color.orange);
nombre.setBounds(20,20,400,200);
campo_nombre=new JTextField("");
campo_nombre.setFont(new Font("Times New Roman",Font.BOLD,40));
campo_nombre.setForeground(Color.LIGHT_GRAY);
campo_nombre.setCaretColor(Color.orange);
campo_nombre.setBounds(240,100,700,50);
campo_nombre.setOpaque(false);
email=new JLabel("E M A I L : ");
email.setFont(new Font("Times New Roman",Font.BOLD,40));
email.setForeground(Color.orange);
email.setBounds(20,80,400,200);
with_feedback=new JCheckBox("Intelligent Feedback",toShow_feedback);
with_feedback.setFont(new Font("Times New Roman",Font.BOLD,30));
with_feedback.setForeground(Color.LIGHT_GRAY);
with_feedback.setBounds(60,260,800,40);
with_feedback.setOpaque(false);
with_feedback.addActionListener(this::seleccionarFeedbackInteligente);
with_feedback2=new JCheckBox("Final Linguistic Feedback via email",false);
with_feedback2.setFont(new Font("Times New Roman",Font.BOLD,30));
with_feedback2.setForeground(Color.LIGHT_GRAY);
with_feedback2.setBounds(60,320,1000,40);
with_feedback2.setOpaque(false);
castellano=new JCheckBox("Castellano",false);
castellano.setFont(new Font("Times New Roman",Font.BOLD,20));
castellano.setForeground(Color.orange);
castellano.setBounds(20,0,150,100);
castellano.setOpaque(false);
castellano.addActionListener(this::seleccionarCastellano);
ingles=new JCheckBox("English",true);
ingles.setFont(new Font("Times New Roman",Font.BOLD,20));
ingles.setForeground(Color.orange);
ingles.setBounds(200,0,150,100);
ingles.setOpaque(false);
ingles.addActionListener(this::seleccionarIngles);
campo_email=new JTextField("");
campo_email.setFont(new Font("Times New Roman",Font.BOLD,40));
campo_email.setForeground(Color.LIGHT_GRAY);
campo_email.setCaretColor(Color.orange);
campo_email.setBounds(240,160,700,50);
campo_email.setOpaque(false);
comenzar=new JButton("P R E S S
S T A R T");
comenzar.setBounds(700,620,310,100);
comenzar.setOpaque(false);
comenzar.setFont(new Font("Times New Roman",Font.BOLD,20));
comenzar.setBackground(Color.BLACK);
comenzar.setForeground(Color.orange);
comenzar.setHorizontalTextPosition(JButton.CENTER);
comenzar.setVerticalTextPosition(JButton.CENTER);
comenzar.addActionListener(this::pulsarBotonComenzar);
salir=new JButton("E X I T");
salir.setBounds(200,620,310,100);
salir.setOpaque(false);
salir.setFont(new Font("Times New Roman",Font.BOLD,20));
salir.setBackground(Color.BLACK);
salir.setForeground(Color.ORANGE);
salir.setHorizontalTextPosition(JButton.CENTER);
salir.setVerticalTextPosition(JButton.CENTER);
salir.addActionListener(this::pulsarBotonSalir);
add(comenzar);
add(salir);
add(nombre);
add(campo_nombre);
add(email);
add(campo_email);
add(with_feedback);
add(with_feedback2);
add(castellano);
add(ingles);
}
public void pulsarBotonComenzar(ActionEvent e) {
if ( campo_nombre.getText().isEmpty() && campo_email.getText().isEmpty()
JOptionPane.showMessageDialog(null,"DEBES INTRODUCIR TU NOMBRE",
"YADY Computer Game",JOptionPane.PLAIN_MESSAGE);
}else {
JOptionPane.showMessageDialog(null,"LANZAR EL VIDEO JUEGO",
"YADY Computer Game",JOptionPane.PLAIN_MESSAGE);
if ( castellano.isSelected()){
JOptionPane.showMessageDialog(null,"EN IDIOMA CASTELLANO",
"YADY Computer Game",JOptionPane.PLAIN_MESSAGE);
}
if ( with_feedback2.isSelected()) {
JOptionPane.showMessageDialog(null,"Y CON FEEDBACK ACTIVADO",
"YADY Computer Game",JOptionPane.PLAIN_MESSAGE);
}
}
}
public void pulsarBotonSalir(ActionEvent e) {
System.exit(0);
}
public void seleccionarCastellano(ActionEvent e) {
JOptionPane.showMessageDialog(null,
"SE SELECCIONO CASTELLANO",
"YADY Computer Game",JOptionPane.PLAIN_MESS
ingles.setSelected(false);
nombre.setText("NOMBRE:");
email.setText("CORREO:");
with_feedback.setText("Activar FeedBack Inteligente");
with_feedback2.setText("Enviar feedback por correo electronico");
comenzar.setText("COMENZAR");
salir.setText("SALIR");
}
public void seleccionarIngles(ActionEvent e) {
JOptionPane.showMessageDialog(null,"SE SELECCIONO INGLES",
"YADY Computer Game", JOptionPane.PLAIN_MESSAGE);
castellano.setSelected(false);
nombre.setText("N A M E:");
email.setText("E M A I L:");
with_feedback.setText("Intelligent Feedback");
with_feedback2.setText("Linguistic Feedback via email");
comenzar.setText("P R E S S
S T A R T");
salir.setText("E X I T");
}
public void seleccionarFeedbackInteligente(ActionEvent e) {
JOptionPane.showMessageDialog(null,"SE SELECCIONO FEEDBACK INTELIGENTE",
"YADY Computer Game",JOptionPane.PLAIN_MESSAGE);
toShow_feedback = with_feedback.isSelected();
}
@Override
public void paintComponent(Graphics g) {
Dimension d=getSize();
BufferedImage fondo=null;
try {
fondo = ImageIO.read(new File(RUTA_DIRECTORIO+"/imagenes/fondo.jpg"));
} catch (IOException ex) {
Logger.getLogger(PanelMenuInicial.class.getName()).log(Level.SEVERE,
}
g.drawImage(fondo,0,0, d.width, d.height,null);
super.paintComponents(g);
}
}
4.3.
Panel de Configuración para cambiar velocidad personajes
Es habitual que en tiempo de ejecución se necesite modificar los parámetros iniciales de una aplicación. Por ejemplo en el proyecto en el que estamos
trabajando que el usuario pueda cambiar la velocidad de las entidades.
VentanaPrincipal.java
import java.awt.BorderLayout;
import javax.swing.JFrame;
import javax.swing.JSplitPane;
public class VentanaPrincipal extends JFrame implements Constantes{
public JSplitPane panelSeparador;//panel separador
public PanelDeJuego panelJuego;//panel de juego (contiene lienzo)
public PanelConfiguracion panelConfiguracion;//panel configuracion
public VentanaPrincipal() {
panelSeparador=new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
panelSeparador.setOneTouchExpandable(true);//lo puedo desplazar
panelJuego=new PanelDeJuego();
panelConfiguracion=new PanelConfiguracion(panelJuego);
panelSeparador.setLeftComponent(panelJuego);
panelSeparador.setRightComponent(panelConfiguracion);
panelSeparador.setDividerLocation(panelJuego.getWidth()+20);
panelSeparador.setDividerSize(8);
getContentPane().setLayout(new BorderLayout());
getContentPane().add(panelSeparador,BorderLayout.CENTER);
this.setSize(SCREEN_SIZE.width-50,SCREEN_SIZE.height-50);
}
}
PanelDeJuego.java
import java.awt.BorderLayout;
import javax.swing.JPanel;
public class PanelDeJuego extends JPanel implements Constantes {
public Lienzo lienzo;
public PanelDeJuego() {
this.setLayout(new BorderLayout());
lienzo=new Lienzo();
lienzo.setFocusable(true);
lienzo.requestFocus();
this.add(lienzo);
this.setSize(lienzo.getWidth(),lienzo.getHeight());
}
}
PanelConfiguracion.java
import
import
import
import
import
import
import
java.awt.BorderLayout;
java.awt.Color;
javax.swing.JLabel;
javax.swing.JPanel;
javax.swing.JSlider;
javax.swing.event.ChangeEvent;
javax.swing.event.ChangeListener;
public class PanelConfiguracion extends JPanel implements Constantes{
public JLabel velocidad;
public JSlider cambiarVelocidad;
public PanelDeJuego panelJuego;
public PanelConfiguracion(PanelDeJuego panelJuego) {
this.panelJuego=panelJuego;
//configuramos etiqueta
velocidad=new JLabel("Cambiar Velocidad");
velocidad.setForeground(Color.yellow);
velocidad.setFont(FUENTE);
//
cambiarVelocidad = new JSlider(JSlider.VERTICAL,
VELOCIDAD_MINIMA, VELOCIDAD_MAXIMA,VELOCIDAD_INICIAL);
//cambiarVelocidad.addChangeListener(this::escuchadorslider);
cambiarVelocidad.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
escuchadorslider(e);
}
});
cambiarVelocidad.setMajorTickSpacing(100);
cambiarVelocidad.setPaintTicks(true);
this.setBackground(Color.gray);
this.setLayout(new BorderLayout());
add(velocidad,BorderLayout.WEST);
add(cambiarVelocidad,BorderLayout.CENTER);
}
public void escuchadorslider(ChangeEvent e) {
JSlider source = (JSlider)e.getSource();
if (!source.getValueIsAdjusting()) {
System.out.println(" Velocidad "+source.getValue());
panelJuego.lienzo.jugador.tiempoDormido=source.getValue();
}
}
}
Lienzo.java, ahora lanzamos las entidades en cada instante:
lanzadorTareas=new Timer();
lanzadorTareas.scheduleAtFixedRate(adversario,0,1);
lanzadorTareas.scheduleAtFixedRate(adversario2,0,1);
lanzadorTareas.scheduleAtFixedRate(jugador,0,1);
Jugador.java, ahora dormimos el jugador cada cierto tiempo
@Override
public void run() {
if (direccion==0) moverJugadorDerecha();
else moverJugadorIzquierda();
try {
Thread.sleep(tiempoDormido);
} catch (InterruptedException ex) {
}
laberinto.lienzoPadre.repaint();
}
Ahora jugador debe contar con un nuevo atributo int tiempoDormido,
que indicara el tiempo en que el jugador se duerme.
public class Jugador extends TimerTask
implements Constantes{
public Laberinto laberinto;
public Celda jugador;
public int direccion;//para saber hacia donde me muevo
public int tiempoDormido;
public Jugador(Laberinto laberinto,int tiempoDormido) {
this.tiempoDormido=tiempoDormido;
[...]
}
}
Notar que, al hacer Thread.sleep(tiempo), los Adversarios también se
duermen ese tiempo. Para que cada elemento se duerma de forma independiente hay que transformar los TimerTask en Threads. Es decir, en lugar
de heredar de TimerTask, heredar de Thread. Y en lugar de lanzarlos con
un timer, lanzarlos con el método start().
Capítulo 5
Interacción de Entidades
En este capitulo vamos a estudiar como se puede implementar la interacción entre las entidades. Interacción con entidades inmóviles (obstáculos) e
interacción con entidades móviles (objetos que al chocar se muevan).
La clase Jugador.java que daría como sigue:
public class Jugador implements Constantes{
public Laberinto laberinto;
public Celda celdaJugador;
public Jugador(Laberinto laberinto) {
this.laberinto=laberinto;
celdaJugador=new Celda(0,0,’J’);
laberinto.celdas[0][0].tipo=’J’;
}
public void moverJugadorArriba(){
if (celdaJugador.y > 0 ) {
if ( laberinto.celdas[celdaJugador.x][celdaJugador.y-1].tipo != ’O’){
if ( laberinto.celdas[celdaJugador.x][celdaJugador.y-1].tipo == ’P’){
if ( celdaJugador.y-2 >=0 ) {
laberinto.celdas[celdaJugador.x][celdaJugador.y-2].tipo=’P’;
laberinto.celdas[celdaJugador.x][celdaJugador.y-1].tipo=’V’;
}
}else {
laberinto.celdas[celdaJugador.x][celdaJugador.y].tipo=’V’;
celdaJugador.y=celdaJugador.y-1;
laberinto.celdas[celdaJugador.x][celdaJugador.y].tipo=’J’;
}
}
}
}
public void moverJugadorAbajo(){
51
if (celdaJugador.y+1 < alturaMundoVirtual ) {
if ( laberinto.celdas[celdaJugador.x][celdaJugador.y+1].tipo != ’O’) {
if ( laberinto.celdas[celdaJugador.x][celdaJugador.y+1].tipo == ’P’ ) {
if ( celdaJugador.y+2 < alturaMundoVirtual ){
laberinto.celdas[celdaJugador.x][celdaJugador.y+2].tipo=’P’;
laberinto.celdas[celdaJugador.x][celdaJugador.y+1].tipo=’V’;
}
}else {
laberinto.celdas[celdaJugador.x][celdaJugador.y].tipo=’V’;
celdaJugador.y=celdaJugador.y+1;
laberinto.celdas[celdaJugador.x][celdaJugador.y].tipo=’J’;
}
}
}
}
public void moverJugadorDerecha(){
if (celdaJugador.x < anchuraMundoVirtual-1 ) {
if ( laberinto.celdas[celdaJugador.x+1][celdaJugador.y].tipo != ’O’) {
if ( laberinto.celdas[celdaJugador.x+1][celdaJugador.y].tipo == ’P’ ) {
if ( celdaJugador.x+2 < anchuraMundoVirtual-1){
laberinto.celdas[celdaJugador.x+2][celdaJugador.y].tipo=’P’;
laberinto.celdas[celdaJugador.x+1][celdaJugador.y].tipo=’V’;
}
}else {
laberinto.celdas[celdaJugador.x][celdaJugador.y].tipo=’V’;
celdaJugador.x=celdaJugador.x+1;
laberinto.celdas[celdaJugador.x][celdaJugador.y].tipo=’J’;
}
}
}
}
public void moverJugadorIzquierda(){
if (celdaJugador.x > 0 ) {
if ( laberinto.celdas[celdaJugador.x-1][celdaJugador.y].tipo != ’O’) {
if ( laberinto.celdas[celdaJugador.x-1][celdaJugador.y].tipo == ’P’ ) {
if ( celdaJugador.x-2 >=0){
laberinto.celdas[celdaJugador.x-2][celdaJugador.y].tipo=’P’;
laberinto.celdas[celdaJugador.x-1][celdaJugador.y].tipo=’V’;
}
}else {
laberinto.celdas[celdaJugador.x][celdaJugador.y].tipo=’V’;
celdaJugador.x=celdaJugador.x-1;
laberinto.celdas[celdaJugador.x][celdaJugador.y].tipo=’J’;
}
}
}
}
}
La clase quedaría como sigue:
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import javax.swing.JComponent;
public class Laberinto extends JComponent
implements Constantes {
public
public
public
public
int anchuraLaberinto,alturaLaberinto;//dimensiones del laberinto
Celda[][] celdas;//las casillas n x m
Celda celdaMovimiento;
Lienzo lienzoPadre;
public Laberinto(Lienzo lienzoPadre) {
this.lienzoPadre=lienzoPadre;
celdas=new Celda[anchuraMundoVirtual][alturaMundoVirtual];
//inicializar el array de celdas
for(int i=0; i < anchuraMundoVirtual; i++)
for ( int j=0 ; j < alturaMundoVirtual ; j++)
celdas[i][j]=new Celda(i+(i*anchuraCelda),
j+(j*alturaCelda),’V’);
celdaMovimiento=new Celda(0,0,’J’);
celdas[celdaMovimiento.x][celdaMovimiento.y].tipo=’J’;
//obstaculos
celdas[5][5].tipo=’O’;
celdas[5][7].tipo=’O’;
celdas[6][3].tipo=’P’;
//ancho y largo del laberinto
this.anchuraLaberinto=anchuraMundoVirtual*anchuraCelda;
this.alturaLaberinto=alturaMundoVirtual*alturaCelda;
this.setSize(anchuraLaberinto,alturaLaberinto);
}
@Override
public void update(Graphics g) {
for(int i=0; i < anchuraMundoVirtual ; i++)
for ( int j=0 ; j < alturaMundoVirtual; j++)
celdas[i][j].update(g);
}
@Override
public void paintComponent(Graphics g) {
update(g);
}
public void moverCelda( KeyEvent evento ) {
switch( evento.getKeyCode() ) {
case KeyEvent.VK_UP:
System.out.println("Mover arriba");
lienzoPadre.jugador.moverJugadorArriba();
break;
case KeyEvent.VK_DOWN:
System.out.println("Mover abajo");
lienzoPadre.jugador.moverJugadorAbajo();
break;
case KeyEvent.VK_LEFT:
System.out.println("Mover izquierda");
lienzoPadre.jugador.moverJugadorIzquierda();
break;
case KeyEvent.VK_RIGHT:
System.out.println("Mover derecha");
lienzoPadre.jugador.moverJugadorDerecha();
break;
}
}
}
La clase Lienzo.java quedaría como sigue:
import
import
import
import
java.awt.Canvas;
java.awt.Graphics;
java.awt.Image;
java.awt.event.KeyEvent;
import
import
import
import
java.io.File;
java.io.IOException;
java.util.Timer;
javax.imageio.ImageIO;
public class Lienzo extends Canvas implements Constantes{
//para
public
public
//para
public
public
//Para
public
public
public
pintar el lienzo
Laberinto laberinto;
Image fondo;
implementar el doble buffer
Graphics graficoBuffer;
Image imagenBuffer;
animacion basica
Adversario adversario,adversario2;
Jugador jugador;
Timer lanzadorTareas;
public Lienzo() {
laberinto=new Laberinto(this);
adversario=new Adversario(laberinto);
adversario2=new Adversario(laberinto);
jugador=new Jugador(laberinto);
try {
fondo = ImageIO.read(new File("images/fondo.jpg"));
} catch (IOException e) {
System.out.println(e.toString());
}
//dimensiones del lienzo
this.setSize(laberinto.anchuraLaberinto,laberinto.alturaLaberinto);
//escuchador eventos de teclado
addKeyListener(new java.awt.event.KeyAdapter() {
@Override
public void keyPressed(KeyEvent evt) {
laberinto.moverCelda(evt);
repaint();
}
});
lanzadorTareas=new Timer();
lanzadorTareas.scheduleAtFixedRate(adversario,0,1000);
lanzadorTareas.scheduleAtFixedRate(adversario2,0,1000);
}
@Override
public void update(Graphics g) {
//inicializacion de buffer grafico
if(graficoBuffer==null){
imagenBuffer=createImage(this.getWidth(),this.getHeight());
graficoBuffer=imagenBuffer.getGraphics();
}
//volcamos contexto grafico actual
graficoBuffer.setColor(getBackground());
graficoBuffer.fillRect(0,0,this.getWidth(),this.getHeight());
graficoBuffer.drawImage(fondo, 0, 0, null);
laberinto.update(graficoBuffer);
//pintamos la imagen del previa
g.drawImage(imagenBuffer, 0, 0, null);
}
//metodo llamada la primera vez que se pinta
@Override
public void paint(Graphics g) {
update(g);
}
}
La clase Celda.java quedaría como sigue:
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import
import
import
import
import
java.io.File;
java.io.IOException;
java.awt.image.BufferedImage;
javax.imageio.ImageIO;
javax.swing.JComponent;
public class Celda extends JComponent implements Constantes {
public
public
public
public
public
int x;
int y;
char tipo;
BufferedImage jugador,obstaculo,camino, adversario;
BufferedImage pelota;
//constructor
public Celda(int x,int y,char tipo) {
this.x=x;
this.y=y;
this.tipo=tipo;
try {
jugador = ImageIO.read(new File("images/jugador.png"));
obstaculo = ImageIO.read(new File("images/obstaculo.png"));
adversario = ImageIO.read(new File("images/adversario.png"));
pelota=ImageIO.read(new File("images/pelota.png"));
} catch (IOException e) {
System.out.println(e.toString());
}
}
//metodo llamado cuando repaint
@Override
public void update(Graphics g) {
switch(tipo) {
case ’J’: g.drawImage(jugador,x,y, null); break;
case ’O’: g.drawImage(obstaculo,x,y, this); break;
case ’V’: g.setColor(COLORFONDO);
g.fillRect(x, y,anchuraCelda,alturaCelda);
break;
case ’A’: g.drawImage(adversario,x,y, this); break;
case ’P’: g.drawImage(pelota,x,y, this); break;
}
}
//metodo para dibujar una casilla
@Override
public void paintComponent(Graphics g) {
update(g);
}
//si el click esta sobre la celda
public boolean celdaSeleccionada(int xp,int yp) {
Rectangle r=new Rectangle(x,y,anchuraCelda,alturaCelda);
return r.contains(new Point(xp,yp));
}
}
Capítulo 6
Técnicas de Inteligencia
Artificial: Búsqueda
6.1.
6.1.1.
Búsqueda sin información del dominio.
Contexto previo: ¿Por qué estudiar búsqueda?
Gran parte de los primeros trabajos desarrollados en el campo de la Inteligencia Artificial (décadas de los cincuenta y sesenta) abordaban problemas que eran idealizaciones o simplificaciones muy fuertes del mundo real:
demostración automática de teoremas, planificación en el mundo de los bloques, problemas de juegos, etc. La metodología propia de esta época consistía
en la realización de procesos de búsqueda en espacios de estados, dando énfasis al empleo de conocimiento específico sobre el dominio. Las estrategias
de control que guían la búsqueda marcaron uno de los principales temas de
interés e este periodo y todavía hoy constituyen una parte importante de la
inteligencia artificial aplicada.
6.1.2.
¿Qué conocimientos previos se suponen necesarios
para comprender este capítulo?
Conceptos básicos sobre árboles y grafos. Nociones elementales de uso (no
de implementación) de estructura de datos como pilas, colas, listas o tablas
hash. Conocimientos sobre el cálculo de la complejidad de un algoritmo.
Nociones sobre conceptos propios de programación tales como recursividad
e el empleo de bucles.
6.2.
6.2.1.
Contenido teórico
Introducción
Las técnicas de búsqueda tienen sentido aplicarlas en problemas que
reúnen una seria de características:
59
Figura 6.1: Técnica de Búsqueda
1. Existe la posibilidad de asociar un conjunto de estados a las diferentes
situaciones en que se puede encontrar el objeto del dominio sobre el
que se define el problema.
2. Hay una serie de estados iniciales desde los que se empezaría el proceso
de búsqueda.
3. Existen ciertos operadores, tal que un operador aplicado sobre un estado producirá otro estado.
4. Existe al menos un estado meta o estado solución.
Cualquier proceso de búsqueda persigue, asociando nodos con estados y
arcos con operadores, encontrar un camino que con que conduzca de un nodo inicial a otro meta. Se define el espacio de estados como el conjunto de los
mismos que podrían obtenerse si se aplicaran todos los operadores posibles
a todos los estados que se fueran generando. La búsqueda sin información
del dominio pretende realizar una exploración exhaustiva del espacio de estados -dado que no hay conocimiento que pueda guiar la misma- que, en
principio, no deje ningún nodo sin ser examinado. Existen diversas formas
de llevar a cabo el proceso anterior, la diferencia reside en el orden de generar
los diferentes estados a partir del estado final.
6.3.
6.3.1.
Tipos de búsqueda
Amplitud
La búsqueda en amplitud consiste en usar una cola para almacenar los
estados que se van generando. Este tipo de exploración recibe el nombre de
búsqueda en amplitud y garantiza la obtención de la solución de menor coste
(óptima), si es que ésta existe.
6.3.2.
Implementación en nuestro proyecto
En nuestro proyecto por ahora el jugador se mueve mediante el teclado.
La implementación del algoritmo de búsqueda en amplitud nos permitirá que
el jugador se mueva de forma autónoma. Evidentemente debemos indicar al
algoritmo dónde está situado el jugador y a donde queremos que llegue. Por
ejemplo, el jugador lo situamos en la posición (0,0) y queremos que vaya a
la celda donde la pelota está situada.
Antes de nada haremos un cambio, situaremos la pelota en la esquina
final de abajo. Debemos ir a la clase Laberinto.java y y modificar la instrucción de la pelota:
celdas[anchuraMundoVirtual-1][alturaMundoVirtual-1].tipo=’P’;
nos debería quedar algo similar a lo siguiente:
public Laberinto(Lienzo lienzoPadre) {
this.lienzoPadre=lienzoPadre;
celdas=new Celda[anchuraMundoVirtual][alturaMundoVirtual];
//inicializar el array de celdas
for(int i=0; i < anchuraMundoVirtual; i++)
for ( int j=0 ; j < alturaMundoVirtual ; j++)
celdas[i][j]=new Celda(i+(i*anchuraCelda),
j+(j*alturaCelda),’V’);
celdaMovimiento=new Celda(0,0,’J’);
celdas[celdaMovimiento.x][celdaMovimiento.y].tipo=’J’;
//obstaculos
celdas[5][5].tipo=’O’;
celdas[5][7].tipo=’O’;
celdas[anchuraMundoVirtual-1][alturaMundoVirtual-1].tipo=’P’;
//ancho y largo del laberinto
this.anchuraLaberinto=anchuraMundoVirtual*anchuraCelda;
this.alturaLaberinto=alturaMundoVirtual*alturaCelda;
this.setSize(anchuraLaberinto,alturaLaberinto);
}
La técnica de búsqueda en un espacio de estado requiere emplear una
estructura de datos Estado que guarda información sobre el estado del problema en un instante dado. Por ejemplo, en nuestro caso, nuestra posición.
Además el estado cuenta con un atributo char oper que guarda información de la operación que se realizó para llegar a ese estado y una referencia
Estado predecesor que apunta al estado a partir del cual se generó dicho
estado. Una posible implementación de esta clase sería la siguiente:
public class Estado {
//posicion x e y de la entidad
public int x;
public int y;
public char oper;
public Estado predecesor;
public Estado(int x, int y, char oper,Estado predecesor) {
this.x=x;
this.y=y;
this.oper=oper;
this.predecesor=predecesor;
}
@Override
public boolean equals(Object x) {
Estado e=(Estado)x;
return this.x==e.x && this.y==e.y;
}
@Override
public int hashCode() {
int hash = 3;
hash = 89 * hash + this.x;
hash = 89 * hash + this.y;
return hash;
}
@Override
public String toString() {
return "("+x+","+y+")";
}
}
La clase BusquedaAnchura está formada por las estructuras de datos necesarias para implementar el algoritmo de búsqueda en anchura. Recibe laberinto para conocer la situación de cada elemento. Una cola estados donde
se irán almacenando los estados que están siendo explorados. Un historial
para saber que estados fueron ya explorados. Los pasos, que será la salida del algoritmo, es decir, qué pasos he de dar para ir del estado inicial al
meta/objetivo.
public class BusquedaAnchura extends TimerTask implements Constantes{
public
public
public
public
public
public
public
public
public
Laberinto laberinto;
ArrayList<Estado> colaEstados;
ArrayList<Estado> historial;
ArrayList<Character> pasos;
int index_pasos;
Estado inicial;
Estado objetivo;
Estado temp;
boolean exito;
public BusquedaAnchura(Laberinto laberinto) {
this.laberinto=laberinto;
colaEstados=new ArrayList<>();
historial=new ArrayList<>();
pasos=new ArrayList<>();
index_pasos=0;
exito=false;
}
El método buscar es muy simple y está formado por unas pocas lineas de
código:
public void buscar(int x1,int y1,int x2,int y2) {
//creamos el estado inicial y el objetivo
inicial=new Estado(x1,y1,’N’,null);
objetivo=new Estado(x2,y2,’P’,null);
//los añadimos a la cola de estados y al historial
colaEstados.add(inicial);
historial.add(inicial);
//si el inicial es final, salimos
if ( inicial.equals(objetivo)) exito=true;
// si no mientras que la cola no este vacia y no hayamos
// alcanzado el meta hacemos lo siguiente
while ( !colaEstados.isEmpty() && !exito ){
//tomamos el primero y lo quitamos de cola de estad
temp=colaEstados.get(0);
colaEstados.remove(0);
//lo exploramos, es decir, generamos sus sucesores,
// es decir, los estados a los que podemos ir desde el
// estado actual
moverArriba(temp);
moverAbajo(temp);
moverIzquierda(temp);
moverDerecha(temp);
}
if ( exito ) System.out.println("Ruta calculada");
else System.out.println("La ruta no pudo calcularse");
}
Por último implementamos los métodos de expansión que consiste en generar los estados sucesores:
private void moverArriba(Estado e) {
if ( e.y > 0 ) {
if ( laberinto.celdas[e.x][e.y-1].tipo != ’O’ ) {
Estado arriba=new Estado(e.x,e.y-1,’U’,e);
if ( !historial.contains(arriba)) {
colaEstados.add(arriba);
historial.add(arriba);
if ( arriba.equals(objetivo)) {
objetivo=arriba;
exito=true;
}
}
}
}
}//fin del metodo moverArriba
private void moverAbajo(Estado e) {
if ( e.y+1 < alturaMundoVirtual ) {
if ( laberinto.celdas[e.x][e.y+1].tipo != ’O’ ) {
Estado abajo=new Estado(e.x,e.y+1,’D’,e);
if ( !historial.contains(abajo)) {
colaEstados.add(abajo);
historial.add(abajo);
if ( abajo.equals(objetivo)) {
objetivo=abajo;
exito=true;
}
}
}
}
}
private void moverIzquierda(Estado e) {
if ( e.x > 0 ) {
if ( laberinto.celdas[e.x-1][e.y].tipo != ’O’ ) {
Estado izquierda=new Estado(e.x-1,e.y,’L’,e);
if ( !historial.contains(izquierda)) {
colaEstados.add(izquierda);
historial.add(izquierda);
if ( izquierda.equals(objetivo)) {
objetivo=izquierda;
exito=true;
}
}
}
}
}// fin del metodo izquierda
private void moverDerecha(Estado e) {
if ( e.x < anchuraMundoVirtual-1 ) {
if ( laberinto.celdas[e.x+1][e.y].tipo != ’O’ ) {
Estado derecha=new Estado(e.x+1,e.y,’R’,e);
if ( !historial.contains(derecha)){
colaEstados.add(derecha);
historial.add(derecha);
if ( derecha.equals(objetivo)) {
objetivo=derecha;
exito=true;
}
}
}
}
}
Una vez construido el corazón del algoritmo de búsqueda, si hubo exito
entonces podemos reconstruir la soluciòn:
public void calcularRuta() {
Estado predecesor=objetivo;
do{
pasos.add(predecesor.oper);
predecesor=predecesor.predecesor;
}while ( predecesor != null);
index_pasos=pasos.size()-1;
}
En pasos están almacenados las acciones a realizar, por tanto el método
run() tendrá la siguiente implementación:
@Override
public synchronized void run() {
if ( index_pasos >= 0 ) {
switch(pasos.get(index_pasos)) {
case
case
case
case
’D’:
’U’:
’R’:
’L’:
laberinto.lienzoPadre.jugador.moverJugadorAbajo();break;
laberinto.lienzoPadre.jugador.moverJugadorArriba(); break;
laberinto.lienzoPadre.jugador.moverJugadorDerecha();break;
laberinto.lienzoPadre.jugador.moverJugadorIzquierda();break;
}
laberinto.lienzoPadre.repaint();
index_pasos--;
}else {
this.cancel();
}
}
A continuación debemos extender la clase Jugador, ya que esté tendrá
asociado una inteligencia:
public class Jugador implements Constantes{
public Laberinto laberinto;
public Celda celdaJugador;
public BusquedaAnchura inteligencia;
<-----------------------
public Jugador(Laberinto laberinto) {
this.laberinto=laberinto;
celdaJugador=new Celda(0,0,’J’);
laberinto.celdas[0][0].tipo=’J’;
inteligencia=new BusquedaAnchura(laberinto); <-----------}
[...]
}
Por último debemos lanzar el jugador en lienzo:
laberinto.lienzoPadre.jugador.inteligencia.buscar(0,0,
anchuraMundoVirtual-1,alturaMundoVirtual-1);
laberinto.lienzoPadre.jugador.inteligencia.calcularRuta();
lanzadorTareas=new Timer();
lanzadorTareas.scheduleAtFixedRate(jugador.inteligencia,0,1000);
//lanzadorTareas.scheduleAtFixedRate(adversario,0,1000);
//lanzadorTareas.scheduleAtFixedRate(adversario2,0,1000);
6.3.3.
Búsqueda Anchura Multiobjetivo
La solución anterior nos permite ir de una posición inicial a una posición
final. Pero no es efectiva si se produce algún cambio en la ruta. Por ejemplo,
poner un obstáculo. Piense, por ejemplo, en un sistema software que nos
proporcione rutas en una determinada zona geográfica. Si el día que consultamos la ruta es lluvioso es posible que una determinada ruta no se pueda
aconsejar. Por tanto se debe crear una solución que tenga en cuenta posible
cambios en la ruta en cada paso.
Para ello debemos añadir tres atributos a la clase BusquedaAnchura.java.
Una lista de destinos que nos proporcione una secuencia de estados que queremos visitar, una variable Jugador que que nos permita saber el jugador al
que pertenece el algoritmo de búsqueda y una variable booleana parar para
saber cuándo hemos terminado de recorrer todos los destinos. El constructor
de la clase se modifica para pasar el jugador como argumento.
import java.util.ArrayList;
import java.util.TimerTask;
public class BusquedaAnchura extends TimerTask implements Constantes{
//para tener un busqueda anchura multiobjetivo
public Jugador jugador;
public ArrayList<Estado> destinos;
public boolean parar;
//el resto de atributos quedarian igual
[...]
public BusquedaAnchura(Laberinto laberinto,Jugador jugador) {
[...]
this.jugador=jugador;
destinos=new ArrayList<>();
parar=false;
}
Posteriormente debemos modificar el método buscar, ahora devolverá un
booleano y recibirá como parámetros un estado inicial y un estado objetivo.
public boolean buscar(Estado inicial,Estado objetivo) {
index_pasos=0;
colaEstados.add(inicial);
historial.add(inicial);
this.objetivo=objetivo;
exito=false;
if ( inicial.equals(objetivo)) exito=true;
while ( !colaEstados.isEmpty() && !exito ){
temp=colaEstados.get(0);
colaEstados.remove(0);
moverArriba(temp);
moverAbajo(temp);
moverIzquierda(temp);
moverDerecha(temp);
}
if ( exito ) {
System.out.println("Ruta calculada");
this.calcularRuta();
return true;
}
else {
System.out.println("La ruta no pudo calcularse");
return false;
}
}
Los métodos de mover quedan sin modificación. El método calcular ruta
sufre una pequeña modificación
public void calcularRuta() {
Estado predecesor=objetivo;
do{
pasos.add(0,predecesor.oper);<-----------------predecesor=predecesor.predecesor;
}while ( predecesor != null);
index_pasos=pasos.size()-1;
}
El método que más cambios va a sufrir es el método run() ya que será
lazando en cada instante de tiempo por el lanzador de tareas. Por tanto, en
cada paso debemos reiniciar las estructuras de datos empleadas para la búsqueda y comprar si hemos alcanzado el destino. Si alcanzamos el destino, lo
eliminamos de la lista y seguimos con el siguiente. Adicionalmente debemos
comprobar que la lista de destino no es vacía, en caso contrario debemos
parar el algoritmo, poner el atributo parar igual a true y cancelar el hilo de
ejecución.
@Override
public void run() {
//solo cuando quedan destinos donde ir
if ( ! parar ) {
//inicializacion de la busqueda
colaEstados.clear();
historial.clear();
pasos.clear();
Estado subinicial,subobjetivo;
boolean resultado;
do{
//el estado inicial es donde estoy
subinicial=new Estado(jugador.celdaJugador.x,
jugador.celdaJugador.y,’N’,null);
//el estado final es a donde quiero ir
subobjetivo=destinos.get(0);
//busco ruta
resultado=this.buscar(subinicial,subobjetivo);
if ( subinicial.equals(subobjetivo))
destinos.remove(subobjetivo);
if ( destinos.isEmpty() ) {
System.out.println("Se acabo a donde ir");
this.cancel();
}
}while(!resultado && !destinos.isEmpty());
if ( pasos.size() > 1 ) {
switch(pasos.get(1)) {
case ’D’: jugador.moverJugadorAbajo();break;
case ’U’: jugador.moverJugadorArriba(); break;
case ’R’: jugador.moverJugadorDerecha();break;
case ’L’: jugador.moverJugadorIzquierda();break;
}
laberinto.lienzoPadre.repaint();
}
}
}
La clase Jugador.java sufre una ligera modificación:
public Jugador(Laberinto laberinto) {
this.laberinto=laberinto;
celdaJugador=new Celda(0,0,’J’);
laberinto.celdas[0][0].tipo=’J’;
inteligencia=new BusquedaAnchura(laberinto,this);
}
Por último debemos lanzar al jugador en la clase Lienzo, note que el código
de la versión anterior donde llamábamos al método buscar desparece y sólo
se incluye lo necesario para lanzar la inteligencia del jugador (solo indicamos
la parte que sufre modificación)
public Lienzo() {
[...]
jugador.inteligencia.destinos.add(new Estado(5,5,’N’,null));
jugador.inteligencia.destinos.add(new Estado(14,4,’N’,null));
lanzadorTareas=new Timer();
lanzadorTareas.scheduleAtFixedRate(jugador.inteligencia,0,500);
//lanzadorTareas.scheduleAtFixedRate(adversario,0,1000);
//lanzadorTareas.scheduleAtFixedRate(adversario2,0,1000);
}
6.3.4.
Tarea voluntaria para tener un 4 en el certamen de
laboratorio al primer alumno que la realice
Notar que cuando:
celdas[5][5].tipo=’O’;
celdas[5][7].tipo=’O’;
Y la búsqueda se realiza con estos destinos:
jugador.inteligencia.destinos.add(new Estado(5,5,’N’,null));
jugador.inteligencia.destinos.add(new Estado(14,4,’N’,null));
El jugador no se mueve, no se encuentra ruta al primer subobjetivo. El
algoritmo debería ir al siguiente destino ante la imposibilidad de ir al primero. Además en caso de que los destinos estén correctamente se debería
comportar de forma adecuada.
Sobre el código presentado en esta sección tratar de solucionar este problema.
Una de las soluciones para este problema consiste en añadir un nuevo
caso en el condicional del método run().
if ( subinicial.equals(subobjetivo) )
destinos.remove(subobjetivo);
else {
if ( !resultado) {
colaEstados.clear();
historial.clear();
pasos.clear();
destinos.remove(subobjetivo);
}
}
6.3.5.
Dándole inteligencia a los Adversarios
Para darle inteligencia a los adversarios crearemos una BusquedaProfundidad.java
que será análoga a la clase BusquedaAnchura.java con la diferencia del
método run()
@Override
public void run() {
if ( ! parar ) {
colaEstados.clear();
historial.clear();
pasos.clear();
Estado subinicial,subobjetivo;
boolean resultado;
subinicial=new Estado(adversario.celdaAdversario.x,
adversario.celdaAdversario.y,’N’,null);
subobjetivo=new Estado(
adversario.laberinto.lienzoPadre.jugador.celdaJugador.x,
adversario.laberinto.lienzoPadre.jugador.celdaJugador.y,’N’,null);
resultado=this.buscar(subinicial,subobjetivo);
if ( pasos.size() > 1 ) {
switch(pasos.get(1)) {
case ’D’: adversario.moverJugadorAbajo();break;
case ’U’: adversario.moverJugadorArriba(); break;
case ’R’: adversario.moverJugadorDerecha();break;
case ’L’: adversario.moverJugadorIzquierda();break;
}
laberinto.lienzoPadre.repaint();
}
}
}
El siguiente paso es modificar la clase Adversario.java, ésta deja de ser
un timertask, ahora tendrá un atributo BusquedaProfundidad. Notar que
en lugar de ’J’ se debe poner ’A’ cuando se produce un movimiento. Sólo
indicamos un movimiento, el resto serían de la misma forma.
public class Adversario implements Constantes{
public Laberinto laberinto;
public Celda celdaAdversario;
public BusquedaProfundidad inteligencia;
public Adversario(Laberinto laberinto) {
this.laberinto=laberinto;
celdaAdversario=new Celda(anchuraMundoVirtual-1,alturaMundoVirtual-1,’A’);
laberinto.celdas[celdaAdversario.x][celdaAdversario.y].tipo=’A’;
inteligencia=new BusquedaProfundidad(laberinto,this);
}
public void moverJugadorArriba(){
if (celdaAdversario.y > 0 ) {
if ( laberinto.celdas[celdaAdversario.x][celdaAdversario.y-1].tipo != ’O’) {
if ( laberinto.celdas[celdaAdversario.x][celdaAdversario.y-1].tipo==’P’) {
if ( celdaAdversario.y-2 >=0 ) {
laberinto.celdas[celdaAdversario.x][celdaAdversario.y-2].tipo=’P’;
laberinto.celdas[celdaAdversario.x][celdaAdversario.y-1].tipo=’V’;
}
}else {
laberinto.celdas[celdaAdversario.x][celdaAdversario.y].tipo=’V’;
celdaAdversario.y=celdaAdversario.y-1;
laberinto.celdas[celdaAdversario.x][celdaAdversario.y].tipo=’A’;
}
}
}
}
[...] //resto de movimientos
}
Por último en Lienzo.java quitamos el segundo atributo Adversario y
nos quedamos con uno.
public class Lienzo extends Canvas implements Constantes{
[...]
public Adversario adversario;
[...]
public Lienzo() {
[...]
adversario=new Adversario(laberinto);
[...]
jugador.inteligencia.destinos.add(new
jugador.inteligencia.destinos.add(new
jugador.inteligencia.destinos.add(new
jugador.inteligencia.destinos.add(new
jugador.inteligencia.destinos.add(new
Estado(5,5,’N’,null));
Estado(14,4,’N’,null));
Estado(5,5,’N’,null));
Estado(0,0,’N’,null));
Estado(5,5,’N’,null));
lanzadorTareas=new Timer();
lanzadorTareas.scheduleAtFixedRate(jugador.inteligencia,0,500);
lanzadorTareas.scheduleAtFixedRate(adversario.inteligencia,0,500);
}
[...]
}
Capítulo 7
Búsqueda Informada
Queremos emplear un lista de estados ordenadas por algún valor. Aquí
consideramos que un estado es mejor si pasa cerca del adversario. Hay
que emplear la estructura de datos PriorityQueue. Para poder utilizarla
todo elemento que esté en esta estructura deberá implementar de la interfaz
Comparable. Esto es debido a que la comparación de objetos en las colas de
prioridad en Java se hacen a través del método compareTo().
7.0.6.
Cambios la clase Estado
class Estado implements Comparable{
public int x;
public int y;
//’N’=nada, ’L’: izquierda, ’R’: derecha, ’U’: Arriba, ’D’: abajo
public char oper;
public Estado predecesor;
public double prioridad;
public Estado(int x, int y, char oper,Estado predecesor) {
this.x=x;
this.y=y;
this.oper=oper;
this.predecesor=predecesor;
}
@Override
public boolean equals(Object x) {
Estado e=(Estado)x;
return this.x==e.x && this.y==e.y;
}
@Override
public String toString() {
return "("+x+","+y+"): Prioridad= "+this.prioridad;
75
}
@Override
public int compareTo(Object o) {
Estado e=(Estado)o;
if ( this.prioridad == e.prioridad ) return 0;
else {
if ( this.prioridad > e.prioridad ) return 1;
else return -1;
}
}
}
7.0.7.
Cambios en la clase Busqueda Anchura
Además de modificar la estructura colaEstados, notar que cada vez que
se genera un estado se calcula la prioridad del mismo.
import
import
import
import
java.util.ArrayList;
java.util.PriorityQueue;
java.util.Queue;
java.util.TimerTask;
public class BusquedaAnchura extends TimerTask implements Constantes{
public
public
public
public
public
public
public
public
public
Laberinto laberinto;
Queue<Estado> colaEstados;
ArrayList<Estado> historial;
ArrayList<Character> pasos;
int index_pasos;
Estado inicial;
Estado objetivo;
Estado temp;
boolean exito;
public BusquedaAnchura(Laberinto laberinto) {
this.laberinto=laberinto;
colaEstados=new PriorityQueue<>();
historial=new ArrayList<>();
pasos=new ArrayList<>();
index_pasos=0;
exito=false;
}
public void buscar(int x1,int y1,int x2,int y2) {
inicial=new Estado(x1,y1,’N’,null);
inicial.prioridad=distancia(x1,y1,laberinto.lienzoPadre.adversario.adversar
laberinto.lienzoPadre.adversario.adversario.y);
objetivo=new Estado(x2,y2,’P’,null);
colaEstados.add(inicial);
historial.add(inicial);
if ( inicial.equals(objetivo)) exito=true;
while ( !colaEstados.isEmpty() && !exito ){
temp=colaEstados.poll();
moverArriba(temp);
moverAbajo(temp);
moverIzquierda(temp);
moverDerecha(temp);
}
if ( exito ) System.out.println("Ruta calculada");
else System.out.println("La ruta no pudo calcularse");
}
//distancia adversario
public double distancia(int x1,int y1, int x2, int y2) {
double valor;
double parte1=Math.pow(Math.abs(x1-x2),2);
double parte2=Math.pow(Math.abs(y1-y2),2);
parte1+=parte2;
valor=Math.sqrt(parte1);
return valor;
}
private void moverArriba(Estado e) {
if ( e.y > 0 ) {
if ( laberinto.celdas[e.x][e.y-1].tipo != ’O’ ) {
Estado arriba=new Estado(e.x,e.y-1,’U’,e);
arriba.prioridad=distancia(arriba.x,arriba.y,
laberinto.lienzoPadre.adversario.adversario.x,
laberinto.lienzoPadre.adversario.adversario.y);
if ( !historial.contains(arriba)) {
colaEstados.add(arriba);
historial.add(arriba);
if ( arriba.equals(objetivo)) {
objetivo=arriba;
exito=true;
}
}
}
}
}
private void moverAbajo(Estado e) {
if ( e.y+1 < alturaMundoVirtual ) {
if ( laberinto.celdas[e.x][e.y+1].tipo != ’O’ ) {
Estado abajo=new Estado(e.x,e.y+1,’D’,e);
abajo.prioridad=distancia(abajo.x,abajo.y,
laberinto.lienzoPadre.adversario.adversario.x,
laberinto.lienzoPadre.adversario.adversario.y);
if ( !historial.contains(abajo)) {
colaEstados.add(abajo);
historial.add(abajo);
//laberinto.celdas[e.x][e.y+1].tipo=’A’;
if ( abajo.equals(objetivo)) {
//laberinto.celdas[e.x][e.y+1].tipo=’P’;
objetivo=abajo;
exito=true;
}
}
}
}
}
private void moverIzquierda(Estado e) {
if ( e.x > 0 ) {
if ( laberinto.celdas[e.x-1][e.y].tipo != ’O’ ) {
Estado izquierda=new Estado(e.x-1,e.y,’L’,e);
izquierda.prioridad=distancia(izquierda.x,izquierda.y,
laberinto.lienzoPadre.adversario.adversario.x,
laberinto.lienzoPadre.adversario.adversario.y);
if ( !historial.contains(izquierda)) {
colaEstados.add(izquierda);
historial.add(izquierda);
if ( izquierda.equals(objetivo)) {
objetivo=izquierda;
exito=true;
}
}
}
}
}
private void moverDerecha(Estado e) {
if ( e.x < anchuraMundoVirtual-1 ) {
if ( laberinto.celdas[e.x+1][e.y].tipo != ’O’ ) {
Estado derecha=new Estado(e.x+1,e.y,’R’,e);
derecha.prioridad=distancia(derecha.x,derecha.y,
laberinto.lienzoPadre.adversario.adversario.x,
laberinto.lienzoPadre.adversario.adversario.y);
if ( !historial.contains(derecha)){
colaEstados.add(derecha);
historial.add(derecha);
if ( derecha.equals(objetivo)) {
objetivo=derecha;
exito=true;
}
}
}
}
}
public void calcularRuta() {
Estado predecesor=objetivo;
do{
pasos.add(predecesor.oper);
predecesor=predecesor.predecesor;
}while ( predecesor != null);
index_pasos=pasos.size()-1;
}
@Override
public synchronized void run() {
if ( index_pasos >= 0 ) {
switch(pasos.get(index_pasos)) {
case ’D’:
laberinto.lienzoPadre.jugador.moverJugadorAbajo();
break;
case ’U’:
laberinto.lienzoPadre.jugador.moverJugadorArriba();
break;
case ’R’:
laberinto.lienzoPadre.jugador.moverJugadorDerecha();
break;
case ’L’: laberinto.lienzoPadre.jugador.moverJugadorIzquierda();
break;
}
laberinto.lienzoPadre.repaint();
index_pasos--;
}else {
this.cancel();
}
}
}
Descargar