Gestión de eventos temporizados. La librerÃa Timer.h
Gestión de eventos temporizados. La librerÃa Timer.h.
Una de las caracterÃsticas más interesantes de la máquina Glulx es la del soporte de eventos temporizados. La capacidad de que el juego evolucione de forma independiente y en tiempo real, sin esperar a que el jugador escriba su siguiente orden, ayuda a crear juegos más dinámicos y realistas. Ya sea simplemente como un medio para llevar a cabo «efectos especiales» o como parte integrante de la historia que se pretende contar, los eventos temporizados son útiles (y divertidos).
Por desgracia, el soporte que la máquina Glulx ofrece a la temporización es bastante pobre y sencillo, lo que complica desarrollar «a mano» nuestros propios eventos temporizados. Básicamente todo se reduce a una alarma (también llamado timer) que se activa cada X milisegundos y que, una vez disparada, provoca la ejecución de una rutina que llevará a cabo todos los procesos relacionados con el tiempo real. El hecho de que sólo podamos activar un timer dificulta bastante la tarea de disponer de diferentes eventos que se activen en momentos diferentes a intervalos diferentes. Por ejemplo, si queremos que suene un trueno cada seis segundos y que tu compañero de aventuras te incordie con sus chistes malos cada nueve segundos, en principio nos resultará imposible porque no podemos activar dos timers diferentes para que funcionen de forma paralela.
Funcionamiento del temporizador de Glulx
La función glk_request_timer_events(x) activa el temporizador para que se dispare un timer cada x milisegundos. Una vez disparada, el control se bifurca a la rutina HandleGlkEvent mediante una llamada HandleGlkEvent(ev, context, buffer), donde:
-
ev es un array cuya primera posición (ev-->0) toma el valor evtype_Timer,
-
context valdrá 1 si la llamada se hizo desde las rutinas KeyCharPrimitive o KeyDelay, y 0 en caso de hacerlo desde KeyboardPrimitive, y
-
buffer contendrá la lÃnea de órdenes que haya tecleado el jugador en el momento de dispararse el timer.
Con todo esa esa información, la rutina HandleGlkEvent tendrá que decidir qué evento se activa (el trueno, el chiste del compañero o lo que sea), además de otras tareas relacionadas con la lÃnea de órdenes del jugador. La rutina, además, deberá devolver:
-
Nada, 0 ó 1, lo cual no afecta a la entrada del jugador, o bien
-
2, lo cual aborta la entrada del jugador, de forma que se tendrá en cuenta lo que haya en la variable buffer y se tratará como si lo hubiera tecleado el jugador (seguido de Enter).
Si durante la ejecución de la rutina se decide que hay que hacer sonar el trueno, simplemente se le hace sonar y se retorna cualquier valor distinto de 2. Aquà no ha habido ningún problema, ya que no se ha mostrado ningún texto en la pantalla ni se ha tenido que cambiar la entrada del jugador.
En cambio, si el evento a llevar a cabo es mostrar por pantalla el chiste del compañero de viaje, la cosa ya cambia, pues tenemos dos problemas:
-
No podemos imprimir nada en una ventana en la que haya pendiente una petición de entrada de lÃnea o de carácter.
-
Debido a lo anterior, tenemos que cancelar la petición de entrada pendiente para poder imprimir, pero, una vez cancelada la petición y mostrado el chiste, tenemos que volver a componer el prompt del intérprete y recuperar la entrada que el jugador haya podido teclear hasta ese momento. Y todo esto hay que hacerlo sólo en el caso de que se vaya a imprimir algo.
Para cancelar una petición de entrada de lÃnea pendiente, se hace una llamada a glk_cancel_line_event(gg_mainwin, gg_event), suponiendo que la ventana en la que está pendiente la petición sea la ventana principal de texto, gg_mainwin. En gg_event-->2 se vuelca la longitud (número de caracteres) de la entrada del jugador en ese momento.
Para recuperar la entrada del jugador, hay que:
-
Reasignar la longitud del buffer:
buffer-->0 = gg_event-->2;
-
Volver a generar una nueva petición de entrada de lÃnea, volcando el buffer recibido por HandleGlkEvent:
glk_request_line_event(gg_mainwin, buffer + WORDSIZE,
INPUT_BUFFER_LEN - WORDSIZE, buffer-->0);
La gestión de los eventos temporizados ya deja de ser, por tanto, una tarea tan trivial, pues hay que tener en consideración si se va a imprimir algo o no, y actuar en consecuencia.
Gestión de varios eventos simultáneos
Añadamos ahora el problema de tratar con varios eventos simultáneos, como estamos intentando hacer desde el principio de este artÃculo. ¿Cómo podemos gestionar eventos que se producen en momentos diferentes si sólo disponemos de un único timer, programado mediante glk_request_timer_events(x)?
La solución que aquà se propone es la siguiente:
-
Supongamos (sólo a tÃtulo de ejemplo, y sin perder generalidad) que tenemos los dos eventos con los que hemos estado peleando durante todo el artÃculo: el trueno cada seis segundos (6000 ms) y el chiste cada nueve segundos (9000 ms).
-
Calculamos el máximo común divisor (mcd) de ambos números (6000 y 9000). Ese número debe ser el número más grande que divida a 6000 y a 9000, de manera que tanto 6000 como 9000 sean múltiplos de ese número. En este caso, el mcd de 6000 y 9000 es 3000. A partir de ahora, llamaremos tick a dicho número, y ese será el argumento que usaremos al activar el timer de eventos temporizados llamando a glk_request_timer_events(). Por tanto, al principio del juego haremos:
glk_request_timer_events(3000);
Cada vez que se dispare el timer (por haber pasado un número de milisegundos igual al tick) diremos que se ha producido un tick.
-
En la rutina HandleGlkEvent llevaremos la cuenta de cuántos ticks se han producido. Usaremos un contador que se incrementará en uno cada vez que entremos en la rutina.
-
Ahora calcularemos el número de veces que hay que incrementar el contador para que se pueda activar un evento:
-
Para el evento del trueno, 6000ms / tick = 2. Por tanto, el trueno sonará cada vez que se produzcan dos ticks.
-
Para el evento del chiste, 9000ms / tick = 3. Por tanto, nuestro compañero nos contará un chiste cada vez que produzcan tres ticks. De esta forma, si al dividir el número de ticks entre 3 nos da de resto cero, querrá decir que toca molestar al jugador con un chiste.
-
En caso de haber tres, cuatro o más eventos, actuarÃamos de forma análoga, calculando el mcd de todos los tiempos correspondientes.
Aspectos a tener en cuenta
-
Lo ideal serÃa que el tick fuese igual a la duración del timer con menor duración de entre todos los existentes, ya que de lo contrario a veces se llamará a la rutina HandleGlkEvent pero no se ejecutará ningún evento, lo que resulta sub-óptimo. En nuestro ejemplo, se llamará a dicha rutina, de forma innecesaria, aproximadamente una vez cada tres ticks:
-
1º tick (a los 3000 ms): no se hace nada.
-
2º tick (a los 6000 ms): se lanza el trueno.
-
3º tick (a los 9000 ms): se lanza el chiste.
-
4º tick (a los 12000 ms): se lanza el trueno.
-
5º tick (a los 15000 ms): no se hace nada.
-
6º tick (a los 18000 ms): se lanzan el trueno y el chiste.
-
7º tick (a los 21000 ms): no se hace nada.
-
8º tick (a los 24000 ms): se lanza el trueno.
-
9º tick (a los 27000 ms): se lanza el chiste.
-
Etcétera...
-
Podemos definir la duración de un timer en función del número de ticks que se deben producir para disparar dicho timer. En nuestro caso, el timer del trueno durará dos ticks y el del chiste durará tres ticks. Eso significa que el valor máximo del contador será tres, y que una vez alcanzado dicho máximo podrá reiniciarse a uno, con idea de no incrementarlo indefinidamente, para evitar posibles overflows.
La librerÃa Timer.h
Para facilitar al programador de Inform la tarea de gestionar eventos simultáneos y abstraerse de toda la problemática relacionada con la impresión de mensajes durante la ejecución de un timer, he desarrollado una librerÃa sencilla pero flexible que se encarga de todos los aspectos tediosos y complejos.
Puede descargarse la última versión de la librerÃa desde mi repositorio de Alpha Pack:
-
https://raw.github.com/ricpelo/alpha_pack/master/Timer.h: versión para InformATE
-
https://raw.github.com/ricpelo/alpha_pack/infsp6/Timer.h: versión para INFSP6
Para la versión InformATE, se recomienda usar la última versión de la misma, con parches aplicados, que puede descargarse de mi repositorio:
http://gitorious.org/~ricpelo/informate/informate611
La librerÃa Timer, en esencia, define una clase GestorTimer y un objeto ControlTimer. Todos los timers deberán ser instancias de la clase GestorTimer. Por su parte, el objeto ControlTimer se encarga del control, planificación y «despacho» de los gestores de timers.
Ejemplo
Object timer_complejo
class GestorTimer
with
duracion TIMER_DURACION_LABERINTO,
condicion [;
return location ofclass Complejo && alien notin location;
],
evento [;
if (alien notin location && alien.condicion_movimiento()) {
alien.movimiento();
}
];
En este ejemplo podemos observar lo siguiente:
-
El objeto timer_complejo es un timer, es decir, un evento que se disparará según un patrón temporizado. Esto lo indicamos al decir que es un objeto de la clase GestorTimer.
-
La duración del timer se define en la propiedad duracion, y se expresa en número de ticks (NO en milisegundos).
-
La propiedad condicion define la condición (opcional) que se debe cumplir para ejecutar el evento cuando le toca según su duración. Por tanto, si la condición devuelve false, el evento no se ejecutará aunque le toque hacerlo.
-
El evento es el código que se ejecutará cuando haya pasado el número de ticks indicados en duracion (y si la condicion es true).
Inicialización y uso
-
Poner
Replace KeyDelay;
antes de
Include “Parser”;
-
Poner
Include “Timer”;
después de
Include “VerbLib”;
-
Definir en la aventura una rutina HandleGlkEvent desde la que se llame a
ControlTimer.CT_HandleGlkEvent(ev, context, buffer);
-
Para que un evento declarado como tal (o sea, instancia de la clase GestorTimer) pueda funcionar adecuadamente, es necesario agregarlo a la lista de gestores actuales (normalmente se hace al iniciar la aventura, en la rutina Initialise()) usando el método AgregarGestor:
timer_complejo.AgregarGestor();
Este método añadirá el gestor a la lista de gestores actuales en el primer hueco libre que encuentre. Es importante observar que el orden de agregación de los gestores puede ser importante, como se verá después.
-
Finalmente, una vez agregados todos los gestores, activamos el tick usando:
ControlTimer.ActivarTick(n);
siendo n la duración del tick (en milisegundos). La llamada a este método también se suele hacer en Initialise().
A partir de este momento, los ticks van contando y cada timer se ejecutará cuando corresponda según su duración.
Impresión en pantalla
Por las razones esgrimidas al comienzo de este artÃculo, hay que tener especial cuidado a la hora de imprimir texto en pantalla desde un evento temporizado. La librerÃa Timer.h simplifica esta labor. Cada vez que un evento tenga que imprimir algo en la ventana de texto, tan sólo tendrá que asegurarse de llamar previamente al método
ControlTimer.PrepararImpresion();
Este método se encarga de cancelar la petición de entrada de lÃnea que hubiera pendiente, y además se asegura de hacerlo una sola vez. Es importante que el programador se asegure de llamar a este método únicamente cuando se vaya a imprimir algo, ya que de lo contrario el jugador verá que la lÃnea de órdenes se reescribe con saltos de lÃnea inexplicables y molestos.
Control de eventos
Un evento puede controlar hasta cierto punto el flujo de ejecución de los restantes eventos haciendo uso de dos mecanismos principales (pero no únicos): el valor de retorno de la rutina evento y la asignación de semáforos de exclusión mutua (o mutex).
Valor de retorno de la rutina evento
La rutina evento puede afectar el funcionamiento de los demás eventos dependiendo del valor que retorne:
-
Si retorna false (que es el valor «normal»), no se afectará a la ejecución de los demás eventos.
-
Si retorna true, impedirá que se activen los demás eventos que queden por ejecutarse en el presente tick.
Esto quiere decir, por tanto, que el orden en el que se agreguen los eventos a la lista de eventos es importante, ya que los primeros eventos en ser agregados podrán interferir en el funcionamiento o no de los demás eventos que se hayan agregado posteriormente (no asà al contrario, de forma que un gestor no podrá afectar a otro que se haya agregado antes que él).
Semáforos de exclusión mutua
Un semáforo de exclusión mutua (también llamado mutex) es un mecanismo que otorga exclusividad a un evento. Si un evento tiene asignado el mutex, él será el único evento que podrá ejecutarse hasta que el mutex se libere. La utilidad del mutex es asegurarse que un determinado evento dispondrá de plena exclusividad sin verse afectado por otros posibles eventos que se ejecuten en paralelo.
Es una herramienta potente pero que hay que usar con cuidado, ya que si el mutex no se libera nunca se podrÃa llegar a un estado conocido como muerte por inanición, en el que los demás eventos (todos menos el que tiene asignado el mutex) nunca se activan, lo cual resultarÃa catastrófico si se da la circunstancia de que uno de esos eventos es el responsable de liberar el mutex.
Los métodos asociados con la gestión del mutex son:
-
ControlTimer.ActivarMutex(gestor): activa el mutex para el gestor indicado, de manera que, a partir de ese momento, sólo podrá ejecutarse ese evento y no los demás.
-
ControlTimer.DesactivarMutex(): levanta el mutex al gestor que lo tuviera activado. A partir de ese momento, todos los eventos podrán ejecutarse sin exclusividad.
Referencia de la librerÃa
Clase GestorTimer:
-
condicion: si es false, no se ejecutará este evento
-
duracion: número de ticks que necesita este evento para ejecutarse
-
evento: el evento a ejecutar
-
AgregarGestor(): agrega este gestor en el siguiente hueco de la lista
-
InsertarGestor(pos): inserta este gestor en la posición pos, empujando los demás
-
AsignarGestor(pos): coloca este gestor en la posición pos de la lista (se usa poco)
-
EliminarGestor(): elimina este gestor de la lista de gestores
-
ActivarMutex(): activa el mutex sobre este gestor
-
PosicionDelGestor(): en qué posición está este gestor en la lista de gestores
-
SustituirGestor(nuevo): sustituye este gestor por el nuevo
-
IntercambiarConGestor(g): intercambia este gestor con el gestor g en la lista
Objeto ControlTimer:
-
DentroDeEvento(): true si se está ejecutando un evento desde el timer
-
BuscarPosicion(g): da la posición de un gestor en el array
-
SustituirGestor(viejo, nuevo): cambia un gestor por otro
-
IntercambiarGestores(g1, g2): intercambia la posición de dos gestores en la lista
-
AgregarGestor(g): agrega un nuevo gestor en el primer hueco libre
-
InsertarGestor(g, pos): inserta un gestor en una posición, empujando los demás
-
AsignarGestor(g, pos): asigna un gestor a una posición de la lista (poco uso)
-
EliminarGestor(g, pos): elimina un gestor dados él o su posición en la lista
-
PrepararImpresion(): es llamada por los eventos antes de imprimir algo
-
ReiniciarImpresion(): reinicia el indicador de «se ha imprimido algo» (poco uso)
-
ActivarTick(t): activa el timer (opcionalmente, asignando el tick antes)
-
DesactivarTick(): desactiva el tick
-
ReactivarTick(): reactiva el tick (útil en algunos casos)
-
PausarTick(): detiene el tick temporalmente
-
ReanudarTick(): reanuda el tick detenido
-
PausarGestores(): no ejecuta los eventos, pero el tiempo sigue corriendo
-
ReanudarGestores(): reanuda la ejecución de los eventos
-
ActivarMutex(g): activa el mutex sobre un gestor
-
DesactivarMutex(): desactiva el mutex si lo hubiera
-
Reiniciar(): pone todas las propiedades a sus valores por defecto
Más ejemplos
Si deseas ver ejemplos más elaborados sobre una aventura real, puedes echar un vistazo al código fuente de A·L·I·E·N: La aventura - Edición Especial:
https://raw.github.com/ricpelo/alien/infsp6/alien.inf
Para cualquier consulta, no dudes en ponerte en contacto conmigo en ricpelo@gmail.com.