-
Notifications
You must be signed in to change notification settings - Fork 29
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.
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()
.
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.
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.
La clase ComportamientoAnimado
tiene tres responsabilidades importantes, y son la razón de que esta exista: animar, definir hooks adicionales y definir verificaciones:
- 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. - Agrega dos hooks para ejecutar comportamiento, que se agregan al viejo hook "actualizar" (en esta clase, el
actualizar()
está deprecado): -
preAnimacion()
se ejecutará antes de comenzar a animar al personaje. -
postAnimacion()
se ejecutará luego de finalizar la animación del personaje. -
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). - 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:
Puede usarse directamente de esta manera:
actor.hacer_luego(ComportamientoAnimado,{nombreAnimacion: 'correr'});
De esta manera el actor se animará sin hacer otra cosa.
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();
}
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.
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:
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.
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;
}
El ComportamientoColision
sirve para que el actor realice una acción cuando está colisionando con otro (si no está colisionando, lanza una excepción)
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
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})
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.
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
.
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.
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.