Skip to content

Modelo de Comportamientos

Alf Sanzo edited this page Oct 18, 2016 · 13 revisions

Modelo de Comportamientos

Pilas Bloques construye todas las acciones que puede hacer cada autómata a partir de la idea de Comportamiento existente en Pilas Web. Ésta es una idea muy poderosa, ya que permite tratar a las acciones como objetos, y gracias a esto se logran dos características importantes:

  • Primero, los comportamientos son independientes de los actores, por lo que son -casi- completamente intercambiables con actores diferentes.
  • Segundo, al ser objetos, se pueden modelar abstracciones y maximizar la reutilización de lógica, implementando, por ejemplo, una jerarquía de comportamientos.

El presente documento describe la jerarquía de comportamientos existente en Pilas Bloques, y justifica un poco cada decisión tomada.

Ejemplo en Pilas Web

En Pilas Web, si se tiene a un actor "Mono" y se desea que el mono salte, se debe hacer:

  var mono = pilas.actores.Mono(0,0);
  mono.hacer_luego(pilas.comportamientos.Saltar, {velocidad_inicial:50})

En otras palabras, los actores entienden el mensaje "hacer_luego", que recibe una subclase de Comportamiento (sin instanciar), y un objeto con las configuraciones que el comportamiento necesita (en Pilas Engine estas configuraciones se denominan argumentos).

Para funcionar, el comportamiento debe implementar dos métodos muy importantes: el método iniciar() y el método actualizar().

Comportamiento

El método iniciar(receptor) debe, además de guardar el receptor del comportamiento (que es el actor que estará haciendo la acción), realizar todas las configuraciones iniciales. Se ejecuta apenas se llama el método actor.hacer_luego.

Ejemplo rudimentario:

  class Saltar extends Comportamiento {
    iniciar(receptor) {
      super.iniciar(receptor);
      this.velocidad = this.argumentos.velocidad_inicial
    }
  }

El método actualizar() es el que realiza la modificación al actor. Debe pensarse como un método que "loopea", es decir, Se ejecuta en cada ciclo de la escena. Y se sigue ejecutando hasta retornar true. Entonces, en todo método actualizar() siempre habrá un momento en el que el método retorne true para avisar que el comportamiento ha finalizado.

Ejemplo rudimentario:

  class Saltar extends Comportamiento {
    actualizar() {
      this.receptor.y += this.velocidad;
      this.velocidad -= 0.3;
      if (this.receptor.y <= 0) return true;
    }
  }

Sin embargo, si bien es necesario entender este modelo, en Pilas Bloques es muy raro que se implemente de esta manera, porque hay una clase ComportamientoAnimado que resuelve algunos problemas comunes a todos los comportamientos en Pilas Bloques.

Clase ComportamientoAnimado (Pilas Bloques)

Como en Pilas Bloques hay varios comportamientos que realizan acciones sólo al final ó al principio, y además casi todas las acciones tienen algún tipo de animación, se creó la clase ComportamientoAnimado, de la que heredan casi todos los comportamientos de Pilas Bloques.

Comportamiento

Interfaz

La clase ComportamientoAnimado tiene tres responsabilidades importantes, y son la razón de que esta exista: animar, definir hooks adicionales y definir verificaciones:

  1. Configurando un nombre de animación, ejecuta una animación en un actor. Esto se consigue ya sea pasando el argumento nombreAnimacion ó redefiniendo el método de mismo nombre. Una vez terminado el comportamiento, vuelve a cargar la animación anterior que tenía el actor.
  2. Agrega dos hooks para ejecutar comportamiento, que se agregan al viejo hook "actualizar" (en esta clase, el actualizar() está deprecado):
  3. preAnimacion() se ejecutará antes de comenzar a animar al personaje.
  4. postAnimacion() se ejecutará luego de finalizar la animación del personaje.
  5. doActualizar() reemplaza al actualizar(). Es el análogo al método actualizar del comportamiento, sólo que cambia de nombre para no confundirlo (y porque el otro método no tiene).
  6. Permite definir verificaciones, que son chequeos que se hacen antes y después de ejecutarse el comportamiento, y sirven para definir cierta lógica de dominio (por ejemplo, no caerse de la cuadrícula, no cerrar los ojos si ya están cerrados, etc.) Se consigue redefiniendo el método configurarVerificaciones().
  configurarVerificaciones() {
    // son varios llamados a verificacionesPre.push
    // y a verificacionesPost.push
  }

En el código hay ejemplos útiles:

Ejemplo sólo para animar

Puede usarse directamente de esta manera:

    actor.hacer_luego(ComportamientoAnimado,{nombreAnimacion: 'correr'});

De esta manera el actor se animará sin hacer otra cosa.

Ejemplo para animar y hacer algo al final

Otra manera de usarlo es así:

    actor.hacer_luego(Explotar);

Donde Explotar es una subclase y tiene definidos los siguientes métodos:

    nombreAnimacion(){
		return 'explosion'
	};
    postAnimacion(){
		this.receptor.eliminar();
	}

Ejemplo para usarlo como el clásico Comportamiento

Otra manera de usarlo es independientemente de la animación (Para decidir uno cuándo termina el comportamiento)

    actor.hacer_luego(MoverEnX,{destino: 50});

Donde MoverEnX es subclase de ComportamientoAnimado y define:

	nombreAnimacion(){
		return 'correr';
	};
	doActualizar(){
		super.doActualizar();
		this.receptor.x = this.receptor.x + 1;
		if (this.receptor.x = this.argumentos.destino){
			return true;
		}
	}

Mientras, la animación se ejecuta en un loop hasta que doActualizar devuelve true.

Ejemplo para definir una verificación

Si quiero verificar, antes de que el comportamiento "colisión" se ejecute, que efectivamente esté tocando algo:

class ComportamientoColision extends ComportamientoAnimado {
  configurarVerificaciones() {
    this.verificacionesPre.push(new Verificacion(() => this.colisiona(), "¡Acá no hay manzana!"));
  }
}

La Verificacion es un objeto que se construye con dos parámetros: Una función (que es la que debe devolver true si está todo bien) y el string de error a mostrar al usuario.

La colección verificacionesPre tiene las verificaciones que se realizan previo a la ejecución del comportamiento. La colección verificacionesPost tiene las que se realizan luego. Cuando una verificación falla, el resultado en Pilas Bloques es que el autómata dice con un globo de diálogo el mensaje anterior:

verificacion

Clase ComportamientoConVelocidad (Pilas Bloques)

El ComportamientoConVelocidad existe para solucionar el problema de elegir la velocidad de ejecución del ComportamientoAnimado.

Quizás se podría haber codeado directamente sobre el ComportamientoAnimado. Podría hacerse un refactor. En todo caso, el refactor es necesario porque hoy no todos los comportamientos pasan por el comportamiento con velocidad.

Utiliza dos conceptos: la cantidad de pasos necesaria para completar el comportamiento, y la velocidad propiamente dicha a la cual deben ejecutarse.

De la documentación de la clase:

/**
 * @class ComportamientoConVelocidad
 *
 * Argumentos:
 *    velocidad: Es un porcentaje. 100 significa lo más rápido. Debe ser 1 ó más.
 *               Representa la cantidad de ciclos que efectivamente se ejecutan.
 *    cantPasos: Mayor cantidad de pasos implica mayor "definicion" del movimiento.
 *               Tambien tarda mas en completarse. Jugar tambien con la velocidad.
 *               Como esto juega con la animacion, es preferible no tocarlo.
 */

Nota importante: Poniendo la cantidad de pasos en 1, el comportamiento debería requerir sólo 1 iteración para completarse. Esto puede ser usado para agilizar la ejecución de los tests.

Para que esto funcione, es necesario definir las subclases con dos métodos:

  darUnPaso(){
    // Debe redefinirse. Es el comportamiento a realizar en cada tick.
  }
  setearEstadoFinalDeseado(){
    // Debe redefinirse. Sirve para asegurar que al terminar los pasos se llegue al estado deseado
    // Por ejemplo, si me estoy moviendo a un lugar, setear ese lugar evita problemas de aproximación parcial.
  }

La combinación obligada de los métodos darUnPaso() y setearEstadoFinalDeseado() reemplazan al doActualizar() y similares.

Comportamiento

Lo interesante es que al crear un comportamiento heredando de ComportamientoConVelocidad, automáticamente gano la posibilidad de definir la velocidad de animación. Es el ejemplo del comportamiento Eliminar:

class Eliminar extends ComportamientoConVelocidad {
	postAnimacion(){
		this.receptor.eliminar();
	}
}

Notar que sólo define el postAnimacion(), porque un objetivo de heredar de ComportamientoConVelocidad es ganar la posibilidad de definir la velocidad de ejecución de la animación. Un ejemplo de redefinición de los métodos darUnPaso() etc. está en MovimientoAnimado:

darUnPaso(){
    // el vector de avance se define a partir de la cantidad de pasos
    this.receptor.x += this.vectorDeAvance.x;
    this.receptor.y += this.vectorDeAvance.y;
}

setearEstadoFinalDeseado(){
    this.receptor.x = this.destino.x;
    this.receptor.y = this.destino.y;
}

Comportamientos comúnmente usados

ComportamientoColision

El ComportamientoColision sirve para que el actor realice una acción cuando está colisionando con otro (si no está colisionando, lanza una excepción)

Comportamiento

Ejemplos bastante usados de comportamientos que heredan de ComportamientoColision son:

  • RecogerPorEtiqueta , en sus argumentos recibe una etiqueta, que permite armar una acción de "recoger manzana" ó "recoger sandía"
  //Recoge (hace desaparecer) una banana
  actor.hacer_luego(RecogerPorEtiqueta,{etiqueta: "BananaAnimada"})
  • ContarPorEtiqueta , en sus argumentos recibe una etiqueta, que permite "contar" (incrementar un contador).
  //Cuenta una manzana
  actor.hacer_luego(ContarPorEtiqueta,{etiqueta: "Manzana"})

Por Gauss, hay que cambiar el nombre del método metodo() También podría heredar de ComportamientoConVelocidad, en lugar de heredar de ComportamientoAnimado. Otro refactor pendiente es sacarle el comportamientoAdicional, y quien quiera hacer dos comportamientos que use la SecuenciaAnimada

Movimientos en la cuadrícula

MovimientoAnimado

Antes de pasar a Movimientos en cuadrícula, conviene hacer una pequeña introducción sobre la implementación del MovimientoAnimado. Este comportamiento permite mover a un actor a cualquier punto ó en cualquier dirección y distancia, mientras avanza con una transición espacial y una animación.

Como es un MovimientoConVelocidad, pueden definirse los pasos y la velocidad.

* Argumentos:
*    distancia: la distancia deseada de recorrer
*    destino: alternativamente se puede proveer un objeto con x e y, que es el destino.
*       NOTA: Si se proveen ambos distancia y destino, deben concordar, sino no se
*              garantiza el comportamiento correcto del Movimiento.
*    direccion: Sino, se puede proveer una direccion de movimiento. (instancia de Direc)

Ejemplo:

  // Avanza en diagonal 50 px hacia arriba, a la velocidad por defecto
  actor.hacer_luego(MovimientoAnimado,{direccion:new Direct(3,4), distancia:50})

  // Avanza a la posición (40,100), a la velocidad 60%
  actor.hacer_luego(MovimientoAnimado,{destino:{x:40,y:100}, velocidad:60})

movAnimado

Aquí se puede notar el uso del MovimientoAnimado en conjunto con Direct. Notar que al avanzar hacia la izquierda "espeja" al actor. También es interesante notar otras cosas que nos regalan las superclases:

  • ComportamientoAnimado nos da el feature que antes y después de ejecutar el comportamiento, sigue la misma animación que había.
  • ComportamientoAnimado también nos da el feature de poder definir para el movimiento la animación "correr". De todas formas, si no se ponía dicho argumento, esa es la animación por defecto.
  • ComportamientoConVelocidad está definiendo la velocidad a la que se ejecuta.

Direccion de movimiento

Direct puede construirse de diferentes formas:

  //Indicando un vector dirección
  actor.hacer_luego(MovimientoAnimado,{direccion:new Direct(3,4), distancia:50})
  //Indicando un ángulo en radianes
  actor.hacer_luego(MovimientoAnimado,{direccion:new Direct(Math.PI), distancia:50})
  //Indicando dos objetos ó dos puntos que definen una recta
  actor.hacer_luego(MovimientoAnimado,{direccion:new Direct({x:0,y:5},{x:4,y:70}), distancia:50})

En cualquier caso, Direct tiene sólo dirección y sentido, pero descarta información sobre el módulo. Es decir, transforma todo a un versor. Por eso es necesario acompañar siempre con la distancia deseada cuando se usa Direct.

Ahora sí, en la cuadrícula

Como los movimientos en una cuadrícula son 4 y tienen muchos chequeos y cálculos de distancia en común, todos heredan de un único MovimientoEnCuadricula, que a su vez hereda de MovimientoAnimado, que es el que resuelve todos los problemas de animación.

Comportamiento

Los nombres de las clases son bastante autoexplicatorios. De todas formas, se explica:

  • "MoverACasilla..." Mueve al actor 1 casilla en la dirección correspondiente (si no se puede se lanza excepción).
  • "MoverTodoA..." Mueve al actor hasta el extremo indicado de la cuadrícula (siempre funciona)
  • "SiguienteFila" y "SiguienteColumna" Son exactamente lo mismo que "MoverACasillaAbajo" y "MoverACasillaDerecha", sólo que sólo pueden realizarse al principio de la fila/columna.

La ventaja de este comportamiento es que no necesita argumentos, porque calcula las direcciones en base a la cuadrícula existente del actor:

  actor.hacer_luego(MoverACasillaArriba)

De más está decir que para que los movimientos en cuadrícula funcionen, el actor debe tener seteada su cuadricula. Y la animación por defecto que ejecuta es "correr", como todo movimiento animado.