-
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.
![Comportamiento](http://yuml.me/diagram/plain;dir:LR/class/[Comportamiento|receptor|iniciar(receptor);actualizar()(abstract)]%5E-[ComportamientoAnimado%7Bbg:wheat%7D||preAnimacion() (abstracto);postAnimacion() (abstracto);doActualizar();configurarVerificaciones() (abstracto)],[ComportamientoAnimado]%5E%E2%81%BB[ComportamientoConVelocidad],[ComportamientoAnimado]%5E%E2%81%BB[Decir],[ComportamientoAnimado]%5E%E2%81%BB[...%20etc%20...])
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. - 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(), "No hay nada para colisionar"));
}
}
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.
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
.
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](http://yuml.me/diagram/plain;dir:LR/class/[ComportamientoAnimado]%5E-[ComportamientoConVelocidad%7Bbg:wheat%7D|pasosRestantes|darUnPaso() (abstracto);setearEstadoFinalDeseado() (abstracto)],[ComportamientoConVelocidad]%5E%E2%81%BB[MovimientoAnimado],[ComportamientoConVelocidad]%5E%E2%81%BB[SaltarAnimado],[ComportamientoConVelocidad]%5E%E2%81%BB[...%20etc%20...])
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;
}