Iniciar, mover, cambiar y matar al jugadorEl modelo del mundoLuz y oscuridadEl paso del tiempo: daemons, relojes, turnos

El paso del tiempo: daemons, relojes, turnos

Daemons

En la filosofía medieval neo-platónica, los demonios eran intermediarios de Dios, planeando invisibles sobre el mundo e interfiriendo con él. Podían ser espíritus guardianes de lugares o personas. En Inform, un daemon es un espíritu intermediario, asociado con un objeto del juego, que tiene la oportunidad de interferir en cada turno (si está "activo"). El ejemplo clásico es el del enano de Aventura, que aparece en la cueva de vez en cuando: una rutina daemon asociada al enano es la encargada de hacerle aparecer, lanzar cuchillos, etc. Cada objeto del juego puede tener su propia rutina daemon que puede ser activada y desactivada llamando a las rutinas de la librería:

  ArrancarDaemon(objeto);
  PararDaemon(objeto);

Una vez activada, la rutina daemon del objeto es llamada (ejecutada) cada turno. A menudo las rutinas daemon son activadas por Inicializar al principio del juego y permanecen activas durante todo el juego. Algunos de estos daemons hacen algo en cada turno (como por ejemplo una linterna a pilas que se va quedando poco a poco sin carga), otros cuentan turnos o comprueban si se cumple una condición y empiezan a hacer algo cuando se ha alcanzado un cierto número de turnos o si la condición es cierta.

(!) De hecho un daemon no deja de funcionar aunque el jugador se haya movido a otra parte desde donde no ve ya al objeto. Un daemon no se detiene nunca a menos que lo paremos con la función PararDaemon. Esto es muy útil ya que los daemons pueden usarse para activar consecuencias de las acciones del jugador.

(?) EJERCICIO 34  Algunos juegos tienen "monstruos" que caminan al azar de un lugar a otro del juego. Utiliza un daemon para programar un "ladrón" que camine libremente por las habitaciones del juego.
(Solución) La solución es larga porque hay que comprobar qué salidas puede tomar y elegir una al azar. No es evidente cómo hacer esto para el principiante.

(!)
(?) EJERCICIO 35  A veces el daemon no tiene por qué estar asociado a ningún objeto "físico" de los que componen el juego. Puedes inventar un objeto "imaginario" que tenga asociado un daemon que se ocupe de comprobar si ocurren ciertas cosas en el juego. Por ejemplo, puedes hacer un objeto "ComprobadorDePeso" que tenga un daemon asociado para que, tras cada turno, se compruebe cuánto peso está llevando el jugador.

Escribe un daemon así de forma que si el jugador lleva demasiado peso, empiece a debilitarse, hasta el punto de que, si no deja algo, se le empezarán a caer las cosas. Además podemos asignar a cada objeto del juego un peso diferente, de forma que el daemon haga caerse el más pesado de los que el jugador lleva.

La solución es muy compleja, y seguramente no se te ocurrirá a la primera, pero es muy interesante para ver lo flexible que es la librería. No la pases por alto. (Solución)

Relojes

A un objeto también se le puede asociar un "reloj", aunque casi sería más correcto llamarlos "alarmas" (tradicionalmente se llamaban "fusibles"). Si un objeto tiene un reloj asociado, podremos arrancarlo mediante la función:

  ArrancarReloj(objeto, tiempo);  

Es como programar una alarma, que se activará cuando hayan transcurrido el número de turnos especificados en "tiempo". Cuando esto haya ocurrido, se llamará a la rutina tiempo_agotado de ese objeto (sólo se llamará entonces). Podemos desactivar el reloj, de forma que tiempo_agotado no será llamada nunca, mediante la función:

  PararReloj(objeto);  

Un objeto que tenga un reloj, debe proporcionar una propiedad llamada tiempo_restante, que la librería usará para almacenar cuánto tiempo le falta a la alarma para dispararse. Si el objeto no proporciona esta propiedad, se producirá un error en el momento de ejecutar el juego. El valor de esta propiedad puede ser cambiado desde cualquier rutina de cualquier otro objeto. Si le damos un valor de 0, esto indica la inmediata activación de la alarma (siempre que su "reloj" hubiera sido activado con ArrancarReloj).

(!) La librería pone un límite al número de relojes y daemons que pueden estar activados a la vez. El límite es de 32 (no hay límite al número de ellos inactivos). Este límite, no obstante, puede cambiarse fácilmente si tu juego así lo necesita. Basta para ello definir la constante MAX_RELOJES y darle un valor mayor, poniendo esta definición antes de incluir el fichero "EParser".

Cada turno

Y hay una tercera forma de eventos temporizados. Si una habitación proporciona la rutina cada_turno, entonces esta rutina será llamada al final de cada turno mientras el jugador esté en esa habitación; si un objeto proporciona esta rutina, será llamada mientras el objeto esté cerca del jugador. Por ejemplo, una radio puede emitir música mientras el jugador está cerca. Una espada puede resplandecer cuando hay enemigos cerca, etc.

cada_turno es especialmente útil para crear criaturas que permanecen fijas en una habitación y que sólo se activan cuando el jugador está cerca. Un ogro con paciencia limitada puede tener una rutina cada_turno que avise al jugador ("El ogro patea el suelo enfurecido") y a la vez tener un reloj que dispare una alarma cuando su paciencia se acabe.

(!) "Cerca" significa realmente "al alcance", una idea que será explicada con detalle más adelante, pero que de momento puedes tomar como "a la vista".

(!)(!) Pero eso significa que si una radio está metida en un recipiente opaco, dejaría de ejecutarse su rutina cada_turno (dejaría de oírse, si el sonido se creaba en esta rutina). La única forma de evitar este comportamiento es cambiar las reglas que definen qué está y qué no está "al alcance". Para ello deberías programar una rutina llamada AlAlcance. Si esta rutina existe, será llamada por el parser en varias ocasiones para saber si un objeto está o no "al alcance". En esta rutina deberás consultar la variable razon_alcance para saber si el parser se halla en la fase de parsing o en la fase cada_turno (en este caso el valor de razon_alcance sería RAZON_CADA_TURNO. Modificar el alcance puede dar lugar a efectos muy interesantes, por ejemplo puedes poner la radio al alcance en las habitaciones vecinas, para que siga oyéndose desde ellas, o puedes hacer que el ladrón que camina aleatoriamente por el laberinto pueda ser oído por el jugador cuando está cerca (como en Zork).

(?) EJERCICIO 36  Hagamos que en el juego Ruinas la oscuridad sea mucho más amenazante. Mientras el jugador esté a oscuras debe escuchar "pisadas sigilosas" y tras cuatro turnos consecutivos a oscuras, finalmente será asesinado por los seres de la oscuridad.
(Solución)

(!)
(?) EJERCICIO 37  Más difícil: programa las pisadas sigilosas como si fueran un objeto, y dentro de él todo el código necesario para que ocurra lo que dice el ejercicio anterior. No debe haber código relacionado con estas pisadas sigilosas en ninguna otra parte del programa, ni siquiera en la rutina Inicializar, sólo en el objeto "pisadas" (por tanto no vale usar un daemon ya que éste habría que activarlo en Inicializar).
Solución

¿Qué hora es?

La librería tiene unas variables para almacenar la hora que es en el juego (no tiene nada qué ver con la hora que es en la vida real, de hecho Inform no tiene posibilidad de acceder al reloj del ordenador para saber la hora real). La hora del juego se almacena en la variable llamada la_hora, que contiene el número de minutos transcurridos desde medianoche (los segundos no se cuentan). Por tanto puede tomar valores entre 0 (es medianoche) y 1439 (falta un minuto para medianoche). Podemos poner una hora concreta usando:

  PonerLaHora(60*horas+minutos, velocidad);

El parámetro velocidad controla si la librería debe cambiar esta hora cada turno de juego. Si velocidad es 0, la librería no cambiará esta hora nunca (a menos que el juego llame de nuevo a la función PonerLaHora). Si velocidad es positivo (por ejemplo +3) significa que en cada turno de juego transcurren velocidad minutos (3 minutos en cada turno, en el ejemplo). Si es negativo (por ejemplo -4) significa que transcurre 1 minuto cada velocidad turnos (1 minuto cada 4 turnos, en el ejemplo).

Lo habitual es llamar a PonerLaHora una sola vez en la rutina Inicializar. Puedes hacer que la hora sea visible en la barra de título (barra de estado) del juego si pones en algún lugar del programa la directiva:

  StatusLine time;  

(?) EJERCICIO 38  ¿Cómo harías para que el juego detectase el paso por medianoche, y así poder llevar la cuenta también del día en que estás?
(Solución)

(!)
(?) EJERCICIO 39  Haz que la luz aparezca y desaparezca durante el juego, en función de la hora (a la puesta del sol, la luz desaparece, al amanecer aparece de nuevo).
(Solución)

(!) Para el que necesite conocer los detalles, lo que ocurre realmente tras cada turno es lo siguiente, y en este orden:
  1. El contador de turnos se incrementa (variable turnos)
  2. La variable la_hora es actualizada (si es necesario, según la velocidad del reloj que haya elegido el programador).
  3. Se ejecutan todos los daemons y todos los relojes que estén activados en todos los objetos del juego (pero no se garantiza ningún orden concreto de activación entre ellos).
  4. Se ejecuta la rutina cada_turno de la localizacion donde se halla el jugador.
  5. Se ejecuta la rutina cada_turno de todos los objetos al alcance del jugador (sin garantizar ningún orden concreto).
  6. Se llama a la rutina global PasaElTiempo (si es que existe, lo normal es que no exista, pero el programador puede poner una).
  7. Se comprueba de nuevo si hay luz para el jugador (puesto que pueden haber cambiado muchas cosas desde el turno anterior).
La secuencia puede ser interrumpida si el jugador gana o muere como consecuencia de la ejecución de uno cualquiera de estos puntos.

(!)
(?) EJERCICIO 40  Imagina que el jugador está flotando mágicamente en el aire. Cualquier cosa que suelte mientras está así, debe desaparecer (se supone que cae al lejano suelo). La forma más normal de programar esto sería usar un daemon que tras cada turno mire lo que hay en el suelo y lo haga desaparecer (es mejor hacerlo así en vez de capturar la acción Dejar, porque podría haber varias formas diferentes de hacer que un objeto termine en el suelo). De todas formas ¿por qué sería mejor aún usar cada_turno en vez de un daemon? (Pista: tiene que ver con el orden en que se ejecutan las cosas)
(Solución)

(?) EJERCICIO 41  ¿Cómo harías un juego en el que, entre turno y turno, pueden transcurrir desde minutos hasta días?
(Solución)


Zak McKraken - spinf@geocities.com

Iniciar, mover, cambiar y matar al jugadorEl modelo del mundoLuz y oscuridadEl paso del tiempo: daemons, relojes, turnos