Vista previa del material en texto
Capítulo 4. Interacción y Movimientos Clemente Rubio Manzano (clrubio@ubiobio.cl) Departamento de Sistemas de Información Universidad del Bío-Bío Versión: 18 de Abril del 2022 Qué vamos a aprender en este capítulo Las interacciones en el mundo virtual son muy importantes ya que son las encargadas de determinar que ocurre cuando dos tipos de celdas diferentes colisionan. Por ejemplo, si en una celda esta la entidad y en la otra hay una pared, la entidad nunca podrá ocupar el espacio de la pared. Sin embargo si en el lado derecho hay una recompensa entonces la entidad la puede ocurrir y la recompensa desaparecerá (si es así como lo hemos decidido). Por otro, que una o varias entidades se puedan mover permitirá dar vida a las mismas. En este capítulo vamos a explicar cómo se puede tener diferentes tipos de entidades en el escenario y cómo podemos crear cierta interacción en- tre ellas para dotarlas de inteligencia, aunque ésta sea muy sencilla y solo permitamos por ahora movimientos autónomos y aleatorios. Añadiendo entidades sin movimiento y con inter- acción al escenario Una forma de situar nuevas entidades en el escenario es cambiar el pro- tocolo pintado con el que estábamos trabajando habitualmente. Hasta ahora el pintado de celdas consistía en dibujar un rectángulo sin fondo (celdas) y una matriz de celdas (escenario). Ahora asociaremos diferentes tipos de cel- das con diferente tipo de pintado. Por ejemplo, podremos tener celdas pared que se pintaran de color negro, celdas de tipo adversario que se pintaran de color rojos, etc. En realidad el tipo de celdas y sus colores es una decisión de diseño y dependerá del tipo de mundo virtual que se quiera implementar. Emplearemos una nueva variable que nos diga de que tipo será cada una de ellas. Como el pintado del escenario depende de la clase celda podemos 1 2 extender su pintarCelda para que en función del tipo que queramos visua- lizar se opte por esa opción. Por tanto el primer paso consiste en extender dicha clase. import java.awt.Color; import java.awt.Graphics; /** * * @author klemenzza */ public class Celda implements Constantes { public int xPixel,yPixel; //añado un nuevo atributo tipo public char tipo; //constructor public Celda(int x,int y,char t) { this.xPixel=x; this.yPixel=y; this.tipo=t; } //funcion de pintado en funcion del tipo public void pintarCelda(Graphics g) { switch(tipo) { case CAMINO: g.drawRect(xPixel,yPixel,PIXELS,PIXELS); break; case OBSTACULO: g.fillRect(xPixel,yPixel,PIXELS,PIXELS); break; case AMOR: g.setColor(Color.pink); g.fillRect(xPixel,yPixel,PIXELS,PIXELS); g.setColor(Color.black); break; } } } Como se puede observar ahora contamos con un nuevo atributo del tipo char que almacena información sobre el tipo de celda que se desea pintar al instanciar un objeto de la clase Celda. Tendremos tres tipos: CAMINO, OBSTACULO y AMOR. El tipo CAMINO dibuja una celda de la forma habitual (un rectángulo simple sin fondo). El tipo OBSTACULO dibujará un rectángulo con fondo negro y su funcionalidad será impedir el paso de la entidad o de los adversarios (ver Figura 1). Por último el tipo AMOR pintará un rectángulo rosa y su funcionalidad será la de modificar el estado de la entidad de alguna forma (lo veremos más adelante). Como hemos añadido nuevos elementos en el escenario necesitamos rea- lizar la nueva programación del comportamiento del agente donde tendremos 3 Figura 1: Nuevas entidades sin movimiento en el escenario con alguna inter- acción respecto de la entidad en cuenta diferentes casuísticas de interacción. Para ello vamos a extender los movimientos de la entidad teniendo considerando que, ahora, cuando la entidad se mueve hacia alguna celda cercana podría toparse con alguno de los nuevos elementos presentes en el escenario: una pared, una celda de amor o un adversario. De esta forma, el protocolo de actuación consiste en conocer el tipo de la celda a la que nos queremos mover y en función de dicho tipo implementaremos un tipo de interacción u otra. Veamos primero como queda la clase Entidad. import java.awt.Color; import java.awt.Graphics; import javax.swing.JComponent; /** * * @author klemenzza */ public class Entidad extends JComponent implements Constantes{ public int xMov; public int yMov; public String estado; public Entidad(int x,int y) { xMov=x; yMov=y; estado="VIVO"; } public void moverArriba() { yMov-=1; } public void moverAbajo() { 4 yMov+=1; } public void moverIzquierda() { xMov-=1; } public void moverDerecha() { xMov+=1; } //algoritmo de pintado de una entidad @Override public void paintComponent(Graphics g) { g.drawRect(xMov*PIXELS+DESPLAZAMIENTO, yMov*PIXELS+DESPLAZAMIENTO,PIXELS,PIXELS); g.setColor(Color.green);//cambiamos color g.fillRect(xMov*PIXELS+DESPLAZAMIENTO, yMov*PIXELS+DESPLAZAMIENTO,PIXELS,PIXELS); g.setColor(Color.BLACK);//volvemos a cambiar g.setFont(FUENTE); g.drawString("E",(xMov*PIXELS+DESPLAZAMIENTO)+5, (yMov*PIXELS+DESPLAZAMIENTO)+28); g.setColor(Color.white);//volvemos a cambiar g.fillRect((xMov*PIXELS+DESPLAZAMIENTO), (yMov*PIXELS+DESPLAZAMIENTO)-15,PIXELS,PIXELS/2); g.setColor(Color.BLACK);//volvemos a cambiar g.setFont(FUENTE2); g.drawString(estado, (xMov*PIXELS+DESPLAZAMIENTO)+1, (yMov*PIXELS+DESPLAZAMIENTO)-5); } } Junto a la E aparece el estado de la entidad: VIVO, si la entidad está viva; AMOR, si la entidad paso por una celda del tipo AMOR; PARED, si la entidad chocó con obstáculo negro; MUERTO, si la entidad fue capturada por le adversario. Para completar la implementación añadiremos tres nuevas constantes y un método especial que nos permite mostrar mensajes gráficos mediante una ventana de diálogo. import java.awt.Font; public interface Constantes { // el resto permanece igual public final Font FUENTE2=new Font("Times New Roman",Font.BOLD,10); 5 //para la interaccion public final char OBSTACULO=’O’; public final char CAMINO=’C’; public final char AMOR=’A’; default void lanzar_mensaje(String mensaje) { JOptionPane.showMessageDialog(null, mensaje, "Mundo Virtual", JOptionPane.PLAIN_MESSAGE); } } Antes de pasar al MundoVirual para implementar la interacción veamos como queda el constructor de Escenario y la diferencia con la definición de las Celdas. Ahora cada Celda lleva el tipo CAMINO. Todas las celdas son camino inicialmente, será en MundoVritual donde iremos definiendo cómo será nuestro mundo. public Escenario() { celdas=new Celda[NUMERO_CELDAS_ANCHO][NUMERO_CELDAS_LARGO]; //inicializar el array de celdas for(int i=0; i < NUMERO_CELDAS_ANCHO; i++) { for ( int j=0 ; j < NUMERO_CELDAS_LARGO ; j++) { celdas[i][j]=new Celda(i*PIXELS+DESPLAZAMIENTO, j*PIXELS+DESPLAZAMIENTO,CAMINO); } } } Ahora extendemos la funcionalidad de movimientos en MundoVirtual. La clase MundoVirtual quedaría de la siguiente forma. Primero mostramos los atributos y la definición del MundoVirtual import java.awt.Canvas; import java.awt.Color; import java.awt.Graphics; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; /** * * @author klemenzza */ public class MundoVirtual extends Canvas implements Constantes{ 6 //para pintar el lienzo public Escenario escenario; public Entidad ent; public Adversario adv1; //public Adversario adv2; //public Adversario adv3; //public ArrayList<Adversario> adversarios; public MundoVirtual(){ escenario=new Escenario(); ent=new Entidad(1,1); //se indican donde estarán los obstaculos //celda (2,2) es obstaculo escenario.celdas[2][2].tipo=OBSTACULO; //celda (2,3) es obstaculo escenario.celdas[2][3].tipo=OBSTACULO; //escenario.celdas[8][8].tipo=OBSTACULO; //escenario.celdas[9][8].tipo=OBSTACULO; //celda (10,8) es amor escenario.celdas[10][8].tipo=AMOR; adv1=new Adversario("#1",5,5); //adversarios /*adversarios=new ArrayList<>(); adv1=new Adversario("#1",5,5); adv2=new Adversario("#2",6,6); adv3=new Adversario("#3",7,7); adversarios.add(adv1); adversarios.add(adv2); adversarios.add(adv3);*/ //color de fondo this.setBackground(Color.orange); this.setFocusable(true); //queremosque escuches en este elemento //escuchador eventos de teclado addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent evt) { mover(evt); repaint(); } }); } @Override 7 public void paint(Graphics g) { ent.paintComponent(g); escenario.paintComponent(g); adv1.pintarAdversario(g); //adv2.paintComponent(g); //adv3.paintComponent(g); } public void pre_movimiento( KeyEvent evento ) { ent.estado="VIVO"; switch( evento.getKeyCode() ) { case KeyEvent.VK_UP: System.out.println("Mover arriba"); if ( ent.yMov > 0 ) { if ( escenario.celdas[ent.xMov][ent.yMov-1].tipo!=OBSTACULO ) { ent.moverArriba(); }else ent.estado="PARED"; } break; case KeyEvent.VK_DOWN: System.out.println("Mover abajo"); if ( ent.yMov < NUMERO_CELDAS_LARGO-1 ) { if ( escenario.celdas[ent.xMov][ent.yMov+1].tipo!=OBSTACULO) { ent.moverAbajo(); }else ent.estado="PARED"; } break; case KeyEvent.VK_LEFT: System.out.println("Mover izquierda"); if ( ent.xMov!=0) { if ( escenario.celdas[ent.xMov-1][ent.yMov].tipo!=OBSTACULO) { ent.moverIzquierda(); }else ent.estado="PARED"; } break; 8 case KeyEvent.VK_RIGHT: System.out.println("Mover derecha"); if ( ent.xMov < NUMERO_CELDAS_ANCHO-1 ) { if ( escenario.celdas[ent.xMov+1][ent.yMov].tipo!=OBSTACULO) ent.moverDerecha(); else ent.estado="PARED"; } break; } } //metodo llamada la primera vez que se pinta public void mover( KeyEvent evento ) { //mientras no está muerto if ( !ent.estado.equals("MUERTO")) { //arriba, abajo, izquierda o derecha? pre_movimiento(evento); //consulto si estoy en celda amor, si es así cambio etado if ( amor2() ) ent.estado="AMOR"; else { //consulto si no fui atrapado por adversario if ( !chocarAdversario(adv1) ) { if ( !ent.estado.equals("PARED")) //si no choqué con pared ent.estado="VIVO"; }else { //cuando me atrapa el adversario lanzar_mensaje("El adversario te atrapó"); ent.estado="MUERTO"; } } } } public boolean amor() { return escenario.darCelda(ent.xMov, ent.yMov).tipo==AMOR; } public boolean amor2() { return ent.xMov==10 && ent.yMov==8; } public boolean chocarAdversario(Adversario adv) { 9 return (ent.xMov==adv.xMov && ent.yMov==adv.yMov); } /*public boolean chocarAdversarios() { boolean resultado=false; for(int i=0; i < adversarios.size();i++) { resultado=chocarAdversario(adversarios.get(i)); } return resultado; }*/ } Una vez que disponemos de la funcionalidad para pintar nuestro mundo virtual vamos mostrar cómo quedaría la lógica de los movimientos. El com- portamiento de nuestro mundo se implementa en varios métodos. El método mover es el encargado de implementar los cambios de estado en función de lo que haya ocurrido, en nuestro caso: solo podremos mover la entidad mientras esté viva, si es así identificamos el movimiento y comprobamos si la entidad pasa por la celda amor, si es así cambiamos su estado, si no es así chequeamos si tocamos al adversario. Si fuera ese caso, cambiamos el estado a MUERTO e informaremos al usuario y además ya no nos podremos mover. Note que también se va a implementar la funcionalidad de cambiar el estado a PARED cuando nos topamos con una de ellas. public void mover( KeyEvent evento ) { if ( !ent.estado.equals("MUERTO")) { identificar_Tecla_Pulsada(evento); if ( amor()) ent.estado="AMOR"; else { if ( !adversario() ) { if ( !ent.estado.equals("PARED")) ent.estado="VIVO"; } else { lanzar_mensaje("El adversario te atrapó"); ent.estado="MUERTO"; } } } } 10 public boolean amor() { return escenario.darCelda(ent.xMov, ent.yMov).tipo==AMOR; } public boolean adversario() { return ent.xMov==adv1.xMov && ent.yMov==adv1.yMov; } Una vez explicado el método principal que implementa la funcionalidad en la interacción de ciertas entidades vamos a completar la explicación con el método identificar tecla pulsada que implementa los movimientos. Nos vamos a centrar un movimiento, el resto se implementan de forma análoga public void identificar_Tecla_Pulsada( KeyEvent evento ) { ent.estado="VIVO"; switch( evento.getKeyCode() ) { case KeyEvent.VK_UP: System.out.println("Mover arriba"); if ( ent.yMov > 0 ) { if ( escenario.celdas[ent.xMov][ent.yMov-1].tipo!=OBSTACULO ) { ent.moverArriba(); }else ent.estado="PARED"; } break; //...resto de movimientos } } En este caso si xMov es mayor que cero y la celda a la que queremos ir es diferente de pared entonces nos moveremos. En caso contrario, la entidad se quedará en el mismo sitio. La clave está en la instrucción: if ( escenario.celdas[ent.xMov][ent.yMov-1].tipo!=OBSTACULO ) donde se comprueba si la celda a la que me quiero mover (en este caso arriba) es distinto del tipo OBSTACULO. Por último, se puede observar que existe un atributo nuevo denomina- do Adversario adv1. Hemos decidido crear una clase Adversario que será diferente a los otros elementos ya que tendrá método de movimientos (al fi- nal un algoritmo guiará al adversario). El código del adversario se muestra a continuación: 11 import java.awt.Color; import java.awt.Graphics; /** * * @author klemenzza */ public class Adversario implements Constantes{ public int xMov,yMov; public String nombre; public Adversario(String n,int x,int y) { xMov=x; yMov=y; nombre=n; } public void moverArriba() { yMov-=1; } public void moverAbajo() { yMov+=1; } public void moverIzquierda() { xMov-=1; } public void moverDerecha() { xMov+=1; } //algoritmo de pintado de un adversario public void pintarAdversario(Graphics g) { g.drawRect(xMov*PIXELS+DESPLAZAMIENTO, yMov*PIXELS+DESPLAZAMIENTO,PIXELS,PIXELS); g.setColor(Color.RED);//cambiamos color g.fillRect(xMov*PIXELS+DESPLAZAMIENTO, yMov*PIXELS+DESPLAZAMIENTO,PIXELS,PIXELS); g.setColor(Color.BLACK);//volvemos a cambiar g.setFont(FUENTE); g.drawString("A",(xMov*PIXELS+DESPLAZAMIENTO)+5, (yMov*PIXELS+DESPLAZAMIENTO)+28); g.setColor(Color.white);//volvemos a cambiar g.fillRect((xMov*PIXELS+DESPLAZAMIENTO), (yMov*PIXELS+DESPLAZAMIENTO)-15,PIXELS,PIXELS/2); g.setColor(Color.BLACK);//volvemos a cambiar g.setFont(FUENTE2); g.drawString(nombre, (xMov*PIXELS+DESPLAZAMIENTO)+1, (yMov*PIXELS+DESPLAZAMIENTO)-5); } } Finalmente el escenario con todos los elementos: entidad, obstáculos (ne- gros y rosas) y adversario quedaría cómo se muestra en la Figura 2. Note además que el tamaño de las celdas es mayor en esa visualización, basta con ir a Constantes y poner la constante PIXELS a 48. 12 Figura 2: Nuevas entidades sin movimiento en el escenario con alguna inter- acción respecto de la entidad Movimientos: animación e inteligencia básica El objetivo de esta sección es explicar cómo se pueden animar las entida- des, esto es, proporcionales movimiento de forma automática. Para nosotros animar significa indicarle a la entidad qué pasos debe realizar, por tanto, será nuestra primera inteligencia, una inteligencia muy básica. Esta funcionali- dad 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 de forma concurrente o paralela junto a otros hilos de ejecución (el propio método main() se ejecuta como un hilo). En Java existen varias formas de implementar un hilo de ejecución (here- dando de la clase Thread o implementando la clase Runnable). También se puede utilizar la funcional de los TimerTask. Un TimerTask puede lanzarse cada X segundos por un lanzador de tareas. En cada lanzamiento se ejecuta el código situado en el método run(). Una primera implemtación de la clase Inteligencia podría ser la siguiente: iimport java.awt.Color; import java.awt.Graphics; import java.util.TimerTask; public class Entidad extends TimerTask implements Constantes{ public int xMov; public int yMov; public String estado; public MundoVirtual mv; public Entidad(int x,int y, MundoVirtual mv) { xMov=x; yMov=y; estado="VIVO"; this.mv=mv; } 13 ... @Override public void run() { realizar_Movimiento_Aleatorio(); actualizarMundoVirtual(); } } Si observamos la clase tiene un atributo MundoVirtual que será la infor-mación que la entidad tiene del mundo en el que se encuentra. El método run() es donde se implementa la inteligencia de la Entidad. En este caso es muy sencilla: se genera un número aleatorio entre 0 y 3. Si es 0 la entidad se mueve arriba, si es 1 la entidad se mueve hacia abajo, si es 2 se mueve a la derecha y si es 3 se mueve a la izquierda. En todos lo casos imprimimos un mensaje que indica que la entidad se ha movido y en qué dirección. Note que la inteligencia sustituye a la parte del código que teníamos implementadas para los eventos de teclado. Ya que ahora no es el humano quien dirigirá a la entidad, será la inteligencia artificial (su método run) de cada entidad quien lo haga. Por último una vez se ha definido la entidad y su inteligencia (hilo de ejecución) necesitamos un objeto especial que lance o ejecute dichas tareas. Para ello haremos uso de la funcionalidad Timer que permite lanzar o ejecu- tar objetos del tipo TimerTask. La clase VentanaPrincipal sufre un pequeño cambio: import java.util.Timer; import javax.swing.JFrame; public class VentanaPrincipal extends JFrame implements Constantes{ //nuestra clase se compone de un lienzo de dibujo (herada de canvas) public MundoVirtual mundo_virtual; public Timer lanzadorEntidadesAutonomas; //constructor public VentanaPrincipal() { mundo_virtual=new MundoVirtual(); this.getContentPane().add(mundo_virtual); //el tamaño de la venta es la del escenario y el incremento de los bordes this.setSize(ANCHURA_ESCENARIO,LARGO_ESCENARIO); lanzadorEntidadesAutonomas=new Timer(); lanzadorEntidadesAutonomas. scheduleAtFixedRate(mundo_virtual.ent,0,1000); } 14 } Las parte de código destacable es la llamada al método scheduleAtFixedRate que permite lanzar un objeto TimerTask cada cierto tiempo, en nuestro caso, cada segundo. De esta forma al ejecutar nuestra aplicación la entidad se irá movimiento de celda en celda de forma aleatoria. A partir de aquí comen- zaremos a ver algoritmos de inteligencia artificial para resolver problemas, nuestra primera parada será los basados en búsqueda en un espacio de es- tados de los que su mayor exponente es el algoritmo A estrella o también conocido por A*.