En la sección anterior vimos distintas manipulaciones básicas que se pueden llevar a cabo con entidades, como quitarlas, ponerlas o cambiarlas de sitio. Pero a veces, puede interesarnos tener entidades que puedan estar en diferentes estados dependiendo de lo que hagamos con ellas o del momento del juego en que estemos (por ejemplo, un televisor puede estar encendido o apagado, un cuchillo puede estar afilado o romo). También puede ser interesante guardar valores relacionados con alguna entidad (como un número que mida la cantidad de batería de un teléfono móvil que se puede utilizar durante un tiempo limitado); o incluso a veces valores relacionados con dos entidades (como un valor de “simpatía” que mida cómo de simpático le cae Fulanito a Menganito). Esta funcionalidad se puede conseguir en AGE mediante las propiedades y relaciones.
Las propiedades nos permiten asociar un valor a una entidad. Este valor se almacena asociado a la entidad, y podemos consultarlo y modificarlo en cualquier momento de la partida. El valor tiene un nombre que lo identifica, que es una cadena (String). De esta manera, una misma entidad puede tener distintas propiedades, cada una de las cuales está identificada por un nombre diferente, y tiene un valor independiente del de las demás.
Para fijar el valor de una propiedad, podemos utilizar la siguiente función:
void set ( Entity ent , String name , <tipo básico o String> value )
que hace que la propiedad de nombre name
de la entidad ent
pase a valer value
.
Para obtener el valor de una propiedad, podemos utilizar la siguiente función:
<tipo básico o String> get ( Entity ent , String name )
que nos devuelve el valor de la propiedad de nombre name
de la entidad ent
.
De este modo, podemos hacer cosas como éstas:
Entity televisor = item("televisor"); set ( televisor , "encendido" , true ); boolean b = get ( televisor , "encendido" ); //devuelve true set ( televisor , "precioEnEuros" , 1000 ); set ( televisor , "marca" , "Telefunken" ); set ( televisor , "pulgadas" , 24 ); int a = get ( televisor , "pulgadas" ); //devuelve 24
Nótese que a una propiedad le podemos asignar valores de cualquier tipo básico o bien valores de tipo cadena (String); pero no le podemos asignar otros objetos. Sin embargo, es útil saber que si queremos asociar a una propiedad una entidad (Entity
), podemos en su lugar guardar el nombre único de ese objeto Entity
mediante el método set
, y luego recuperar la entidad:
Entity televisor = item("televisor"); set ( televisor , "propietario" , "Manolito" ); Mobile elPropietario = mobile ( get ( televisor , "propietario" ) ); //devuelve true
Con lo cual a efectos prácticos es como si el valor de la propiedad fuese una entidad (aunque para relacionar entre sí dos entidades, como en “el propietario del televisor es Manolo”, puede ser más adecuado usar la funcionalidad de relaciones, que veremos más tarde).
Las propiedades son útiles para tener en nuestras aventuras entidades que puedan estar en distintos estados y que reaccionen de manera diferente según el estado en que esté. Un ejemplo puede ser un televisor en el que pongamos un código como éste:
void parseCommand ( Mobile aCreature , String verb , String args ) { if ( get ( self , "encendido" ) ) { if ( equals ( verb , "mirar" ) ) { aCreature.write("Están echando un aburrido documental sobre bacterias.\n"); end(); } if ( equals ( verb , "encender" ) ) { aCreature.write("¡El televisor ya está encendido!\n"); end(); } if ( equals ( verb , "apagar" ) ) { aCreature.write("Apagas el televisor.\n"); set ( self , "encendido" , false ); end(); } } else { if ( equals ( verb , "mirar" ) ) { aCreature.write("El televisor está apagado.\n"); end(); } else if ( equals ( verb , "encender" ) ) { aCreature.write("Pulsando el botón, enciendes el televisor.\n"); set ( self , "encendido" , true ); end(); } else if ( equals ( verb , "apagar" ) ) { aCreature.write("¡El televisor ya está apagado!\n"); end(); } } }
Con esto, implementamos un televisor que se puede encender y apagar, y que si está encendido, al mirarlo muestra un documental sobre bacterias. Añadiendo más propiedades podríamos hacerlo más complejo: por ejemplo, podríamos tener una propiedad “canal” a la que asignáramos un valor de tipo int
, de forma que el televisor nos mostrara programas distintos al cambiar de canal.
NOTA IMPORTANTE: Este código no funciona por sí solo, porque para que funcione es necesario darle un valor inicial a la propiedad “encendido” del televisor, es decir, establecer si al principio de la aventura el televisor va a estar encendido o apagado. Esto es muy sencillo de hacer, para ver cómo, sigue leyendo hasta la subsección de inicialización de propiedades un poco más abajo.
En el ejemplo anterior, utilizamos una propiedad para poner una entidad en uno u otro estado según lo que hiciese con ella el jugador. Otra posibilidad es utilizar las propiedades para poner entidades en un estado durante un determinado tiempo, de forma que el estado pueda cambiar al terminar ese tiempo. Por ejemplo, nos puede interesar tener un teléfono móvil que podamos encender pero que sólo aguante encendida hasta que se le acaben las pilas. Para hacer este tipo de cosas, primero debemos hacer un pequeño receso para explicar cómo funciona la temporización en AGE.
El sistema de tiempo de AGE no se basa en turnos, sino en el concepto de unidades de tiempo. Una unidad de tiempo es la cantidad más pequeña de tiempo que se puede manejar en un juego de AGE. Una acción, como coger una cosa o moverse de una localidad a otra, puede consumir una unidad de tiempo o puede llevar más. Por ejemplo, las acciones de coger y dejar objetos consumen una unidad de tiempo, al igual que las de mirar o consultar el inventario. Por otra parte, las acciones de moverse a una localidad contigua consumen un número de unidades de tiempo que depende de la “longitud del camino” (que se fija en el PUCK); y las acciones de combate consumen una cantidad de unidades de tiempo que dependerán de las características del arma que usemos, nuestra pericia con ellas y otros factores relacionados.
Cualquier mundo de AGE tiene dos modos de juego, que puede seleccionar el jugador aunque también se pueden cambiar desde BeanShell: el modo síncrono (“turnos” aparentes) y el modo de tiempo real. En el modo síncrono, cada vez que el jugador teclea una orden se simulan del tirón todas las unidades de tiempo que correspondan hasta la siguiente orden. Esto puede dar la impresión de que se juega “por turnos”; pero no es exactamente así: por ejemplo, si un jugador se mueve de una habitación a otra y esto le consume diez unidades de tiempo, tal vez en esas diez unidades de tiempo un goblin que está en otra habitación pueda estar cogiendo y dejando un objeto cinco veces. En el modo tiempo real, por otra parte, las unidades de tiempo del juego se traducen en unidades de tiempo de la vida real: es decir, se fija cuánto dura una unidad de tiempo (por ejemplo, cincuenta milisegundos) y cada cincuenta milisegundos transcurre una. Esto quiere decir que si el jugador teclea una orden que consume diez unidades de tiempo, AGE tardaría medio segundo en responder a su orden y permitirle teclear otra. Igual que en el caso anterior, el goblin podría mientras tanto coger y dejar un objeto cinco veces: lo que puede suceder en el mundo del juego no varía entre un modo u otro, sólo cambia cómo lo ve el jugador.
Dicho esto, es interesante saber que cuando fijamos el valor de una propiedad, podemos ponerle asimismo un contador de tiempo que indica el número de unidades de tiempo que tardará en actualizarse esa propiedad. A partir de ese momento, el contador de tiempo irá decrementándose en una unidad cada vez que pase una unidad de tiempo, hasta que al llegar a cero la propiedad se actualizará. “Actualizarse” consiste en llamar a un método update
que definimos nosotros, y donde podemos programar una actualización de la propiedad o cualquier otra cosa que nos venga bien que suceda en ese tiempo: las propiedades con contador de tiempo no sólo van bien para poner en los objetos estados que duren una determinada cantidad de tiempo, sino también como herramienta para temporizar en general (podemos utilizar una propiedad con contador de tiempo como un “reloj” para lanzar eventos que deban suceder en un momento dado).
Para fijar el valor del temporizador de una propiedad, utilizamos la siguiente función setTime
:
void setTime ( Entity ent , String name , long time )
que sirve para cambiar el valor del temporizador de la propiedad name
de la entidad ent
, fijándolo al valor time
. El tipo de dato long
que tiene el parámetro time
viene a ser lo mismo que int
, sólo que admite números más grandes. En la práctica podemos tratarlo como si fuese un int
. Así, si quisiéramos fijar el valor de una propiedad junto con su temporizador, podríamos hacer algo como:
//encender la antorcha y poner el temporizador correspondiente a cien unidades de tiempo: set ( item("antorcha"), "encendida", true ); setTime ( item("antorcha"), "encendida", 100 );
Para obtener el temporizador de una propiedad, podemos utilizar la siguiente función:
long getTime ( Entity ent , String name )
Que nos devuelve el temporizador asociado a la propiedad name
de la entidad ent
.
Los temporizadores de las propiedades no son útiles si no se define además el método de actualización que, como acabamos de explicar, se ejecutará cuando el temporizador de cada propiedad llegue a cero. Para definir este método en PUCK, vamos al campo de código del formulario correspondiente a la entidad donde hemos definido la propiedad, y en el menú contextual seleccionamos: Insertar código → Redefinir métodos de (entidad) → Método de actualización de (la entidad). Se nos generará una plantilla como ésta:
/*Método de actualización de esta entidad*/ //pe: propiedad que se actualiza //(pe.getName(): nombre) //w: el mundo void update ( PropertyEntry pe , World w ) { }
El método update
se llamará cada vez que el contador de una propiedad cualquiera de la entidad en la que estamos (self
) llegue a cero. Para saber cuál es exactamente la propiedad cuyo temporizador ha llegado a cero, podemos utilizar pe.getName()
: el primer parámetro del método, de tipo PropertyEntry
, contiene toda la información sobre esa propiedad que se actualiza (pe.getName()
nos da el nombre, y pe.getValueAsWrapper()
el valor; aunque esto último no lo necesitamos porque simplemente podemos obtener el valor con un get
). El parámetro World w
es redundante, nos devuelve el mundo que siempre podemos acceder mediante world
así que no sirve para nada, es un parámetro que se mantiene por compatibilidad con versiones beta anteriores de AGE y podemos simplemente hacer como si no existiera.
De esta forma, podemos programar un radiador con termostato que se encienda y se apague cada diez unidades de tiempo:
void update ( PropertyEntry pe , World w ) { if ( equals ( pe.getName() , "encendido" ) ) //mirar si la propiedad cuyo temporizador llegó a 0 es "encendido" { if ( get ( self , "encendido" ) ) { set ( self , "encendido" , false ); setTime ( self , "encendido" , 10 ); //apagamos y se vuelve a actualizar en 10 UT's if ( mobile("jugador").getRoom().hasItem(self) ) //si el jugador está en la habitación del radiador, le decimos que se ha apagado { //hay formas mejores de hacer esto, véase nota abajo mobile("jugador").write("El radiador se apaga solo por el efecto del termostato.\n"); } } else { set ( self , "encendido" , true ); setTime ( self , "encendido" , 10 ); //encendemos y se vuelve a actualizar en 10 UT's if ( mobile("jugador").getRoom().hasItem(self) ) //si el jugador está en la habitación del radiador, le decimos que se ha encendido { //hay formas mejores de hacer esto, véase nota abajo mobile("jugador").write("El radiador se enciende solo por el efecto del termostato.\n"); } } } }
Si ponemos este código en una entidad radiador, cada diez unidades de tiempo cambiará de estado, de encendido a apagado y viceversa. Además, si el jugador está en la habitación del radiador, se le mostrará un mensaje informándole de que el radiador se ha encendido o apagado.
Hay dos notas que hacer a este ejemplo. La primera es que, igual que el ejemplo anterior, es necesario inicializar la propiedad (dándole un valor al principio de la aventura) para que funcione (enseguida veremos cómo se hace).
La segunda puntualización es que, debido a que todavía no conocemos a fondo todo lo que se puede hacer con el AGE, la forma de notificar al jugador en este ejemplo es bastante chapucera: sirve para aventuras para un solo jugador (donde hemos puesto a la entidad del jugador el nombre único “jugador”), pero, ¿qué pasa en aventuras multijugador?
Para hacer estas cosas de forma más genérica y que funcionen bien (por ejemplo) en el caso multijugador, existen métodos para que se muestre un mensaje a todos los jugadores que están en una habitación, o incluso para que una entidad (como el radiador) emita un mensaje que llegue a todos los jugadores de las habitaciones en donde esté. Pero esto lo veremos más adelante. De momento, conformémonos con saber que, aunque ésta no es la forma más general de notificar que ha ocurrido algo en una habitación, al menos en el caso de aventuras monojugador nos servirá.
Es útil saber que, si no queremos que una propiedad llame nunca a su método update, podemos conseguirlo poniendo su temporizador al valor -1. El valor -1 significa “infinito”, es decir, la propiedad tardará infinito en actualizarse (no se actualizará nunca). Nótese también que un temporizador debería ponerse siempre a un valor positivo o bien a -1 (infinito), nunca a cero. Poner un temporizador a cero no garantiza que el correspondiente método update vaya a ejecutarse inmediatamente; sino que su comportamiento está indefinido, así que debe evitarse. Para ejecutar algún código inmediatamente, es mejor simplemente llamar a ese código en lugar de usar temporizadores.
En el ejemplo de esta sección, hemos visto un posible uso de las propiedades con temporizador: tener un objeto que cambie cíclicamente de estado cada cierto tiempo. Pero existen otros muchos usos, como por ejemplo:
Normalmente nos interesará que una determinada propiedad tenga un valor dado ya desde el principio de la aventura. Por ejemplo, en el caso del televisor anterior, querremos fijar en qué estado está al principio de la partida, cuando el jugador lo encuentre (por ejemplo, apagado).
Además, si hacemos un get
sobre una propiedad sin antes haber fijado ningún valor para ella, el valor que se nos devolverá será un valor nulo (null
). Este valor nulo nos dará un error si intentamos asignárselo a un tipo básico: es decir, por ejemplo,
boolean b = get ( item("televisor") , "encendido" )
nos dará un error si antes no hemos fijado el valor de “encendido”, porque no podemos asignar a una variable boolean el valor null
.
Por lo tanto, en general suele ser altamente recomendable dar un valor inicial a las propiedades que vayamos a utilizar en un mundo dado. Esto se puede hacer de dos maneras: o bien desde Puck, o bien mediante código BeanShell.
Para inicializar las propiedades de una entidad desde PUCK, basta con ir al panel de formularios de esa entidad y seleccionar la pestaña “Código y propiedades”. En la parte inferior, debajo del área de código, hay un formulario que dice “Propiedades”. Para dar un valor inicial a una propiedad de la entidad, tecleamos el nombre de la propiedad (sin comillas) en el campo “nombre”, su valor inicial en el campo “valor”, y el valor del temporizador en “tiempo restante”. Si no queremos usar el temporizador (es decir, si queremos que la propiedad no se actualice nunca), usamos -1 como valor del temporizador. Tras teclear en los tres campos, le damos al botón “Añadir” y veremos en la lista cómo el valor inicial de nuestra propiedad queda guardado.
Si nos hemos equivocado al introducir algún valor inicial de propiedades o queremos cambiarlo, podemos seleccionar dicha propiedad en la lista, editar los campos “nombre”, “valor” y “temporizador” en el formulario, y darle al botón “cambiar” para guardar los cambios. El botón “borrar” nos permite borrar una fila de la lista, es decir, borrar el valor inicial de la propiedad que seleccionemos.
Los valores de las propiedades que especificamos aquí serán los que tomen dichas propiedades al principio de la aventura, que luego podrán cambiar durante las partidas. Así, por ejemplo, podemos rellenar los campos poniendo el nombre “encendido”, el valor “false” y el temporizador “-1” para que funcione el ejemplo del televisor que veíamos con anterioridad, y el televisor comience apagado. Poniendo el valor “true”, comenzaría encendido.
Si por cualquier motivo preferimos inicializar las propiedades de una entidad usando código BeanShell en lugar del formulario anterior, también podemos hacerlo, redefiniendo el evento que se ejecuta al inicializarse la entidad. Los eventos son métodos BeanShell que nos permiten actuar cuando ocurre algún hecho determinado en el mundo. Concretamente, el evento de inicialización de una entidad (llamado onInit
) nos permite actuar justo cuando se acaba de inicializar esa entidad.
Para redefinirlo, vamos al menú contextual del campo de código de la entidad y seleccionamos: Insertar código → Definir eventos de (entidad) → Al inicializarse la (entidad). Se nos generará una sencilla plantilla como ésta:
//código a ejecutar cuando se inicializa la cosa void onInit() { }
Y en este método onInit() podemos poner código para dar valores iniciales a las propiedades:
void onInit() { set ( self , "encendido" , true ); set ( self , "canal" , 4 ); }
Aparte de incluir cualquier otro código que queramos que se ejecute cuando esa entidad se inicializa.
De la misma forma que las propiedades nos permiten asociar un valor a una entidad, las relaciones sirven para asociar un valor a un par de entidades. Esto suele ser útil para, como su nombre indica, expresar relaciones entre dos objetos.
Algunos ejemplos en los que se pueden utilizar relaciones son los siguientes:
true
si Fulanito conoce a Menganito, y false
de lo contrario.int
de 0 a 10 según ese grado de atracción.true
para esos objetos.Es importante saber que las relaciones siempre son unidireccionales, es decir, no es lo mismo una relación entre A y B que una relación entre B y A. Si queremos expresar que a Juan le atrae María pero además a María también le atrae Juan, necesitaremos dos relaciones, una en cada sentido.
Para fijar el valor de una relación, podemos usar la siguiente función:
void set ( Entity e1 , String relName , Entity e2 , <tipo básico o String> value )
que hace que la relación relName
de la entidad e1
a la entidad e2
pase a valer value
.
Podemos fijar también el contador de tiempo de la relación, tal y como hacíamos para las propiedades, de esta manera:
void setTime ( Entity e1 , String relName , Entity e2 , long time )
que hace que el temporizador de la relación relName
de la entidad e1
a la entidad e2
pase a valer time
.
NOTA: Aunque el temporizador de las relaciones va bajando hasta llegar a cero como el de las propiedades, y por lo tanto se podría usar para medir tiempos; por el momento no existe un método update
que se pueda redefinir para las relaciones como lo había en las entidades. En posteriores versiones de AGE seguramente se añadirá este método.
Para obtener el valor de una relación, podemos utilizar la función siguiente:
<tipo básico o String> get ( Entity e1 , String relName , Entity e2 )
que devuelve el valor de la relación relName
de la entidad e1
a la entidad e2
. Nótese que, igual que en el caso de las propiedades, si la relación no está inicializada (nunca le hemos dado un valor) este método devolverá el valor especial null
, que puede dar problemas. Por lo tanto, se recomienda inicializar todas las relaciones de las que vayamos a hacer un get
, cosa que se puede hacer por ejemplo en el evento onInit()
de alguna de las entidades relacionadas.
Así, podemos hacer cosas como éstas:
set ( mobile("troll") , "gusta" , item("manzana") , false ); //al troll no le gusta la manzana set ( mobile("troll") , "gusta" , item("plátano") , true ); //al troll le gusta el plátano get ( mobile("troll") , "gusta" , item("manzana") ); //devuelve false (al troll no le gusta la manzana). get ( mobile("troll") , "gusta" , item("pera") ); //esto devuelve null (no false).
Al igual que las propiedades, las relaciones también se pueden inicializar directamente usando PUCK. Para ello, creamos una flecha entre las dos entidades que queramos relacionar. Seleccionando la flecha en el mapa de PUCK, nos aparecerá un panel asociado a la flecha. En su ficha “Otras relaciones”, nos aparecerá una lista de “Relaciones personalizadas” que funciona de la misma manera que la lista de propiedades de los objetos: podemos añadir relaciones aportando su nombre, valor y temporizador.
Nótese que crear una flecha entre determinados tipos de objetos en PUCK crea por defecto lo que se llama una relación estructural, que es una relación especial que usa AGE para determinar dónde están los objetos y no es lo mismo que las relaciones personalizadas que aquí estamos creando: por ejemplo, si los objetos son dos habitaciones se crea por defecto un camino, si son una habitación y una cosa se crea una relación “contiene” que significa que la cosa está dentro de la habitación. Si lo único que queremos es crear relaciones personalizadas, nos interesará desactivar estas relaciones estructurales: esto se hace desmarcando el botón “hay camino”, entre habitaciones, o poniendo el campo “Relación estructural” a “Ninguna” en la ficha “Relación estructural” del panel de la flecha, en el resto de los casos. Si queremos tener tanto una relación estructural como personalizada a la vez entre dos objetos, no necesitaremos desactivar la estructural de esta manera.
Dos métodos muy útiles cuando trabajamos con relaciones son los siguientes:
/*class Entity*/ List getRelatedEntities ( String relName ) /*class Entity*/ List getRelatedEntitiesByValue ( String propertyName , int/boolean boolVal )
Ambos son métodos de la clase Entity
, con lo cual se pueden ejecutar sobre objetos de las clases Room
, Item
, Mobile
, etc (que son subclases de Entity
). El primero nos devuelve una lista con todas las entidades que están relacionadas con aquélla con la que se invoca, independientemente del valor que tenga la relación. El segundo nos devuelve una lista con todas las entidades relacionadas con aquélla con la que se invoca, y donde además la relación tiene el valor dado. Es decir, por ejemplo:
mobile("troll").getRelatedEntities("gusta")
nos devuelve todas las cosas de la aventura para las cuales hemos especificado si le gustan al troll o no (es decir, tales que hemos fijado la relación “gusta” del troll hacia esas cosas, sea a true o a false). Nótese que el método va en una dirección, es decir, no nos devolvería cosas que estén relacionadas en sentido inverso (de la cosa al troll).
mobile("troll").getRelatedEntities("gusta",true)
nos devuelve todas las cosas de la aventura para las cuales hemos especificado que le gustan al troll (es decir, hemos fijado la relación “gusta” del troll hacia esas cosas, y concretamente la hemos puesto a true).
Estos métodos nos permiten extraer todo el potencial de las relaciones, al poder consultar en todo momento qué objetos hay relacionados con uno dado, y sin temer encontrar valores nulos. Eso sí, lo que devuelve el método es un objeto de la clase List
, que no hemos visto todavía cómo podemos manejar. Lo veremos en la sección sobre listas.