documentacion age

171
Aetheria Game Engine Documentación de AGE Cómo crear aventuras con Aetheria Game Engine Editor: Notxor Coordinador: Al-Khwarizmi Varios Autores versión de 15 de octubre de 2012

Upload: notxor

Post on 04-Jul-2015

270 views

Category:

Education


7 download

DESCRIPTION

Documentación de Aetheria Game Engine. Una herramienta para crear aventuras conversacionales.

TRANSCRIPT

Page 1: Documentacion age

Aetheria Game Engine

Documentación de AGE

Cómo crear aventuras con AetheriaGame Engine

Editor:Notxor

Coordinador:Al-Khwarizmi

Varios Autores

versión de 15 de octubre de 2012

Page 2: Documentacion age

Presentación de ladocumentación

Estimado lector:Esta documentación pretende servir de guía y referencia para crear aventuras

y otros juegos basados en texto para Aetheria Game Engine.Los juegos para AGE se crean mediante su herramienta de desarrollo, lla-

mada PUCK (Playable Universe Construction Kit). Esta herramienta permitecrear los elementos del mundo (habitaciones, cosas, criaturas...) y sus interac-ciones básicas mediante un entorno gráfico, sin necesidad de programar. Paraconseguir comportamientos más complejos, dinámicos y personalizados, este en-torno gráfico se complementa con un lenguaje de programación (similar a Java)llamado BeanShell.

Pero no te asustes: para aprender a crear juegos para AGE con este docu-mento no es necesario tener ningún conocimiento previo de programación. Todoslos conceptos necesarios se explican desde cero, y crear juegos basados en textocon PUCK es más sencillo que crear otro tipo de programas. Por lo tanto, aun-que no sepas nada de programación, con esta guía estarás rápidamente creandotus propios mundos interactivos; y si ya sabes algo, podrás saltarte varias de lassecciones introductorias.

Esta versión en PDF de la documentación se ha generado a partir de la wiki(que está en http://www.caad.es/aetheria/doc/doku.php), gracias al traba-jo de Notxor, que ha maquetado el PDF. La documentación está en constanteproceso de ampliación y mejora, y las actualizaciones irán apareciendo primeroen la wiki para pasar después a sucesivas versiones de este fichero.

No dudes dirigirme cualquier sugerencia o comentario, tanto como para me-jorar el contenido de la documentación como para mejorar el propio sistema, alemail [email protected].

Atentamente,

Al-KhwarizmiCreador de AGE

Page 3: Documentacion age

Índice general

1. Uso básico de puck 81.1. Crear mundos con PUCK . . . . . . . . . . . . . . . . . . . . . . 81.2. Creación de Mundos para AGE con PUCK: Resumen . . . . . . 81.3. Primeros Pasos: Creando Habitaciones y Caminos . . . . . . . . . 10

1.3.1. Habitaciones y Caminos . . . . . . . . . . . . . . . . . . . 101.3.2. Creando un Personaje Jugador . . . . . . . . . . . . . . . 14

1.4. Seres inertes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161.4.1. Descripciones de componentes . . . . . . . . . . . . . . . . 161.4.2. Cosas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17

2. Uso del lenguaje BeanShell 242.1. Primeros pasos con BeanShell . . . . . . . . . . . . . . . . . . . . 24

2.1.1. Los formularios de código . . . . . . . . . . . . . . . . . . 252.1.2. Los métodos . . . . . . . . . . . . . . . . . . . . . . . . . 252.1.3. Variables y entrada/salida sencilla . . . . . . . . . . . . . 282.1.4. La estructura condicional (if) . . . . . . . . . . . . . . . . 332.1.5. Los bucles . . . . . . . . . . . . . . . . . . . . . . . . . . . 362.1.6. Recapitulación . . . . . . . . . . . . . . . . . . . . . . . . 40

2.2. Manipulación básica de entidades . . . . . . . . . . . . . . . . . . 412.2.1. Método de análisis de la entrada referida a una entidad . 412.2.2. Métodos para quitar, poner y mover entidades . . . . . . 432.2.3. Las variables self y world . . . . . . . . . . . . . . . . . . . 452.2.4. Métodos para comprobar dónde están las entidades . . . . 462.2.5. Método de análisis de la entrada referida a dos entidades 482.2.6. Variantes del método referido a dos entidades . . . . . . . 52

2.3. propiedades y relaciones . . . . . . . . . . . . . . . . . . . . . . . 552.3.1. Propiedades . . . . . . . . . . . . . . . . . . . . . . . . . . 552.3.2. Temporización y método update . . . . . . . . . . . . . . 562.3.3. Inicialización de propiedades . . . . . . . . . . . . . . . . 602.3.4. Relaciones . . . . . . . . . . . . . . . . . . . . . . . . . . . 61

2.4. Manejo de arrays y listas . . . . . . . . . . . . . . . . . . . . . . 642.4.1. Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 642.4.2. Listas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65

2.5. Errores comunes con BeanShell . . . . . . . . . . . . . . . . . . . 722.5.1. Mensajes de error . . . . . . . . . . . . . . . . . . . . . . 722.5.2. Tipos de error comunes . . . . . . . . . . . . . . . . . . . 762.5.3. Funcionalidad de depuración . . . . . . . . . . . . . . . . 77

Page 4: Documentacion age

4 Índice general

3. Aspectos avanzados del modelo de mundo 803.1. Descripciones y nombres dinámicos . . . . . . . . . . . . . . . . . 80

3.1.1. Descripciones dinámicas . . . . . . . . . . . . . . . . . . . 813.1.2. Nombres dinámicos . . . . . . . . . . . . . . . . . . . . . . 82

3.2. Cosas abribles y cerrables . . . . . . . . . . . . . . . . . . . . . . 833.2.1. Definiendo cosas abribles y cerrables . . . . . . . . . . . . 833.2.2. Llaves . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 853.2.3. Puertas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 863.2.4. Contenedores abribles y cerrables . . . . . . . . . . . . . . 87

3.3. Contenedores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 873.3.1. Definición y uso de contenedores . . . . . . . . . . . . . . 873.3.2. Acciones sobre objetos contenidos . . . . . . . . . . . . . 89

3.4. Miembros y prendas . . . . . . . . . . . . . . . . . . . . . . . . . 923.4.1. Miembros . . . . . . . . . . . . . . . . . . . . . . . . . . . 923.4.2. Prendas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93

3.5. Estados de las criaturas . . . . . . . . . . . . . . . . . . . . . . . 953.5.1. Las propiedades «state» y «target» . . . . . . . . . . . . . 963.5.2. Cambios de estado . . . . . . . . . . . . . . . . . . . . . . 983.5.3. Programación con estados . . . . . . . . . . . . . . . . . . 99

3.6. Combate y armas . . . . . . . . . . . . . . . . . . . . . . . . . . . 993.6.1. Elementos de rol básicos . . . . . . . . . . . . . . . . . . . 1003.6.2. Mecánica de combate . . . . . . . . . . . . . . . . . . . . 1033.6.3. Armas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1053.6.4. Matemática de las armas . . . . . . . . . . . . . . . . . . 1093.6.5. Esquivadas . . . . . . . . . . . . . . . . . . . . . . . . . . 1123.6.6. Armaduras . . . . . . . . . . . . . . . . . . . . . . . . . . 1133.6.7. Entrando en combate . . . . . . . . . . . . . . . . . . . . 113

3.7. Entidades abstractas . . . . . . . . . . . . . . . . . . . . . . . . . 1143.8. Hechizos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116

3.8.1. Creación de hechizos en PUCK . . . . . . . . . . . . . . . 1163.8.2. Uso de hechizos por los jugadores . . . . . . . . . . . . . . 1183.8.3. Funcionamiento de los hechizos . . . . . . . . . . . . . . . 1203.8.4. Gestión de los puntos mágicos . . . . . . . . . . . . . . . . 1263.8.5. Uso de hechizos por los personajes no jugadores . . . . . . 127

3.9. Mensajes por defecto . . . . . . . . . . . . . . . . . . . . . . . . . 1283.9.1. Cambiar los mensajes por defecto . . . . . . . . . . . . . . 1283.9.2. Generar dinámicamente los mensajes por defecto . . . . . 130

4. Métodos redefinibles 1314.1. Eventos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1314.2. Otros métodos redefinibles . . . . . . . . . . . . . . . . . . . . . . 131

4.2.1. Métodos de Mobile (para PSIs) . . . . . . . . . . . . . . . 1314.2.2. Ejemplo de uso . . . . . . . . . . . . . . . . . . . . . . . . 132

5. El análisis de la entrada 1335.1. Métodos de análisis de la entrada (parseCommand) . . . . . . . . 134

5.1.1. Los métodos de análisis de la entrada . . . . . . . . . . . 1345.1.2. Ejemplo de uso . . . . . . . . . . . . . . . . . . . . . . . . 1345.1.3. Tipos de métodos de análisis de la entrada . . . . . . . . 1355.1.4. El proceso de análisis de AGE . . . . . . . . . . . . . . . 137

Page 5: Documentacion age

Índice general 5

5.2. Preprocesado de la entrada . . . . . . . . . . . . . . . . . . . . . 1395.3. Gestión de verbos . . . . . . . . . . . . . . . . . . . . . . . . . . . 140

5.3.1. Visualización de la lista de verbos por defecto . . . . . . . 1415.3.2. Añadir y quitar verbos . . . . . . . . . . . . . . . . . . . . 1425.3.3. Verbos adivinables y no adivinables . . . . . . . . . . . . 142

6. Presentación del mundo 1456.1. Estilos de texto . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1466.2. Prompt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146

6.2.1. Métodos de manipulación del prompt . . . . . . . . . . . 1466.2.2. Ejemplos de código . . . . . . . . . . . . . . . . . . . . . . 146

6.3. Tipografías en AGE . . . . . . . . . . . . . . . . . . . . . . . . . 1476.3.1. Control básico de la tipografía . . . . . . . . . . . . . . . 1476.3.2. Control avanzado de la tipografía . . . . . . . . . . . . . . 148

6.4. Métodos gráficos . . . . . . . . . . . . . . . . . . . . . . . . . . . 1496.4.1. Imágenes . . . . . . . . . . . . . . . . . . . . . . . . . . . 1496.4.2. Frames . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152

6.5. sonido . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1536.5.1. Audio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1536.5.2. Música MIDI . . . . . . . . . . . . . . . . . . . . . . . . . 156

6.6. Otros aspectos de la presentación . . . . . . . . . . . . . . . . . . 158

7. Referencia de métodos invocables 1597.1. Manipulación del modelo de mundo . . . . . . . . . . . . . . . . . 1597.2. Obtención de nombres de cosas y criaturas . . . . . . . . . . . . . 159

7.2.1. Obtención de nombres para mostrar . . . . . . . . . . . . 1597.3. Notificación de acciones y sucesos . . . . . . . . . . . . . . . . . . 162

7.3.1. Notificar sobre algo que ha ocurrido en una habitación . . 1627.3.2. Notificar sobre algo que ha ocurrido con una cosa . . . . . 1637.3.3. Notificar sobre algo que ha ocurrido con una criatura . . . 164

7.4. Ejecución automática de órdenes . . . . . . . . . . . . . . . . . . 1647.4.1. Añadir una orden a la cola de órdenes . . . . . . . . . . . 1657.4.2. Ejecutar una orden lo antes posible . . . . . . . . . . . . . 166

7.5. Presentación general . . . . . . . . . . . . . . . . . . . . . . . . . 1687.6. Métodos útiles de la API de Java . . . . . . . . . . . . . . . . . . 168

8. Distribución como juego online 169

Page 6: Documentacion age

Lista de tablas

Page 7: Documentacion age

Índice de figuras

1.1. Ventana de trabajo de puck. . . . . . . . . . . . . . . . . . . . . . 91.2. Iconos de puck. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10

3.1. Relación tiempo ataque y Habilidad . . . . . . . . . . . . . . . . 1103.2. Relación tiempo ataque y Habilidad (real) . . . . . . . . . . . . . 1113.3. Probabilidad de éxito en el combate . . . . . . . . . . . . . . . . 1123.4. Diagrama del proceso de funcionamiento de un hechizo. . . . . . 121

Page 8: Documentacion age

Capítulo 1

Uso básico de puck

1.1. Crear mundos con PUCK

El PUCK (Playable Universe Construction Kit) es una herramienta gráficade desarrollo que permite la creación de mundos interactivos para ser jugadoscon el Aetheria Game Engine. Estos mundos pueden utilizarse para jugaraventuras de texto, MUDs o juegos de rol mono y multijugador.

En las siguientes secciones explicaremos detalladamente cómo se puede uti-lizar PUCK para crear un juego completo. Empezaremos con un resumen quenos dará una idea general de lo que tendremos que hacer, para luego entrar másespecíficamente en cada una de las etapas de creación de un mundo.

En la figura 1.1 puedes ver un ejemplo de aventura en PUCK.

1.2. Creación de Mundos para AGE con PUCK:Resumen

Un juego en Aetheria Game Engine viene dado por un mundo, y un mundoes una colección de diferentes objetos, que pueden ser de diversos tipos:

Habitaciones (también llamadas localidades), que son lugares del mundoconectados entre sí por caminos. Una habitación puede representar literal-mente una dependencia de una casa; pero también podría ser, dependiendode la ambientación, un cruce de caminos, una calle, etc.

Personajes, que son seres vivos que pueblan el mundo, incluyendo aljugador, y que se pueden mover entre habitaciones a través de los caminos.Por ejemplo, tenderos, camareros, enemigos, animales, el propio jugador,etc.

Cosas, que son seres inertes que se encuentran en las habitaciones o quellevan los personajes. Por ejemplo, puertas, llaves, cofres, sillas, mesas,espadas, dinero, etc.

Para crear un juego completo, necesitaremos hacer las siguientes cosas (nonecesariamente en orden):

Page 9: Documentacion age

Creación de Mundos para AGE con PUCK: Resumen 9

Figura 1.1: Ventana de trabajo de puck.

Crear los objetos del juego,

Crear las relaciones entre ellos (por ejemplo, un camino es una relaciónentre dos habitaciones. O, si un personaje lleva un objeto, entonces elpersonaje y el objeto están relacionados mediante una relación «leva»),

Dar a los objetos nombres y descripciones para que se muestren en eljuego,

Dotar a los objetos de comportamiento, es decir, que puedan reaccionarante lo que hace el jugador y los demás objetos.

El PUCK (Playable Universe Construction Kit) da soporte a todas estasactividades de la siguiente manera:

La creación de objetos se hace en el editor gráfico que ocupa la parte iz-quierda de la ventana de PUCK. Las herramientas crear habitación, crearpersonaje y crear cosa permiten crear los objetos y colocarlos en el mundo:simplemente haz click sobre la herramienta correspondiente en la barra deherramientas, y a continuación lleva el objeto creado hasta el lugar delmundo en el que lo quieres poner. Nota: la representación gráfica del mun-do es simplemente una herramienta de conveniencia. El lugar del paneldonde coloques un objeto no tendrá ninguna relevancia en el juego, asíque simplemente colócalos como más cómodos te sean para verlos y ma-nipularlos.

La creación de relaciones entre objetos se hace también en el editor gráfico,mediante las herramientas crear relación estructural y crear relación au-xiliar. Para crear una relación, basta con hacer click sobre la herramienta

Page 10: Documentacion age

10 Uso básico de puck

Figura 1.2: Iconos de puck.

correspondiente, y a continuación sobre los dos objetos que se desea rela-cionar. La representación gráfica de cada relación es una flecha.

La descripción de los objetos se hace en los paneles de objeto que apare-cen en la parte derecha de la ventana de PUCK. Para acceder al panel deun objeto dado, basta con hacer click sobre él en la representación gráfi-ca cuando ninguna herramienta está seleccionada. Se nos mostrará todala información de ese objeto (nombres, descripciones, etc.) y podremosmodificarla.

Por último, la descripción del comportamiento se hace también en lospaneles de objeto. Ésta es la parte de la creación de un mundo en laque entra en juego la programación, para lo cual se utiliza el lenguaje descripting BeanShell. El código para cada objeto se modifica en la ficha«Código y Propiedades» de su panel de objeto correspondiente.

1.3. Primeros Pasos: Creando Habitaciones y Ca-minos

1.3.1. Habitaciones y Caminos

Veamos ahora cómo podemos usar PUCK para definir un mundo. Lo primeroque haremos será crear un par de habitaciones unidas entre sí mediante uncamino, de manera que el jugador pueda hacer algo como esto:

>mirar

Page 11: Documentacion age

Primeros Pasos: Creando Habitaciones y Caminos 11

Te encuentras al este del gran río Pecos. Hay muchos árboles bajos a tu alrededor.Un precario puente de madera lo atraviesa, hacia el oeste.

>ir hacia el oesteAtraviesas el puente hacia el oeste cuidadosamente. Las tablas de madera que

lo forman crujen inquietantemente; pero llegas sano y salvo a la otra orilla.Te encuentras al oeste del gran río Pecos. Hay unos cuantos árboles altos a tu

alrededor. Un tosco puente de madera lo atraviesa, hacia el este.>ir por el puenteAtraviesas el puente hacia el este con sumo cuidado. Las tablas que lo componen

emiten crujidos inquietantes; pero llegas sin incidencias a la otra orilla.Te encuentras al este del gran río Pecos. Hay muchos árboles bajos a tu alrede-

dor. Un precario puente de madera lo atraviesa, hacia el oeste.

Para ello, lo primero que debemos hacer es ir al menú «Archivo – Nuevo»,que nos creará un nuevo mundo en blanco para trabajar.

Al hacer esto veremos a la izquierda el editor gráfico en blanco, que es dondecolocaremos nuestros objetos; y a la derecha el panel de objeto asociado almundo. En este panel aparece un formulario con una serie de propiedades delmundo. Es recomendable cubrir al menos el nombre corto y el nombre largo, queAGE utilizará para identificar el mundo. El nombre corto debería estar formadopor una única palabra, aconsejablemente de no más de doce caracteres; mientrasque el nombre largo puede ser un título de la longitud que se desee, y es lo quese mostrará al jugador como nombre de la aventura. El resto de campos (autor,versión, fecha...) son datos meramente informativos que se mostrarán al jugadorcuando vaya a cargar el juego.

Si queremos dejar estas propiedades para más tarde, o modificarlas en algúnmomento, es conveniente saber que podemos volver cuando queramos a estepanel de mundo, simplemente seleccionando una zona vacía del editor gráfico.

Pero pasemos a la acción: vamos a crear las dos habitaciones del ejemplo,situadas una al oeste de otra y separadas por un camino (puente). Para ellohacemos lo siguiente:

Hacemos click en icono de la herramienta «añadir habitación» de la barra

de herramientas.

Movemos el cursor por el editor gráfico. Veremos que aparece un cuadrado(la habitación que estamos creando) que se mueve junto a nuestro cursor.Hacemos click, y la habitación quedará fija en el editor. A la derechapodremos ver su panel de objeto.

Hagamos que esta habitación represente la orilla oeste del gran río Pecos.Para ello, cubrimos los campos del panel de objeto:

• Donde pone «Nombre único», pondremos un nombre corto como porejemplo «Oeste Pecos». Este nombre no se mostrará al jugador, seutiliza para identificar internamente la habitación en el juego y dis-tinguirla de las demás. Nosotros también lo utilizaremos cuando nosqueramos referir a esta habitación en el futuro.

Page 12: Documentacion age

12 Uso básico de puck

• Donde pone «Descripciones» irán las descripciones de la habitación,que son lo que se muestra al jugador cuando teclea «mirar» o cuandoentra en la misma. Puede extrañarte que no haya una sola «Des-cripción», sino un campo para «Descripciones». Esto es porque lasdescripciones en AGE no son un texto estático, sino algo que puedevariar según distintas condiciones (por ejemplo, la descripción de unalocalidad puede ser distinta según si es de día o de noche). El sistemaque se utiliza para hacer posible esto es que la descripción que semuestra al jugador esté formada por distintos trozos que se puedenmostrar o no dependiendo de las condiciones que se den. En este ca-so, nos conformaremos con una descripción estática, con lo cual sólonecesitaremos uno de estos trozos, y sin ninguna condición asocia-da. Para ello, donde pone «Descripción» teclearemos la descripcióncompleta de la habitación:Te encuentras al oeste del gran río Pecos. Hay unos cuantos árboles altosa tu alrededor. Un tosco puente de madera lo atraviesa, hacia el este.Y pulsamos el botón «Añadir». Vemos que la descripción introduci-da aparecerá en el cuadro «Descripciones», precedida de la palabra«Siempre». Esto último quiere decir que esta descripción se mostrarásiempre que un jugador mire la habitación, es decir, no está sujeta acondición alguna.

Por el momento no necesitamos añadir nada más a esta habitación. Esbueno saber que, si más tarde queremos modificar sus datos, basta conhacer click sobre su representación en el editor gráfico (cuando no estemosusando ninguna herramienta) para poder ver y modificar de nuevo su panelde objeto.

Del mismo modo que creamos la habitación anterior, creamos una nuevapara representar la orilla este del río. Para ello, tenemos que volver a hacerclick en la herramienta «añadir habitación», ya que cada uso de la herra-mienta sirve para añadir una sola vez. Colocaremos la nueva habitación ala derecha de la anterior, dado que se encuentra al este. Si no hemos dejadoespacio suficiente en el editor gráfico para colocarla, podemos utilizar lasherramientas de desplazamiento y zoom:

• Para usar la herramienta de desplazamiento, hacemos click sobre su

icono en la barra de herramientas: A continuación, hace-mos un click sobre el editor gráfico, y movemos el cursor: los objetosdel editor se desplazarán con él. Con otro click dejamos de usar laherramienta.

• La herramienta zoom se utiliza del mismo modo. Después de hacer

click en su icono y en el editor gráfico, podemos mover elratón hacia arriba para alejar la vista o hacia abajo para acercarla.

Page 13: Documentacion age

Primeros Pasos: Creando Habitaciones y Caminos 13

Rellenamos el nombre único y la descripción de la nueva habitación dela misma manera que lo habíamos hecho con la anterior, reflejando queestamos al este del Pecos.

Ahora vamos a crear la conexión entre las dos habitaciones. Como po-demos ver en el texto de lo que queremos conseguir, las dos localidadesdeben estar unidas por un puente que cruza de este a oeste. El puente sedebe poder cruzar en las dos direcciones; pero es importante saber que enAGE los caminos son de una sola dirección. Por lo tanto, para permitirel tránsito en las dos direcciones tenemos que crear dos caminos: uno deoeste a este y otro de este a oeste. Nótese que, aunque esto pueda parecerincómodo para casos simples, hace el sistema más poderoso, porque sepuede dar tratamiento distinto a cada sentido del camino, incluyendo per-mitir el paso en un solo sentido, tener descripciones distintas para ambos,hacer que suceda algo al cruzar en un sentido dado, etc.

Para crear los caminos, utilizaremos la herramienta «añadir relación es-tructural» de la barra de herramientas, que está identificada por este icono:

Las relaciones en AGE son formas de expresar cualquier tipo de relación,permanente o temporal, entre dos objetos. Existen dos tipos de relaciones:relaciones estructurales y relaciones auxiliares. Las estructurales se llamanasí porque son las que definen la estructura del mundo: nos dicen dóndeestán los objetos. También se caracterizan porque un objeto no tendríaningún sentido si no estuviese (o al menos pudiese estar) conectado al restodel mundo mediante relaciones estructurales. Las relaciones auxiliares seutilizan para definir otros aspectos de las interacciones entre objetos.

Un camino se modela en AGE como una relación estructural entre unahabitación y otra, de ahí que se cree con esta herramienta. Para crear uncamino entre las dos habitaciones, seguimos los siguientes pasos:

• Hacemos click sobre la herramienta «añadir relación estructural»

• Hacemos click sobre la habitación que representa el Oeste del Pecos.

• Hacemos click sobre la otra habitación. Veremos que aparece unaflecha entre las dos habitaciones, que representa el camino que lasune.

Al igual que los objetos, podemos ver que las relaciones también tienenpaneles de objeto. Nada más crear esta relación camino, podremos veren la parte derecha de la ventana de PUCK su panel correspondiente.El panel tiene algunos campos ya cubiertos, como son las habitacionesorigen y destino (que son las que marcamos en el mapa) y la dirección

Page 14: Documentacion age

14 Uso básico de puck

estándar, que es un punto cardinal asociado al camino. Como habíamoscolocado una habitación a la derecha de la otra, el camino aparecerá auto-máticamente marcado con la dirección estándar «este». Esto implica quese podrá utilizar con comandos como «ir al este». Si a pesar de habercolocado las habitaciones de esa manera éste no era el comportamientoque queríamos, podemos cambiar la dirección estándar (o quitarla parano tener ninguna) en el panel de objeto correspondiente al camino. Delmismo modo podríamos cambiar en cualquier momento las habitacionesorigen y destino, podemos ver que para esto se utiliza el «nombre único»que hace referencia a cada habitación.

Debajo de estos campos podemos ver una lista, inicialmente vacía, de co-mandos personalizados. En esta lista podemos introducir todas aquellaspalabras que el jugador pueda usar para referirse a este camino ademásdel punto cardinal dado por la dirección estándar. Como queremos que eljugador pueda teclear cosas como «ir puente» o «ir por el puente», añadi-mos «puente»: para ello, introducimos la palabra donde pone «Comando»y pulsamos el botón «Añadir».

A continuación podemos ver un área para descripciones, que ya deberíaresultarnos familiar. Las descripciones para los caminos funcionan exacta-mente igual que para las habitaciones; pero se muestran cuando el jugadorcruza el camino correspondiente. Así, para obtener el comportamiento quebuscamos para este camino, debemos teclear en el campo «Descripción»:

Atraviesas el puente hacia el este con sumo cuidado. Las tablas que lo com-ponen emiten crujidos inquietantes; pero llegas sin incidencias a la otra orilla.

Con esto tenemos todo lo que necesitábamos por el momento para estecamino. Cuando volvamos a necesitar modificar este panel de objeto, bastacon que hagamos click sobre la flecha que representa el camino en el mapapara acceder a él.

Para crear el otro camino, como es imaginable, basta con repetir el mismoproceso; pero haciendo click primero sobre la habitación del este y luegosobre la del oeste. Nos aparecerá otra flecha en dirección opuesta, queno se solapa con la anterior para que podamos hacer click fácilmente encualquiera de ellas accediendo a su correspondiente panel de objeto. Ladescripción y el comando personalizado se introducen del mismo modo.

1.3.2. Creando un Personaje Jugador

Con esto ya hemos creado un mínimo mundo con el que jugar; pero nos faltaun elemento necesario para que la aventura tenga sentido: un personaje que pue-da recorrerlo. En AGE, los jugadores están en la piel de personajes que habitanen el mundo e interactúan con él (aunque también pueden crearse personajesque no sean jugadores). Por lo tanto, incluso en un juego para un solo jugadordeberemos poner al menos un personaje que represente a ese jugador para quese pueda jugar. Para ello, utilizaremos la herramienta «añadir personaje» de labarra de herramientas:

Page 15: Documentacion age

Primeros Pasos: Creando Habitaciones y Caminos 15

Y haremos lo siguiente:

Para usar esta herramienta, procedemos de la misma manera que cuandoañadimos habitaciones al mundo: hacemos click sobre su icono en la ba-rra de herramientas, deslizamos el cursor por el editor gráfico y hacemosclick sobre el punto en que queremos dejar al personaje. Será convenienteque lo coloquemos cerca de la habitación donde queremos que el jugadoraparezca al principio del juego; pero –ojo, importante– no dentro de ella.Esto es porque en el editor gráfico el hecho de que algo esté dentro deuna habitación no se representa colocando su icono dentro de la misma;sino uniendo la habitación a ese algo mediante una relación estructural.Representar unas cosas dentro de otras podría parecer más natural a sim-ple vista; pero resultaría engorroso cuando una habitación contuviese grannúmero de personajes o cosas.

Así pues, el siguiente paso será crear la relación estructural entre la ha-bitación (que puede ser la oeste o la este, dependiendo de dónde queréisque empiece el personaje: lo que creamos con PUCK es siempre el estadoinicial del mundo, que luego podrá cambiar durante el juego cuando lospersonajes se muevan y manipulen los objetos) y el personaje. Para ello,utilizamos la herramienta «crear relación estructural» que ya conocemos:

y hacemos click primero sobre la habitación y luego sobre elpersonaje (el orden es importante). Esto nos creará una relación estructu-ral de tipo «contiene» que significa que la habitación contiene al personaje,y que, al contrario que la relación «camino» que veíamos antes, no tienenada que configurar.

Para que todo funcione, sólo falta una cosa: especificar que el personajeque hemos creado es un jugador. Para ello, hacemos click sobre él paraobtener su panel de objeto, y marcamos la casilla que dice «jugador».Esto hace que, cuando alguien arranque la aventura, el AGE lo ponga enla piel de este personaje (en juegos multijugador las cosas son algo máscomplicadas; pero ya las veremos).

Si hurgamos en el panel de objeto asociado al personaje podemos verque tiene diversos datos configurables, como por ejemplo nombre únicoy descripciones. Estas últimas funcionan del mismo modo que las de lashabitaciones y caminos, y en este caso se mostrarían si el jugador se mirasea sí mismo. Puedes cubrirlas si quieres; aunque no son necesarias para elcomportamiento que queríamos.

Ahora sí que tenemos un mundo de AGE completo y jugable (aunque, demomento, no muy emocionante). Para jugarlo, no tenemos más que hacer clicken la herramienta Ejecutar mundo:

Page 16: Documentacion age

16 Uso básico de puck

La herramienta nos recuerda que para probar la aventura tiene que guardarel fichero (lo hará automáticamente) y nos da a elegir entre dos interfaces de eje-cución: el SDI (ventana simple) o el MDI (ventana con sub–ventanas). Podemosescoger cualquiera de los dos; aunque para propósitos de prueba de aventurasde un solo jugador el SDI suele resultar suficiente y más sencillo. Después, ledamos al botón Aceptar para ejecutar la aventura.

Si lo hemos hecho todo bien, podremos jugar la partida que hemos visto alprincipio de la sección.

No se puede decir que haya sido difícil, ¿verdad? Pues ahora vamos a mejo-rarlo.

1.4. Seres inertes

1.4.1. Descripciones de componentes

En la sección 1.3, hemos visto algunos conceptos básicos de PUCK y hemosdefinido un mundo rudimentario con un par de habitaciones y caminos. Por elmomento, lo único que este mundo permite hacer al jugador es desplazarse deuna habitación a otra a través de los caminos; pero la interacción con el mundono va mucho más allá. Por ejemplo:

>mirarTe encuentras al este del gran río Pecos. Hay muchos árboles bajos a tu alrededor.

Un precario puente de madera lo atraviesa, hacia el oeste.>mirar los árboles¿Qué pretendes mirar?>mirar el puente¿Qué pretendes mirar?Parece que una primera característica deseable para mejorar la interacción

del jugador con el mundo sería que se pudiesen mirar los objetos que apare-cen mencionados en las descripciones. Así, nos gustaría que cuando el jugadorpusiese «mirar los árboles», apareciese una descripción de los mismos para darambientación. La forma más sencilla de conseguir esto en AGE mediante lasllamadas descripciones de componentes o descripciones extra. Las descripcionesde componentes pueden añadirse a casi cualquier objeto del mundo (incluyendohabitaciones, personajes, cosas, etc.) y siguen siempre el mismo sistema, que esuna extensión del que seguían las descripciones convencionales que hemos visto.

Así, si en la sección anterior veíamos que las descripciones convencionaleseran realmente listas de descripciones, cada una de las cuales tenía asociada unacondición (que por el momento habíamos dejado en blanco para que se mostraransiempre) y un texto; las descripciones extra también serán listas de descripciones;pero cada una tendrá asociada una serie de nombres, una condición y un texto.Si añadimos una descripción extra a una localidad y el jugador teclea «mirar‘algo’» (o alguna frase sinónima) en ella, entonces se mostrará la descripción siese ‘algo’ coincide con uno de los nombres y además la condición asociada secumple.

Page 17: Documentacion age

Seres inertes 17

Vamos a hacer que el jugador pueda mirar los árboles y el puente desde lalocalidad oeste. Para ello, hacemos click sobre esta localidad en el editor grá-fico, y nos aparece su panel de objetos. Debajo de la sección de descripcionesconvencionales que ya tenemos cubierta, vemos la sección «Descripciones decomponente». En el campo «Nombres de ref. sep. por comas» tecleamos, sin lascomillas: «árbol,árboles»; y en el campo «Descripción» ponemos lo que quere-mos que le aparezca al jugador cuando mire los árboles, como puede ser «Sonunos árboles muy verdes y muy bonitos.» Como en ocasiones anteriores, dejamosel campo «Condición» vacío, ya que esta descripción no dependerá de ningunacondición externa, y pulsamos el botón «Añadir». En la lista de descripcionesextra veremos algo como «árbol,árboles: Siempre: Son unos árboles muy verdesy muy bonitos», donde se muestran los nombres y la descripción; y la palabra«Siempre» indica que la descripción va a mostrarse siempre que el jugador mire,sin depender de ningún otro factor.

Lo mismo podemos hacer con el puente si añadimos una descripción extra connombre «puente» y descripción «Es un precario puente de madera.» o cualquiertexto similar. Si queremos que el puente se vea desde las dos localidades, comosería lógico, tenemos que poner su descripción extra en ambas.

1.4.2. Cosas

Las descripciones de componentes proporcionan una primera manera de ha-cer el mundo más interesante y llegar un poco más allá de las habitaciones ycaminos; pero tienen sus limitaciones. Las descripciones de componentes nospermiten definir elementos del mundo que los jugadores pueden mirar; pero laposible interacción con ellos se reduce sólo a eso: a mirar.

A la hora de crear un juego decente, esto seguramente no sea suficiente. Nosinteresan objetos físicos con los que los jugadores puedan interactuar de másmaneras: cogerlos, llevarlos en su inventario, dejarlos en alguna otra parte, yseguramente hacer más cosas dependiendo del tipo de objeto que sea. Este tipode objetos físicos son lo que en AGE denominamos «cosas».

Para probar las cosas, vamos a crear una piedra que el jugador pueda almenos coger, llevar y dejar. En capítulos posteriores del tutorial veremos cómohacer cosas más complejas (por ejemplo que puedan abrirse y cerrarse, llevarotras cosas dentro, etc.); pero por el momento empezaremos por lo básico.

Creación de cosas

Al contrario que las descripciones extra, las cosas son objetos de pleno dere-cho en AGE, y tienen una representación gráfica en el editor del PUCK. Paraañadir una cosa, utilizamos la herramienta «añadir cosa»:

A la hora de utilizar esta herramienta, procedemos de manera análoga acuando habíamos añadido un personaje al mundo: hacemos click sobre el iconode «añadir cosa», movemos el cursor por el editor gráfico hasta dejarlo en laubicación deseada para el objeto (que no debe coincidir con la habitación en laque queremos ponerlo; aunque sí es conveniente que esté cerca), y hacemos click

Page 18: Documentacion age

18 Uso básico de puck

sobre esa ubicación. Para colocar la cosa en una localidad, igual que habíamoshecho con el personaje, creamos una relación estructural: hacemos click en laherramienta «crear relación estructural»

y a continuación en la habitación y en la cosa, por ese orden. Esto creará unarelación «contiene» entre habitación y cosa que indica que la cosa está situadaen dicha habitación.

Características básicas de cosas

Una vez que tenemos nuestra cosa creada y vinculada a una habitación, ha-cemos click en su icono del editor gráfico para ver su panel de objeto. Los panelesde objeto de las cosas son algo más complicados que los de las habitaciones; peroalgunos de sus componentes nos resultarán familiares, y otros los aprenderemosfácilmente.

En primer lugar podemos ver el campo para el nombre único, que funcionaigual que el de las habitaciones, sirviendo para distinguir internamente el objeto.En este punto es conveniente aclarar que los nombres únicos deben ser realmenteúnicos, es decir, no debe haber en un mundo dos objetos con el mismo nombreúnico, ni siquiera si son objetos de tipos completamente distintos. Esto quieredecir que no debes ponerle a una cosa un nombre que hayas utilizado para, porejemplo, una habitación. A nuestro objeto podemos llamarle «Piedra».

Bajo el nombre único, debajo de unos campos «Heredar de:» y «Ejemplode:» que no veremos por el momento, podemos ver un campo de «Género»,que nos da a elegir entre masculino y femenino. El género de una cosa es elgénero que tiene en castellano el nombre de esa cosa, y se utiliza para construirlos textos del juego. Si pusiésemos que la piedra es «Masculino», podríamosobtener textos en el juego como «aquí hay un piedra» o «coges el piedra delsuelo», así que es importante cubrir bien este campo si queremos un mundo quehable decentemente el idioma.

Debajo del género, podemos ver unos campos para el «Peso» y el «Volumen»de la cosa que, como es imaginable, representan el peso y el volumen que tiene elobjeto en el mundo virtual que estamos creando. Aunque se pueden utilizar paramás cosas, la consecuencia más inmediata de estos valores es que un personaje nopodrá llevar consigo objetos que superen el máximo peso y volumen que puedaacarrear. Si en tu juego no quieres prestar atención a esos detalles, simplementepuedes dejar el peso y el volumen de todos los objetos a cero. En este casopodemos poner, por ejemplo, peso y volumen 5.

Después de los campos correspondientes al peso y el volumen, hay en elformulario dos opciones llamadas «Contenedor» y «Fijo en el sitio». La opción«Contenedor» puede marcarse para representar cosas que puedan tener otrascosas dentro, como una bolsa o un baúl.1 La opción «Fijo en el sitio» denota un

1El autor de AGE recomienda a título personal no usar los contenedores en juegos másde lo estrictamente necesario, pues tienden a complicar innecesariamente la vida al jugadoral tener que preocuparse de sacar y meter objetos de dentro de otros, cosa que puede serinteresante cuando es parte del argumento de la aventura (encontrar la llave de un antiguobaúl, etc.) pero no aporta mucho si se usa como elemento de ambientación.

Page 19: Documentacion age

Seres inertes 19

objeto que no se puede coger, como una farola o una montaña; todas las cosasque no tengan esta opción marcada serán por defecto susceptibles de ser cogidaspor los jugadores. En el caso de la piedra, lo lógico será no marcar ninguna delas dos opciones, ya que una piedra no puede contener otras cosas en su interior,y queremos que el jugador la pueda coger.

A continuación están las descripciones y las descripciones de componentes,que se rellenan de la misma manera que en las habitaciones. Las descripcio-nes en este caso aparecerán cuando un jugador ponga «mirar ‘nuestra cosa’», yfuncionarán tanto si el jugador lleva consigo la cosa como si simplemente estáen la misma localidad. Las descripciones de componentes se pueden usar paradescribir partes o características de los objetos: por ejemplo, si nuestro objetoes una linterna, podemos usar una descripción extra para describir el botón deencendido de la linterna. Si ponemos como nombre para la descripción de com-ponente «botón», y nuestra linterna respondía al nombre «linterna», el jugadorpodrá ver la descripción extra tecleando «mirar el botón de la linterna». Noso-tros nos limitaremos a añadir una descripción convencional para la piedra, algocomo: «Es una piedra estándar, de las de toda la vida. Podrías abrir cabezascon ella.»

Nombres para mostrar y nombres de referencia

Todo lo mencionado está en la ficha «General» del panel de objeto, quees la que hemos visto hasta ahora en todos los objetos que hemos creado. Sinembargo, puede que ya te hayas fijado en que algunos de ellos tienen más fichasaparte de ésta, y es el caso de nuestra recién creada piedra. Ahora vamos a ira la segunda ficha, llamada «Nombres». Cubrir esta ficha es esencial para crearuna cosa, porque nos permite definir el nombre o nombres de dicha cosa.

Hasta ahora, en la ficha «General» del panel habíamos visto el «Nombreúnico» de la cosa, que nos servía para identificarla. Pero, como ya mencionamosal hablar de habitaciones, los nombres únicos de los objetos sólo se usan paraque los distinga el creador del juego y el propio juego; sin que se muestren enningún caso al jugador. Por lo tanto, tendremos que definir otros nombres (quepueden coincidir o no con el nombre único) para mostrar al jugador y para queel jugador pueda interactuar con la piedra.

En la ficha «Nombres» podemos ver espacio para poner cuatro tipos de nom-bres: «Nombres singulares para mostrar», «Nombres plurales para mostrar»,«Nombres singulares de referencia» y «Nombres plurales de referencia».

Los dos primeros tipos son los nombres que se mostrarán al jugador, y fun-cionan exactamente igual que las descripciones. Si queremos que el objeto sellame siempre de una manera determinada y ya está, basta con que tecleemossu nombre en el campo «Nombre:» de «Nombres singulares para mostrar» ypulsemos «Añadir». En nuestro caso, si añadimos de esta manera el nombre«piedra», esto se traducirá en el juego a textos como «aquí puedes ver unapiedra», «coges la piedra» «dejas la piedra», «llevas una piedra»... Si quisiése-mos que el nombre de un objeto cambiase según circunstancias del juego (porejemplo, la piedra podría convertirse en una «piedra mojada» si le echásemosagua por encima), tendríamos que añadirle más nombres singulares y asociarlesdiferentes condiciones. Nosotros nos conformaremos con un nombre estático, asíque añadimos «piedra» sin especificar condición alguna.

Los «Nombres plurales para mostrar» se utilizan para mostrar al jugador

Page 20: Documentacion age

20 Uso básico de puck

si hay varios objetos iguales en un mismo sitio. Por ejemplo, para que el juegopudiese construir frases como «llevas dos piedras» o «aquí hay tres piedras»,tendríamos que añadir «piedras» como nombre plural. Si no va a haber variosobjetos iguales en el juego que se puedan agrupar de esta manera, como esnuestro caso, podemos dejar los nombres plurales en blanco. Más adelante ve-remos cómo se pueden crear varios objetos idénticos que se puedan agrupar, yutilizaremos estos nombres plurales.

Los «Nombres singulares de referencia» son aquellos nombres por los cuales eljugador se puede referir al objeto. Estos nombres no se muestran en la aventura;pero serán los que el sistema utilice para saber que el jugador se ha referido en suorden a un objeto dado. Como al jugador puede ocurrírsele referirse a una cosausando una palabra que no sea el nombre que se le muestra en pantalla, sueleser recomendable incluir sinónimos en los nombres de referencia. Por ejemplo,en este caso podemos añadir los siguientes nombres de referencia para la piedra:«piedra», «roca» y «pedrusco». Después de teclear cada nombre en el campo«Nombre», pulsamos el botón «Añadir» para colocarlos en la lista, o podemospulsar «Cambiar» para modificar alguno si nos hemos equivocado. De este modo,el jugador podrá referirse a la piedra mediante comandos como «coger piedra»,«dejar roca» o «mirar pedrusco», y todos serán entendidos como referidos anuestro objeto piedra.

En los «Nombres plurales de referencia» pondremos los nombres por loscuales el jugador se puede referir a un conjunto de objetos que incluya éste.Normalmente, tendremos aquí las versiones de los nombres singulares de refe-rencia: en este caso «piedras», «rocas» y «pedruscos». De esta forma, si hayvarias piedras (todas con esos nombres de referencia) y el jugador pone «cogerlas piedras» o «coger todas las rocas», su orden se ejecutará sobre todas ellas.Un truco que suele venir bien, y podemos hacer en este caso, es poner comoúltimo nombre plural de referencia «todo». De esta manera, cuando el jugadorponga «coger todo», su orden actuará sobre todos los objetos que tengan esenombre plural de referencia.

Interacciones básicas con las cosas

Una vez rellenados de este modo los nombres para mostrar y de referencia denuestra piedra, podemos utilizar la herramienta «Ejecutar mundo» para probarel objeto que hemos creado. Deberíamos ser capaces de hacer estas cosas:

>mirarTe encuentras al este del gran río Pecos. Hay muchos árboles bajos a tu alrededor.

Un precario puente de madera lo atraviesa, hacia el oeste.Aquí hay una piedra.>mirar los árbolesSon unos árboles muy verdes y muy bonitos.>mirar la piedraEs una piedra estándar, de las de toda la vida. Podrías abrir cabezas con ella.>coger la rocaCoges la piedra.Es una piedra estándar, de las de toda la vida. Podrías abrir cabezas con ella.>inventarioTienes una piedra.>dejar todo

Page 21: Documentacion age

Seres inertes 21

Dejas la piedra.>inventarioNo tienes nada.>mirarTe encuentras al este del gran río Pecos. Hay muchos árboles bajos a tu alrededor.

Un precario puente de madera lo atraviesa, hacia el oeste.Aquí hay una piedra.

Cosas con el mismo nombre de referencia

Como hemos visto, los nombres de referencia nos determinan cómo el jugadorse puede referir a una cosa. Pero a veces podría darse el caso de que varias cosascompartan un mismo nombre de referencia. Por ejemplo, supongamos que hemosdefinido las siguientes dos cosas:

Una piedra blanca, que tiene como nombre para mostrar «piedra blan-ca», y como nombres de referencia «piedra blanca», «pedrusco blanco»,«piedra», «pedrusco».

Una piedra negra, que tiene como nombre para mostrar «piedra negra», ycomo nombres de referencia «piedra negra», «pedrusco negro», «piedra»,«pedrusco».

En este ejemplo, hemos puesto «piedra» como nombre de referencia de ambaspiedras para que cuando el jugador encuentre una de ellas pueda simplementeteclear comandos como «coger piedra» y funcionen, sin especificar el color:

Aquí hay una piedra blanca.>coger la piedraCoges la piedra blanca.Sin embargo, ¿qué pasará si el jugador teclea «coger la piedra» en una habi-

tación donde está tanto la piedra blanca como la negra? ¿Cuál de ellas cogerá?La respuesta es que los nombres de referencia tienen una prioridad corres-

pondiente a la posición que ocupan en la lista de nombres de referencia dePUCK (posición más alta quiere decir mayor prioridad). Cuando un jugadorteclea una orden, las palabras que siguen al verbo se comparan con los nombresde referencia de los objetos que están al alcance del jugador, seleccionándoseaquellos nombres de referencia que aparezcan mencionados en las palabras te-cleadas. En el caso de que haya un único nombre de referencia seleccionado deeste modo (o varios, pero todos pertenecientes a la misma cosa), la acción seejecuta sobre la cosa que tiene ese nombre. En el caso de que se seleccionennombres de referencia de diferentes cosas, la acción se ejecuta sobre aquélla ala que perteneza el nombre de referencia de mayor prioridad (es decir, el queaparece más arriba en la lista) de entre los seleccionados. En el caso de que hayaun empate a prioridades, se ejecuta sobre un objeto cualquiera.

Por ejemplo, si en el caso de las piedras blanca y negra hemos introducidolos nombres de referencia en el orden especificado arriba, las prioridades para lapiedra blanca serían:

Prioridad 1. piedra blanca

Prioridad 2. pedrusco blanco

Page 22: Documentacion age

22 Uso básico de puck

Prioridad 3. piedra

Prioridad 4. pedrusco

Y para la piedra negra serían:

Prioridad 1. piedra negra

Prioridad 2. pedrusco negro

Prioridad 3. piedra

Prioridad 4. pedrusco

Si el jugador teclea «coger la piedra blanca», en la orden tecleada aparecennombres de prioridad 1 y 3 de la piedra blanca (aparece «piedra blanca», deprioridad 1, pero también aparece «piedra», de prioridad 3, como parte de lacadena). Sin embargo, de la piedra negra, sólo aparece un nombre de prioridad3 («piedra»). Como la prioridad 1 le gana a la prioridad 3, el jugador cogerá lapiedra blanca.

Si el jugador teclea «coger el pedrusco negro», en la orden aparece un nombrede prioridad 4 de la piedra blanca («pedrusco») y nombres de prioridad 2 y 4de la piedra negra («pedrusco negro» y «pedrusco»). Como la prioridad 2 esmayor que la prioridad 4, el jugador cogerá la piedra negra.

¿Qué pasa si el jugador teclea simplemente «coger una piedra»? En este casopara ambos objetos se seleccionaría un nombre de prioridad 3 («piedra»). Comohay empate a prioridades, el jugador cogería una piedra cualquiera: se suponeque ambas se ajustan por igual a la orden dada por el jugador, así que si sólo haespecificado «coger una piedra», debería servirle cualquiera de los dos. Si poralgún motivo quisiéramos asegurarnos de que con esta orden el jugador siemprecogiese una piedra dada (por ejemplo, la blanca); tendríamos que hacer que elnombre de referencia «piedra» tuviese más prioridad en esa piedra que en laotra.

En general, para que este sistema de prioridades para los nombres de referen-cia nos conduzca a una interpretación natural de las órdenes de los jugadores,lo único que tendremos que hacer es poner los nombres más específicos por en-cima de los más genéricos (tal y como hemos hecho en el ejemplo de la piedra).Entrando más en detalle, lo que necesitamos es que si un nombre de referenciaes específico de un objeto, tenga más prioridad que los nombres más genéricosde otros objetos que puedan entrar en conflicto con ese nombre específico. Porejemplo, si el nombre específico «piedra blanca» tuviera prioridad 3 en la piedrablanca y el nombre genérico «piedra» tuviera prioridad 1 en la piedra negra,tendríamos problemas, porque al «coger la piedra blanca» ganaría la piedranegra al tener el nombre genérico «piedra» con más prioridad que el nombreespecífico «piedra blanca» de la piedra blanca. Sin embargo, casos como ésteprácticamente nunca se podrán dar si seguimos la norma general de que, dentrode cada cosa, los nombres específicos aparecen antes que los genéricos. Siguien-do esta simple regla, normalmente no hará falta acordarse de cómo funciona elsistema de prioridades salvo para casos avanzados en los que se quiera tener uncontrol muy detallado de las maneras de referirse a cada objeto.

Nótese que, si en lugar de nombres singulares de referencia hablamos denombres plurales de referencia, con estos últimos la acción no se ejecutará sobre

Page 23: Documentacion age

Seres inertes 23

un solo objeto, sino con todos los que tengan un nombre plural de referenciaseleccionado. Es decir, si las dos piedras del ejemplo tienen como nombre pluralde referencia la palabra «piedras», entonces la orden «coger las piedras» haráque el jugador coja ambas, independientemente de las prioridades.

Page 24: Documentacion age

Capítulo 2

Uso del lenguaje BeanShell

Los formularios de PUCK, como los vistos en el capítulo anterior, propor-cionan una manera sencilla de construir un mundo con diferentes localizacionesy objetos que los jugadores puedan manipular; sin necesidad de escribir códigoen ningún lenguaje de programación. Sin embargo, si queremos conseguir mun-dos complejos donde las entidades exhiban comportamientos dinámicos y dondepodamos definir acciones personalizadas que vayan más allá de los comporta-mientos por defecto, sí que necesitaremos programar.

Para estas definiciones de comportamientos complejos que requieren progra-mación, AGE utiliza el lenguaje BeanShell. BeanShell es un lenguaje de scriptingmuy parecido a Java. De hecho, la sintaxis de Java se puede utilizar tal cual enBeanShell; pero éste además permite usar variables con tipado dinámico y tieneotras características donde «relaja» los requisitos de Java.

Para añadir código BeanShell a un mundo con PUCK, se puede ir a la pesta-ña «Código y Propiedades» de cualquier entidad (las entidades son los objetosdel mundo representados por iconos que añadimos en el mapa de PUCK, comohabitaciones, cosas y criaturas) o bien del propio mundo, y nos aparecerá unárea de texto (con un botón «Ampliar» que la convierte en una ventana inde-pendiente) para introducir el código. AGE es un sistema orientado a objetos, demodo que el código que modifique el comportamiento de una entidad determina-da deberá introducirse en el campo de código de esa entidad. El campo de códigodel mundo permite hacer modificaciones más globales en el comportamiento dela aventura.

A lo largo de las secciones de este capítulo veremos una introducción a cómoprogramar en BeanShell para crear aventuras en AGE. Esta información seampliará en subsiguientes capítulos, donde la entremezclaremos con más cosasque se pueden hacer en los formularios de PUCK; ya que ambas cosas no sonindependientes sino que hay aspectos de PUCK que hacen uso o interactúancon el código BeanShell, haciendo necesario tratarlas de forma entrelazada.

2.1. Primeros pasos con BeanShell

BeanShell es un lenguaje orientado a objetos, basado en Java, que se utilizapara definir comportamientos avanzados en entidades y mundos de AGE. Enésta y las siguientes secciones, describiremos cómo se puede usar BeanShell

Page 25: Documentacion age

Primeros pasos con BeanShell 25

para este propósito. Esto quiere decir que no veremos exhaustivamente todaslas características de BeanShell, sino que sólo describiremos lo necesario parautilizarlo en AGE de forma lo más sencilla posible. Los programadores quequieran un conocimiento más completo y riguroso de BeanShell, incluyendotodas sus características y no limitado a AGE, pueden consultar su página webhttp://www.beanshell.org.

2.1.1. Los formularios de códigoEn los formularios de «Código y propiedades» de PUCK se puede escribir

código BeanShell. Este código puede estar asociado a una entidad concretadel mundo (una habitación, cosa, etc.), en el caso de que lo escribamos en elformulario de una entidad; o al mundo en su conjunto, si lo escribimos en el paneldel mundo. La idea es que el comportamiento de cada entidad se especifiquedentro de esa entidad, de modo que las entidades sean unidades autocontenidasque se puedan llevar fácilmente de un mundo a otro. Por ejemplo, si definimosuna máquina de coser, querremos que el código que usamos para que cosa estédefinido en la entidad «máquina de coser»: de este modo no sólo queda más clarodónde buscar el código de cada cosa, sino que además nos podríamos llevar esaentidad a otra aventura y seguiría cosiendo. El panel de código del mundo, por lotanto, se utilizará para comportamientos que no estén asociados a una entidadparticular, sino al juego en general.

2.1.2. Los métodosEl código que escribamos en un formulario siempre tendrá que constar de

uno o más métodos. Un método es una porción de código que recibe unos datosde entrada y los procesa de una u otra manera, y, para los que vengan de otroslenguajes, es algo análogo al concepto de función o subrutina.

El código de un método consta de una cabecera, que indica qué datos esperael método como entrada y cuáles produce como salida, y un cuerpo escrito entrellaves que contiene las instrucciones ejecutadas por el método. Las cabeceras delos métodos no hace falta escribirlas, las podemos generar directamente con losmenús del PUCK. Sólo hará falta escribir, pues, el cuerpo de los métodos (partedelimitada por llaves).

Por ejemplo, en el siguiente método:

vo id parseCommand ( Mobi le aCreatu re , S t r i n g verb , S t r i n g a r g s ){

i f ( e qu a l s ( verb , " s a l u d a r " ) )aC r ea tu r e . w r i t e ( "Hola . \ n" ) ;

end ( ) ;}

La cabecera es

vo id parseCommand ( Mobi le aCreatu re , S t r i n g verb , S t r i n g a r g s )

donde:

1. Mobile aCreature, String verb, String args es la lista de argumentos o pa-rámetros de entrada del método. Cada parámetro corresponde a un datoque el método espera recibir cuando se ejecute. Cuando usemos BeanShell

Page 26: Documentacion age

26 Uso del lenguaje BeanShell

para mundos en AGE, normalmente será el AGE quien invoque la mayoríade los métodos y nos proporcione los datos de los parámetros.

2. Cada una de las tres partes separadas por comas en la lista de parámetros(por ejemplo, String verb) es la definición de un parámetro de entrada.Un método puede tener cualquier cantidad de parámetros de entrada,incluyendo no tener ninguno (en cuyo caso no habría nada dentro de losparéntesis). La declaración de un parámetro consta de un tipo de datoy un identificador o nombre. En el ejemplo, String sería el tipo de dato(indicando que ese parámetro es una cadena de texto) y verb sería elnombre. El tipo de dato tiene que ser uno de los que soporta AGE oJava (hay formas de crearlos nuevos, pero no las usaremos); mientras queel nombre es totalmente arbitrario mientras dentro del mismo métodonos refiramos al parámetro siempre por el mismo nombre: por ejemplo,podríamos haberle llamado a ese parámetro verbo en lugar de verb, y todofuncionaría igual mientras cambiáramos ese nombre también en el cuerpodel método (if ( equals(verbo,"saludar") )); pero no podríamos hacer uncambio semejante con el tipo de dato.

3. Algunos de los tipos de datos más usados son:

int: número entero.boolean: representa algo que puede ser verdadero o falso, tiene dosvalores válidos: true o false.double: permite representar números con cifras decimales.char: representa una letra o símbolo.String: cadena de texto.World: mundo de AGE.Entity: cualquier entidad del mundo.Item: cosa del mundo.Mobile: criatura del mundo.Room: habitación del mundo.

4. Al escribir los tipos de datos, es importante respetar las convenciones demayúsculas y minúsculas que se ven en la lista (BeanShell es sensible amayúsculas y minúsculas). El motivo de que unos tipos de dato se escribancon minúscula y otros con mayúscula es que los que son con mayúsculacorresponden a objetos (y se llaman clases, es decir, Room es un tipo dedato que es una clase y la habitación de Pedro sería un objeto de tipoRoom) mientras que los que son con minúscula son tipos de datos que sellaman básicos y corresponden a valores (como el entero −4, el booleanofalse o el double 3,25) y no a objetos. En AGE usaremos muy pocos tiposbásicos (de hecho, sólo los de la lista y uno más que mencionaremos ahoramismo); sin embargo podremos usar bastantes clases (no sólo las de lalista), que iremos viendo.

5. parseCommand es el nombre del método, que junto con los tipos de losparámetros (no sus nombres) es lo que lo identifica y distingue de otros.En general, el nombre puede ser cualquier palabra que cumpla ciertas

Page 27: Documentacion age

Primeros pasos con BeanShell 27

reglas (por ejemplo si tiene sólo letras mayúsculas y minúsculas siempreservirá).

6. Lo que viene antes del nombre, en este caso void, es el tipo de retorno delmétodo. Y es que, además de procesar unos parámetros de entrada, unmétodo puede devolver un resultado como salida. El tipo básico void esun tipo básico especial que no tiene ningún valor, y se utiliza en el casoen que un método no devuelve nada.

7. En general, podemos definir métodos con cualquier combinación de nom-bre, parámetros y tipo de retorno. Pero como normalmente vamos a quererque AGE ejecute nuestros métodos, para que así queden integrados en elconjunto de la aventura, necesitaremos ponerles unos nombres (y tipos deparámetros y de retorno, aunque no necesariamente nombres de paráme-tros, que como dijimos son arbitrarios) determinados que son los que elAGE espera encontrar. Sin embargo, no es necesario saber de memoria losnombres y parámetros de los métodos que invoca AGE, ya que el PUCKnos generará automáticamente una plantilla de los mismos desde los me-nús contextuales de los formularios de código. Por ejemplo, si en el PUCKabrimos el área en que se introduce el código del mundo y vamos a sumenú contextual con el botón derecho, seleccionando la opción «Insertarcódigo – Método de análisis de la entrada (estándar)» se nos generará au-tomáticamente una plantilla que contendrá la cabecera del método y unoscomentarios sobre para qué sirve el método y la función de cada pará-metro. Eso sí, el cuerpo del método que aparece automáticamente estarávacío y por lo tanto no hará nada, tendremos que rellenarlo nosotros paraque haga algo.

8. Si generamos esta plantilla, o cualquier otra con el PUCK, veremos tex-to que viene después de dobles barras. El texto que viene en una líneadespués de una doble barra es un comentario de código y no se ejecuta,es simplemente para que nosotros escribamos explicaciones de qué haceese código o cosas que queramos recordar. Lo mismo sucede con el textocomprendido entre /* y */, que puede ocupar una o varias líneas. Loscomentarios de código pueden ir en cualquier parte del mismo: sea en lacabecera de un método, en su cuerpo, o incluso fuera de cualquier método,ya que AGE los ignora por completo y no los ejecuta.

// e s t o es un comenta r i o/∗ y e s t otambién ∗/

El cuerpo del método del ejemplo anterior es el código entre llaves:{

i f ( e qu a l s ( verb , " s a l u d a r " ) )aC r ea tu r e . w r i t e ( "Hola . \ n" ) ;

end ( ) ;}

Y lo que hace es que, si la cadena (verbo) que nos han pasado como segundoparámetro corresponde a la palabra «saludar» (sin comillas), entonces escribi-mos una línea que dice «Hola.» en la salida asociada a la criatura que vienedada como primer parámetro.

Page 28: Documentacion age

28 Uso del lenguaje BeanShell

2.1.3. Variables y entrada/salida sencilla

Ahora que ya sabemos que tenemos que definir métodos y que estructurabásica tienen, y tenemos una idea de cómo obtener sus cabeceras con el PUCK,vamos a ver qué tipo de cosas podemos poner en los cuerpos de los métodos paraejecutar código útil. Para ello, por el momento siempre partiremos del métodovoid parseCommand(Mobile aCreature, String verb, String args) cuya cabecera segenera automáticamente en la opción «Insertar código – Método de análisis dela entrada (estándar)» del menú contextual del campo de código de mundo.Ese método lo invoca AGE cuando un jugador introduce una entrada, y losparámetros que recibe son, por orden: el objeto que representa al jugador queha escrito esa entrada (de momento el único jugador, ya que nos centraremosen aventuras para un solo jugador), el verbo que ha puesto (o primera palabrade la cadena que ha tecleado en la entrada), y el resto de la cadena de entrada.Por ejemplo, si el jugador teclea «comer el plátano de Canarias», entonces elparámetro aCreature corresponderá a ese jugador, el parámetro verb a la cadena«comer», y args a la cadena «el plátano de Canarias».

Mostrándole texto al jugador

Una de las primeras cosas que podemos hacer es probar a mostrar un textoal jugador cada vez que se ejecute el método (es decir, en el caso de este métodoparticular, cada vez que escribe algo). Esto podríamos hacerlo así:

vo id parseCommand ( Mobi le aCreatu re , S t r i n g verb , S t r i n g a r g s ){

aC r ea tu r e . w r i t e ( "Hola . \ n" ) ;}

Si ponemos este código en el mundo, cada vez que el jugador escriba algo,AGE le dirá «Hola». La sintaxis aCreature.write("Hola.\n") significa quequeremos al método write del objeto aCreature, y pasarle como parámetro lacadena "Hola.\n". Para ello, tiene que haber definido en la clase a la quepertenece aCreature (o sea, la clase Mobile) un método llamado write que cojauna cadena como único parámetro, y efectivamente, este método existe, y lo quehace es escribir algo por la ventana o consola de esa criatura. El punto y comasirve para terminar la instrucción. Se pueden ejecutar varias instrucciones ensecuencia, una después de otra, escribiéndolas una después de otra. No se debeolvidar poner un punto y coma después de cada una:

vo id parseCommand ( Mobi le aCreatu re , S t r i n g verb , S t r i n g a r g s ){

aC r ea tu r e . w r i t e ( " Esta l í n e a se e s c r i b e p r ime ro . \ n" ) ;aC r ea tu r e . w r i t e ( "Y é s t a se e s c r i b e " ) ;aC r ea tu r e . w r i t e ( " después . \ n" ) ;

}

La secuencia de caracteres \n dentro de una cadena significa un salto delínea. Por lo tanto, este código escribirá dos líneas, una que dirá «Esta línea seescribe primero.» y otra que dirá «Y ésta se escribe después.».

Page 29: Documentacion age

Primeros pasos con BeanShell 29

La función end()

Si probamos a ejecutar nuestra aventura de ejemplo con el código que hemosañadido, veremos que la salida es algo similar a esto:

> inventarioEsta línea se escribe primero.Y ésta se escribe después.No tienes nada.> asdasfEsta línea se escribe primero.Y ésta se escribe después.No entiendo...

Es decir, nuestra aventura está diciéndole escribiéndole al jugador el textocada vez que escribe algo, pero después está siguiendo el procesado normal quehace AGE (por ejemplo, mostrar el inventario si lo que escribió fue «inventario»).

Si en lugar de esto se quiere que el método que hemos definido sustituya alprocesado por defecto de AGE, es decir, que cuando un jugador escriba algo sele escriba el texto dado y nada más, podemos utilizar la función end():

vo id parseCommand ( Mobi le aCreatu re , S t r i n g verb , S t r i n g a r g s ){

aC r ea tu r e . w r i t e ( " Esta l í n e a se e s c r i b e p r ime ro . \ n" ) ;aC r ea tu r e . w r i t e ( "Y é s t a se e s c r i b e " ) ;aC r ea tu r e . w r i t e ( " después . \ n" ) ;end ( ) ;

}

De esta forma nuestra salida será

> inventarioEsta línea se escribe primero.Y ésta se escribe después.> asdasfEsta línea se escribe primero.Y ésta se escribe después.

Podemos utilizar la función end() para interrumpir el procesado normal delAGE en cualquier momento de la mayoría de los métodos. La plantilla que elPUCK genera de cada método da información explícita sobre si se puede usaro no la función end().

A end() le hemos llamado función, y no método, porque no está asocia-da a un objeto. Un método se invoca sobre un objeto, con la sintaxis obje-to.método(parámetros),1 mientras que una función se invoca sin más, con lasintaxis función(parámetros).

Variables y asignaciones

En BeanShell, como en otros muchos lenguajes de programación, una varia-ble es un nombre simbólico que se asocia a un determinado valor u objeto en

1O a veces sobre una clase, con la sintaxis Clase.método(parámetros). Los métodos que seinvocan sobre una clase se llaman métodos estáticos

Page 30: Documentacion age

30 Uso del lenguaje BeanShell

memoria, que pueden cambiar a lo largo de la ejecución del código. Los pará-metros de los métodos, que vimos con anterioridad, son un tipo particular devariables que almacenan los datos y objetos que se pasan como entradas a unmétodo; pero como programadores también podemos crear otras variables paraalmacenar todo tipo de información temporal que se utilice en el interior de unmétodo.

Una variable tiene tres atributos esenciales: su nombre, su tipo de dato y suvalor. El nombre es simplemente una palabra que el programador escoge parareferirse a la variable, mientras que el tipo de dato describe qué valores puedecontener la variable. Los tipos de datos son los mismos que hemos visto en lasección sobre los parámetros de los métodos, que pueden ser tipos básicos oclases.

Un ejemplo de uso de variables podría ser el siguiente: si estamos escribiendoun código para describir todos los objetos que un jugador lleva en su inventario,probablemente usaremos una variable para llevar cuenta de cuántos objetosllevamos descritos (que será de tipo int, pues el número de objetos descritosen cada momento es un número entero) y otra variable donde almacenaremostemporalmente cada objeto al consultarlo en el inventario para describirlo (queserá de la clase Item, que representa las cosas en AGE).

Para crear una nueva variable, se utiliza una sintaxis como ésta:

i n t i n d e x ;

donde int es el tipo de datos de la variable que queremos declarar, e index essu nombre.

Para asignarle un nuevo valor a una variable ya declarada, lo hacemos de lasiguiente manera:

i n d e x = 0 ;

Con este código estamos haciendo que la variable entera index tome comovalor el número entero 0. Es importante tener en cuenta que cada variable sólopuede tomar valores legales según su tipo de datos. Por ejemplo, a una variableentera le podemos dar el valor 0; pero no le podemos dar el valor «Hola» o 3,5,porque éstos no son números enteros.

Aquí vemos cómo podemos asignar valores a variables de distintos tipos:

i n t i n d e x ;double number ;boolean c o n d i t i o n ;char l e t t e r ;S t r i n g name ;Room aRoom ;Item anItem ;Mobi le aC r ea tu r e ;

i nd e x = −4;number = 7 . 2 3 ;c o n d i t i o n = t rue ; //o f a l s el e t t e r = ’ a ’ ; // l o s v a l o r e s de t i p o c a r á c t e r s e e s c r i b e n

// e n t r e c om i l l a s s imp l e sname = "Pablo P i c a s s o " ; // l o s v a l o r e s de t i p o cadena ( S t r i n g )

// se e s c r i b e n e n t r e c om i l l a s dob l e saRoom = room ( " Sa l a d e l t r ono " ) ; //room ( ) es una f un c i ó n a l a que

// l e pasamos una cadena y nos d e vu e l v e l a

Page 31: Documentacion age

Primeros pasos con BeanShell 31

// h a b i t a c i ó n que t i e n e esa cadena comonombre ún i co

anItem = item ( " B o l í g r a f o " ) ; // i tem ( ) e s aná logo a room ( ) ,// pero con una cosa

aCr ea tu r e = mob i l e ( "Juan" ) ; // mob i l e ( ) e s aná logo a room ( ) ,// pero con una c r i a t u r a

Este proceso se puede abreviar asignando un valor inicial a las variables a lavez que las creamos, de la siguiente manera:

i n t i n d e x = −4;double number = 7 . 2 3 ;boolean c o n d i t i o n = t rue ;char l e t t e r = ’ a ’ ;S t r i n g name = "Pablo P i c a s s o " ;Room aRoom = room ( " Sa l a d e l t r ono " ) ;I tem anItem = item ( " B o l í g r a f o " ) ;Mobi le aC r ea tu r e = mob i l e ( "Juan" ) ;

A las variables también se les puede asignar el valor de otra variable, o elvalor que devuelve una expresión (que puede contener operaciones o llamadas amétodos o funciones):

i n t i n d e x = 7 ;i n t i ndex2 = index ; // copiamos e l v a l o r de i ndex a index2 ,

// ahora i ndex2 v a l e 7i n t i ndex3 = index1 + index2 ; // l a e x p r e s i ó n i ndex1 + index2

// de vu e l v e l a suma de index1 e index2 , ahora i ndex3v a l e 14

i ndex3 = index3 + 1 ; // incrementamos i ndex3 en 1

I tem arma = item ( "Espada" ) ; // obtenemos e l o b j e t o de nombre// ún i co "Espada" y l o almacenamos en l a v a r i a b l e

armai n t pesoArma = arma . getWeight ( ) ; // getWeight ( ) e s un método de l a

// c l a s e Item que nos d e vu e l v e e l peso de una cosa .Aquí ponemos

// e l peso de nu e s t r a espada en l a v a r i a b l e pesoArma. Nótese

//que podemos hace r e s t o porque e l t i p o de r e t o r n od e l

//método getWeight ( ) e s i n t , y e l t i p o de l av a r i a b l e

//pesoArma también es i n t . En g e n e r a l no podemosa s i g n a r

//a una v a r i a b l e un v a l o r que no sea de su t i p oi n t doblePesoArma = arma . getWeight ( ) ∗ 2 ; // e l ∗ e s e l ope rado r

//de m u l t i p l i c a c i ó n . Como vemos , se pueden combinarl l amadas

//a métodos con op e r a c i o n e s para fo rmar unae x p r e s i ó n que

// de vu e l v e un va l o r , v a l o r que se puede despuésmeter en una v a r i a b l e

arma = 7 ; // e s t o f a l l a r í a porque in t en tamos a s i g n a r a l a// v a r i a b l e arma , de t i p o Item , e l v a l o r 7 , de t i p o

i n t .//No c o i n c i d e n l o s t i p o s , a s í que l a a s i g n a c i ó n no

se puede// l l e v a r a cabo

Page 32: Documentacion age

32 Uso del lenguaje BeanShell

Operaciones con los tipos básicos

En la sección anterior hemos visto algunos ejemplos de cómo se hacen opera-ciones, como por ejemplo una suma. Veamos aquí una lista algo más exhaustivade operaciones útiles.

Con int:

i n t a = 7 ;i n t b = 4 ;i n t c ;c = a + b ; //suma a y b ( c v a l d r í a 11)c = a − b ; // r e s t a a menos b ( c v a l d r í a 3)c = a ∗ b ; // m u l t i p l i c a a por b ( c v a l d r í a 28)c = a / b ; // d i v i s i ó n en t e r a de a e n t r e b ( c v a l d r í a 1)c = a % b ; // r e s t o de l a d i v i s i ó n en t e r a de a e n t r e b ( c v a l d r í a 3)

.

a++; // inc r ementa l a v a r i a b l e a ( e s e q u i v a l e n t e a poner a = a + 1)a−−; // decrementa l a v a r i a b l e a ( e s como poner a = a − 1)

a ∗= 3 ; // a b r e v i a t u r a de a = a ∗ 3 . Esto se puede hace raná logamente

// con todos l o s ope r ado r e s de l o s t i p o s b á s i c o s queapa recen

// en e s t a s e c c i ó n .

Con double:

double a = 7 . 0 ; // nó t e s e e l . 0 para i n d i c a r que e l v a l o r s e// c o n s i d e r a un doub l e y no un i n t , aunque matemáticamente// sea e l mismo v a l o r

double b = 4 . 0 ;double c ;c = a + b ; //suma a y b ( c v a l d r í a 11 . 0 )c = a − b ; // r e s t a a menos b ( c v a l d r í a 3 . 0 )c = a ∗ b ; // m u l t i p l i c a a por b ( c v a l d r í a 28 . 0 )c = a / b ; // d i v i s i ó n con de c ima l e s de a e n t r e b ( c v a l d r í a 1 . 75 )

Con cadenas:

S t r i n g a = "Juan" ;S t r i n g b = "Pepe" ;S t r i n g c = a + b ; // l a "suma" de cadenas e s su

// conca t enac i ón : l a cadena "JuanPepe"

i n t a = 7 ;double b = 3 . 0 ;S t r i n g mensaje = "La v a r i a b l e a v a l e " + a + " y l a v a r i a b l e b v a l e

" + b ;// l a s cadenas también se pueden conca t ena r con o t r o s t i p o s

de datos ,// en cuyo caso e s o s v a l o r e s se c o n v i e r t e n a cadenas . En

e s t e// caso mensaje qu eda r í a como "La v a r i a b l e a v a l e 7 y l a

v a r i a b l e b// v a l e 3 .0"

Con booleanos:

boolean s i = t rue ;boolean no = f a l s e ;boolean r e s u l t a d o ;

Page 33: Documentacion age

Primeros pasos con BeanShell 33

r e s u l t a d o = ! s i ; // l a ! e s e l ope rado r not ( negac i ón ) .// Ap l i c ado a t rue , d e vu e l v e f a l s e , y a p l i c a d o a f a l s e ,// d e vu e l v e t r u e .

r e s u l t a d o = s i | | no ; // ope rado r or l ó g i c o .// Devue lve t r u e s i a l menos uno de l o s dos operandos// es t r u e ( en e s t e caso d e v o l v e r í a t r u e ) .

r e s u l t a d o = s i && no ; // ope rado r and l ó g i c o .// Devue lve t r u e s ó l o cuando l o s dos operandos son// t r u e ( en e s t e caso d e v o l v e r í a f a l s e ) .

Los operadores se pueden combinar en expresiones complejas, utilizando pa-réntesis si es necesario para definir el orden:

i n t d = a + b + c ; //suma de a , b y cd = ( a + b ) ∗ c ; // sumar a y b , y m u l t i p l i c a r e l r e s u l t a d o por cS t r i n g j u an e s = a + a + a ; //" JuanJuanJuan"j u an e s += juane s ; //" JuanJuanJuanJuanJuanJuan"boolean comple jo = ( s i && s i ) | | ( no | | no ) ; // e s t o da t r u e

2.1.4. La estructura condicional (if)

Hasta ahora, hemos visto cómo crear un método, escribir cosas en la pan-talla o ventana del jugador y crear o actualizar variables. Estas instruccionesse pueden combinar en secuencias para que se ejecuten unas después de otras;pero con lo que de momento hemos visto, nuestro método siempre hará exac-tamente lo mismo, una secuencia de operaciones predeterminadas. Esto limitamucho el conjunto de cosas que podemos hacer. Para crear métodos más intere-santes, necesitamos de alguna forma poder hacer que se comporten de maneradiferente según las circunstancias: por ejemplo, que el método parseCommandque estamos definiendo actúe de manera distinta según el verbo que ha escritoel jugador, o según la habitación en la que está, si ha realizado o no con an-terioridad una determinada acción, o cualquier otra circunstancia que se nosocurra.

Para conseguir estos comportamientos diferentes según las circunstancias,podemos utilizar la estructura condicional, llamada if. La estructura condicionalnos permite definir un código que sólo se ejecuta si se cumple una determina-da condición (por ejemplo, que el jugador esté en la habitación «cocina»), y,opcionalmente, otro código que sólo se ejecuta si no se cumple la condición:

Mobi le j ugado r = aCrea tu r e ;i f ( e qu a l s ( room ( "Coc ina " ) , j ugado r . getRoom ( ) ) )

j ugado r . w r i t e ( "En e s t o s momentos e s t á s en l a co c i n a . \ n" ) ;e l s e

j u gado r . w r i t e ( "En e s t o s momentos e s t á s en o t r a pa r t e que no es l ac o c i n a . \ n" ) ;

En general, la sintaxis de la instrucción if para ejecutar un código si y sólosi se cumple una determinada condición es la siguiente:

if ( condición ) cuerpoDonde el código de cuerpo sólo se ejecutará en el caso de que condición sea

cierta. En concreto, condición tiene que ser siempre una expresión que devuelvaun valor de tipo boolean (true o false). El código de cuerpo se ejecutará si elvalor de condición es true, y no se ejecutará si dicho valor es false.

Page 34: Documentacion age

34 Uso del lenguaje BeanShell

En el caso de que el cuerpo del if esté formado por varias instrucciones, sedeben delimitar dichas instrucciones con llaves, como si fueran el cuerpo de unmétodo:

i f ( e qu a l s ( room ( "Coc ina " ) , j ugado r . getRoom ( ) ) ){

j ugado r . w r i t e ( "En e s t o s momentos e s t á s en l a co c i n a . \ n" ) ;j ugado r . w r i t e ( "Te dan ganas de pone r t e a c o c i n a r a l go . \ n" ) ;

}

Si el cuerpo del if está formado por una única instrucción, se puede poner lainstrucción sin más, sin llaves:

i f ( e qu a l s ( room ( "Coc ina " ) , j ugado r . getRoom ( ) ) )j ugado r . w r i t e ( "En e s t o s momentos e s t á s en l a co c i n a . \ n" ) ;

O bien ponerla con llaves como antes, que en este caso es equivalente:

i f ( e qu a l s ( room ( "Coc ina " ) , j ugado r . getRoom ( ) ) ){

j ugado r . w r i t e ( "En e s t o s momentos e s t á s en l a co c i n a . \ n" ) ;}

La sintaxis de la estructura if para ejecutar un código si se cumple unacondición y otro distinto si no se cumple es la siguiente:

if ( condición ) cuerpo1 else cuerpo2De nuevo, cuerpo1 y cuerpo2 tienen que ir necesariamente entre llaves si

tienen más de una instrucción, y pueden ir sin llaves si tienen sólo una.

i f ( e qu a l s ( room ( "Coc ina " ) , j ugado r . getRoom ( ) ) ){

j ugado r . w r i t e ( "En e s t o s momentos e s t á s en l a co c i n a . \ n" ) ;j ugado r . w r i t e ( "Te dan ganas de pone r t e a c o c i n a r a l go . \ n" ) ;

}e l s e

j u gado r . w r i t e ( "No e s t á s en l a co c i n a . \ n" ) ;

Una estructura «if–else» se puede combinar con otras estructuras «if–else»para dar una estructura «if–else if–else if...–else», como ésta:

i f ( e qu a l s ( ve rb , "comer" ) )j ugado r . w r i t e ( "Has pues to e l ve rbo comer . \ n" ) ;

e l s e i f ( e qu a l s ( ve rb , " bebe r " ) )j ugado r . w r i t e ( "Has pues to e l ve rbo bebe r . \ n" ) ;

e l s e i f ( e qu a l s ( ve rb , " dormi r " ) )j ugado r . w r i t e ( "Has pues to e l ve rbo dormi r . \ n" ) ;

e l s ej u gado r . w r i t e ( "No en t i e ndo e l ve rbo que has pues to . \ n" ) ;

Este código escribe una cosa u otra en la consola del jugador según el valordel parámetro verb del método.

Comparaciones

Para decidir entre una rama u otra de una estructura condicional, necesita-mos que nuestro código utilice alguna expresión o variable de tipo boolean, demodo que ejecute la rama del if si esta expresión tiene un valor verdadero, y larama del else (o nada) si su valor es falso.

Page 35: Documentacion age

Primeros pasos con BeanShell 35

¿Cómo obtenemos expresiones de tipo boolean que sean relevantes para de-finir nuestros métodos if? Existen muchas maneras, dependiendo de lo que unoquiera hacer; pero una de las más básicas y comunes son las comparaciones.Muchas veces queremos ejecutar un código si un valor es igual a otro (por ejem-plo, si el valor de una variable es igual a un valor dado). Esto se consigue conla función de comparación de igualdad equals.2

La comparación de igualdad de AGE es una función equals al que se le pasandos parámetros que pueden ser de cualquier tipo (tipos básicos u objetos). Lafunción equals devuelve true si los dos parámetros que se le han pasado tienenel mismo valor, y false si son distintos.

Así, por ejemplo, podríamos hacer lo siguiente:e qua l s ( 3 , 4 ) ; // de vu e l v e f a l s ee qua l s ( 3 , 3 ) ; // de vu e l v e t r u ee qua l s (2+2 ,4) ; // de vu e l v e t r u ee qua l s (2+2 ,5) ; // de vu e l v e f a l s ee qua l s ( " F u l a n i t o " , "Menganito " ) ; // de vu e l v e f a l s eS t r i n g f = " Fu l a n i t o " ;e qu a l s ( " F u l a n i t o " , f ) ; // de vu e l v e t r u eS t r i n g g = " Fu la " ;S t r i n g h = " n i t o " ;e qu a l s ( f , g ) ; // de vu e l v e f a l s ee qua l s ( f , h ) ; // de vu e l v e f a l s ee qua l s ( f , g+h ) ; // de vu e l v e t r u ee qua l s ( room ( "Coc ina " ) , room ( "Coc ina " ) ) ; // de vu e l v e t r u ee qua l s ( room ( "Coc ina " ) , room ( "Baño" ) ) ; // de vu e l v e f a l s e ( s a l v o que no

e x i s t a n h a b i t a c i o n e s l l amadas Coc ina n i Baño , en cuyo caso sec on s i d e r a n i g u a l e s porque n inguna de l a s dos e x i s t e )

Podemos ver un uso práctico de una comparación de igualdad en conjun-ción con una estructura condicional si, en el método parseCommand visto conanterioridad, queremos hacer que se responda de una manera determinada sólocuando el jugador escribe un determinado verbo. Por ejemplo:vo id parseCommand ( Mobi le aC r ea tu r e , S t r i n g ve rb , S t r i n g a r g s ){

i f ( e qu a l s ( verb , "comer" ) ){

aC r ea tu r e . w r i t e ( "No puedes comer ahora , que e s t á s a rég imen . \ n") ;

end ( ) ;}

}

Con esto conseguimos que cuando el jugador use el verbo comer, se le digaque está a régimen, sin cambiar el comportamiento de los otros verbos:

> inventarioTienes una manzana.> comer la manzanaNo puedes comer ahora, que estás a régimen.

2Nota para programadores: BeanShell tiene otras comparaciones de igualdad por defecto,que son el método equals de la clase Object y el operador ==. La función equals que se cubreaquí es una función de más alto nivel creada a propósito para simplificar las comparacionesen AGE, cubriendo los casos de uso más comunes en las aventuras. Los programadores quetengan conocimientos de Java o de BeanShell pueden utilizar en su lugar las comparacionestradicionales con el método equals de Object y con ==, que por supuesto funcionan en AGEcomo en Java.

Page 36: Documentacion age

36 Uso del lenguaje BeanShell

Nótese que el end() está dentro del cuerpo del if, con lo cual sólo se impideque AGE ejecute sus comportamientos estándar si realmente se usa el verbocomer. Si hubiésemos puesto el end() fuera del cuerpo del if, haríamos que dejarade funcionar el comando «inventario» (y cualquier otro) porque el end() seejecutaría siempre e impediría que AGE ejecutase sus comportamientos pordefecto. Si no hubiésemos puesto un end() en absoluto, ni siquiera dentro del if,entonces al usar el verbo «comer» se imprimiría nuestro mensaje pero despuésse seguiría procesando la entrada como si tal cosa, cosa que provocaría efectosno deseados:

> comer la manzanaNo puedes comer ahora, que estás a régimen.¿Cómo? ¿Comer?

En este caso, imprimimos el mensaje pero luego AGE procesa el comando, ycomo por defecto no lo entiende (no está definido por defecto en AGE qué pasaal comer algo), da un mensaje de error. Hay que tener cuidado, pues, de usarlos end() en los momentos en que se necesitan.

Si en lugar de comprobar si dos cosas son iguales queremos comprobar sison distintas, podemos utilizar el operador ! visto antes, que niega un valorbooleano. Así, equals(verb,"comer") devuelve true si y sólo si el parámetroverb tiene como valor «comer»; mientras que !equals(verb,"comer") devuelvetrue sólo si el valor del parámetro no es «comer».

En el caso de trabajar con números (sean int o double), a menudo nos intere-sará hacer comparaciones de superioridad y de inferioridad, en lugar de las deigualdad. Es decir, querremos saber si un número es mayor o menor que otro.Esto se hace con los operadores <, >, <= y >=:

boolean b ;b = ( 4 > 3 ) ; // de vu e l v e t r u eb = ( 4 >= 3 ) ; // de vu e l v e f a l s eb = ( 4 >= 4 ) ; // de vu e l v e t r u eb = ( 4 .2 > 3 .5 ) ; // de vu e l v e t r u eb = ( 4 < 3 ) ; // de vu e l v e t r u eb = ( 3 < 4 ) ; // de vu e l v e t r u e ;i f ( x >= 0 ) aCr ea tu r e . w r i t e ( " Equ i s e s mayor que ce ro . \ n" ) ; // para

e s t o tendremos que haber d e c l a r a do an t e s l a v a r i a b l e x

2.1.5. Los bucles

La estructura condicional (if–else) que hemos visto es una de las principalesestructuras de control, es decir, maneras de gestionar qué camino va siguiendo unprograma en BeanShell para ejecutar sus instrucciones. Las otras estructuras decontrol importantes que necesitaremos para gestionar el flujo de los programasson los bucles.

Como hemos visto, una estructura condicional nos permite escoger entre uncódigo a ejecutar y otro según si se da o no una determinada condición. Estecódigo que se ejecuta lo hace únicamente una vez (salvo que estemos llamandoal if varias veces desde código externo a él, claro).

Los bucles nos permiten ejecutar un bloque de código varias veces, repitién-dose mientras una condición determinada se cumpla. El código del cuerpo del

Page 37: Documentacion age

Primeros pasos con BeanShell 37

bucle sólo parará de ejecutarse cuando esa condición no se cumpla (¡ojo, por-que si nunca deja de cumplirse, el bucle se ejecutará indefinidamente, cosa quenormalmente no es lo que se busca!).

Cada una de las ejecuciones del cuerpo de un bucle se llama una iteración.Ejemplos de aplicaciones típicas de los bucles son:

Ejecutar un código un número determinado de veces. Por ejemplo, supon-gamos que queremos sacar un texto por la pantalla cien veces. Podríamoscopiar cien veces el código jugador.write("...");; pero sería un proce-so pesado y daría como resultado un código largo y farragoso. Así que loque hacemos es declarar una variable que empiece a cero (número de vecesque hemos escrito el texto), y crear un bucle que escriba el texto mientrasla variable no llegue a cien. En el cuerpo del bucle, además de escribirel texto, le sumamos uno a la variable, de tal manera que efectivamentecuando el código haya escrito el texto cien veces la variable llegará a cien,y saldremos del bucle.

Ejecutar un código mientras no suceda algo que sabemos que al final tie-ne que ocurrir. Por ejemplo, si el jugador quiere entrar en una cueva ynosotros le pedimos que confirme poniendo «sí» o «no», podemos quererpedirle esa confirmación mientras no dé una respuesta clara. Es decir, sile preguntamos «¿De verdad que quieres entrar en la cueva?» y contes-ta «Cachifú», queremos volver a preguntarle. Esto sería un bucle que serepetiría mientras la respuesta dada no sea «sí» ni «no».

Ejecutar un código sobre una serie de objetos. Por ejemplo, para mostrar elinventario del jugador, podemos querer escribir un código (si no estuvieseya hecho por defecto en AGE) que fuese sacando por pantalla el nombre decada objeto del inventario. Esto también lo podemos hacer con un bucle,donde vamos tomando en cada iteración un objeto del inventario, y elbucle se ejecuta mientras aún queden objetos por imprimir.

En BeanShell existen tres tipos de bucle: el bucle while, el bucle for y el bucledo while. En realidad los tres se basan en la misma idea básica y con cualquierade ellos se puede hacer lo mismo que con cualquiera de los demás, es decir, desdeel punto de vista funcional llegaría con usar un solo bucle para todo. Lo quepasa es que, dependiendo de lo que queramos hacer en cada caso, puede resultarmás cómodo, más sencillo o más claro usar uno u otro bucle.

El bucle while

El bucle while tiene la siguiente sintaxis:

while ( condición ) cuerpo;

Es decir, una sintaxis análoga a la de un if. Al igual que en éste, el cuerpopuede ser una única instrucción (no necesariamente delimitada por llaves), ouna serie de una o más instrucciones delimitadas por llaves.

El significado del bucle while es el siguiente: si se cumple la condición (esdecir, si vale true) se ejecuta el cuerpo del bucle, si no, no se ejecuta nada (hastaaquí funciona como el if). Ahora bien, si se ha ejecutado el cuerpo del bucle, unavez terminada esta ejecución (iteración) se comprueba la condición de nuevo, y

Page 38: Documentacion age

38 Uso del lenguaje BeanShell

a partir de ahí se repite el proceso: si la condición sigue siendo cierta, se vuelvea ejecutar el cuerpo del bucle (y así sucesivamente), mientras que si es falsa, sesale del bucle.

Así, por ejemplo, el siguiente código hace que cuando el jugador ponga elverbo «saludar», el juego muestre la palabra «Hola» cinco veces:

vo id parseCommand ( Mobi le aC r ea tu r e , S t r i n g ve rb , S t r i n g a r g s ){

i f ( e qu a l s ( verb , " s a l u d a r " ) ){

i n t v e c e s = 0 ;whi le ( v e c e s < 5 ){

aCr ea tu r e . w r i t e ( "Hola \n" ) ;v e c e s++;

}end ( ) ;

}}

La variable veces lleva cuenta del número de veces que ya hemos impreso lacadena "Hola\n", y se incrementa en uno en cada iteración del bucle. Por lotanto, se ejecutará el cuerpo del bucle cinco veces (en la primera, la variableveces empieza valiendo 0, y en las siguientes vale 1, 2, 3 y 4, respectivamente.Como al final de esa última iteración (en la que empieza valiendo 4) la variableveces ya toma un valor de 5, al volver a comprobar la condición se sale del bucle.

Nótese también que en este ejemplo hemos puesto la estructura while dentrode una estructura if. En general, tanto la estructura if como la while se puedenponer en cualquier lugar donde pudiese ir una instrucción. Por lo tanto, sepueden anidar unas dentro de otras (es decir, incluir una de estas estructurasdentro del cuerpo de otra estructura); como hemos hecho aquí.

Otra cosa a tener en cuenta es que, cuando una variable de declara dentro deun bloque entre llaves, sólo tiene validez en el ámbito de ese bloque. Esto quieredecir que la variable veces, que se ha declarado dentro del cuerpo del if, sólopodemos usarla en el interior de ese cuerpo del if (o de los bloques de códigoque estén dentro de él, como por ejemplo el cuerpo del while). Si quisiéramosusarla en bloques de código ajenos al cuerpo del if, tendríamos que declararlaen ellos: por ejemplo, si hubiésemos declarado la variable nada más empezar elmétodo, tendría validez en todo el método.

Los valores de las variables nunca se conservan entre ejecuciones de métodos.En cuanto un bloque de código delimitado entre llaves termina de ejecutarse,todas las variables que estaban en él se descartan y su valor se pierde. Existenmaneras de almacenar valores que se conserven entre distintas ejecuciones de unmétodo o de distintos métodos, como las propiedades, que veremos más adelante.

El bucle for

El bucle for es una alternativa al bucle while que permite definir algunostipos de bucles de una manera más compacta y clara (aunque no permite hacernada nuevo que no podamos hacer con el while). La idea del bucle for se basa enque en la práctica muchos bucles siguen un patrón común de funcionamiento enel que justo antes del bucle se declara e inicializa alguna variable de control quesirve para ver cómo vamos progresando en el bucle y cuánto falta para terminar

Page 39: Documentacion age

Primeros pasos con BeanShell 39

(como la variable veces del ejemplo anterior), y al final del cuerpo del bucle secambia esa variable (como hacemos con veces++ en el ejemplo). El bucle forproporciona una sintaxis abreviada que nos permite representar la declaraciónde la variable de control, la condición del bucle y la modificación de la variabletodas juntas. La sintaxis es así:

for ( inicialización ; condición ; modificación ) cuerpo;

Y es exactamente equivalente a esto:

inicialización;while ( condición ){cuerpo;modificación;}

Así, lo mismo que hicimos antes con un bucle while lo podríamos hacer conun bucle for de la siguiente manera:

vo id parseCommand ( Mobi le aC r ea tu r e , S t r i n g ve rb , S t r i n g a r g s ){

i f ( e qu a l s ( verb , " s a l u d a r " ) ){

f o r ( i n t v e c e s = 0 ; v e c e s < 5 ; v e c e s++ ){

aCrea tu r e . w r i t e ( "Hola \n" ) ;}end ( ) ;

}}

Que queda un poco más claro y compacto (y más compacto aún si omitimoslas llaves del cuerpo del bucle for, cosa que ahora podemos hacer porque se haquedado con sólo una instrucción).

En el caso de que un bucle determinado no necesite inicialización o modi-ficación de variables, esos campos se pueden dejar en blanco. Aunque en estecaso tendríamos que plantearnos si realmente el bucle for nos clarifica las cosaspara lo que queremos hacer, o más bien sería más adecuado un simple while. Porúltimo, la condición del for también se puede dejar en blanco, en este caso elbucle se ejecutará indefinidamente como si la condición fuese la constante true.

El bucle do while

Existe un tercer tipo de bucle utilizable en BeanShell, llamado bucle do while:

do cuerpo while (condición);

Su diferencia con el while es que el cuerpo del bucle se ejecuta al menos unavez (antes de evaluar la condición), y la condición se evalúa después (en lugarde antes) de cada iteración.

Todo lo que se puede hacer con do while se puede hacer con while sin compli-carse mucho más, y algunos conocidos programadores (como Bjarne Stroustrup)incluso defienden no usar nunca do while porque no aporta mucho. Al hilo de

Page 40: Documentacion age

40 Uso del lenguaje BeanShell

estas reflexiones, en este tutorial no utilizaremos bucles do while; aunque quienquiera puede utilizarlos en AGE y hay más información sobre ellos en cualquiertutorial de Java o de BeanShell.

break y continue

Existen dos instrucciones especiales, llamadas break y continue, que permitenque nuestro código «haga trampa» en un bucle, rompiendo el flujo de ejecuciónnormal del mismo. Concretamente, la instrucción break hace que salgamos delbucle inmediatamente en el momento en que se ejecuta, sin comprobar la condi-ción del bucle, y sin importar que se cumpla o no. Por otra parte, la instruccióncontinue hace que en el momento en que se ejecuta nos saltemos el resto dela iteración actual y pasemos directamente a la siguiente comprobación de lacondición del bucle.

Así, podríamos hacer algo como esto:

vo id parseCommand ( Mobi le aC r ea tu r e , S t r i n g ve rb , S t r i n g a r g s ){

i f ( e qu a l s ( verb , " s a l u d a r " ) ){

i n t v e c e s = 0 ;whi le ( t rue ){

aC r ea tu r e . w r i t e ( "Hola \n" ) ;i f ( e qu a l s ( veces , 5 ) )

break ;v e c e s++;

}end ( ) ;

}}

Que sería otra manera de imprimir «Hola» cinco veces; aunque menos claraque las anteriores ya que hay que mirar el cuerpo del bucle para saber realmentecuál es su sentido y cúando terminará. Así pues, esto es un ejemplo para ver loque hace la instrucción break pero no es un ejemplo de buen uso. Un buen usosería cuando tenemos un bucle que normalmente debe ejecutarse hasta que dejede cumplirse una condición dada; pero adicionalmente hay alguna otra condiciónsecundaria que lo puede hacer parar, esta condición la podríamos poner con elbreak si consideramos que quedaría feo añadirla a la condición principal delbucle.

Cuando la instrucción break aparece dentro de varios bucles a la vez (porejemplo, en el cuerpo de un for que está dentro del cuerpo de un while), sólo seinterrumpirá la ejecución del bucle más interno. Para que afecte al otro, habríaque poner otro break en el cuerpo del bucle externo. Con la instrucción continuesucede lo mismo: sólo termina la iteración actual del bucle más interno.

2.1.6. RecapitulaciónEn esta sección hemos introducido los ingredientes básicos que necesitamos

para escribir código en BeanShell. Pero para poder utilizarlos para programarsituaciones, comportamientos y problemas interesantes en el contexto de unaaventura; necesitaremos ver cómo se hace que el código BeanShell interactúecon las entidades que componen un mundo en AGE.

Page 41: Documentacion age

Manipulación básica de entidades 41

2.2. Manipulación básica de entidadesAhora que ya hemos visto los Primeros pasos con BeanShell, y conocemos

los elementos básicos que podemos utilizar en este lenguaje (métodos, varia-bles, funciones, estructuras condicionales, bucles, entrada/salida?); ya podemosempezar a aplicarlos a la programación de comportamientos interesantes paranuestros mundos de AGE. Pero para eso, primero debemos ver cómo se puedeutilizar el código BeanShell para manejar las entidades del mundo.

Por ejemplo, supongamos que queremos programar la siguiente situación:tenemos un plátano que puede ser comido. Cuando el jugador teclea «comer elplátano», queremos que éste desaparezca de su inventario, y que en su lugarquede una piel de plátano. Una forma sencilla de conseguirlo detectar cuándo eljugador ha tecleado el verbo «comer» referido a esa entidad plátano, y a conti-nuación deberemos quitarlo de su inventario y meter una entidad representandola piel. En esta sección veremos cómo hacer cosas como ésta.

2.2.1. Método de análisis de la entrada referida a una en-tidad

Para los ejemplos de código de la sección anterior, utilizábamos el método deanálisis de la entrada del mundo, que nos permitía interceptar todas las entradasque un jugador pusiese en un mundo dado. Para programar acciones que actúensobre una entidad dada, como «comer el plátano», será más cómodo utilizar unmétodo de análisis de la entrada referida a esa entidad. Los métodos de análisisde la entrada referida a una entidad sólo se llaman cuando un jugador tecleaun verbo que actúe sobre esa entidad (nombrándola por uno de sus nombresde referencia). De este modo, no tenemos que comprobar nosotros a mano siel jugador se ha referido al plátano comprobando cosas como si ha tecleado lapalabra «plátano» como parte de la entrada: AGE comprueba por nosotros aqué entidades se está refiriendo el jugador y lanza por nosotros sus métodosde análisis de la entrada, que sólo tenemos que redefinir para programar lasacciones correspondientes.

Por ejemplo, creemos en el PUCK una entidad plátano. Para ello, usamos laherramienta «Crear cosa» para crear una cosa con las siguientes características:

Nombre único: plátano

Género: masculino

Descripción: cualquier cosa que se nos ocurra (como por ejemplo «Unbonito plátano.»)

Nombre singular para mostrar: plátano

Nombres singulares de referencia: plátano, platano, fruta

Nombres plurales de referencia: plátanos, platanos, frutas, comida, todo

Añadimos el plátano a la habitación donde esté el jugador, creando unaflecha desde la habitación hasta él, y a continuación vamos a la pestaña «Códigoy propiedades» del plátano. Agrandamos el campo de código y, en el menúcontextual (botón derecho), seleccionamos «Insertar código – Redefinir métodos

Page 42: Documentacion age

42 Uso del lenguaje BeanShell

de cosa – Método de análisis de la entrada (estándar) – Referente a esta cosa».Se nos generará el siguiente código:

/∗Método de a n á l i s i s s i n t á c t i c o de l a en t r ada r e f e r i d a a una cosa ∗/vo id parseCommand ( Mobi le aC r ea tu r e , S t r i n g ve rb , S t r i n g a r g s ){

// aCr ea tu r e : c r i a t u r a que i n t r o d u c e un comando .// ve rb : comando que i n t r oduc e , por e j emp lo "comer"// a r g s : r e s t o de l a orden que i n t r oduc e , por e j emp lo " l a

s e t a "

// t e rm i n a r con end ( ) : i n t e r c ep t amos l a f r a s e , no se e j e c u t al o que se tenga que e j e c u t a r

// por d e f e c t o ante e l l a// t e rm i n a r normal : de spués de nu e s t r o procesado , s e l l e v a a

cabo e l a n á l i s i s normal d e l//comando y e j e c u c i ó n de l a a c c i ón c o r r e s p o n d i e n t e

}

Podemos observar que el método que se nos ha generado tiene exactamente elmismo nombre y tipo de parámetros que el que estábamos utilizando antes en elmundo. Sin embargo, al definirlo en una entidad, el método tiene un significadodistinto al que tenía en el mundo. Mientras AGE invocará el método del mundopara todas las entradas que introduzca un jugador en ese mundo, el método delplátano sólo será invocado cuando una entrada se refiera al plátano.

Concretamente, este método del plátano se ejecutará cuando el jugador pon-ga un comando en el que mencione «plátano» (u otro nombre de referencia) yademás cuente con el plátano a su alcance (es decir, en el inventario o bien en lahabitación en la que está). El método no se ejecutará, por ejemplo, si el jugadorescribe «comer el plátano» en una habitación cuando el plátano está en otra,que es lo que normalmente interesa.

Los parámetros del método son tal y como vienen descritos en la plantilla,es decir:

aCreature es la criatura (jugador) que ha introducido el comando (quequiere hacer algo con el plátano).

verb es el verbo que ha introducido esa criatura, por ejemplo «comer».

args es el resto del texto que ha introducido la criatura, como por ejemplo«el plátano».

Así, por ejemplo, si queremos que cuando el jugador ponga «comer el plá-tano» el juego le diga que no puede, podemos hacerlo así:

/∗Método de a n á l i s i s s i n t á c t i c o de l a en t r ada r e f e r i d a a una cosa ∗/vo id parseCommand ( Mobi le aC r ea tu r e , S t r i n g ve rb , S t r i n g a r g s ){

i f ( e qu a l s ( ve rb , "comer" ) ){

aC r ea tu r e . w r i t e ( "No puedes comer p l á t ano : tu r e l i g i ó n t e imp idemorder co sa s a l a r g a d a s y a m a r i l l a s . \ n" ) ;

end ( ) ;}

}

Esto producirá salidas como la siguiente:

Page 43: Documentacion age

Manipulación básica de entidades 43

> mirar Aquí hay un plátano.> coger el plátanoCoges el plátano.> comer el plátanoNo puedes comer plátano: tu religión te impide morder cosas alargadas y amarillas.> come el platanoNo puedes comer plátano: tu religión te impide morder cosas alargadas y amarillas.> como la frutaNo puedes comer plátano: tu religión te impide morder cosas alargadas y amarillas.

El motivo de que funcione tanto cuando ponemos «plátano» como «pla-tano» o «fruta» es porque hemos puesto todas esas palabras como nombresde referencia. Pero algo un poco más misterioso puede ser ¿por qué funcionatanto «come» como «como» o «comer», si en la condición del if hemos puestoequals( verb , "comer" ), que debería comprobar si el verbo que ha puestoel jugador es textualmente «comer»?

La razón de esto es porque la entrada del jugador se preprcesa antes de serpasada a parseCommand. Es decir, la entrada que recibimos como parámetrodel método parseCommand no es exactamente la que ha puesto el jugador, sinoque está ligeramente simplificada, y una de las simplificaciones que se le hacen esconvertir los imperativos y primeras personas de los verbos a infinitivos. Se puedeencontrar más información sobre lo que hace exactamente este preprocesado enla sección 5.1 de Métodos de análisis de la entrada (parseCommand).

2.2.2. Métodos para quitar, poner y mover entidades

Con el código que acabamos de ver, podemos proporcionar una «excusa» aljugador para no dejarle que se coma el plátano. Pero lo que realmente queríamoshacer es que se lo coma de verdad. Para ello, tendremos que quitar el plátanodel inventario del jugador y, si queremos darle un mayor realismo, hacer quese quede con una piel de plátano en su lugar. El AGE proporciona una seriede métodos que sirven para llevar a cabo este tipo de operaciones que muevenentidades de un lado a otro. He aquí algunos de ellos, donde expresamos en uncomentario a qué clase pertenecen (un método de la clase X se puede ejecutarhaciendo obj.metodo(parametros) si obj es un objeto de la clase X):

/∗ c l a s e Mobi le ∗/ boolean removeItem ( Item o ld I t em )

m.removeItem ( oldItem ) sirve para quitar la cosa oldItem del inventario dela criatura m (sea un jugador o no). Si la cosa que le pasamos realmente está enel inventario de la criatura, la quita y devuelve true. Si no está en el inventario,no hace nada y devuelve false.

/∗ c l a s e Mobi le ∗/ vo id addItem ( Item newItem )

m.addItem ( newItem ) sirve para agregar la cosa newItem al inventario de lacriatura m (sea un jugador o no).

/∗ c l a s e Room∗/ boolean removeItem ( Item o ld I t em )

r.removeItem ( oldItem ) sirve para quitar la cosa oldItem del inventario de lahabitación r. Funciona igual que el método análogo de la clase Mobile.

Page 44: Documentacion age

44 Uso del lenguaje BeanShell

/∗ c l a s e Room∗/ vo id addItem ( Item newItem )

r.addItem ( newItem ) sirve para agregar la cosa newItem al inventario de lahabitación r. Funciona igual que el método análogo de la clase Mobile.

Es importante tener en cuenta que, en el modelo de mundo de AGE, unacosa puede estar en varios sitios a la vez (por ejemplo, en varias habitaciones, oen varios inventarios de criaturas). Por lo tanto, para mover una cosa que estáen un lugar a un nuevo lugar, no es suficiente con usar el método addItem paraañadirlo al nuevo; sino que hay que quitarlo del antiguo. Alternativamente,también existen métodos que quitan una cosa del lugar o lugares donde seencuentre en un momento dado, y la pone en uno nuevo. Estos métodos sonlos siguientes:

/∗ c l a s e Item ∗/ vo id moveTo ( Mobi le m )

cosa.moveTo ( criatura ) quita la cosa dada de todos los sitios donde esté yla pone en el inventario de la criatura.

/∗ c l a s e Item ∗/ vo id moveTo ( Room r )

cosa.moveTo ( sala ) quita la cosa dada de todos los sitios donde esté y lapone en el inventario de la sala.

Nota: en caso de estar usando el sistema de pesos y volúmenes, hay que te-ner en cuenta que todos los métodos que ponen una cosa en un inventario (dehabitación o de criatura) pueden tirar las excepciones VolumeLimitExceededEx-ception y WeightLimitExceededException, si ese inventario está limitado en pesoo volumen y no puede contener esa cosa. Veremos detalles sobre esto cuandoveamos manejo de excepciones, por ahora supondremos que no estamos limi-tando explícitamente los inventarios en peso (el límite por defecto son 10.000unidades de peso y volumen, que debería llegar siempre si no ponemos objetosmuy pesados).

Además de los métodos para quitar, poner y mover cosas, también tenemosun método para mover una criatura a una habitación distinta del mundo:

/∗ c l a s e Mobi le ∗/ vo id setRoom ( Room newRoom )

criatura.setRoom ( sala ) quita la criatura de la habitación donde esté y lapone en el inventario de la sala dada.

Sobre este método, hay que destacar que, mientras que una cosa puede estaren cero, uno o más sitios (habitaciones e inventarios de criaturas), una criaturasiempre estará en un y sólo un sitio a la vez (y ese sitio siempre tiene que ser unahabitación). El método setRoom cambia la habitación en la que está la criatura,quitándola de donde estuviese antes.

Si queremos quitar una cosa de la circulación y que no se pueda acceder aella, basta con quitarla de todos los sitios en los que esté, mediante el métodoremoveItem correspondiente. Esto no borrará la cosa de memoria (seguiremospudiendo acceder a ella mediante item("nombreÚnico") si queremos); pero síhará imposible que los jugadores la vean e interactúen con ella. Si en algún mo-mento queremos volver a poner la cosa en el mundo, basta añadirla a cualquierhabitación o inventario de criatura.

Si queremos quitar una criatura (Mobile) de la circulación, podemos crearuna habitación artificial llamada Limbo, a la que no se pueda acceder mediante

Page 45: Documentacion age

Manipulación básica de entidades 45

ningún camino (es decir, que esté desconectada del resto del mapeado en elPUCK) y mover la criatura a esa habitación.

Aplicando en la práctica uno de los métodos que hemos visto, si queremos queel jugador pueda comerse el plátano y entonces éste desaparezca de la circulaciónen la aventura (dejando, de momento, lo de la piel para más tarde), podríamoshacer algo así:

/∗Método de a n á l i s i s s i n t á c t i c o de l a en t r ada r e f e r i d a a una cosa ∗/vo id parseCommand ( Mobi le aC r ea tu r e , S t r i n g ve rb , S t r i n g a r g s ){

i f ( e qu a l s ( ve rb , "comer" ) ){

aC r ea tu r e . w r i t e ( "Te comes e l p l á t ano . Ñam, ñam . ¡Qué r i c o !\ n" ) ;aC r ea tu r e . removeItem ( i tem ( " p l á t ano " ) ) ;end ( ) ;

}}

Nótese que con item("plátano") obtenemos el objeto correspondiente a laentidad plátano, como vimos en capítulos anteriores, y con removeItem se loquitamos del inventario al jugador que se lo come.

2.2.3. Las variables self y world

En el código que acabamos de ver, que se define en la entidad plátano,utilizamos item("plátano") para referirnos a dicha entidad. En lugar de haceresto, podemos utilizar siempre una variable especial llamada self para referirnosa la entidad en la que estamos definiendo el código. Así, el código que definimosantes se podría reescribir como:

/∗Método de a n á l i s i s s i n t á c t i c o de l a en t r ada r e f e r i d a a una cosa ∗/vo id parseCommand ( Mobi le aC r ea tu r e , S t r i n g ve rb , S t r i n g a r g s ){

i f ( e qu a l s ( ve rb , "comer" ) ){

aC r ea tu r e . w r i t e ( "Te comes e l p l á t ano . Ñam, ñam . ¡Qué r i c o !\ n" ) ;aC r ea tu r e . removeItem ( s e l f ) ;end ( ) ;

}}

Donde self es una variable que representa la entidad plátano (y, por lo tanto,es de la clase Item) porque es ahí donde estamos definiendo este método. Si, ensu lugar, lo definiésemos en el formulario correspondiente a una piedra, o al ju-gador; la variable self tomaría el valor item("piedra") o mobile("jugador"),respectivamente.

Además de la variable self, otra variable especial que podemos utilizar encualquier momento es world. La variable world siempre almacena el objeto de laclase World que representa el mundo. Más adelante veremos cosas que se puedenhacer con él.

Las variables especiales self y world son las únicas que están presentes yse pueden usar por defecto en cualquier método de BeanShell que definamos,sin necesidad de que vengan dadas como parámetros ni de que las declaremosexplícitamente.

Page 46: Documentacion age

46 Uso del lenguaje BeanShell

2.2.4. Métodos para comprobar dónde están las entidadesEl método que acabamos de definir para que el jugador coma el plátano

todavía está incompleto, pues tiene un problema: si el plátano no está en elinventario del jugador sino en el suelo de la habitación, el método también seejecutará (pues ya hemos dicho que este método de análisis de la entrada seejecuta tanto cuando alguien se refiere a un objeto que lleva como a objetos dela habitación en la que está) pero no le podemos quitar el plátano al jugadorporque realmente no lo lleva.

Para mejorar esto, tenemos dos opciones:

1. Comprobar si el jugador lleva el plátano en su inventario, y si no lo lleva,no dejar que se lo coma (diciendo algo así como «Primero tienes quecogerlo»).

2. Dejar que el jugador se coma el plátano incluso si no lo lleva, pero eneste caso, borrar el plátano de la habitación, en lugar de del inventario deljugador.

Para implementar cualquiera de estas dos opciones, necesitamos comprobarla situación y ver dónde está cada cosa: por ejemplo, si el jugador lleva el plátano,o si éste está en el suelo. Los siguientes métodos se pueden utilizar para hacercomprobaciones sobre dónde está una entidad dada:

/∗ c l a s e Mobi le ∗/ Room getRoom ( )

criatura.getRoom() nos devuelve el objeto de la clase Room que representa lahabitación donde está la criatura sobre la que se invoca.

/∗ c l a s e Mobi le ∗/ boolean has I tem ( Item i t )

criatura.hasItem( cosa ) nos devuelve true si la criatura sobre la que se invocatiene en su inventario la cosa dada, y false de lo contrario.

/∗ c l a s e Room∗/ boolean has I tem ( Item i t )

sala.hasItem( cosa ) nos devuelve true si la sala sobre la que se invoca tieneen su inventario la cosa dada, y false de lo contrario.

/∗ c l a s e Room∗/ boolean Mobi le ( Mobi le c r i a t u r a )

sala.hasMobile( criatura ) nos devuelve true si en la sala sobre la que se invocaestá presente la criatura dada, y false de lo contrario.

Con estos mimbres, ya podemos mejorar el método anterior de forma quecontrole si el plátano está en el inventario del jugador o si está en la sala.

Para no dejarle comer el plátano en caso de que no lo tenga, se podría hacerde esta manera:

/∗Método de a n á l i s i s s i n t á c t i c o de l a en t r ada r e f e r i d a a una cosa ∗/vo id parseCommand ( Mobi le aC r ea tu r e , S t r i n g ve rb , S t r i n g a r g s ){

i f ( e qu a l s ( ve rb , "comer" ) ){

i f ( aC r ea tu r e . has I tem ( s e l f ) ){

Page 47: Documentacion age

Manipulación básica de entidades 47

aC r ea tu r e . w r i t e ( "Te comes e l p l á t ano . Ñam, ñam . ¡Qué r i c o !\ n") ;

aC r ea tu r e . removeItem ( s e l f ) ;}e l s e{

aCrea tu r e . w r i t e ( "Para comer e l p l á tano , n e c e s i t a r í a s c o g e r l op r ime ro . \ n" ) ;

}end ( ) ;

}}

Y para dejárselo comer directamente aunque esté en la habitación:

/∗Método de a n á l i s i s s i n t á c t i c o de l a en t r ada r e f e r i d a a una cosa ∗/vo id parseCommand ( Mobi le aC r ea tu r e , S t r i n g ve rb , S t r i n g a r g s ){

i f ( e qu a l s ( ve rb , "comer" ) ){

i f ( aC r ea tu r e . has I tem ( s e l f ) ){

aC r ea tu r e . w r i t e ( "Te comes e l p l á t ano . Ñam, ñam . ¡Qué r i c o !\ n") ;

aC r ea tu r e . removeItem ( s e l f ) ;}e l s e i f ( aC r ea tu r e . getRoom ( ) . has I tem ( s e l f ) ){

aC r ea tu r e . w r i t e ( "Coges e l p l á t ano d e l s u e l o y t e l o comes .Ñam, ñam . ¡Qué r i c o !\ n" ) ;

aC r ea tu r e . getRoom ( ) . removeItem ( s e l f ) ;}e l s e{

aCrea tu r e . w r i t e ( " E r r o r : e s t o no d e b e r í a s u c ede r nunca , porquee l p l á t ano o e s t á en tu i n v e n t a r i o o en tu h a b i t a c i ó n . \ n

" ) ;}end ( ) ;

}}

Nótese que el tercer mensaje no se debería mostrar nunca, porque nunca seentrará por esa rama del else. Sólo lo hemos puesto por motivos aclaratorios.

Por último, ¿qué hacemos si queremos que cuando el jugador se coma el plá-tano, aparezca en su lugar una piel de plátano? Aunque es posible crear modelosde objetos complejos (por ejemplo, se puede hacer que el plátano se compongade un interior y una piel, y que lo que se coma el jugador sea el interior y quedela piel; o bien que la misma entidad plátano pase de estar intacta a conservarsólo la piel, cambiándole las descripciones); para un comportamiento como esteeso tendría una complejidad excesiva. Una solución mucho más sencilla, y quefuncionará igual, es que tengamos un objeto «piel de plátano» creado de ante-mano pero no enlazado con el mundo, y que cuando el jugador ponga el plátanodemos el «cambiazo» quitando la entidad plátano y sustituyéndola por la piel.

Así, creamos un objeto «piel de plátano» con las siguientes características:

Nombre único: piel de plátano

Page 48: Documentacion age

48 Uso del lenguaje BeanShell

Género: femenino

Descripción: cualquier cosa que se nos ocurra

Nombre singular para mostrar: piel de plátano

Nombres singulares de referencia: piel de plátano, piel de platano, piel

Nombres plurales de referencia: pieles de plátano, pieles de platano, pieles

Y luego podemos modificar el código como sigue:

/∗Método de a n á l i s i s s i n t á c t i c o de l a en t r ada r e f e r i d a a una cosa ∗/vo id parseCommand ( Mobi le aC r ea tu r e , S t r i n g ve rb , S t r i n g a r g s ){

i f ( e qu a l s ( ve rb , "comer" ) ){

i f ( aC r ea tu r e . has I tem ( s e l f ) ){

aC r ea tu r e . w r i t e ( "Te comes e l p l á t ano . Ñam, ñam . ¡Qué r i c o !\ n") ;

aC r ea tu r e . removeItem ( s e l f ) ;aC r ea tu r e . w r i t e ( "Te quedas con l a p i e l , por s i t e r e s u l t a

ú t i l . \ n" ) ;aC r ea tu r e . addItem ( i tem ( " p i e l de p l á t ano " ) ) ;

}e l s e{

aCrea tu r e . w r i t e ( "Para comer e l p l á tano , n e c e s i t a r í a s c o g e r l op r ime ro . \ n" ) ;

}end ( ) ;

}}

Como vemos, hemos resuelto el problema de transformar un objeto en otrode una forma muy sencilla y efectiva.

2.2.5. Método de análisis de la entrada referida a dos en-tidades

Con lo que hemos visto en las secciones anteriores, podemos conseguir mu-chos comportamientos habituales en puzzles de aventuras, como por ejemplo

Dejar pasar al jugador por un lugar sólo si lleva una cosa,

Quitarle una cosa cuando hace una determinada acción,

Darle una cosa, o poner en la habitación una cosa o criatura,

Mover entidades por el mapa,

«Teletransportar» al jugador de una habitación a otra,

Transformar una cosa en otra, como el plátano en piel de plátano.

Page 49: Documentacion age

Manipulación básica de entidades 49

Con esto se puede implementar una parte significativa de los puzzles queaparecen en aventuras sencillas, tipo «Vampiro». Pero tenemos una limitación:por el momento, sólo sabemos implementar acciones que se refieren como muchoa una entidad3: podemos responder a «comer el plátano», «encender la tele»,«pulsar el botón» o «frotar la lámpara mágica»; pero no a «abrir el barril conla palanca», «atar al perro al tronco», «echar sal al guiso» o «dar una monedaal mendigo»: estas últimas acciones se refieren a dos entidades, no a una.

Para implementar acciones como éstas con facilidad, existen métodos deanálisis de la entrada específicos que AGE ejecuta sólo cuando el jugador se hareferido en una misma orden a dos entidades, y que nos proporcionan dichasentidades como parámetro. Por ejemplo, supongamos que tenemos un objeto«moneda de oro» que le podemos dar a diversos personajes de nuestra aventuramediante la orden «dar moneda a Fulanito». Para implementarlo, haremos losiguiente:

Creamos la cosa «moneda de oro», con los nombres, descripciones y ca-racterísticas correspondientes.

Creamos los personajes a los que les podremos dar la moneda. Para ello,usaremos la herramienta «Añadir personaje» (pero sin marcarlos comojugador) y rellenaremos las fichas «General» y «Nombres» del formularioexactamente igual que al crear cosas. Por ejemplo, un personaje podríaser así:

• Nombre único: mendigo

• Género: masculino

• HP, HP máx, MP, MP máx: los dejamos en los valores por defecto(éstos son campos para utilizar en juegos tipo rol, con puntos de viday magia)

• Jugador: no

• Descripción: lo que queramos, por ejemplo «Un hombre triste y sucio,con pinta de haber sido vapuleado por la vida.»

• Nombre singular para mostrar: mendigo

• Nombres singulares de referencia: mendigo, pedigüeño, hombre

Vamos al formulario de código de la moneda de oro, abrimos el menúcontextual, y seleccionamos: «Insertar código – Redefinir métodos de cosa– Método de análisis de la entrada (estándar) – Referente a ésta y otracosa (en ese orden)». Con lo cual se nos generará la siguiente plantilla:

/∗Método de a n á l i s i s s i n t á c t i c o de l a en t r ada r e f e r i d a a doscosas , que no e s t án den t ro de o t r a s ∗/

/∗ Este método se e j e c u t a cuando e l j ugado r i n vo ca una ordensob r e dos ob j e t o s , que no e s t án en contenedo re s , y e lp r ime ro de l o s c u a l e s e s é s t e .

∗/vo id parseCommandObj1 ( Mobi le aC r ea tu r e , S t r i n g ve rb ,

S t r i n g a rg s1 , S t r i n g a rg s2 , E n t i t y ob j2 ){

3También sabemos implementar acciones que no se refieren a ninguna entidad, con elmétodo de análisis de la entrada del mundo que utilizábamos al principio

Page 50: Documentacion age

50 Uso del lenguaje BeanShell

// aCr ea tu r e : c r i a t u r a que i n t r o d u c e un comando .// ve rb : comando que i n t r oduc e , por e j emp lo " a f i l a r "// a rg s1 : p a r t e de l a orden que se r e f i e r e a un p r ime r

ob j e t o ( que es e s t e ob j e t o ) , por e j emp lo " e lc u c h i l l o " .

// a rg s2 : p a r t e de l a orden que se r e f i e r e a un segundoob j e to , por e j emp lo "con e l a f i l a d o r "

// ob j2 : segundo ob j e t o a l que se r e f i e r e l a a c c i ón d e lj ugado r ( en e l e jemplo , e l o b j e t o a f i l a d o r ) .

// t e rm i n a r con end ( ) : i n t e r c ep t amos l a f r a s e , no see j e c u t a l o que se tenga que e j e c u t a r

// por d e f e c t o ante e l l a// t e rm i n a r normal : de spués de nu e s t r o procesado , s e

l l e v a a cabo e l a n á l i s i s normal d e l//comando y e j e c u c i ó n de l a a c c i ón c o r r e s p o n d i e n t e

}

Como en casos anteriores, la plantilla que PUCK genera nos da una explica-ción de cuándo se llama este método y de qué significan sus parámetros, que yadebería ayudarnos a entender lo que hace. En concreto, este método que hemosdefinido sirve para capturar acciones sobre dos entidades, de las cuales aquéllaen que se define es la primera mencionada (es decir, si lo definimos en la moneda,podemos usarlo para responder a verbos como «dar la moneda a Fulano» perono a otros como «forzar la cerradura con la moneda», donde la moneda apareceen segundo lugar (hay otro método, que veremos después, que es independientedel orden).

Los primeros dos parámetros hacen la misma función que en el método deanálisis de la entrada referida a una entidad, proporcionándonos el objeto querepresenta al jugador que ha tecleado la orden así como el verbo que ha introdu-cido. Los siguientes dos parámetros, que aparecen como args1 y args2, nos danel resto de la orden introducida dividida en dos partes, una con cada entidad ala que se refiere: así, en «dar la moneda de oro al mendigo», args1 será la cadena«la moneda de oro», y args2 será «al mendigo».

Por último, el quinto y último parámetro (que aparece con el nombre obj2)nos proporciona la segunda entidad (clase Entity) a la que se ha referido el juga-dor. Así, si éste teclea «dar moneda al mendigo», obj2 será la entidad mendigo(mobile("mendigo")); mientras que si teclea «dar moneda al embajador», seríael objeto embajador. Nótese que en este ejemplo obj2 se refiere a criaturas, peropodría referirse a cosas también: tanto Item (cosas) como Mobile (criaturas) sonsubtipos de la clase Entity (entidades), así que nos pueden aparecer como valorde un parámetro de clase Entity.4

Nótese que tenemos un parámetro obj2, que nos devuelve la segunda entidada la que se refirió el jugador; pero no tenemos ningún parámetro obj1 correspon-diente a la primera entidad. Esto es porque la primera entidad ya sabemos cuáles: siempre tiene que ser aquélla en la que definimos el método (es decir, en estecaso, la moneda) y por lo tanto podemos acceder a ella mediante la variable self.

4Las habitaciones (Room) también son entidades y en principio podrían aparecer comovalor donde nos encontremos algo de clase Entity. Sin embargo, en este caso particular nuncanos aparecerán: las habitaciones no tienen nombre de referencia, por lo que AGE no compruebasi el jugador se ha referido a ellas y no se las pasa a los métodos de análisis de la entrada.

Page 51: Documentacion age

Manipulación básica de entidades 51

Sabiendo todo esto, podemos programar reacciones a la entrega de la monedarellenando el código del método, haciendo uso de los parámetros:

vo id parseCommandObj1 ( Mobi le aC r ea tu r e , S t r i n g ve rb , S t r i n ga rg s1 , S t r i n g a rg s2 , E n t i t y ob j2 )

{i f ( e qu a l s ( ve rb , " dar " ) ){

i f ( e qu a l s ( ob j2 , mob i l e ( "mendigo" ) ) ){

aC r ea tu r e . w r i t e ( " O f r e c e s l a moneda a l mendigo . \ n" ) ;aC r ea tu r e . w r i t e ( " E l mendigo acepta l a moneda y se l a mete en

e l b o l s i l l o . \ n" ) ;aC r ea tu r e . removeItem ( s e l f ) ;ob j2 . addItem ( s e l f ) ;ob j2 . say ( " ¡Muchas g r a c i a s , e x t r a n j e r o ! E re s muy amable . " ) ;end ( ) ;

}e l s e i f ( e qu a l s ( ob j2 , mob i l e ( " embajador " ) ) ){

aC r ea tu r e . w r i t e ( " O f r e c e s l a moneda a l embajador . \ n" ) ;aC r ea tu r e . w r i t e ( " E l embajador mira l a moneda con d e s p r e c i o y

hace un ge s t o de r echazo . \ n" ) ;ob j2 . say ( "¿Qué t e c r e e s , que t e puedes ganar mi f a v o r con tu

s u c i o d i n e r o ? ¡Vete , c a n a l l a ! " ) ;end ( ) ;

}}

}

Como vemos, en el caso de que el verbo sea «dar», comparamos obj2 condistintas criaturas para programar sus reacciones. Hemos usado un método queno habíamos visto antes, el método

/∗ c l a s e Mobi le ∗/ vo id say ( S t r i n g t e x t )

Que sirve para que la criatura (Mobile) sobre la que lo invocamos diga eltexto pasado por parámetro.

Así, con este código (y poniendo las entidades moneda, embajador y mendigoen donde corresponden en el mapa) podemos obtener una interacción como ésta:

> inventarioTienes una moneda de oro.> mirarEstás en una habitación muy bonita.Aquí está un embajador y un mendigo.> dar la moneda al embajadorOfreces la moneda al embajador.El embajador mira la moneda con desprecio y hace un gesto de rechazo.El embajador dice: "¿Qué te crees, que te puedes ganar mi favor con tu sucio dinero? ¡Vete, canalla!"> inventarioTienes una moneda de oro.> dar la moneda al mendigoOfreces la moneda al mendigo.El mendigo acepta la moneda y se la mete en el bolsillo.El mendigo dice: "¡Muchas gracias, extranjero! Eres muy amable."

Page 52: Documentacion age

52 Uso del lenguaje BeanShell

2.2.6. Variantes del método referido a dos entidades

El método de análisis de la entrada que acabamos de ver nos permite res-ponder a órdenes que se refieren a dos entidades, definiendo el método en laprimera entidad y obteniendo la segunda entidad como parámetro.

Sin embargo, muchas veces es más conveniente hacerlo al revés: definir elmétodo en la segunda entidad, y obtener la primera como parámetro. Imagi-nemos que en nuestra aventura sólo hay un personaje al que podemos intentardarle diferentes cosas, y según la cosa que le demos reaccionará de una u otramanera: está claro que en este caso será más cómodo definir el comando «dar Xa personaje» en el personaje, y programar allí la reacción a todas las posiblescosas X, en lugar de tener que definir el método una vez en cada cosa.

La conveniencia de definir un método para dos entidades en la primera enti-dad o en la segunda también puede depender de consideraciones de reutilizaciónde objetos: por ejemplo, incluso si el único objeto que le pudiésemos dar al men-digo fuese la moneda, en realidad la reacción del mendigo cuando se la damos esalgo que lógicamente depende del mendigo, más que de la moneda. Así pues, esmás útil definir este código en el mendigo: de este modo, si copiamos la entidadmendigo y la llevamos a otra aventura, podremos tener fácilmente un mendigoque acepta monedas de oro; cosa que parece tener más sentido que ponerlo enla moneda y poder copiar a otra aventura una moneda con el código para seraceptada por mendigos (ya que en muchas aventuras con monedas no habrámendigos).

Por supuesto, todo esto es subjetivo, y dependerá de la conveniencia decada situación; pero el caso es que AGE nos permite definir los métodos deanálisis de la entrada para dos entidades tanto en la primera entidad a la que serefieren las acciones como en la segunda. Para definirlo en la segunda, vamos alformulario de código de esa entidad (el mendigo, en este caso), abrimos el menúcontextual, y seleccionamos: «Insertar código – Redefinir métodos de cosa –Método de análisis de la entrada (estándar) – Referente a otra entidad y ésta(en ese orden)». Con lo cual se nos generará la siguiente plantilla:

/∗Método de a n á l i s i s s i n t á c t i c o de l a en t r ada r e f e r i d a a dos cosas ,que no e s t án den t ro de o t r a s ∗/

/∗ Este método se e j e c u t a cuando e l j ugado r i n vo ca una orden sob r edos ob j e t o s , que no e s t án en contenedo re s , y e l segundo de l o sc u a l e s e s é s t e .

∗/vo id parseCommandObj2 ( Mobi le aC r ea tu r e , S t r i n g ve rb , S t r i n g

a rg s1 , S t r i n g a rg s2 , E n t i t y ob j1 ){

// aCr ea tu r e : c r i a t u r a que i n t r o d u c e un comando .// ve rb : comando que i n t r oduc e , por e j emp lo " a f i l a r "// a rg s1 : p a r t e de l a orden que se r e f i e r e a un p r ime r

ob j e to , por e j emp lo " e l c u c h i l l o " .// a rg s2 : p a r t e de l a orden que se r e f i e r e a un segundo

ob j e t o ( que es e s t e ob j e t o ) , por e j emp lo "con e la f i l a d o r "

// ob j2 : p r ime r ob j e t o a l que se r e f i e r e l a a c c i ón d e lj ugado r ( en e l e jemplo , e l o b j e t o c u c h i l l o ) .

// t e rm i n a r con end ( ) : i n t e r c ep t amos l a f r a s e , no se e j e c u t al o que se tenga que e j e c u t a r

// por d e f e c t o ante e l l a

Page 53: Documentacion age

Manipulación básica de entidades 53

// t e rm i n a r normal : de spués de nu e s t r o procesado , s e l l e v a acabo e l a n á l i s i s normal d e l

//comando y e j e c u c i ó n de l a a c c i ón c o r r e s p o n d i e n t e

}

Como vemos, el funcionamiento del método es igual que el del anterior, salvoque lo definimos en la segunda entidad a la que hace referencia el jugador (men-digo) y se nos pasa un parámetro, llamado en la plantilla obj1, que representa laprimera (moneda). Así, podríamos implementar el comportamiento para «darla moneda al mendigo» de la siguiente manera:

vo id parseCommandObj2 ( Mobi le aC r ea tu r e , S t r i n g ve rb , S t r i n ga rg s1 , S t r i n g a rg s2 , E n t i t y ob j1 )

{

i f ( e qu a l s ( verb , " dar " ) && equa l s ( obj1 , i tem ( "moneda" ) ) ){

aC r ea tu r e . w r i t e ( " O f r e c e s l a moneda a l mendigo . \ n" ) ;aC r ea tu r e . w r i t e ( " E l mendigo acepta l a moneda y se l a mete en

e l b o l s i l l o . \ n" ) ;aC r ea tu r e . removeItem ( ob j1 ) ;s e l f . addItem ( ob j1 ) ;s e l f . say ( " ¡Muchas g r a c i a s , e x t r a n j e r o ! E re s muy amable . " ) ;end ( ) ;

}

}

que producirá el mismo efecto que el código visto anteriormente.Otra situación que se produce con cierta frecuencia en castellano es que

a veces una orden significa lo mismo independientemente del orden en que sedigan las entidades. De hecho, el caso que estamos tratando es un ejemplo: sepuede decir «dar la moneda al mendigo», pero también podría interesarnos quela aventura entendiese «dar al mendigo la moneda», aunque sea menos común.

Por supuesto, podríamos hacer esto con los métodos anteriores, definiendopor separado respuestas a «dar la moneda al mendigo» y a «dar al mendigola moneda»; pero existe una forma más cómoda de implementar el soporte deambas cosas a la vez, sin trabajar el doble. Para ello, vamos al campo de códigode la entidad en la que queremos definir el código (que puede ser cualquiera delas dos, supongamos por ejemplo que es el mendigo) y, en el menú contextual,seleccionamos «Insertar código – Redefinir métodos de cosa/personaje – Méto-do de análisis de la entrada (estándar) – Referente a esta y otra entidad, encualquier orden».

En este caso, la plantilla que se nos genera es algo como esto:

/∗Método de a n á l i s i s s i n t á c t i c o de l a en t r ada r e f e r i d a a dos cosas ,que no e s t án den t ro de o t r a s ∗/

/∗ Este método se e j e c u t a cuando e l j ugado r i n vo ca una orden sob r edos o b j e t o s no con t e n i d o s en o t ro s , uno c u a l q u i e r a de l o sc u a l e s e s é s t e .

∗/vo id parseCommandTwoObjects ( Mobi le aC r ea tu r e , S t r i n g ve rb ,

S t r i n g a rg s1 , S t r i n g a rg s2 , E n t i t y o the rEnt ){

// aCr ea tu r e : c r i a t u r a que i n t r o d u c e un comando .

Page 54: Documentacion age

54 Uso del lenguaje BeanShell

// ve rb : comando que i n t r oduc e , por e j emp lo " a t a r "// a rg s1 : p a r t e de l a orden que se r e f i e r e a e s t e ob j e to ,

por e j emp lo " l a p i e d r a " .// a rg s2 : p a r t e de l a orden que se r e f i e r e a l o t r o ob j e to ,

por e j emp lo "a l a t a b l a "// othe rEnt : o b j e t o a l que se r e f i e r e l a a c c i ón d e l jugador ,

apa r t e de é s t e ( o d e l o b j e t o con t en i do en é s t e ) .// s i s e l f e s l a p i ed r a , o the rEnt s e r í a l a t a b l a ; s i

s e l f e s l a t ab l a , o the rEnt s e r í a l a p i e d r a .

// t e rm i n a r con end ( ) : i n t e r c ep t amos l a f r a s e , no se e j e c u t al o que se tenga que e j e c u t a r

// por d e f e c t o ante e l l a// t e rm i n a r normal : de spués de nu e s t r o procesado , s e l l e v a a

cabo e l a n á l i s i s normal d e l//comando y e j e c u c i ó n de l a a c c i ón c o r r e s p o n d i e n t e

}

Este método tiene la característica de que se llama siempre que un jugadorteclea una orden referida a dos entidades, de las cuales una es la entidad enla que lo definimos, independientemente de que sea la segunda (dar la monedaal mendigo) o la primera (dar el mendigo a la moneda). El parámetro EntityotherEnt siempre contiene el objeto correspondiente a la otra entidad, es de-cir, aquélla donde no definimos el método (pues la entidad donde definimos elmétodo se puede acceder mediante self). Otra forma de verlo es que este mé-todo funciona igual los otros dos métodos de análisis de la entrada para dosentidades que hemos visto; pero con la diferencia de que si el jugador teclealas entidades «al revés» de como las esperamos, el método les «da la vuelta»automáticamente.

De esta forma, podemos implementar nuestro comportamiento por defectocomo sigue, en el código para la entidad mendigo:

vo id parseCommandTwoObjects ( Mobi le aC r ea tu r e , S t r i n g ve rb ,S t r i n g a rg s1 , S t r i n g a rg s2 , E n t i t y o the rEnt )

{

i f ( e qu a l s ( verb , " dar " ) && equa l s ( otherEnt , i tem ( "moneda" ) ) ){

aC r ea tu r e . w r i t e ( " O f r e c e s l a moneda a l mendigo . \ n" ) ;aC r ea tu r e . w r i t e ( " E l mendigo acepta l a moneda y se l a mete en

e l b o l s i l l o . \ n" ) ;aC r ea tu r e . removeItem ( o the rEnt ) ;s e l f . addItem ( o the rEnt ) ;s e l f . say ( " ¡Muchas g r a c i a s , e x t r a n j e r o ! E re s muy amable . " ) ;end ( ) ;

}

}

Y este código funcionará exactamente igual para «dar la moneda al mendigo»que para «dar al mendigo la moneda».

Con las versiones que hemos visto del método de análisis de la entrada re-ferida a dos entidades, y los métodos anteriores que vimos, podemos cubrirrazonablemente todas las variantes de órdenes que se nos puedan presentar.

Page 55: Documentacion age

propiedades y relaciones 55

2.3. propiedades y relaciones

En la sección anterior vimos distintas manipulaciones básicas que se puedenllevar a cabo con entidades, como quitarlas, ponerlas o cambiarlas de sitio.Pero a veces, puede interesarnos tener entidades que puedan estar en diferentesestados dependiendo de lo que hagamos con ellas o del momento del juego en queestemos (por ejemplo, un televisor puede estar encendido o apagado, un cuchillopuede estar afilado o romo). También puede ser interesante guardar valoresrelacionados con alguna entidad (como un número que mida la cantidad debaterí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). Estafuncionalidad se puede conseguir en AGE mediante las propiedades y relaciones.

2.3.1. Propiedades

Las propiedades nos permiten asociar un valor a una entidad. Este valor sealmacena asociado a la entidad, y podemos consultarlo y modificarlo en cual-quier momento de la partida. El valor tiene un nombre que lo identifica, que esuna cadena (String). De esta manera, una misma entidad puede tener distintaspropiedades, 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:

vo id s e t ( E n t i t y ent , S t r i n g name , <t i p o b á s i c o o S t r i ng > va l u e)

que hace que la propiedad de nombre name de la entidad ent pase a valervalue.

Para obtener el valor de una propiedad, podemos utilizar la siguiente función:

<t i p o b á s i c o o S t r i ng > get ( En t i t y ent , S t r i n g name )

que nos devuelve el valor de la propiedad de nombre name de la entidad ent.De este modo, podemos hacer cosas como éstas:

En t i t y t e l e v i s o r = item ( " t e l e v i s o r " ) ;s e t ( t e l e v i s o r , " encend ido " , t rue ) ;boolean b = get ( t e l e v i s o r , " encend ido " ) ; // de vu e l v e t r u es e t ( t e l e v i s o r , p r e c i oEnEu ro s , 1000 ) ;s e t ( t e l e v i s o r , marca , " Te l e funken " ) ;s e t ( t e l e v i s o r , pu l gada s , 24 ) ;i n t a = get ( t e l e v i s o r , " pu l gadas " ) ; // de vu e l v e 24

Nótese que a una propiedad le podemos asignar valores de cualquier tipobásico o bien valores de tipo cadena (String); pero no le podemos asignar otrosobjetos. Sin embargo, es útil saber que si queremos asociar a una propiedad unaentidad (Entity), podemos en su lugar guardar el nombre único de ese objetoEntity mediante el método set, y luego recuperar la entidad:

En t i t y t e l e v i s o r = item ( " t e l e v i s o r " ) ;s e t ( t e l e v i s o r , " p r o p i e t a r i o " , " Mano l i to " ) ;Mobi le e l P r o p i e t a r i o = mob i l e ( ge t ( t e l e v i s o r , " p r o p i e t a r i o " ) )

; // de vu e l v e t r u e

Page 56: Documentacion age

56 Uso del lenguaje BeanShell

Con lo cual a efectos prácticos es como si el valor de la propiedad fuese unaentidad (aunque para relacionar entre sí dos entidades, como en «el propieta-rio del televisor es Manolo», puede ser más adecuado usar la funcionalidad derelaciones, que veremos más tarde).

Las propiedades son útiles para tener en nuestras aventuras entidades quepuedan estar en distintos estados y que reaccionen de manera diferente segúnel estado en que esté. Un ejemplo puede ser un televisor en el que pongamos uncódigo como éste:

vo id parseCommand ( Mobi le aC r ea tu r e , S t r i n g ve rb , S t r i n g a r g s ){

i f ( ge t ( s e l f , " encend ido " ) ){

i f ( e qu a l s ( ve rb , " m i r a r " ) ) { aC r ea tu r e . w r i t e ( " Estánechando un abu r r i d o documenta l s ob r e b a c t e r i a s . \ n" ) ; end ( ) ;}

i f ( e qu a l s ( ve rb , " encende r " ) ) { aC r ea tu r e . w r i t e ( " ¡ E lt e l e v i s o r ya e s t á encend ido !\ n" ) ; end ( ) ; }

i f ( e qu a l s ( ve rb , " apagar " ) ){

aC r ea tu r e . w r i t e ( "Apagas e l t e l e v i s o r . \ n" ) ;s e t ( s e l f , " encend ido " , f a l s e ) ;end ( ) ;

}}e l s e{

i f ( e qu a l s ( ve rb , " m i r a r " ) ) { aC r ea tu r e . w r i t e ( " E lt e l e v i s o r e s t á apagado . \ n" ) ; end ( ) ; }

e l s e i f ( e qu a l s ( ve rb , " encende r " ) ){

aC r ea tu r e . w r i t e ( " Pulsando e l botón , e n c i e nd e s e l t e l e v i s o r . \ n" ) ;

s e t ( s e l f , " encend ido " , t rue ) ;end ( ) ;

}e l s e i f ( e qu a l s ( ve rb , " apagar " ) ) { aC r ea tu r e . w r i t e ( " ¡ E l

t e l e v i s o r ya e s t á apagado !\ n" ) ; end ( ) ; }}

}

Con esto, implementamos un televisor que se puede encender y apagar, y quesi está encendido, al mirarlo muestra un documental sobre bacterias. Añadiendomás propiedades podríamos hacerlo más complejo: por ejemplo, podríamos teneruna propiedad «canal» a la que asignáramos un valor de tipo int, de forma queel televisor nos mostrara programas distintos al cambiar de canal.

Nota importante: Este código no funciona por sí solo, porque para que fun-cione 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 encendidoo apagado. Esto es muy sencillo de hacer, para ver cómo, sigue leyendo hasta lasubsección de inicialización de propiedades un poco más abajo.

2.3.2. Temporización y método update

En el ejemplo anterior, utilizamos una propiedad para poner una entidad enuno u otro estado según lo que hiciese con ella el jugador. Otra posibilidad es

Page 57: Documentacion age

propiedades y relaciones 57

utilizar las propiedades para poner entidades en un estado durante un deter-minado 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 encenderpero que sólo aguante encendida hasta que se le acaben las pilas. Para hacereste tipo de cosas, primero debemos hacer un pequeño receso para explicar cómofunciona la temporización en AGE.

El sistema de tiempo de AGE no se basa en turnos, sino en el conceptode unidades de tiempo. Una unidad de tiempo es la cantidad más pequeña detiempo que se puede manejar en un juego de AGE. Una acción, como coger unacosa o moverse de una localidad a otra, puede consumir una unidad de tiempoo puede llevar más. Por ejemplo, las acciones de coger y dejar objetos consumenuna unidad de tiempo, al igual que las de mirar o consultar el inventario. Por otraparte, las acciones de moverse a una localidad contigua consumen un númerode unidades de tiempo que depende de la «longitud del camino» (que se fijaen el PUCK); y las acciones de combate consumen una cantidad de unidadesde tiempo que dependerán de las características del arma que usemos, nuestrapericia con ellas y otros factores relacionados.

Cualquier mundo de AGE tiene dos modos de juego, que puede seleccionar eljugador 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 vezque el jugador teclea una orden se simulan del tirón todas las unidades de tiempoque correspondan hasta la siguiente orden. Esto puede dar la impresión de quese juega «por turnos»; pero no es exactamente así: por ejemplo, si un jugadorse 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ónpueda 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 detiempo de la vida real: es decir, se fija cuánto dura una unidad de tiempo (porejemplo, cincuenta milisegundos) y cada cincuenta milisegundos transcurre una.Esto quiere decir que si el jugador teclea una orden que consume diez unidadesde tiempo, AGE tardaría medio segundo en responder a su orden y permitirleteclear otra. Igual que en el caso anterior, el goblin podría mientras tanto cogery dejar un objeto cinco veces: lo que puede suceder en el mundo del juego novarí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 propie-dad, podemos ponerle asimismo un contador de tiempo que indica el númerode unidades de tiempo que tardará en actualizarse esa propiedad. A partir deese momento, el contador de tiempo irá decrementándose en una unidad cadavez que pase una unidad de tiempo, hasta que al llegar a cero la propiedad seactualizará. «Actualizarse» consiste en llamar a un método update que defini-mos nosotros, y donde podemos programar una actualización de la propiedad ocualquier otra cosa que nos venga bien que suceda en ese tiempo: las propieda-des con contador de tiempo no sólo van bien para poner en los objetos estadosque duren una determinada cantidad de tiempo, sino también como herramientapara temporizar en general (podemos utilizar una propiedad con contador detiempo como un «reloj» para lanzar eventos que deban suceder en un momentodado).

Para fijar el valor del temporizador de una propiedad, utilizamos la siguientefunción setTime:

Page 58: Documentacion age

58 Uso del lenguaje BeanShell

vo id setTime ( En t i t y ent , S t r i n g name , long t im e u n i t s )

que sirve para cambiar el valor del temporizador de la propiedad name de laentidad ent, fijándolo al valor time. El tipo de dato long que tiene el parámetrotime viene a ser lo mismo que int, sólo que admite números más grandes. En lapráctica podemos tratarlo como si fuese un int. Así, si quisiéramos fijar el valorde una propiedad junto con su temporizador, podríamos hacer algo como:

// encende r l a an to r cha y poner e l t empo r i z ado r c o r r e s p o n d i e n t e ac i e n un idade s de t iempo :

s e t ( i tem ( " anto r cha " ) , " encend ida " , t rue ) ;setTime ( i tem ( " anto r cha " ) , " encend ida " , 100 ) ;

Para obtener el temporizador de una propiedad, podemos utilizar la siguientefunción:

long getTime ( En t i t y ent , S t r i n g name )

Que nos devuelve el temporizador asociado a la propiedad name de la entidadent.

También podemos usar

vo id setTime ( En t i t y ent , S t r i n g name , long t ime )

Si queremos cambiar el valor del temporizador de la propiedad name de laentidad ent; pero dejando el valor de la propiedad como está.

Los temporizadores de las propiedades no son útiles si no se define además elmétodo de actualización que, como acabamos de explicar, se ejecutará cuandoel temporizador de cada propiedad llegue a cero. Para definir este método enPUCK, vamos al campo de código del formulario correspondiente a la entidaddonde 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 a c t u a l i z a c i ó n de e s t a en t i d ad ∗/

//pe : p rop i edad que se a c t u a l i z a// ( pe . getName ( ) : nombre )//w: e l mundo

vo id update ( P rope r t yEn t r y pe , World w ){

}

El método update se llamará cada vez que el contador de una propiedad cual-quiera de la entidad en la que estamos (self) llegue a cero. Para saber cuál esexactamente la propiedad cuyo temporizador ha llegado a cero, podemos utilizarpe.getName(): el primer parámetro del método, de tipo PropertyEntry, contienetoda la información sobre esa propiedad que se actualiza (pe.getName() nosda el nombre, y pe.getValueAsWrapper() el valor; aunque esto último no lonecesitamos porque simplemente podemos obtener el valor con un get). El pa-rámetro World w es redundante, nos devuelve el mundo que siempre podemosacceder mediante world así que no sirve para nada, es un parámetro que semantiene por compatibilidad con versiones beta anteriores de AGE y podemossimplemente hacer como si no existiera.

Page 59: Documentacion age

propiedades y relaciones 59

De esta forma, podemos programar un radiador con termostato que se en-cienda y se apague cada diez unidades de tiempo:

vo id update ( P rope r t yEn t r y pe , World w ){

i f ( e qu a l s ( pe . getName ( ) , " encend ido " ) ) // m i r a r s i l ap rop i edad cuyo t empo r i z ado r l l e g ó a 0 es " encend ido "

{i f ( ge t ( s e l f , " encend ido " ) ){

s e t ( s e l f , " encend ido " , f a l s e ) ;setTime ( s e l f , " encend ido " , 10 ) ; //apagamos y se v u e l v e a 10

UT ’ s a c t u a l i z a r en 10 UT ’ si f ( mob i l e ( " j ugado r " ) . getRoom ( ) . has I tem ( s e l f ) ) // s i e l

j u gado r e s t á en l a h a b i t a c i ó n d e l r ad i ado r , l e dec imosque se ha apagado

{// hay formas me jo r e s de hace r es to , v éa s e nota aba jomob i l e ( " j ugado r " ) . w r i t e ( " E l r a d i a d o r se apaga s o l o por e l

e f e c t o d e l t e rmos ta to . \ n" ) ;}

}e l s e{

s e t ( s e l f , " encend ido " , t rue ) ;setTime ( s e l f , " encend ido " , 10 ) ; // encendemos y se v u e l v e a

a c t u a l i z a r en 10 UT ’ si f ( mob i l e ( " j ugado r " ) . getRoom ( ) . has I tem ( s e l f ) ) // s i e l

j u gado r e s t á en l a h a b i t a c i ó n d e l r ad i ado r , l e dec imosque se ha encend ido

{// hay formas me jo r e s de hace r es to , v éa s e nota aba jomob i l e ( " j ugado r " ) . w r i t e ( " E l r a d i a d o r se enc i e nde s o l o por

e l e f e c t o d e l t e rmos t a to . \ n" ) ;}

}}

}

Si ponemos este código en una entidad radiador, cada diez unidades de tiem-po cambiará de estado, de encendido a apagado y viceversa. Además, si el juga-dor está en la habitación del radiador, se le mostrará un mensaje informándolede que el radiador se ha encendido o apagado.

Hay dos notas que hacer a este ejemplo. La primera es que, igual que el ejem-plo anterior, es necesario inicializar la propiedad (dándole un valor al principiode la aventura) para que funcione (enseguida veremos cómo se hace).

La segunda puntualización es que, debido a que todavía no conocemos afondo todo lo que se puede hacer con el AGE, la forma de notificar al jugadoren 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 (porejemplo) en el caso multijugador, existen métodos para que se muestre un men-saje a todos los jugadores que están en una habitación, o incluso para que unaentidad (como el radiador) emita un mensaje que llegue a todos los jugadores delas 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 noti-

Page 60: Documentacion age

60 Uso del lenguaje BeanShell

ficar que ha ocurrido algo en una habitación, al menos en el caso de aventurasmonojugador nos servirá.

Es útil saber que, si no queremos que una propiedad llame nunca a su métodoupdate, podemos conseguirlo poniendo su temporizador al valor −1. El valor −1significa «infinito», es decir, la propiedad tardará infinito en actualizarse (no seactualizará nunca).

En el ejemplo de esta sección, hemos visto un posible uso de las propiedadescon temporizador: tener un objeto que cambie cíclicamente de estado cada ciertotiempo. Pero existen otros muchos usos, como por ejemplo:

Tener un objeto que está en un estado sólo durante un cierto tiempo yluego pasa a otro, para no volver al anterior (como una radio que se apagaporque se le acabó la pila).

Tener un comportamiento que se repita cada cierto tiempo (por ejemplo,que un reloj de cuco dé la hora).

Tener una serie de eventos distintos que cada uno suceda en un momen-to dado del tiempo (podemos tener una propiedad numérica que cuentecuántos eventos se han ejecutado, y cada X unidades de tiempo la incre-mentamos en 1 para ejecutar el siguiente).

Tener una magnitud que va variando sola con el tiempo (por ejemplo, quela sed de nuestro personaje empiece a 0 y cada diez unidades de tiempo seincremente en 1; que al beber se vuelva a poner a 0, y que cuando lleguea 50 el personaje se muera de sed).

Y un largo etcétera con todo lo que se nos ocurra.

2.3.3. Inicialización de propiedadesNormalmente nos interesará que una determinada propiedad tenga un valor

dado ya desde el principio de la aventura. Por ejemplo, en el caso del televisoranterior, querremos fijar en qué estado está al principio de la partida, cuandoel jugador lo encuentre (por ejemplo, apagado).

Además, si hacemos un get sobre una propiedad sin antes haber fijado ningúnvalor para ella, el valor que se nos devolverá será un valor nulo (null). Este valornulo nos dará un error si intentamos asignárselo a un tipo básico: es decir, porejemplo,

boolean b = get ( i tem ( " t e l e v i s o r " ) , " encend ido " )

nos dará un error si antes no hemos fijado el valor de «encendido», porqueno podemos asignar a una variable boolean el valor null.

Por lo tanto, en general suele ser altamente recomendable dar un valor iniciala las propiedades que vayamos a utilizar en un mundo dado. Esto se puede hacerde dos maneras: o bien desde Puck, o bien mediante código BeanShell.

Inicialización desde PUCK

Para inicializar las propiedades de una entidad desde PUCK, basta con ir alpanel de formularios de esa entidad y seleccionar la pestaña «Código y propie-dades». En la parte inferior, debajo del área de código, hay un formulario que

Page 61: Documentacion age

propiedades y relaciones 61

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», suvalor inicial en el campo «valor», y el valor del temporizador en «tiempo restan-te». Si no queremos usar el temporizador (es decir, si queremos que la propiedadno se actualice nunca), usamos −1 como valor del temporizador. Tras teclearen los tres campos, le damos al botón «Añadir» y veremos en la lista cómo elvalor inicial de nuestra propiedad queda guardado.

Si nos hemos equivocado al introducir algún valor inicial de propiedades oqueremos cambiarlo, podemos seleccionar dicha propiedad en la lista, editar loscampos «nombre», «valor» y «temporizador» en el formulario, y darle al botón«cambiar» para guardar los cambios. El botón «borrar» nos permite borrar unafila 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 tomendichas propiedades al principio de la aventura, que luego podrán cambiar du-rante las partidas. Así, por ejemplo, podemos rellenar los campos poniendo elnombre «encendido», el valor «false» y el temporizador «−1» para que funcioneel ejemplo del televisor que veíamos con anterioridad, y el televisor comienceapagado. Poniendo el valor «true», comenzaría encendido.

Inicialización desde BeanShell

Si por cualquier motivo preferimos inicializar las propiedades de una enti-dad usando código BeanShell en lugar del formulario anterior, también podemoshacerlo, redefiniendo el evento que se ejecuta al inicializarse la entidad. Los even-tos son métodos BeanShell que nos permiten actuar cuando ocurre algún hechodeterminado en el mundo. Concretamente, el evento de inicialización de unaentidad (llamado onInit) nos permite actuar justo cuando se acaba de inicializaresa entidad.

Para redefinirlo, vamos al menú contextual del campo de código de la entidady seleccionamos: Insertar código – Definir eventos de (entidad) – Al inicializarsela (entidad). Se nos generará una sencilla plantilla como ésta:// cód igo a e j e c u t a r cuando se i n i c i a l i z a l a cosavo id o n I n i t ( ){}

Y en este método onInit() podemos poner código para dar valores iniciales alas propiedades:vo id o n I n i t ( ){

s e t ( s e l f , " encend ido " , t rue ) ;s e t ( s e l f , " c ana l " , 4 ) ;

}

Aparte de incluir cualquier otro código que queramos que se ejecute cuandoesa entidad se inicializa.

2.3.4. RelacionesDe 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. Estosuele ser útil para, como su nombre indica, expresar relaciones entre dos objetos.

Page 62: Documentacion age

62 Uso del lenguaje BeanShell

Algunos ejemplos en los que se pueden utilizar relaciones son los siguientes:

Queremos saber si Fulanito conoce o no a Menganito. Entonces, utilizamosuna relación «conoce» de Fulanito a Menganito, que tomará valor true siFulanito conoce a Menganito, y false de lo contrario.

En una aventura romántica, queremos saber el grado de atracción queJuan siente por María. Para eso, utilizamos una relación «gusta» de Juana María, que tomará valores int de 0 a 10 según ese grado de atracción.

En un juego de rol, tenemos que sobornar a un troll dándole un objeto quele guste. Para marcar qué objetos le gustan, usamos una relación «gusta»del troll a cada uno de los objetos que le gusten, que tomará valor truepara 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. Siqueremos expresar que a Juan le atrae María pero además a María también leatrae Juan, necesitaremos dos relaciones, una en cada sentido.

Para fijar el valor de una relación, podemos usar la siguiente función:

vo id s e t ( E n t i t y e1 , S t r i n g relName , En t i t y e2 , <t i p o b á s i c o oS t r i ng > va l u e )

que hace que la relación relName de la entidad e1 a la entidad e2 pase a valervalue.

Podemos fijar también el contador de tiempo de la relación, tal y comohacíamos para las propiedades, de esta manera:

vo id setTime ( En t i t y e1 , S t r i n g relName , En t i t y e2 , long t ime )

que hace que el temporizador de la relación relName de la entidad e1 a laentidad e2 pase a valer time.

Nota: Aunque el temporizador de las relaciones va bajando hasta llegara cero como el de las propiedades, y por lo tanto se podría usar para medirtiempos; por el momento no existe un método update que se pueda redefinirpara las relaciones como lo había en las entidades. En posteriores versiones deAGE seguramente se añadirá este método.

Para obtener el valor de una relación, podemos utilizar la función siguiente:

<t i p o b á s i c o o S t r i ng > get ( En t i t y e1 , S t r i n g relName , En t i t y e2)

que devuelve el valor de la relación relName de la entidad e1 a la entidade2. 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 espe-cial null, que puede dar problemas. Por lo tanto, se recomienda inicializar todaslas relaciones de las que vayamos a hacer un get, cosa que se puede hacer porejemplo en el evento onInit() de alguna de las entidades relacionadas.

Así, podemos hacer cosas como éstas:

Page 63: Documentacion age

propiedades y relaciones 63

s e t ( mob i l e ( " t r o l l " ) , " gus ta " , i tem ( "manzana" ) , f a l s e ) ; // a lt r o l l no l e gus ta l a manzana

s e t ( mob i l e ( " t r o l l " ) , " gus ta " , i tem ( " p l á t ano " ) , t rue ) ; // a lt r o l l l e gus ta e l p l á t ano

get ( mob i l e ( " t r o l l " ) , " gus ta " , i tem ( "manzana" ) ) ; // de vu e l v ef a l s e ( a l t r o l l no l e gus ta l a manzana ) .

get ( mob i l e ( " t r o l l " ) , " gus ta " , i tem ( " pe ra " ) ) ; // e s t o d e vu e l v en u l l ( no f a l s e ) .

Al igual que las propiedades, las relaciones también se pueden inicializardirectamente usando PUCK. Para ello, creamos una flecha entre las dos enti-dades que queramos relacionar. Seleccionando la flecha en el mapa de PUCK,nos aparecerá un panel asociado a la flecha. En su ficha «Otras relaciones», nosaparecerá una lista de «Relaciones personalizadas» que funciona de la mismamanera que la lista de propiedades de los objetos: podemos añadir relacionesaportando su nombre, valor y temporizador.

Nótese que crear una flecha entre determinados tipos de objetos en PUCKcrea por defecto lo que se llama una relación estructural, que es una relaciónespecial que usa AGE para determinar dónde están los objetos y no es lo mismoque las relaciones personalizadas que aquí estamos creando: por ejemplo, si losobjetos son dos habitaciones se crea por defecto un camino, si son una habitacióny una cosa se crea una relación «contiene» que significa que la cosa está dentro dela habitación. Si lo único que queremos es crear relaciones personalizadas, nosinteresará desactivar estas relaciones estructurales: esto se hace desmarcandoel botón «hay camino», entre habitaciones, o poniendo el campo «Relaciónestructural» a «Ninguna» en la ficha «Relación estructural» del panel de laflecha, en el resto de los casos. Si queremos tener tanto una relación estructuralcomo personalizada a la vez entre dos objetos, no necesitaremos desactivar laestructural de esta manera.

Dos métodos muy útiles cuando trabajamos con relaciones son los siguientes:

/∗ c l a s s En t i t y ∗/L i s t g e t R e l a t e d E n t i t i e s ( S t r i n g relName )/∗ c l a s s En t i t y ∗/L i s t g e tR e l a t e dEn t i t i e sB yVa l u e ( S t r i n g propertyName , i n t /boolean

boo lVa l )

Ambos son métodos de la clase Entity, con lo cual se pueden ejecutar sobreobjetos de las clases Room, Item, Mobile, etc (que son subclases de Entity). Elprimero nos devuelve una lista con todas las entidades que están relacionadascon aquélla con la que se invoca, independientemente del valor que tenga larelación. El segundo nos devuelve una lista con todas las entidades relacionadascon aquélla con la que se invoca, y donde además la relación tiene el valor dado.Es decir, por ejemplo:

mob i l e ( " t r o l l " ) . g e t R e l a t e d E n t i t i e s ( " gus ta " )

nos devuelve todas las cosas de la aventura para las cuales hemos especificadosi 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 unadirección, es decir, no nos devolvería cosas que estén relacionadas en sentidoinverso (de la cosa al troll).

mob i l e ( " t r o l l " ) . g e t R e l a t e d E n t i t i e s ( " gus ta " , t rue )

Page 64: Documentacion age

64 Uso del lenguaje BeanShell

nos devuelve todas las cosas de la aventura para las cuales hemos especificadoque le gustan al troll (es decir, hemos fijado la relación «gusta» del troll haciaesas cosas, y concretamente la hemos puesto a true).

Estos métodos nos permiten extraer todo el potencial de las relaciones, alpoder 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 unobjeto de la clase List, que no hemos visto todavía cómo podemos manejar. Loveremos en la sección sobre listas.

2.4. Manejo de arrays y listasLos tipos de datos que hemos visto en las secciones anteriores nos permiten

representar datos de uno en uno: por ejemplo, en una variable de la clase Itempodemos guardar una cosa, y en una variable de la clase String, una cadena detexto. Sin embargo, a veces nos interesará referirnos a grupos o listas de objetos:por ejemplo, el inventario de una criatura será una lista de objetos de la claseItem.

Los arrays y listas son tipos de datos que nos permiten manejar una serie deobjetos. Una variable de tipo array o lista contendrá varios objetos, en orden,desde el objeto con número 0 hasta el objeto con número longitud–1, dondelongitud es el número de objetos que tiene. Tanto en los arrays como en las listas,podremos referirnos a cualquiera de sus objetos, recorrerlos todos mediante unbucle, y cambiar sus contenidos.

2.4.1. ArraysUn array es una variable que se utiliza para almacenar una cantidad fija de

datos del mismo tipo. Por ejemplo, un array para almacenar cuatro datos detipo String (array de tamaño/longitud 4) se declara así:

S t r i n g [ ] cadenas = new S t r i n g [ 4 ] ;

Esto viene a ser como declarar cuatro variables String, cuyos nombres seríancadenas[0], cadenas[1], cadenas[2] y cadenas[3]. Es decir, para acceder a cadadato del array, se utiliza un número (llamado el índice de ese dato) que estáentre 0 y el tamaño del array menos uno: el primer objeto String es cadenas[0],el segundo cadenas[1], etc.

Así, podemos hacer cosas como ésta:

S t r i n g [ ] cadenas = new S t r i n g [ 4 ] ;cadenas [ 0 ] = "uno" ;cadenas [ 1 ] = "dos " ;mob i l e ( " j ugado r " ) . w r i t e ( cadenas [ 0 ] + "\n" ) ; // e s c r i b e "uno"cadenas [ 3 ] = cadenas [ 1 ] ; //pone cadenas [ 3 ] a " dos "mob i l e ( " j ugado r " ) . w r i t e ( cadenas [ 2 ] + "\n" ) ; // e s c r i b e " n u l l " :

cadenas [ 2 ] v a l e n u l l porque no se ha i n i c i a l i z a d o .cadenas [ 2 ] = " s i e t e " ;i n t l o n g i t u d = cadenas . l e n g t h ; // e l campo e s p e c i a l " nombrear ray .

l e n g t h " de vu e l v e l a l o n g i t u d d e l a r r a y .

Pero accediendo a los elementos de uno en uno de esta manera, un arrayno nos aporta mucho más que una serie de variables (en este caso, cuatro) porseparado. Lo que sí que nos será más útil será acceder a los elementos del array

Page 65: Documentacion age

Manejo de arrays y listas 65

usando como índice una variable. Con esto podemos, por ejemplo, recorrer todoslos elementos usando un bucle:

f o r ( i n t i = 0 ; i < cadenas . l e n g t h ; i++ ) // cadenas . l e n g t h es l al o n g i t u d d e l a r r a y cadenas

{mob i l e ( " j ugado r " ) . w r i t e ( cadenas [ i ] + "\n" ) ;

}

Con ese código, se mostrarán en la pantalla del jugador los elementos delarray por orden, uno detrás de otro.

Aunque hemos usado un array de String como ejemplo, los arrays pueden serde cualquier tipo de datos, incluyendo tanto tipos básicos como clases:

S t r i n g [ ] cadenas = new S t r i n g [ 4 ] ;i n t [ ] numeros = new i n t [ 1 0 ] ;E n t i t y [ ] e n t i d a d e s = new En t i t y [ 3 ] ;c o s a s [ 0 ] = item ( " l l a v e dorada " ) ;co s a s [ 1 ] = mob i l e ( " j ugado r " ) ;co s a s [ 2 ] = room ( " s a l a grande " ) ;

2.4.2. ListasLas listas son objetos que nos permiten almacenar una cantidad cambiante

de datos de un tipo dado. Es decir, cuando declaramos un array decimos quétamaño va a tener y el array nunca podrá tener más objetos que ese tamañofijo. Sin embargo, las listas pueden crecer y decrecer en tiempo de ejecución,pues en cualquier momento se les puede añadir o quitar objetos.

Existen varias clases diferentes de listas que resultarán útiles en AGE, y enbreve las veremos. Todas ellas tienen en común la forma de acceder a los objetos:con

nombreL i s ta . s i z e ( )

obtenemos el tamaño actual de la lista (que, como de ha dicho, puede variar).Con

nombreL i s ta . ge t ( i )

accedemos al objeto de la lista con índice i. Análogamente al caso de losarrays, los índices van desde 0 hasta nombreLista.size()-1. Si utilizamos uníndice que no está en ese rango, obtendremos una excepción (error). Otro métodoútil que se puede aplicar en todas las listas es

nombreL i s ta . c o n t a i n s ( o b j e t o )

que nos devuelve true o false según si nuestra lista contiene o no el objetodado.

Otras operaciones con listas variarán según el tipo de lista con el que estemostratando. A continuación veremos diferentes tipos de listas que nos serán útiles:

Inventario (Inventory)

La clase Inventory se utiliza para definir inventarios (conjuntos de cosas) enAGE. Esto comprende tanto el inventario de una criatura (el conjunto de cosas

Page 66: Documentacion age

66 Uso del lenguaje BeanShell

que lleva), como el de una habitación (cosas que hay en esa habitación) u objetocontenedor (cosas que están, por ejemplo, dentro de un baúl). Para obtener estosinventarios, se hace de la siguiente manera:

I n v e n t o r y i n v e n t a r i o T r o l l = mob i l e ( " t r o l l " ) . g e t I n v e n t o r y ( ) ;I n v e n t o r y i n v e n t a r i o S a l aG r a n d e = room ( " s a l a grande " ) . g e t I n v e n t o r y ( )

;I n v e n t o r y con t en i doBau l = item ( " baú l " ) . g e tCon ten t s ( ) ;

Los inventarios sólo pueden contener objetos de la clase Item, y además tie-nen la particularidad de que tienen un peso y un volumen máximos. Cuando seañadan objetos que hagan que un inventario supere su peso o volumen máximo,se producirá una excepción (WeightLimitExceededException o VolumeLimitExcee-dedException, respectivamente).

Podemos obtener el peso y volumen de un inventario, así como sus límites,con los siguientes métodos:

/∗ c l a s e I n v e n t o r y ∗/ i n t getWeight ( )/∗ c l a s e I n v e n t o r y ∗/ i n t getVolume ( )/∗ c l a s e I n v e n t o r y ∗/ i n t ge tWe igh tL im i t ( )/∗ c l a s e I n v e n t o r y ∗/ i n t getVo lumeL imi t ( )

Estos métodos devuelven, respectivamente: el peso total de los objetos delinventario, su volumen total, el límite máximo de peso del inventario, y su límitemáximo de volumen. Estos límites también se pueden cambiar:

/∗ c l a s e I n v e n t o r y ∗/ vo id s e tWe i gh tL im i t ( i n t newLimit )/∗ c l a s e I n v e n t o r y ∗/ vo id s e tVo lumeL im i t ( i n t newLimit )

Estos dos métodos cambian el límite de peso y el de volumen, respectiva-mente, al valor dado por el parámetro newLimit.

Podemos obtener una representación de un inventario en forma de cadenamediante los siguientes métodos:

/∗ c l a s e I n v e n t o r y ∗/ S t r i n g t o S t r i n g ( )/∗ c l a s e I n v e n t o r y ∗/ S t r i n g t o S t r i n g ( En t i t y v i ewe r )

El primero devuelve una descripción genérica, mientras que el segundo de-vuelve una descripción adaptada a una criatura determinada que se supone quees la que «ve» el inventario (viewer). Esto último es porque, como se verá másadelante, AGE permite definir descripciones dinámicas que cambien según quiénlas ve (por ejemplo, para un monje sabio, un libro podría verse como «El Códicede Antelys» mientras que para un bárbaro inculto el mismo libro podría ser «ungran tomo»). Por lo tanto, siempre será mejor utilizar el segundo método paramostrarle un inventario a una criatura, por si utilizamos descripciones dinámicasde este tipo o en algún momento futuro queremos utilizarlas.

La cadena que devuelven los métodos toString() es una enumeración de lasiguiente forma: si el inventario está vacío, la cadena es «nada.», mientras que sitiene cosas, es de la forma «una espada, una moneda y un escudo.», por ejemplo.Esta cadena es parte de lo que se muestra a los jugadores cuando miran unahabitación que contiene cosas.

Además de los métodos que hemos visto, la clase Inventory también cuen-ta con métodos que permiten agregar y quitar objetos de un inventario. Sinembargo, es muy importante tener en cuenta que el creador de aventuras no

Page 67: Documentacion age

Manejo de arrays y listas 67

debe usarlos para agregar o quitar objetos a una habitación o criatura. En sulugar, deben usarse los métodos de más alto nivel que hemos visto en la sec-ción 2.2 manipulación básica de entidades para mover objetos: por ejemplo,para añadir una cosa al inventario de una habitación, utilizaremos el métodovoid addItem ( Item newItem ) de la clase Room, y para quitarla el métodoboolean removeItem ( Item oldItem ) de la misma clase, que vimos en esasección. Estos métodos tienen el efecto de poner y quitar cosas del inventariode la habitación; pero además hacen otras manipulaciones internas que el AGEnecesita para llevar su control interno de las cosas.

Para otros usos de los inventarios que se verán en futuras secciones, y dondeagregar o quitar objetos de los mismos sí sea válido, los métodos que lo hacenson éstos:

/∗ c l a s e I n v e n t o r y ∗/ vo id addItem ( Item new ) throwsWeightL imi tExceededExcept ion , Vo lumeL imi tExceededExcept ion

/∗ c l a s e I n v e n t o r y ∗/ boolean removeItem ( Item o ld )

El primero añade una cosa dada al inventario, siempre que lo permitan suslímites de peso y volumen tirando una excepción en caso contrario, y el segundoquita una cosa dada del inventario si estaba en él, devolviendo true, o devuelvefalse en el caso de que no estuviese.

Así pues, dejando al margen de momento este último grupo de métodos queno usaremos hasta secciones más avanzadas, el uso principal de la clase Inventoryy sus métodos es el de poder consultar qué objetos tiene una criatura, habitacióno contenedor, y también nos permite generar una descripción del inventario ymanipular los límites de peso y volumen; pero no se debe usar esta clase paramodificar los inventarios sino los métodos que ya vimos de las clases Room, Itemy Mobile.

Veamos algunos ejemplos prácticos de uso de la clase Inventory. Un usosencillo que se nos puede ocurrir sería comprobar si el jugador tiene un objetodeterminado en su inventario. Esto se puede hacer con el método contains delinventario; pero realmente no es necesario porque el método hasItem de la claseMobile que vimos con anterioridad (ver manipulación básica de entidades) noscubre esta necesidad de forma más simple. Sin embargo, hay comprobacionesmás complejas que no se pueden hacer de forma fácil con hasItem.

Por ejemplo, imaginemos que no queremos dejar al jugador internarse en eldesierto si no tiene bebida, y hay distintos objetos en nuestro mundo que puedenservir como bebida (usamos una propiedad «bebida» para marcar esos objetos).En lugar de usar hasItem uno por uno con todos esos objetos para comprobar siel jugador los tiene, cosa que sería bastante farragosa y poco escalable, podemoshacerlo más fácilmente poniendo el siguiente código en el método parseCommanddel mundo que vimos en 2.1 primeros pasos con BeanShell:

vo id parseCommand ( Mobi le aC r ea tu r e , S t r i n g ve rb , S t r i n g a r g s ){

i f ( e qu a l s ( verb , " i r " ) && equa l s ( args , " no r t e " ) && equa l s (aC r ea tu r e . getRoom ( ) , room ( " a l s u r d e l d e s i e r t o " ) ) )

{I n v e n t o r y i n v = jugado r . g e t I n v e n t o r y ( ) ;boolean t i e n eBeb i d a = f a l s e ;f o r ( i n t i = 0 ; i < i n v . s i z e ( ) ; i++ ){

Item cosa = i n v . ge t ( i ) ;i f ( ge t ( cosa , " beb ida " ) )

Page 68: Documentacion age

68 Uso del lenguaje BeanShell

{t i e n eBeb i d a = t rue ;

}}i f ( ! t i e n eBeb i d a ){

j ugado r . w r i t e ( "No puedes a d e n t r a r t e en e l d e s i e r t o s i n beb ida, t e mo r i r í a s de sed . . . \ n" ) ;

end ( ) ;}

}}

Este código se define en el parseCommand del mundo; pero sólo se aplica si eljugador quiere ir al norte desde una habitación dada, cosa que comprobamos conun if. Una forma más sencilla de hacer lo mismo sería utilizar un parseCommandespecífico para una habitación, que no hemos visto en las secciones anteriores.Para ello, seleccionamos en el mapa del PUCK la habitación que esté directa-mente al sur del desierto, vamos a su campo de código y, en el menú contextual,elegimos Insertar código – Redefinir métodos de habitación – Método de análisisde la entrada. Se nos generará una plantilla como ésta:

/∗Método de a n á l i s i s s i n t á c t i c o de l a en t r ada en una h a b i t a c i ó n ∗/vo id parseCommand ( Mobi le aC r ea tu r e , S t r i n g ve rb , S t r i n g a r g s ){

// aCr ea tu r e : c r i a t u r a que i n t r o d u c e un comando .// ve rb : comando que i n t r oduc e , por e j emp lo " coge r "// a r g s : r e s t o de l a orden que i n t r oduc e , por e j emp lo " e l

c u c h i l l o grande "

// t e rm i n a r con end ( ) : i n t e r c ep t amos l a f r a s e , no se e j e c u t al o que se tenga que e j e c u t a r

// por d e f e c t o ante e l l a// t e rm i n a r normal : de spués de nu e s t r o procesado , s e l l e v a a

cabo e l a n á l i s i s normal d e l//comando y e j e c u c i ó n de l a a c c i ón c o r r e s p o n d i e n t e

}

Este método de análisis de la entrada funciona igual que el del mundo perose ejecuta sólo para comandos de jugadores que están en la habitación en laque se define. Esto nos ayuda a compartimentar mejor el código, definiendo elcomportamiento asociado a una habitación en esa habitación en lugar de tenerlotodo junto en el código del mundo. Usando este método parseCommand, podemosimplementar el ejemplo anterior sin tener que comprobar dónde se encuentra eljugador:

vo id parseCommand ( Mobi le aC r ea tu r e , S t r i n g ve rb , S t r i n g a r g s ){

i f ( e qu a l s ( verb , " i r " ) && equa l s ( args , " no r t e " ) ){

I n v e n t o r y i n v = jugado r . g e t I n v e n t o r y ( ) ;boolean t i e n eBeb i d a = f a l s e ;f o r ( i n t i = 0 ; i < i n v . s i z e ( ) ; i++ ){

Item cosa = i n v . ge t ( i ) ;i f ( ge t ( cosa , " beb ida " ) ){

t i e n eBeb i d a = t rue ;

Page 69: Documentacion age

Manejo de arrays y listas 69

}}i f ( ! t i e n eBeb i d a ){

j ugado r . w r i t e ( "No puedes a d e n t r a r t e en e l d e s i e r t o s i n beb ida, t e mo r i r í a s de sed . . . \ n" ) ;

end ( ) ;}

}}

Todavía hay una forma más sencilla de implementar un comportamiento co-mo éste, que sería mediante eventos (código que se ejecuta cuando tiene lugarun hecho determinado en el mundo, como por ejemplo que un jugador quieramoverse). Esto nos permite definir un comportamiento asociado a un hecho in-dependientemente de la orden que pueda provocar ese hecho (o sea, definiríamosel comportamiento «cuando el jugador va hacia el desierto», en lugar de «cuan-do el jugador teclea ir norte en la habitación X»). Veremos cómo hacer este tipode cosas más adelante, en la sección sobre eventos.

Un ejemplo más complicado de utilización de inventarios sería éste, en el queconsultamos el inventario del jugador para ver si tiene algún objeto inflamabley prenderle fuego (podríamos usarlo para activar una trampa de bola de fuego):

I n v e n t o r y i n v = jugado r . g e t I n v e n t o r y ( ) ;f o r ( i n t i = i n v . s i z e ( )−1 ; i >= 0 ; i−− ){

Item cosa = i n v . ge t ( i ) ;i f ( ge t ( cosa , " i n f l ama b l e " ) ){

j ugado r . w r i t e ( " ¡La s l l ama s c a l c i n a n to t a lmen t e " + cosa .getSingName ( j ugado r ) + " !\ n" ) ;

j ugado r . removeItem ( cosa ) ;}

}

En este ejemplo hay algo que nos puede llamar la atención: hemos recorridolos elementos del inventario del último a primero, y no del primero al último.Esto tiene un motivo, y es el siguiente: cuando quitamos una cosa del inventario,todas las otras cosas se «corren» una posición hacia la izquierda. Es decir, sinuestro inventario tiene el siguiente contenido:

inv.get(0) es item("espada")inv.get(1) es item("libro")inv.get(2) es item("pergamino")inv.get(3) es item("escudo")

y hacemos jugador.removeItem("libro"), entonces el inventario se modificaráy quedará así:

inv.get(0) es item("espada")inv.get(1) es item("libro")inv.get(2) es item("escudo")

Por lo tanto, si recorriésemos el inventario hacia adelante, haríamos lo si-guiente: con i = 0miraríamos la espada, con i = 1miraríamos el libro y entonces

Page 70: Documentacion age

70 Uso del lenguaje BeanShell

lo quitaríamos porque es inflamable, pasaríamos a i = 2 . . . y debido al borradoque hemos hecho i = 2 sería el escudo, con lo cual nos habríamos saltado elpergamino sin poder recorrerlo y darnos cuenta de que es inflamable.

Recorrer el inventario hacia atrás es un viejo truco para que los borrados quehacemos no hagan que nos saltemos cosas en el recorrido. En general, siemprehabrá que tener cuidado cuando modifiquemos un inventario a la vez que loestamos recorriendo, para que las modificaciones no afecten al recorrido. Otrotruco diferente sería crear una nueva lista, ir poniendo en ella los objetos infla-mables y finalmente quitárselos al jugador (más abajo veremos cómo podemoscrear una lista genérica de objetos que serviría para este propósito).

Lista de criaturas (MobileList)

La clase MobileList se utiliza para definir listas de criaturas en AGE, porejemplo, la lista de criaturas que hay en una habitación en un momento dadoes un objeto de esta clase. Podemos obtenerla con el método

/∗ c l a s e Room∗/ Mob i l e L i s t g e tMob i l e s ( )

Por ejemplo:

Mob i l e L i s t c r i a t u r a s E nR e c i b i d o r ;c r i a t u r a s E nR e c i b i d o r = room ( " r e c i b i d o r " ) . g e tMob i l e s ( ) ;i f ( c r i a t u r a s E nR e c i b i d o r . s i z e ( ) > 0 ){

Mobi le p r ime ro = c r i a t u r a s E nR e c i b i d o r . ge t (0 ) ; // p r ime ra c r i a t u r aque hay en e l r e c i b i d o r

}

El manejo de la clase es igual que el de Inventory pero más sencillo, pues unaMobileList no tiene límite de peso ni de volumen. Así pues, de estas listas sólonos interesará usar los métodos size(), get() y contains() comunes a todas laslistas, y posiblemente los métodos toString() que funcionan exactamente igualque los de la clase Inventory, es decir:

/∗ c l a s e Mob i l e L i s t ∗/ S t r i n g t o S t r i n g ( )/∗ c l a s e Mob i l e L i s t ∗/ S t r i n g t o S t r i n g ( En t i t y v i ewe r )

que devuelven una descripción textual de la lista, bien genérica en el casodel primer método, o bien para mostrársela a una criatura dada en el caso delsegundo.

Lista genérica (List)

La clase List de Java se utiliza para crear listas genéricas que pueden contenerobjetos de cualquier tipo, al contrario que las listas anteriores (que estabanrestringidas a objetos de la clase Item, en el caso de Inventory, o de la claseMobile, en el caso de MobileList).

Hay métodos de AGE que devuelven listas de la clase List, por ejemplo losmétodos getRelatedEntities() y getRelatedEntitiesByValue() devuelven entidadesrelacionadas con una dada. Así, podemos hacer cosas como:

L i s t co sa sQueGus tanA lTro l l = mob i l e ( " t r o l l " ) .g e tR e l a t e dEn t i t i e sB yVa l u e ( " gus ta " , t rue ) ;

f o r ( i n t i = 0 ; i < cosa sQueGus tanA lTro l l . s i z e ( ) ; i++ )

Page 71: Documentacion age

Manejo de arrays y listas 71

{En t i t y en t i d ad = cosa sQueGus tanA lTro l l . ge t ( i ) ;i f ( e n t i d ad i n s t anceo f I tem ){

j ugado r . w r i t e ( "Al t r o l l l e gus ta " + en t i d ad . getSingName (j ugado r ) ) ;

}}

El código le saca por pantalla al jugador una lista de todas las cosas delmundo que le gustan al troll. En este código, entidad instanceof Item es unacomprobación de si la entidad dada es de la clase Item (ya que podría haberentidades que le gustasen al troll y no fuesen cosas). En general, la operación ainstanceof B devuelve true si el objeto a es de la clase B, y false de lo contrario.

A veces nos interesará crear nosotros mismos listas (en lugar de obtenerlas deun método como getRelatedEntitiesByValue). Las listas nos pueden servir paraalmacenar temporalmente datos que luego procesaremos.

Para crear una lista, podemos hacerlo de la siguiente manera:

L i s t n u e s t r a L i s t a = new A r r a y L i s t ( ) ;

ArrayList es un tipo concreto de lista. Existen más; pero éste es válido paratodos los usos básicos de las listas que nos hagan falta en AGE.

Además de los métodos size(), get() y contains() que hemos visto que soncomunes a todas las listas de AGE, los siguientes métodos nos serán útiles paramanipular listas de tipo List:

/∗ c l a s e L i s t ∗/ vo id add ( Object o )

Añade un objeto al final de la lista (su tamaño, por lo tanto, se incrementaen 1).

/∗ c l a s e L i s t ∗/ boolean remove ( Object o )

Quita el objeto dado de la lista, si existe en ella. En ese caso, devuelve true.Si el objeto no se encuentra, el método no hace nada y devuelve false.

/∗ c l a s e L i s t ∗/ Object s e t ( i n t i , Ob ject new )

Reemplaza el objeto que está en el índice i de la lista por el objeto new,devolviendo el objeto antiguo.

Como ejemplo de manejo de listas, podemos reescribir el código anterior quequemaba los objetos inflamables del jugador, utilizando una lista temporal parano necesitar recorrer el inventario de derecha a izquierda:

I n v e n t o r y i n v = jugado r . g e t I n v e n t o r y ( ) ;L i s t i n f l am a b l e s = new A r r a y L i s t ( ) ;f o r ( i n t i = 0 ; i < i n v . s i z e ( ) ; i++ ){

Item cosa = i n v . ge t ( i ) ;i f ( ge t ( cosa , " i n f l ama b l e " ) )

i n f l am a b l e s . add ( cosa ) ;}f o r ( i n t i = 0 ; i < i n f l am a b l e s . s i z e ( ) ; i++ ){

Item cosa = i n v . ge t ( i ) ;j u gado r . w r i t e ( " ¡La s l l ama s c a l c i n a n to t a lmen t e " + cosa .

getSingName ( j ugado r ) + " !\ n" ) ;

Page 72: Documentacion age

72 Uso del lenguaje BeanShell

j u gado r . removeItem ( cosa ) ;}

Las operaciones básicas con listas que hemos visto aquí deberían ser sufi-cientes para trabajar con conjuntos de objetos en AGE. Sin embargo, la APIestándar de Java cuenta con una variedad mucho más grande de métodos quetrabajan con listas, así como clases de listas (subclases de List diferentes deArrayList) y otras colecciones de objetos (conjuntos, árboles, mapas, tablas hash,etc.)

Los usuarios que sepan programación en Java o que no teman profundizaren ella pueden usar en AGE todas estas clases, su documentación detallada sepuede consultar en la documentación del sistema de colecciones de Java.

2.5. Errores comunes con BeanShell

2.5.1. Mensajes de error

Con el material visto en las secciones anteriores, puedes experimentar crean-do aventuras básicas que utilicen código BeanShell. Como nadie es perfecto, estoseguramente te llevará a cometer algún error. Los errores en el código BeanShellsuelen aparecer en forma de errores de sintaxis o bien de excepciones no captu-radas, que se muestran en las partidas como un texto largo en rojo. He aquí unejemplo de un error de sintaxis:

Syntax e r r o r i n BeanShe l l code i n o b j e c t : [ eu . i r r e a l i t y . age . P l a y e r: 20000000 : e l c h a r a c t e r #3 ]\\

Loaded to c a l l method o n I n i t \\( w i th no arguments ) \\F i l e : i n l i n e e v a l u a t i o n o f : \ c om i l l a s { vo id o n I n i t ( ) { s e t ( "

prop " , number + 3 ) ; } ;}\\Stack t r a c e : Sourced f i l e : i n l i n e e v a l u a t i o n o f : \ c om i l l a s { o n I n i t (

) ; } : i l l e g a l use o f unde f i n ed v a r i a b l e , c l a s s , o r ’ v o i d ’l i t e r a l : a t \\

L ine : 3 : i n f i l e : i n l i n e e v a l u a t i o n o f : \ c om i l l a s { vo id o n I n i t ( ) {s e t ( " prop " , number + 3 ) ; } ; } : ) ; \\

Ca l l e d from method : o n I n i t : a t L i n e : 1 : i n f i l e : i n l i n ee v a l u a t i o n o f : \ c om i l l a s { o n I n i t ( ) ; } : o n I n i t ( ) \\

at bsh . BSHBinaryExpress ion . e v a l (Unknown Source ) \\at bsh . BSHArguments . getArguments (Unknown Source ) \\at bsh . BSHMethodInvocat ion . e v a l (Unknown Source ) \\at bsh . BSHPr imaryExpress ion . e v a l (Unknown Source ) \\at bsh . BSHPr imaryExpress ion . e v a l (Unknown Source ) \\at bsh . BSHBlock . e v a lB l o c k (Unknown Source ) \\at bsh . BSHBlock . e v a l (Unknown Source ) \\at bsh . BshMethod . i n voke Imp l (Unknown Source ) \\at bsh . BshMethod . i n voke (Unknown Source ) \\at bsh . BshMethod . i n voke (Unknown Source ) \\at bsh .Name . invokeLoca lMethod (Unknown Source ) \\at bsh .Name . invokeMethod (Unknown Source ) \\at bsh . BSHMethodInvocat ion . e v a l (Unknown Source ) \\at bsh . BSHPr imaryExpress ion . e v a l (Unknown Source ) \\at bsh . BSHPr imaryExpress ion . e v a l (Unknown Source ) \\at bsh . I n t e r p r e t e r . e v a l (Unknown Source ) \\at bsh . I n t e r p r e t e r . e v a l (Unknown Source ) \\at bsh . I n t e r p r e t e r . e v a l (Unknown Source ) \\at eu . i r r e a l i t y . age . ObjectCode . run ( ObjectCode . j a v a : 197 ) \\

Page 73: Documentacion age

Errores comunes con BeanShell 73

at eu . i r r e a l i t y . age . Mobi le . execCode ( Mobi le . j a v a : 1394 ) \\at eu . i r r e a l i t y . age . Mobi le . const ructMob ( Mobi le . j a v a : 1060 ) \\at eu . i r r e a l i t y . age . Mobi le .< i n i t >(Mobi le . j a v a : 193 ) \\at eu . i r r e a l i t y . age . P l a y e r .< i n i t >(P l a y e r . j a v a : 100 ) \\at eu . i r r e a l i t y . age . World . loadWorldFromXML ( World . j a v a : 810 )

\\at eu . i r r e a l i t y . age . World . loadWorldFromStream ( World . j a v a

: 1416 ) \\at eu . i r r e a l i t y . age . World .< i n i t >(World . j a v a : 1502 ) \\at eu . i r r e a l i t y . age . f i l emanagement . WorldLoader .

loadWorldFromPath ( WorldLoader . j a v a : 3 5 ) \\at eu . i r r e a l i t y . age . f i l emanagement . WorldLoader . loadWor ld (

WorldLoader . j a v a : 150 ) \\at eu . i r r e a l i t y . age . swing . s d i .

Sw ingSDI In t e r f a ce$Loade rThread . run ( Sw i ngSD I I n t e r f a c e .j a v a : 270 ) \\

\\Cause r e p o r t : [ no e x c e p t i o n ]

Cuando nos encontremos un error como éste, es importante saber interpretar(aunque sea aproximadamente) el mensaje, que nos dirá qué tipo de error es ydónde está. Esto facilita mucho el proceso de arreglar los fallos.

Para ver cómo lo interpretamos, analicemos el mensaje:

Syntax e r r o r i n BeanShe l l code i n o b j e c t : [ eu . i r r e a l i t y . age . P l a y e r: 20000000 : e l j ugado r ]

Esta primera línea nos informa de que el error es de sintaxis. A continuación,nos está diciendo en qué campo de código se ha encontrado el error. En estecaso, el error está en el código del jugador.

Loaded to c a l l method o n I n i t( w i th no arguments )

Esto nos dice cuál es el método que ha causado el error: en este caso, elmétodo onInit() del jugador. Se nos aclara también que el método no tieneargumentos (parámetros), si los tuviera aparecerían aquí.

F i l e : i n l i n e e v a l u a t i o n o f : \ c om i l l a s { vo id o n I n i t ( ) { s e t ( "prop " , number + 3 ) ; } ;

Esto nos proporciona todo el código que dio el error, o un resumen. Nosuele ser muy útil porque el resto de información es más detallada, pero puedeayudarnos a hacernos una composición de lugar.

Stack t r a c e : Sourced f i l e : i n l i n e e v a l u a t i o n o f : \ c om i l l a s { o n I n i t () ; } : i l l e g a l use o f unde f i n ed v a r i a b l e , c l a s s , o r ’ v o i d ’l i t e r a l : a t L i n e : 3 : i n f i l e : i n l i n e e v a l u a t i o n o f : \ c om i l l a s{ vo id

Esto, en cambio, es muy útil: nos dice en qué línea del campo de código seha encontrado el error (en este caso, en la línea 3); y qué es el error exactamente(en este caso, «uso ilegal de variable, clase o literal void no definido»). Efecti-vamente, el error es que en la línea 3 hemos utilizado una variable, number, queno habíamos declarado.

Ca l l e d from method : o n I n i t : a t L i n e : 1 : i n f i l e : i n l i n ee v a l u a t i o n o f : \ c om i l l a s { o n I n i t ( ) ; } : o n I n i t ( ) \\

at bsh . BSHBinaryExpres s ion . e v a l (Unknown Source ) \\

Page 74: Documentacion age

74 Uso del lenguaje BeanShell

( . . . )

La primera línea de esta parte nos vuelve a repetir que el error estaba en elmétodo onInit() y nos dice en qué línea empieza el método. A continuación, vieneun volcado de la pila en el momento del error. Esto normalmente no resultarámuy útil, al menos para un uso básico de AGE. Pero si alguna vez se produceun error que no sea culpa vuestra sino de algún bug interno de AGE (esperemosque no; pero nadie es perfecto :)) seguramente el autor de AGE os pida que lemandéis esta información de la pila para localizar el fallo interno.

Otras veces, en lugar de errores de sintaxis, nos podemos encontrar excep-ciones, como ésta:bsh . Ta r g e tE r r o r found at wor ld ’ s parseCommand , command was mirar ,

e r r o r was Sourced f i l e : i n l i n e e v a l u a t i o n o f : \ c om i l l a s {parseCommand ( arg0 , arg1 , a rg2 ) ; } : a t L i ne : 6 : i n f i l e :i n l i n e e v a l u a t i o n o f : \ c om i l l a s {/∗Método de a n á l i s i s s i n t á c t i c ode l a en t r ada ∗/ vo i d parseCommand ( Mobi le aCreat . . . } : i t

. getWeight ( )

Ca l l e d from method : parseCommand : at L i ne : 1 : i n f i l e : i n l i n ee v a l u a t i o n o f : \ c om i l l a s {parseCommand ( arg0 , arg1 , a rg2 ) ; } :parseCommand ( arg0 , a rg1 , a rg2 )

Target e x c e p t i o n : j a v a . l ang . Nu l l P o i n t e r E x c e p t i o n : Nu l l Po i n t e r i nMethod I n v o c a t i o n

∗∗E r r o r : j a v a . l ang . Nu l l P o i n t e r E x c e p t i o n : Nu l l Po i n t e r i n Method

I n v o c a t i o nLoca t i on : i n l i n e e v a l u a t i o n o f : \ c om i l l a s {/∗Método de a n á l i s i s

s i n t á c t i c o de l a en t r ada ∗/ vo i d parseCommand ( Mobi le aCreat . .. }

L i ne : 6Of f end ing t e x t : i t . getWeight ( )Message : Sourced f i l e : i n l i n e e v a l u a t i o n o f : \ c om i l l a s {parseCommand

( arg0 , arg1 , arg2 ) ; }De t a i l e d t r a c e : Sourced f i l e : i n l i n e e v a l u a t i o n o f : \ c om i l l a s {

parseCommand ( arg0 , arg1 , a rg2 ) ; } : a t L i ne : 6 : i n f i l e :i n l i n e e v a l u a t i o n o f : \ c om i l l a s {/∗Método de a n á l i s i s s i n t á c t i c ode l a en t r ada ∗/ vo i d parseCommand ( Mobi le aCreat . . . } : i t

. getWeight ( )

Ca l l e d from method : parseCommand : at L i ne : 1 : i n f i l e : i n l i n ee v a l u a t i o n o f : \ c om i l l a s {parseCommand ( arg0 , arg1 , a rg2 ) ; } :parseCommand ( arg0 , a rg1 , a rg2 )

Target e x c e p t i o n : j a v a . l ang . Nu l l P o i n t e r E x c e p t i o n : Nu l l Po i n t e r i nMethod I n v o c a t i o n

at bsh . U t i l T a r g e t E r r o r . t oE v a l E r r o r (Unknown Source )at bsh . U t i l E v a l E r r o r . t oE v a l E r r o r (Unknown Source )at bsh . BSHMethodInvocat ion . e v a l (Unknown Source )at bsh . BSHPr imaryExpress ion . e v a l (Unknown Source )at bsh . BSHPr imaryExpress ion . e v a l (Unknown Source )at bsh . BSHBlock . e v a lB l o c k (Unknown Source )at bsh . BSHBlock . e v a l (Unknown Source )at bsh . BshMethod . i n voke Imp l (Unknown Source )at bsh . BshMethod . i n voke (Unknown Source )at bsh . BshMethod . i n voke (Unknown Source )at bsh .Name . invokeLoca lMethod (Unknown Source )at bsh .Name . invokeMethod (Unknown Source )at bsh . BSHMethodInvocat ion . e v a l (Unknown Source )at bsh . BSHPr imaryExpress ion . e v a l (Unknown Source )at bsh . BSHPr imaryExpress ion . e v a l (Unknown Source )

Page 75: Documentacion age

Errores comunes con BeanShell 75

at bsh . I n t e r p r e t e r . e v a l (Unknown Source )at bsh . I n t e r p r e t e r . e v a l (Unknown Source )at bsh . I n t e r p r e t e r . e v a l (Unknown Source )at eu . i r r e a l i t y . age . ObjectCode . run ( ObjectCode . j a v a : 348 )at eu . i r r e a l i t y . age . World . execCode ( World . j a v a : 2050 )at eu . i r r e a l i t y . age . P l a y e r . execCommand ( P l a y e r . j a v a : 867 )at eu . i r r e a l i t y . age . P l a y e r . execCommand ( P l a y e r . j a v a : 578 )at eu . i r r e a l i t y . age . P l a y e r . cha r a c t e rChangeS ta t e ( P l a y e r . j a v a

: 2330 )at eu . i r r e a l i t y . age . P l a y e r . changeSta te ( P l a y e r . j a v a : 2167 )at eu . i r r e a l i t y . age . E n t i t y . update ( En t i t y . j a v a : 308 )at eu . i r r e a l i t y . age . P l a y e r . update ( P l a y e r . j a v a : 252 )at eu . i r r e a l i t y . age . E n t i t y . update ( En t i t y . j a v a : 9 4 )at eu . i r r e a l i t y . age . World . update ( World . j a v a : 2582 )at eu . i r r e a l i t y . age . GameEngineThread . run ( GameEngineThread .

j a v a : 294 )

Target r e p o r t : j a v a . l ang . Nu l l P o i n t e r E x c e p t i o n : Nu l l Po i n t e r i nMethod I n v o c a t i o n

at bsh .Name . invokeMethod (Unknown Source )at bsh . BSHMethodInvocat ion . e v a l (Unknown Source )at bsh . BSHPr imaryExpress ion . e v a l (Unknown Source )at bsh . BSHPr imaryExpress ion . e v a l (Unknown Source )at bsh . BSHBlock . e v a lB l o c k (Unknown Source )at bsh . BSHBlock . e v a l (Unknown Source )at bsh . BshMethod . i n voke Imp l (Unknown Source )at bsh . BshMethod . i n voke (Unknown Source )at bsh . BshMethod . i n voke (Unknown Source )at bsh .Name . invokeLoca lMethod (Unknown Source )at bsh .Name . invokeMethod (Unknown Source )at bsh . BSHMethodInvocat ion . e v a l (Unknown Source )at bsh . BSHPr imaryExpress ion . e v a l (Unknown Source )at bsh . BSHPr imaryExpress ion . e v a l (Unknown Source )at bsh . I n t e r p r e t e r . e v a l (Unknown Source )at bsh . I n t e r p r e t e r . e v a l (Unknown Source )at bsh . I n t e r p r e t e r . e v a l (Unknown Source )at eu . i r r e a l i t y . age . ObjectCode . run ( ObjectCode . j a v a : 348 )at eu . i r r e a l i t y . age . World . execCode ( World . j a v a : 2050 )at eu . i r r e a l i t y . age . P l a y e r . execCommand ( P l a y e r . j a v a : 867 )at eu . i r r e a l i t y . age . P l a y e r . execCommand ( P l a y e r . j a v a : 578 )at eu . i r r e a l i t y . age . P l a y e r . cha r a c t e rChangeS ta t e ( P l a y e r . j a v a

: 2330 )at eu . i r r e a l i t y . age . P l a y e r . changeSta te ( P l a y e r . j a v a : 2167 )at eu . i r r e a l i t y . age . E n t i t y . update ( En t i t y . j a v a : 308 )at eu . i r r e a l i t y . age . P l a y e r . update ( P l a y e r . j a v a : 252 )at eu . i r r e a l i t y . age . E n t i t y . update ( En t i t y . j a v a : 9 4 )at eu . i r r e a l i t y . age . World . update ( World . j a v a : 2582 )at eu . i r r e a l i t y . age . GameEngineThread . run ( GameEngineThread .

j a v a : 294 )∗∗

Como vemos, la estructura del error es parecida, aunque se incluye algo másde información de depuración. La primera parte realmente importante es:

bsh . Ta r g e tE r r o r found at wor ld ’ s parseCommand , command was mirar ,e r r o r was Sourced f i l e : i n l i n e e v a l u a t i o n o f : \ c om i l l a s {parseCommand ( arg0 , arg1 , a rg2 ) ; } : a t L i ne : 6 : i n

En primer lugar, se nos dice que se ha encontrado un bsh.TargetError. Estono es más que un nombre genérico para las excepciones, cuando nos aparezcaquiere decir que el error no está en la sintaxis del código, sino en lo que hace.

Page 76: Documentacion age

76 Uso del lenguaje BeanShell

A continuación, se dice que el error está en el parseCommand del mundo, yque el comando introducido fue «mirar». Después se aclara que el error estabaen la línea 6, que contenía el código it.getWeight().

La otra parte importante es la que nos dice qué tipo de excepción es la queha aparecido:

Target e x c e p t i o n : j a v a . l ang . Nu l l P o i n t e r E x c e p t i o n : Nu l l Po i n t e r i nMethod I n v o c a t i o n

Se nos dice que es una excepción de tipo NullPointerException y, más con-cretamente, el texto nos explica: «puntero nulo en invocación a método». Estonos da la pista de lo que ha pasado: que en el código it.getWeight() que tieneel error, la variable it tiene valor null, y no podemos acceder al peso de unavariable nula (que no está guardando realmente ningún objeto).

Como vemos, si los sabemos interpretar, los mensajes de error de BeanShellnos suelen conducir directamente al error que hemos cometido. A continuaciónveremos una recopilación de mensajes de error comunes y lo que significa cadauno.

2.5.2. Tipos de error comunes

Para quienes no se lleven bien con el inglés o no estén muy familiarizadoscon los mensajes que suelen dar los compiladores e intérpretes de lenguajes deprogramación, he aquí una recopilación de los mensajes de error más comunesque se pueden encontrar en BeanShell y qué significan. La inmensa mayoríade los errores que se cometen en BeanShell son de alguno de estos tipos, y sedepuran muy fácilmente conociendo los mensajes:

Syntax e r r o r ( . . . ) Parse e r r o r a t l i n e 5 , column 5 . Encountered :end

Error genérico de sintaxis: el analizador sintáctico de código BeanShell se haencontrado algún elemento en el código que no esperaba. Esto puede suceder,por ejemplo, si nos olvidamos de poner un punto y coma, de cerrar una llave,un paréntesis, etc.

El mensaje nos detalla la línea y la columna en la que se ha encontrado elerror en el código, así como el elemento inesperado que el analizador sintácticose encontró en ese lugar (en este caso, la palabra end). Con respecto a la infor-mación sobre la columna del error, hay que tener en cuenta que si por algúnmotivo indentaras el código con tabuladores, puede no coincidir con el númerode columna que muestra el editor de PUCK. Por defecto, el editor indenta conespacios y de este modo el número de columna es fiable.

También hay que tener en cuenta que a veces los errores de sintaxis se de-tectan en un lugar un poco distinto de donde nosotros hemos cometido el error.Por ejemplo, si nos hemos olvidado de abrir un paréntesis, es posible que elanalizador sintáctico no se dé cuenta en el sitio donde tendríamos que abrirlo,sino más adelante, cuando se encuentre el cierre de paréntesis que le correspon-día y ha quedado huérfano. Por lo tanto, la información de fila y columna esuna orientación que a veces «acertará» dónde estaba el error, pero no podemosfiarnos ciegamente de ella.

Syntax e r r o r ( . . . ) Command not found : nombre ( )

Page 77: Documentacion age

Errores comunes con BeanShell 77

Se ha intentado utilizar una función de BeanShell que no existe. Por ejemplo,si en lugar de equals(a,b) escribiéramos erróneamente equal(a,b); saltaría esteerror.

j a v a . l ang . Nu l l P o i n t e r E x c e p t i o n : Nu l l Po i n t e r i n Method I n v o c a t i o n

Se ha llamado a un método sobre una variable nula. Es decir, se ha hechovariable.metodo() cuando el valor de variable es null. Éste es uno de los erroresmás comunes; pero también de los más fáciles de depurar, pues siempre significaesto, y no hay más que ir a la línea de código que se menciona y ver qué objetoaparece antes de un punto y es null, y ya está, arreglado.

j a v a . l ang . Nu l l P o i n t e r E x c e p t i o n

(pero sin lo de Null Pointer in Method Invocation)Este error se genera porque se ha llamado a algún método que por dentro

ha invocado a su vez otro método sobre una variable null. En la práctica, enuna aventura de AGE esto casi siempre será porque hemos pasado un null co-mo parámetro a una función o método que requiere parámetros no nulos. Porejemplo, si hacemos algo como String s = null; Item it = item(s); se nosprovocará este error porque hemos pasado el parámetro nulo a la función item().

Syntax e r r o r ( . . . ) E r r o r i n method i n v o c a t i o n :Method has I t eem ( eu . i r r e a l i t y . age . I tem ) not found i n c l a s s ’ eu .

i r r e a l i t y . age . P l a y e r ’

Este error se genera porque hemos invocado a un método que no existe. Nospuede pasar tanto porque nos hayamos equivocado en el nombre del método,como en sus parámetros (por ejemplo, un método que requiera un parámetro deun tipo, pero lo llamemos con un parámetro de otro tipo).

El error es muy fácil de depurar porque el mensaje nos muestra exactamente(además de la línea en la que se ha producido) cuál es el nombre y los parámetrosdel método que hemos intentado llamar, y sobre qué clase. Por ejemplo, en estecaso, la causa del error es que escribimos hasIteem en lugar de hasItem. Por esonos dice que no encontró el método hasIteem( eu.irreality.age.Item ) en la clasePlayer.

j a v a . l ang . Ar ray IndexOutOfBoundsExcept ion

Esta excepción ocurre si hemos intentado acceder a una posición ilegal deun array. Por ejemplo, si tenemos un array de tamaño 7 e intentamos acceder aun índice mayor que seis, o menor que cero.

E r r o r : j a v a . l ang . IndexOutOfBoundsExcept ion : I ndex : 3 , S i z e : 3

Esta excepción es análoga a la anterior, pero para listas. Se nos informa delíndice ilegal al que hemos intentado acceder (en este caso, 3) y el tamaño de lalista (también 3). Con esto, y la posición del error en el código, no deberíamostener problema para depurar el fallo.

2.5.3. Funcionalidad de depuración

AGE incluye la siguiente funcionalidad para ayudar a depurar aventuras:

Page 78: Documentacion age

78 Uso del lenguaje BeanShell

Impresión de información de depuración en los parseCommand

Si ejecutamos el siguiente código:

Debug . setCodeDebugging ( t rue ) ;

los parámetros de todos los métodos parseCommand que se llamen desdenuestra aventura aparecerán impresos en la salida de error estándar (que semuestra por defecto en la consola en los scripts de AGE para Linux y Mac, oen un log en el script de Windows).

Debugger dinámico

AGE dispone de un depurador o «debugger» que permite evaluar expresionesBeanShell durante la ejecución de las aventuras. Se puede activar ejecutando elcódigo

Debug . s e tEva lEnab l e d ( t rue ) ;

y una vez activado, podemos utilizar la orden especial «eval» en la aventurapara obtener el valor de una expresión BeanShell en cualquier momento deljuego. Por ejemplo, podríamos hacer

e v a l 1+1e v a l ge t ( mob i l e ( " j ugado r " ) , " cansado " )e v a l i tem ( " pue r t a r o j a " ) . i s C l o s e d ( )e v a l mob i l e ( " g o b l i n " ) . has I tem ( i tem ( " espada " ) )

y nos aparecerá por pantalla el valor de esas expresiones en ese momento.

Breakpoints

Si queremos hacer una depuración de grano más fino, evaluando expresionesno sólo en los momentos en los que podemos introducir un comando sino enmedio de la ejecución del código, podemos utilizar la funcionalidad de «break-points» que proporciona AGE desde su versión 1.1.6b.

Un breakpoint es un punto donde la ejecución de código (en este caso el códi-go BeanShell) se pausa hasta que nosotros le indiquemos que continúe. Mientrasla ejecución está pausada, podemos evaluar expresiones y ver qué valores tomanlas variables en ese punto de la ejecución.

Para poner un breakpoint en nuestro código, sólo tenemos que ejecutar lafunción BeanShell

b r e a kpo i n t ( ) ;

O bien, si queremos darle un nombre al breakpoint para distinguirlo de otros,

b r e a kpo i n t ( "Nombre d e l b r e a kpo i n t " ) ;

Cuando la ejecución del código llegue a ese punto, se nos mostrará unaventana donde podremos ver:

El nombre del breakpoint en la barra de título (de este modo, si tenemosvarios, podemos ver en cuál estamos parados en ese momento).

Page 79: Documentacion age

Errores comunes con BeanShell 79

Un campo de texto donde podemos introducir una expresión o una sen-tencia o serie de sentencias BeanShell y evaluarlas como si se ejecutaranen ese punto del código, obteniendo debajo su valor. Nótese que si las sen-tencias cambian valores de variables o propiedades, estos cambios tendránefecto en el código (es decir, si ponemos i = 1, aparte de obtener el valorde esa expresión –que es 1– también estaremos cambiando el valor de lavariable en la ejecución).

Una lista de las variables locales que hay declaradas y sus valores. Nóteseque, por un problema con BeanShell, en esta lista no se muestran loscontadores declarados en la cabecera de los bucles for. Esto no debería serun problema, ya que se pueden declarar dichos contadores como variablesexternas al bucle y entonces sí se mostrarán.

Un botón que nos permite continuar la ejecución, saliendo del breakpoint.

Page 80: Documentacion age

Capítulo 3

Aspectos avanzados delmodelo de mundo

Con lo que se ha visto en las secciones 1 y 2 de esta documentación, esmás que suficiente para crear aventuras de texto funcionales: hemos visto cómoconstruir un mundo con habitaciones y cosas conectadas entre sí, cómo respondera órdenes del jugador, manipular las entidades (comprobando si un personajetiene una cosa, quitando y poniendo elementos en el mundo, moviendo objetos,etc.) y guardar y manejar datos mediante las propiedades y relaciones. Hemosvisto también cómo se programa en BeanShell, incluyendo todo lo esencial paracrear código que funcione: métodos, estructuras condicionales, bucles, manejode arrays y listas, e interpretación de los mensajes de error del intérprete.

Una vez visto esto, como autor de AGE y de este documento recomiendoque os pongáis manos a la obra y creéis alguna aventura sencillita para ponerloa prueba y cogerle el truco a la herramienta. Y hecho esto, podéis profundizarmás en ésta y las siguientes secciones.

En la presente sección cubriremos algunos aspectos más avanzados del mo-delo de mundo que no hemos visto por anterioridad, y que sirven sobre todopara facilitar la inclusión de objetos y comportamientos que son comunes enlas aventuras y que; aunque se podrían programar «a mano» con lo que hemosvisto hasta ahora, resulta mucho más cómodo y rápido tener predefinidos: porejemplo, el soporte para puertas y objetos abribles y cerrables, descripcionesdinámicas, armas, etc.

Cada parte de esta sección cubre una funcionalidad concreta y cada unade esas partes se pueden leer independientemente de las demás, de forma quepuedes consultar directamente la funcionalidad que te haga falta.

3.1. Descripciones y nombres dinámicos

Un buen juego no se mantiene siempre estático a lo largo de las partidas;sino que cambia con el tiempo. AGE proporciona un sencillo mecanismo dedescripciones y nombres dinámicos que facilita que las descripciones y nombresde habitaciones, cosas y personajes puedan cambiar según las circunstancias.Con esto, se pueden conseguir efectos como:

Page 81: Documentacion age

Descripciones y nombres dinámicos 81

Que la descripción de una calle cambie según si es de día o de noche,

Que cambie según el humor del personaje (lo que para uno es un día alegrepara otro podría ser un día horrible),

Que la descripción y/o nombre de una cosa cambie según su estado (lamisma espada a lo largo de una aventura puede estar limpia o manchadade sangre, o tal vez oxidada),

Que el nombre de un personaje se muestre distinta según si un jugadorlo conoce o no (el jugador podría ver a un personaje como «un hombrede mediana edad» cuando no sabe quién es, y una vez que se lo hanpresentado verlo como «Benito»),

Que el nombre o descripción de una cosa se muestre distinto según losconocimientos de un jugador (lo que para un bárbaro iletrado es «unlibro muy grande», para un mago podría ser «un tomo sobre magia decombate»).

Todo esto se consigue de forma sencilla especificando condiciones en las áreasde nombres y descripciones de PUCK.

3.1.1. Descripciones dinámicasLas descripciones dinámicas se pueden definir tanto para habitaciones como

para cosas y personajes. Esto se hace introduciendo una lista de condicionesy descripciones en el cuadro «Descripciones» de la ficha «General» de estasentidades. Para añadir un elemento a esta lista, introducimos una condición enel campo «Condición», la descripción asociada en el campo «Descripción», ypulsamos Añadir. La condición que introduzcamos en el campo «Condición» esun pedazo de código BeanShell con las siguientes características:

Tiene que ser una expresión booleana, es decir, un trozo de código simple(sin estructuras if, bucles ni puntos y coma; aunque sí puede tener llamadasa métodos, operaciones, etc.) y que devuelva un valor de tipo boolean (trueo false). Dicho con otras palabras, la condición puede ser cualquier trozode código que pudiese ir como condición de un if.

Podemos usar la variable self para referirnos al objeto en que estamosdefiniendo la descripción, y la variable world para referirnos al mundo,exactamente igual que en los métodos definidos mediante BeanShell.

Adicionalmente, en estos campos de condición tenemos una variable espe-cial viewer que podemos utilizar para referirnos al jugador o criatura queestá mirando aquello que describimos. Esto se puede usar, por ejemplo, enlos ejemplos de aplicación que proponíamos antes para mostrar diferen-tes descripciones a un jugador según su humor o según sus conocimientossobre la entidad.

Así, ejemplos de posibles condiciones podrían ser:get(self,.oxidada") – esta descripción es sólo para si la cosa que describimos

(una espada, por ejemplo) está oxidada.!get(self,.oxidada") – lo contrario, se muestra sólo si la espada no está oxidada.

Page 82: Documentacion age

82 Aspectos avanzados del modelo de mundo

get(viewer,çonoce",self) – descripción que se muestra sólo si el jugador queestá mirando al personaje que describimos ya lo conoce (hecho descrito por unarelación «conoce»).

get(viewer,"humor") >5 – mostrar esa descripción sólo si el humor del jugadorque mira es mayor que 5 (si hemos definido el humor como una propiedad detipo int que toma valores de 0 a 10, por ejemplo, esto sería una descripción quesólo se ve si estás de buen humor).

Cuando un jugador mire la entidad cuya descripción dinámica hemos defi-nido de esta manera, la descripción que se mostrará al jugador será la concate-nación de todas aquellas descripciones de la lista cuyas condiciones asociadas secumplan. Nótese que esto quiere decir que podemos construir descripciones diná-micas «a trozos», dividiéndolas en partes con distintas condiciones. Un ejemplode esto sería el siguiente, en una habitación:

Condición: en blanco («siempre»), descripción: Te encuentras en una habi-tación de hotel.

Condición: get(self,"luzEncendida"), descripción: Es una habitación pequeñay sencilla: sólo tiene una cama, un armario y una pequeña mesilla de noche.

Condición: !get(self,"luzEncendida"), descripción: La luz está apagada, y laescasa luz de las farolas que entra desde la ventana apenas te deja intuir lasilueta de una cama.

Condición: ( get(self,"luzEncendida") && get(viewer,"humor") > 5 ),descripción: En una de las paredes hay un cuadro de dudoso gusto que muestraun hombre a caballo.

Condición: ( get(self,"luzEncendida") && get(viewer,"humor") <= 5 ),descripción: En una de las paredes hay un cuadro de un hombre a caballo, tanhorriblemente pintado que dan ganas de tirarlo por la ventana.

Así, is por ejemplo la luz estuviera encendida y el humor del jugador fuese2, se le mostraría esta descripción de la habitación:

Te encuentras en una habitación de hotel. Es una habitación pequeña y sencilla:sólo tiene una cama, un armario y una pequeña mesilla de noche. En una de lasparedes hay un cuadro de un hombre a caballo, tan horriblemente pintado que danganas de tirarlo por la ventana.

Mientras que si la luz estuviese apagada, se mostraríaTe encuentras en una habitación de hotel. La luz está apagada, y la escasa luz

de las farolas que entra desde la ventana apenas te deja intuir la silueta de unacama.

3.1.2. Nombres dinámicos

El mismo mecanismo que hemos explicado para las descripciones de habita-ciones, cosas y criaturas se aplica también a los nombres singulares y pluralespara mostrar de las cosas y criaturas. Las características que deben cumplir lascondiciones en este caso son las mismas que para las descripciones, incluida laposibilidad de utilizar la variable especial viewer.

Hay, sin embargo, una diferencia: los nombres dinámicos que se muestrana los jugadores no se construyen «a trozos» como las descripciones, ya que esono tendría mucha utilidad, sino que simplemente se muestra el primer nombrecuya condición se cumpla. Si hay más de un nombre cuya condición se cumple,el resto se ignorarán.

Page 83: Documentacion age

Cosas abribles y cerrables 83

3.2. Cosas abribles y cerrables

En muchos juegos basados en texto aparecen objetos que pueden abrirse ycerrarse. Puertas, ventanas, baúles, cajas fuertes o maletas son ejemplos típicosde objetos así. El AGE proporciona soporte para crear fácilmente tales objetos,que los jugadores y criaturas pueden abrir y cerrar con o sin necesitar para ellouna llave, según cómo están configurados.

Grosso modo, el sistema de cosas abribles y cerrables de AGE funciona dela siguiente manera:

En un momento dado, una cosa puede estar abierta o cerrada.

Además, una cosa que está cerrada puede estar cerrada con llave o no.

La orden cerrar cosa introducida por un jugador sirve para cerrar algoque está abierto, la orden abrir cosa para abrir algo que está cerrado. Esposible definir condiciones para que estos comandos funcionen o no segúnel criterio que se quiera (por ejemplo, una puerta que no se pueda abrirsin más porque está atrancada, etc.)

La orden abrir cosa con llave introducida por un jugador sirve para quitarel cerrojo a algo que está cerrado con llave, y la orden cerrar cosa conllave hace lo contrario. AGE permite definir qué cosa del juego es la llavede cada cosa abrible/cerrable con llave. Asimismo, también se puedendefinir condiciones para que estos comandos funcionen o no (además de lacondición implícita de tener la llave).

En cada cosa, se puede marcar individual e independientemente si es abri-ble, cerrable, abrible con llave y cerrable con llave. Esto nos permite laflexibilidad de definir, por ejemplo, algo que se puede abrir pero no sepuede volver a cerrar una vez abierto; o también de obviar el sistema dellaves si no las necesitamos en nuestro juego.

Se pueden crear puertas especificando que una cosa es la puerta asociadaa un camino, de forma que no se dejará pasar al jugador por ella cuandoesté cerrada.

Si una cosa cerrada es un contenedor, no permitirá a los jugadores mani-pular lo que haya en su interior hasta que se abra.

3.2.1. Definiendo cosas abribles y cerrables

Para definir una cosa abrible y/o cerrable en nuestro mundo de AGE, crea-mos una cosa en el PUCK de forma normal (rellenando su nombre único, nom-bres de referencia y demás campos útiles como de costumbre) y a continuaciónvamos a la ficha llamada «Abrir/Cerrar» de su panel de objeto.

Lo primero que vemos en esta ficha son cuatro cuadros que podemos mar-car para determinar si la cosa es «Abrible», «Cerrable», «Cerrable con llave» y«Abrible con llave», respectivamente. Así, por ejemplo, si queremos crear unacaja que se pueda abrir y cerrar pero que no necesite una llave para ello, marca-remos los dos primeros cuadros. Si lo que queremos es una puerta que ademástenga una llave que tenga una cerradura con llave, los marcaremos todos. Es

Page 84: Documentacion age

84 Aspectos avanzados del modelo de mundo

perfectamente posible marcar cualquier combinación de acciones posibles: porejemplo, un plástico donde venga envuelto un CD de música podría modelarsecomo un objeto abrible, pero no cerrable (una vez que lo rompemos, no podemosdevolverlo a su estado original).

A continuación vemos cuatro formularios para textos dinámicos asociadasa cada una de las cuatro acciones: abrir, cerrar, cerrar con llave y abrir conllave. Como es lógico, sólo tendremos que cubrir los campos correspondientes alas acciones que hayamos marcado como posibles en los cuadros anteriores (porejemplo, a abrir y cerrar en el caso de la caja).

El formulario de estos textos dinámicos es análogo al de las descripciones ynombres dinámicos; pero con un añadido: además de especificar una condicióny un texto asociado, también tenemos la posibilidad de marcar o no una casillallamada «Con éxito». Esta casilla determina si la acción correspondiente (abrir,cerrar, etc.) se llevará a cabo con éxito o no, en el caso de que la condiciónasociada sea cierta. De este modo, en el formulario de «Texto al abrir» no sóloestamos definiendo el texto en sí, sino también las condiciones que son necesariaspara que la cosa se pueda abrir; y lo análogo para «Texto al cerrar» y el restode formularios similares.

Más en detalle, cuando un jugador teclea una orden abrir sobre una cosa,AGE hace lo siguiente:

1. Si la cosa no está marcada como «Abrible», se le dice al jugador que notiene sentido abrir eso.

2. Si la cosa sí está marcada como «Abrible», se recorren las entradas delformulario «Texto al abrir» comprobando si se cumple la condición dealguna de ellas. En el caso de que se cumpla la condición de una de ellas:

a) Si esa entrada del formulario era de «Éxito» (se marcó la casilla «Conéxito»), entonces se muestra el mensaje correspondiente, y la cosa seabre.

b) Si esa entrada del formulario era sin éxito (no se marcó la casilla«Con éxito»), entonces se muestra el mensaje correspondiente; perono se hace ningún cambio en el estado de la cosa.

El funcionamiento de cerrar es análogo, trabajando en cada caso con la casillay el formulario correspondientes a esa orden.

Las condiciones que se pueden teclear en estos formularios son expresionesbooleanas en BeanShell, exactamente igual que para las descripciones y nombresdinámicos. A menudo, para escribir estas condiciones necesitaremos tener unaforma de saber si una cosa está abierta o cerrada, y si está cerrada con llave ono. Para ello, podemos usar los siguientes métodos de la clase Item:

/∗ c l a s e Item ∗/ boolean i sOpen ( )/∗ c l a s e Item ∗/ boolean i s C l o s e d ( )/∗ c l a s e Item ∗/ boolean i s Lo ck ed ( )/∗ c l a s e Item ∗/ boolean i sUn l o c k ed ( )

Estos métodos nos permiten comprobar el estado de las cosas abribles/ce-rrables: son métodos booleanos que devuelven true si la cosa sobre la que losejecutamos está abierta, cerrada, cerrada con llave o no cerrada con llave, res-pectivamente; y false de lo contrario.

Page 85: Documentacion age

Cosas abribles y cerrables 85

De este modo, por ejemplo, podemos definir una caja que se abra y se cierremarcándola como abrible y cerrable en la ficha correspondiente, y luego intro-duciendo lo siguiente en los formularios:

Texto al abrir 1:

• Condición: self.isClosed()

• Éxito: sí

• Texto: Abres la caja.

Texto al abrir 2:

• Condición: self.isOpen()

• Éxito: no

• Texto: ¡No puedes abrir la caja porque ya está abierta!

Texto al cerrar 1:

• Condición: self.isClosed()

• Éxito: no

• Texto: ¡No puedes cerrar la caja porque ya está cerrada!

Texto al cerrar 2:

• Condición: self.isOpen()

• Éxito: sí

• Texto: Cierras la caja.

De esta forma, conseguimos una caja que funciona de la manera más nor-mal: se puede abrir si está cerrada, se puede cerrar si está abierta, y las otrasposibles combinaciones (como abrirla si ya está abierta) no funcionan y nos danun mensaje que podemos personalizar. Sin embargo, el sistema de condicionestambién nos da la posibilidad de crear cosas abribles y cerrables que funcionende maneras más extrañas, si es necesario.

Para definir el estado inicial de un objeto abrible/cerrable, podemos utilizarla propiedad closed: si vamos a la ficha «Propiedades» del panel de nuestra cajay añadimos una propiedad closed con valor true y tiempo restante −1 (infinito),la caja comenzará estando cerrada. De lo contrario, empezará abierta hasta quealguien la cierre.

3.2.2. Llaves

Si además queremos que para abrir una cosa abrible o cerrable haga faltatener en posesión otro objeto (que comúnmente llamamos llave), podemos ha-cerlo marcando la cosa como abrible/cerrable con llave y utilizando los otrosdos formularios, que procesan las órdenes de tipo cerrar X con Y y abrir X conY. Éstos funcionan exactamente igual que los formularios para abrir X y cerrar Xque hemos visto antes, con la salvedad de que AGE nos proporciona una manerade definir qué objeto u objetos Y son los que sirven para abrir X.

Page 86: Documentacion age

86 Aspectos avanzados del modelo de mundo

Para hacer esto, creamos una relación (flecha) que vaya de X (el objeto quese abre) a Y (su llave asociada), y en el panel de la relación, donde se nospregunta «Relación estructural», marcamos «Se abre con» para expresar queX se abre con Y . Es posible definir de esta manera varias llaves que abran unamisma puerta, o varias puertas que se abran con una misma llave, creando variasflechas.

De este modo, cuando un jugador teclea una orden abrir X con Y, si X e Yson cosas válidas y que están al alcance del jugador, AGE hace lo siguiente:

1. Si la cosa X no está marcada como «Abrible con llave», se le dice aljugador que no tiene sentido abrir eso con llave.

2. Si la cosa X está marcada como «Abrible con llave», se recorren las des-cripciones del formulario «Texto al abrir con llave», procediendo de formaanáloga a como se hacía para el formulario «Texto al abrir»; pero con unadiferencia, que es la siguiente: si el objeto Y no es una llave válida paraabrir X, sólo se considerarán las descripciones «Sin éxito»; mientras quesi el objeto Y sí es una de las llaves válidas para abrir X, se considerarántanto las descripciones «Sin éxito» como aquellas «Con éxito».

De esta manera, AGE nos comprueba automáticamente que el jugador llevala llave adecuada, y hace que la acción de abrir con llave no tenga éxito en casocontrario; pero seguimos pudiendo utilizar las condiciones de las descripcionespara determinar si hay éxito o fracaso en el caso de que el jugador sí tenga la llave(por ejemplo, tal vez el jugador tiene la llave pero la puerta está atrancada...)

La acción de abrir con llave se comporta de forma totalmente análoga a laacción cerrar con llave, pero usando los formularios correspondientes.

La propiedad ’locked’ nos permite definir inicialmente si una cosa va a estarcerrada con llave o no: poniendo dicha propiedad a ’true’ y con temporizador−1 en la ficha de «Propiedades» de una cosa, nos aseguraremos de que empiececerrada con llave al principio del juego.

Nótese que con el sistema de apertura y cierre con llave aquí descrito seimplementa por defecto el manejo de llaves típico de los juegos americanosclásicos, en los que para abrir una puerta primero hay que «abrirla con llave»(unlock), que corresponde sólo a girar la llave, y después «abrirla» realmente(open). En el mundo hispanohablante, muchos autores actuales prefieren unsistema más simple en el que al poner «abrir puerta» ya se abra con llaveautomáticamente (incluyendo las acciones unlock+open) en el caso de que eljugador tenga la llave. Este sistema de apertura y cierre fácil de conseguir enAGE, simplemente ignorando las casillas y formularios de «Abrible con llave» y«Cerrable con llave» y definiendo las condiciones del «abrir» y «cerrar» normalespara que miren si el jugador tiene la llave. Otras variantes se pueden definir deforma similar.

3.2.3. Puertas

Un uso común de las cosas abribles o cerrables es que sirvan como puertasde acceso entre una localidad y otra, de forma que haya sea necesario abrir lapuerta para atravesar el camino correspondiente.

Para definir una puerta entre dos habitaciones, hacemos lo siguiente:

Page 87: Documentacion age

Contenedores 87

Creamos una cosa abrible/cerrable que represente la puerta. Podemos ha-cerlo de forma similar a la caja vista en el ejemplo anterior, o podemoshacer cambios (como añadir la funcionalidad de abrir/cerrar con llave)según cómo queramos que funcione la puerta.

Hacemos click en el camino o caminos entre las dos localidades y, en laficha «Relación estructural» de su panel de relación, vamos a donde pone«Puerta:» y seleccionamos nuestra entidad puerta. Lo que hace esto esvincular el camino con la puerta de forma que si el jugador intenta andarpor ese camino y la puerta está cerrada, fracasará en su empeño.

Añadimos la puerta a las dos localidades (mediante flechas de cada una delas localidades a la puerta). Si no hacemos esto, el camino estaría vinculadoa la puerta pero ésta no sería accesible desde las localidades, de maneraque no podríamos abrirla o cerrarla, por ejemplo.

Aunque no es necesario para que la puerta funcione, muchas veces será con-veniente no ponerle nombres para mostrar. Esto hace que no se muestre tras ladescripción de las habitaciones (o sea, que no aparezcan cosas como «Aquí hayuna espada, un escudo y una puerta roja», cosa que suele ser antinatural). Porsupuesto, sí que es necesario que la puerta tenga algún nombre de referenciapara poder manipularla.

3.2.4. Contenedores abribles y cerrables

Otro uso común para las cosas abribles o cerrables es servir para modelarobjetos que hay que abrir para sacar otros objetos de su interior, como baúles ocajones. Para hacer esto, basta con crear un objeto abrible y/o cerrable (comola caja vista anteriormente) y definirlo a la vez como contenedor, y AGE seencargará automáticamente de que sólo se pueda acceder a los objetos que con-tiene cuando esté abierta. En la siguiente sección, contenedores, veremos cómose define una cosa como contenedor.

3.3. Contenedores

Los contenedores son cosas que pueden contener otras cosas, y en donde esposible poner nuevas cosas o quitar las que ya hay.

Así, los contenedores se pueden utilizar para modelar objetos que puedentener a otros en su interior, como un baúl o una maleta; o también para objetosque pueden tener otros encima, como una mesa.

3.3.1. Definición y uso de contenedores

Definir un contenedor con PUCK no podría ser más sencillo: simplementecreamos una cosa y, en la ficha «General» de su panel de entidad, marcamos lacasilla que pone «Contenedor». Con eso, la cosa queda marcada como contenedory se podrán meter otras cosas en ella.

Para que el contenedor empiece el juego conteniendo alguna cosa, no tenemosmás que usar PUCK para crear una relación estructural de tipo «contiene» quevaya del contenedor a la cosa contenida. Éste es el tipo de relación que se crea

Page 88: Documentacion age

88 Aspectos avanzados del modelo de mundo

por defecto cuando creamos una flecha que va de una cosa a otra, así que nonecesitaremos ir a ningún panel sino que con crear la flecha será suficiente.

Por defecto, los usuarios pueden usar las órdenes poner/meter cosa en con-tenedor y coger/sacar/quitar cosa de contenedor para poner y quitar cosas de loscontenedores. En el caso de que un contenedor sea cerrable, esto sólo se podráhacer cuando está abierto.

A la hora de capturar estas acciones en métodos de análisis de la entrada,es útil saber que los verbos «quitar» y «sacar» se traducen automáticamentea «coger», con lo cual con capturar el verbo «coger» es suficiente. Del mismomodo, el verbo «meter» se traduce automáticamente a «poner», así que capturareste último es suficiente.

A la hora de definir la descripción de un contenedor, a menudo interesará queen ella se muestren los objetos que contiene. Por ejemplo, puede interesar quela descripción de un baúl abierto sea «Es un bonito baúl. Está abierto. En suinterior hay una espada y un escudo.» Para conseguir este efecto, lo más sencilloes utilizar en las descripciones del contenedor la palabra clave%INVENTORY,que se sustituirá por la descripción de su inventario. Así, una descripción comola anterior se conseguiría poniendo como descripción «Es un bonito baúl. Es-tá abierto. En su interior hay%INVENTORY». Jugando con las descripcionesdinámicas y el método isClosed() visto en la sección 3.2.1 sobre cosas abriblesy cerrables, podemos conseguir que los objetos que hay dentro del baúl só-lo se muestren cuando está abierto. Pero el mecanismo es muy flexible: si ensu lugar tuviésemos una caja transparente, podríamos poner la palabra cla-ve%INVENTORY tanto en la descripción que se muestra cuando está abiertacomo en la correspondiente a cuando está cerrada, para que se puedan ver losobjetos que contiene a través del cristal.

En aventuras que utilicen contenedores, a menudo nos interesará consultary manipular su contenido mediante código BeanShell: igual que en la secciónsobre manipulación básica de entidades veíamos cómo enterarnos de qué cosashabía en una habitación o en el inventario de una criatura, así como ponerlasy quitarlas; lo mismo se puede hacer con los contenedores. El siguiente métodoBeanShell es útil para ello:/∗ c l a s e Item ∗/ I n v e n t o r y ge tConten t s ( )

Invocado sobre un contenedor c, este método nos devuelve un inventariode su contenido. Este inventario se representa mediante un objeto de la claseInventory (véase 2.4.2 Inventario (Inventory)), y podemos utilizar los métodos dedicha clase no sólo para comprobar si una cosa dada está dentro del contenedor,sino también para añadirle y quitarle cosas.

Nótese que esto contrasta con añadir y quitar cosas de los inventarios dehabitaciones y criaturas, donde vimos que era mejor hacerlo mediante métodosespecíficos en lugar de trabajar con la clase Inventory.

Con esto tenemos todo lo necesario para crear juegos que trabajen con con-tenedores. Sin embargo, es recomendable tener en cuenta una advertencia: no esrecomendable abusar de este recurso. Aunque los contenedores son interesantescuando se integran de forma natural en el argumento de un juego o en los acer-tijos que plantea (por ejemplo, si es necesario encontrar la llave de un baúl paraobtener una poderosa arma); usados en exceso para modelar en el mundo cosasque no los necesitan tiende a hacer que los juegos resulten menos naturales ymás incómodos para los jugadores.

Page 89: Documentacion age

Contenedores 89

Por ejemplo, si tienes un mundo donde para cortar un chorizo es necesario«coger cuchillo de mesa», «coger tabla de cajón», «poner tabla en mesa», «ponerchorizo en tabla» y «cortar chorizo con cuchillo»; en la mayoría de los casos esenivel de detalle no va a aportar nada al argumento o a los acertijos salvo aburriral jugador. Sería un mejor diseño no usar contenedores en absoluto y que sepudiese «cortar chorizo con cuchillo» directamente, poniendo si se quiere untexto descriptivo que diga que el jugador ha usado una tabla.

Este sobreuso de contenedores es, en la opinión personal del autor, uno de losvicios de diseño más comunes que se cometen al crear juegos basados en texto(e incluso otros tipos de juego); y debería evitarse utilizando los contenedoressólo cuando sean estrictamente necesarios y aporten algo al argumento del juegoque no sería posible sin ellos. Es decir, hablando en plata, casi nunca.

3.3.2. Acciones sobre objetos contenidosPor defecto, sobre una cosa que está dentro de un contenedor no se puede

hacer nada sin sacarla antes del contenedor, excepto mirarla: por ejemplo, si hayun libro dentro de un baúl, el jugador tendrá que primero coger libro o sacarlibro de baúl para a continuación poder leerlo, no podrá hacerlo directamente.

Esto quiere decir que, por defecto, las únicas acciones que se pueden llevara cabo sobre objetos dentro de contenedores son las acciones coger y mirar pordefecto de AGE, el resto no funcionarán. Esto incluye también a las accionesque se definan o redefinan mediante los métodos de análisis de la entrada vistosen capítulos anteriores.

Sin embargo, si se quieren definir acciones que sí funcionen sobre objetos queestén dentro de contenedores, también se puede hacer. Aunque no se recomien-da utilizar esta funcionalidad en general porque suele ser síntoma de un diseñocon contenedores innecesarios como se explicaba en la sección anterior, existela posibilidad de hacerlo. Para ello, en los menús del PUCK, cada uno de losmétodos de análisis de la entrada que hemos visto en los capítulos anteriorestiene una versión que pone entre paréntesis (para contenedores y objetos con-tenidos). Ésta es la versión que debemos seleccionar para definir acciones queactúen directamente sobre cosas que estén dentro de contenedores.

Por ejemplo, supongamos que queremos que se pueda «cortar chorizo concuchillo» a pesar de que el chorizo esté en un contenedor (por ejemplo, unatabla de cortar). Para ello, si queremos definir la acción en el chorizo (tambiénpodríamos alternativamente hacerlo en el cuchillo), vamos a su campo de códigoy, en los menús, seleccionamos Insertar código – Redefinir métodos de cosa –Método de análisis de la entrada (para contenedores y objetos contenidos) –Referente a ésta y otra cosa, en ese orden. Obtenemos la siguiente plantilla:

/∗Método de a n á l i s i s s i n t á c t i c o de l a en t r ada r e f e r i d a a dos cosas ,que pueden o no e s t a r den t ro de o t r a s ∗/

/∗ Este método se e j e c u t a :− Cuando e l j ugado r i n voca una orden sob r e dos ob j e t o s , e l

p r ime ro de l o s c u a l e s e s é s t e ( e s t é n o no e s t o s o b j e t o sden t ro de un contenedo r ) .

− Cuando e l j ugado r i n voca una orden sob r e dos ob j e t o s , e lp r ime ro de l o s c u a l e s e s t á con t en i do en é s t e .

∗/vo id parseCommandOnContentsObj1 ( Mobi le aC r ea tu r e , S t r i n g ve rb ,

S t r i n g a rg s1 , S t r i n g a rg s2 , L i s t path1 , L i s t path2 , E n t i t yob j2 )

Page 90: Documentacion age

90 Aspectos avanzados del modelo de mundo

{

// aCr ea tu r e : c r i a t u r a que i n t r o d u c e un comando .// ve rb : comando que i n t r oduc e , por e j emp lo " a f i l a r "// a rg s1 : p a r t e de l a orden que se r e f i e r e a un p r ime r

ob j e to , por e j emp lo " e l c u c h i l l o " . Ese p r ime r ob j e t o esé s t e o e s t á con t en i do

// en é s t e .// a rg s2 : p a r t e de l a orden que se r e f i e r e a un segundo

ob j e to , por e j emp lo "con e l a f i l a d o r "// path1 : camino de con t enedo r e s desde e l p r ime r ob j e t o a l

que r e f e r e n c i a l a orden . Por e jemplo , s i i n t r o d u j o "a f i l a r e l c u c h i l l o con e l

// a f i l a d o r " y e l c u c h i l l o e s t á en una ca ja , s e r á [c u c h i l l o , c a j a ] .

// path2 : camino de con t enedo r e s desde e l segundo ob j e t o a lque r e f e r e n c i a l a orden . Por e jemplo , s i i n t r o d u j o "a f i l a r e l c u c h i l l o con e l

// a f i l a d o r " y e l a f i l a d o r no e s t á den t ro de nada ,s e r á [ a f i l a d o r ] .

// ob j2 : segundo ob j e t o a l que se r e f i e r e l a a c c i ón d e lj ugado r ( en e l e jemplo , e l o b j e t o a f i l a d o r ) .

// t e rm i n a r con end ( ) : i n t e r c ep t amos l a f r a s e , no se e j e c u t al o que se tenga que e j e c u t a r

// por d e f e c t o ante e l l a// t e rm i n a r normal : de spués de nu e s t r o procesado , s e l l e v a a

cabo e l a n á l i s i s normal d e l//comando y e j e c u c i ó n de l a a c c i ón c o r r e s p o n d i e n t e

}

Los métodos de análisis de la entrada para contenedores y objetos contenidosse parecen a sus análogos estándar vistos anteriormente, pero son más complejos.En concreto, las diferencias son éstas:

1. La primera es la diferencia obvia: el método no sólo se ejecutará cuandoel jugador teclee una acción referente al objeto en que lo definimos y ésteesté en el inventario del jugador o en el de la habitación en la que está éste;sino también cuando el objeto esté en un contenedor que a esté en estosinventarios (o bien en un contenedor que a su vez esté en otro contenedorque esté en estos inventarios, etc.)

2. Además, el método se nos ejecutará tanto cuando el jugador teclee unaacción referente al objeto en que lo definimos (siempre que esté en uno delos lugares dichos anteriormente) como cuando teclee una acción referentea un objeto contenido en aquél en que lo definimos. Esto nos da la flexi-bilidad de permitir capturar acciones sobre todo lo que esté contenido enun contenedor (por ejemplo, que al coger cualquier objeto que esté dentrode un baúl, salte una trampa en dicho baúl).

3. Los métodos de análisis para contenedores y objetos contenidos nos pasanunos parámetros path que nos dan el camino completo de contenedoresque va desde el objeto al que se refirió el jugador hasta el contenedormás externo. Por ejemplo, si el jugador ha puesto «cortar chorizo concuchillo» y el chorizo está en una caja que a su vez está dentro de un

Page 91: Documentacion age

Contenedores 91

baúl, path1 sería una lista de longitud 3 tal que path1.get(0) sería el Itemchorizo, path1.get(1) sería la caja y path1.get(2) sería el baúl. En ese mismoejemplo, path2 sería una lista de longitud 1 que sólo contendría el cuchillo(si no está contenido en nada).

4. Para obtener las entidades a las que realmente se refirió el jugador, po-demos utilizar path1.get(0) y path2.get(0). Nótese que path1.get(0) no ne-cesariamente coincide con self (puede ser en su lugar un objeto contenidoen self, por razón de lo explicado en el punto 2). Así pues, si queremosasegurarnos de definir una acción que afecte sólo al objeto en el que sedefine y no a los que puedan estar contenidos en él, tendríamos que haceruna comprobación tipo equals(path1.get(0),self).

El resto de métodos de análisis de la entrada para contenedores y objetoscontenidos se comportan análogamente a éste, teniendo dos parámetros tipo Listen el caso de métodos para dos entidades, o uno en el caso de métodos para unaentidad.

De este modo, podríamos definir la acción de cortar el chorizo con el cuchillode la siguiente manera (IMPORTANTE: no debe tomarse este ejemplo comorecomendación del autor de cómo hacer un juego, ya que el autor es de laopinión de que este tipo de usos barrocos de los contenedores no se deberíautilizar nunca; pero lo incluye en esta documentación por completitud):

vo id parseCommandOnContentsObj1 ( Mobi le aC r ea tu r e , S t r i n g ve rb ,S t r i n g a rg s1 , S t r i n g a rg s2 , L i s t path1 , L i s t path2 , E n t i t yob j2 )

{

i f ( e qu a l s ( s e l f , path1 . ge t (0 ) ) ) // ac c i ón r e f e r i d a a lc h o r i z o

{i f ( e qu a l s ( path1 . s i z e ( ) , 1 ) | | ! e q u a l s ( i tem ( " t a b l a " ) ,

path1 . ge t (1 ) ) ) // s i e l c h o r i z o no e s t á en l a t a b l a{

aCrea tu r e . w r i t eD e n i a l ( "Para c o r t a r e l c ho r i z o , d e b e r í a spon e r l o en una t a b l a p r ime ro . \ n" ) ;

end ( ) ;}e l s e i f ( e qu a l s ( path2 . ge t (0 ) , i tem ( " c u c h i l l o " ) ) ) // s i

e s t á en l a t a b l a y cortamos con e l c u c h i l l o{

i f ( e qu a l s ( path2 . s i z e ( ) , 1 ) ) // e l c u c h i l l o no e s t áden t ro de nada

{aCrea tu r e . w r i t eA c t i o n ( " Cor ta s e l c h o r i z o con e l c u c h i l l o

l imp iamente sob r e l a t a b l a . \ n" ) ;i tem ( " t a b l a " ) . g e t I n v e n t o r y ( ) . removeItem ( i tem ( " c h o r i z o " ) ) ;i tem ( " t a b l a " ) . g e t I n v e n t o r y ( ) . addItem ( i tem ( " c h o r i z o t r o c eado

" ) ) ;s e t ( i tem ( " t a b l a " ) , "manchadaDeChorizo" , t rue ) ;end ( ) ;

}e l s e // e l c u c h i l l o e s t á den t ro de a l go{

aCrea tu r e . w r i t eD e n i a l ( " Pr imero t e n d r í a s que s a c a r e lc u c h i l l o de " + path2 . ge t (1 ) . getSingName ( aCr ea tu r e ) ) ;

end ( ) ;}

Page 92: Documentacion age

92 Aspectos avanzados del modelo de mundo

}e l s e // s i e s t á en l a t a b l a y no cortamos con e l c u c h i l l o{

aCrea tu r e . w r i t eD e n i a l ( "Para c o r t a r e l c ho r i z o , n e c e s i t a r í a sun c u c h i l l o . \ n" ) ;

end ( ) ;}

}

}

Si además quisiéramos mirar si la tabla está en una mesa, tendríamos queirnos a mirar si el tamaño de path1 es al menos 3 y si path1.get(2) es la mesa,complicando más el código así como la vida del sufrido jugador.

3.4. Miembros y prendas

Una prenda es una cosa que los jugadores y criaturas pueden vestir en algunaparte de su cuerpo. Las prendas pueden tener propósito de armadura (en losjuegos que incluyan combates) o no tenerlo (en el resto de los juegos). Así,ejemplos de objetos que se pueden modelar como prendas en AGE podrían seruna cota de mallas, un traje, unos guantes, un guantelete, unas botas o inclusoun anillo o un pendiente.

Las prendas se ponen y se quitan por defecto mediante las órdenes ves-tir prenda y desvestir prenda. En versiones actuales de AGE (de 1.0.3 a 1.2.3),las órdenes poner jugador prenda (ponerse prenda) y coger jugador prenda (qui-tarse/sacarse prenda) también funcionan para ponerse y quitarse prendas. Sinembargo, es importante tener en cuenta que estas últimas órdenes se eliminaránen el futuro, por lo que se recomienda a los creadores de aventuras suponerque sólo existen por defecto vestir y desvestir, y programarse en BeanShell losverbos poner y quitar si se quiere que funcionen para este propósito.

Una prenda en AGE siempre se pone en una o varias partes del cuerpo, estopermite crear un sistema de prendas realista (por ejemplo, si tenemos un yelmopuesto no nos podremos poner otro a la vez, porque ambos ocuparían la cabeza;pero sí que podemos tener puestos a la vez un yelmo y unas botas porque elprimero va en la cabeza y las segundas en los pies).

Así pues, el sistema de prendas en AGE va ligado al sistema de partes delcuerpo (miembros), que es el que sirve para decidir qué prendas pueden combi-narse con otras. No obstante, si en un mundo de AGE no es necesario este nivelde complejidad, se puede simplificar, como se verá más adelante.

3.4.1. Miembros

Los miembros son cosas que representan las partes del cuerpo de un jugador ocriatura. Con la funcionalidad por defecto del AGE, la utilidad de los miembroses que sirven para blandir armas así como para vestir prendas y armaduras.Por supuesto, el creador de aventuras puede dar otros usos a los miembrosprogramando en BeanShell si lo considera necesario.

Para definir un miembro, como por ejemplo la cabeza del jugador, la aña-dimos en el PUCK como una cosa cualquiera, rellenando los campos (nombreúnico, nombre de referencia, género, etc.) que sean necesarios. A continuación,

Page 93: Documentacion age

Miembros y prendas 93

creamos una relación estructural que vaya del jugador a la cabeza. Por defec-to, la flecha resultante está etiquetada con la palabra «tiene», que quiere decirque la cabeza está en el inventario del jugador (como si éste llevase una cabezacortada consigo). Para ponerla como miembro, hacemos click en la flecha dela relación, y en la ficha «Relación estructural» del panel asociado cambiamos«tiene» por «se compone de». De este modo, la cabeza pasa a ser un miembrodel jugador, tal y como queremos.

Es posible hacer que un miembro se componga de otros miembros (por ejem-plo, que una mano tenga varios dedos) creando relaciones «se compone de» entreunos y otros. Sin embargo, esto no es necesario, al menos con la funcionalidadpor defecto del AGE. Si queremos tener anillos que se pongan en los dedos, enla práctica es igualmente válido poner éstos como partes del jugador en lugar decomo partes de la mano; aunque un perfeccionista del modelado pueda quererhacer lo segundo.

Cada criatura puede llevar a cabo acciones sobre sus propios miembros (porejemplo, vendarse el brazo izquierdo, suponiendo que definamos la respuesta alverbo «vendar» adecuadamente en el brazo); pero por defecto no puede llevara cabo acciones sobre los miembros de los demás.

Si queremos trabajar con los miembros de alguna criatura desde códigoBeanShell, podemos utilizar los siguientes métodos:

/∗ c l a s e Mobi le ∗/ I n v e n t o r y g e tP a r t s I n v e n t o r y ( )

Este método devuelve una lista de la clase Inventory que contiene los miem-bros directos de la criatura sobre la que se invoca. Con directos queremos decirque, si un brazo está modelado como parte de la criatura y a su vez una manocomo parte del brazo, la lista contendrá el brazo pero no la mano.

Este inventario se puede modificar, añadiendo o quitando elementos paraañadir o quitar miembros dinámicamente a una criatura.

/∗ c l a s e Item ∗/ I n v e n t o r y g e tPa r t s ( )

Devuelve una lista de clase Inventory con las partes de la cosa dada. Enel ejemplo anterior, si lo invocáramos sobre el brazo, devolvería un inventarioconteniendo la mano.

Este inventario se puede modificar, añadiendo o quitando cosas para añadiro quitar partes dinámicamente a un miembro.

/∗ c l a s e Mobi le ∗/ I n v e n t o r y g e t F l a t t e n e dP a r t s I n v e n t o r y ( )

Devuelve una lista de clase Inventory conteniendo los miembros directos eindirectos de la criatura sobre la que se invoca. Es decir, en el ejemplo anterior,este método devolvería una lista conteniendo tanto el brazo como la mano.

Al contrario que los anteriores, este inventario es de «sólo lectura». Si semodifica, no tendrá el efecto de añadir ni quitar miembros a la criatura.

3.4.2. Prendas

Como se explicó anteriormente, una prenda es una cosa que los jugadorespueden vestir en alguna parte de su cuerpo. Para crear una prenda en PUCK,lo hacemos de la siguiente manera:

Page 94: Documentacion age

94 Aspectos avanzados del modelo de mundo

Creamos una cosa en PUCK, rellenando todos los campos estándar denombres, género, descripción, etc.

Vamos a la ficha «Prenda» del panel de entidad de la cosa, y marcamos lacasilla «Es prenda», indicando que efectivamente se trata de una prenda.

A continuación, tenemos que especificar en qué miembro o miembros de lascriaturas se podrá llevar esa prenda. Por ejemplo, un casco se podrá llevaren la cabeza. Para hacer esto, utilizamos el formulario llamado «Miembrosrequeridos» de la ficha «Prenda»:

Pulsamos el botón «Añadir miembro». Esto hace que nos aparezca unnuevo panel de «Miembro», que nos da la opción de añadir una serie denombres. Introducimos «cabeza», y con esto, el casco ya tiene la informa-ción de que puede ponerse en la cabeza.

Si en lugar de un solo miembro, una prenda necesita ocupar varios (porejemplo, unas botas podrían ocupar el pie izquierdo y el pie derecho, si losmodelamos como objetos distintos); entonces le daremos varias veces al botón«Añadir miembro», y en cada uno de los paneles que aparezcan introduciremosel nombre de uno de los miembros (por ejemplo, «pie izquierdo» en uno, y «piederecho» en el otro).

Por otra parte, si una misma prenda se puede poner en distintos tipos demiembros (pero sólo en uno de cada vez), lo que haremos será darle a «Añadirmiembro» una sola vez; pero añadirle varios nombres: por ejemplo, si un guantees reversible y puede encajar tanto en la mano derecha como en la izquierda,podríamos poner «mano derecha» y «mano izquierda» como nombres en sumiembro requerido. Estos nombres siempre se procesan en orden, es decir, siponemos primero «mano derecha» y después «mano izquierda» y el jugadorteclea vestir guante, se colocará éste en la mano derecha si la tiene libre, y sóloen el caso de que no la tenga libre se lo pondrá en la izquierda.

Si queremos que una criatura comience el juego con una prenda puesta, loharemos creando en PUCK una relación llamada «wears» con valor true quevaya del miembro correspondiente a la criatura. Por ejemplo, si queremos queun jugador comience con un casco puesto en su cabeza:

Creamos una relación (flecha) de la cabeza al casco,

Clickeamos en ella,

Vamos a la ficha «Otras relaciones» del panel de la relación,

Introducimos nombre «wears», valor true, y tiempo restante −1 (infinito).

Si queremos modificar desde el código BeanShell las prendas que lleva puestaso deja de llevar una criatura, basta con poner el valor de la relación «wears»entre cada miembro y la correspondiente prenda a true o false (véase cómohacerlo en la sección de relaciones). También podemos utilizar el mecanismode relaciones para consultar qué prendas lleva puesta una criatura y en quémiembros; pero adicionalmente, AGE nos proporciona métodos para hacer estomás rápidamente:

/∗ c l a s e Mobi le ∗/ I n v e n t o r y getWornItems ( )

Page 95: Documentacion age

Estados de las criaturas 95

Este método devuelve una lista de todas las prendas que lleva puesta lacriatura sobre la que se invoca. El inventario devuelto es de sólo lectura, esdecir, quitarle y ponerle cosas no tendrá ningún efecto sobre lo que lleva puestala criatura (para esto, debemos usar las relaciones).

/∗ c l a s e Mobi le ∗/ I tem getWornItem ( Item l imb )

Devuelve la prenda que lleva puesta la criatura sobre la que se invoca en elmiembro dado por limb o, si no lleva ninguna prenda, devuelve null.

/∗ c l a s e Mobi le ∗/ boolean wears I t em ( Item wea rab l e )

Sirve para comprobar si la criatura sobre la que se invoca lleva o no puestala prenda dada como parámetro. Devuelve true si la lleva puesta (en cualquierade sus miembros) y false de lo contrario.

3.5. Estados de las criaturas

En la sección 2.3.2 sobre temporización vimos cómo funcionaba el modelo detiempo de AGE, basado en unidades de tiempo y en propiedades cuyo métodoupdate se activa cuando su contador de tiempo llega a cero. En esa sección,vimos algunos ejemplos de cómo usar ese mecanismo para definir entidadescon comportamientos que dependieran del tiempo. Pero, además de los quenosotros añadamos, en AGE ya existen comportamientos definidos por defectoque dependen del tiempo, que son los asociados a las acciones de las criatura,como coger un objeto o moverse en una dirección. Todas las acciones que llevana cabo las criaturas consumen un tiempo, y la cuenta de este tiempo se lleva enel temporizador de una propiedad llamada «state».

Por ejemplo, como jugadores, cuando AGE nos acepta una orden de entrada,es porque ha llegado a cero el temporizador de «state» de nuestro personajejugador. Si tecleamos «ir norte», AGE cambiará el valor de «state» a un valorde estado (que se verá más adelante) que indica que nos estamos moviendo,y fijará el temporizador al número de unidades de tiempo que nos consuma irhacia el norte. Una vez llegado el temporizador a cero, esto significa que yahemos terminado de ir hacia el norte y AGE esperará otra orden de entrada,que de nuevo cambiará el valor y temporizador de «state», y así sucesivamente.

Por lo tanto, el método update que define AGE por defecto para la propie-dad «state» de los jugadores es muy importante, porque es nada menos que elque consigue que al ejecutar una aventura monojugador, ésta nos vaya pidien-do órdenes y ejecutándolas una por una. En una aventura multijugador o quecuente con personajes complejos que realicen acciones, el temporizador de estapropiedad será el que se encargue de entrelazar correctamente las acciones: porejemplo, si ir al norte desde una localidad a otra consume tres unidades de tiem-po y coger un objeto consume una unidad de tiempo, el jugador A podrá cogertres objetos en el tiempo en el que el jugador B va al norte (la manera concretaen la que los jugadores perciben esto en los modos síncrono y de tiempo real setrató en la sección de temporización).

En la presente sección veremos en detalle cómo funciona esta propiedadespecial «state» (y otra asociada, llamada «target») y qué podemos hacer conellas.

Page 96: Documentacion age

96 Aspectos avanzados del modelo de mundo

3.5.1. Las propiedades «state» y «target»

Las criaturas (objetos de la claseMobile) tienen una propiedad especialmenteimportante llamada «state» (estado). La importancia de la propiedad «state»radica en que esta propiedad es la que define qué cosa está haciendo una criaturaen cada momento, y su temporizador es el que indica cuánto tiempo tardará enhacerlo.

Asociada a la propiedad «state» aparece a veces otra propiedad, llamada«target», que nos proporciona información extendida sobre el estado. Así, porejemplo, si en un instante dado del juego la criatura Pepito está yendo de la salasur a la sala norte, su propiedad «state» en ese momento valdrá Mobile.MOVING(valor que indica que se está moviendo) y su propiedad «target» contendrá unidentificador de la sala norte.1

El manejo de las propiedades «state» y «target» ha de considerarse un temaavanzado, pues en la mayoría de los juegos no hace falta manipularlas. Sólodebería ser necesario en aventuras en las que se quiera tener un control fino delcombate o de la temporización de eventos.

En la siguiente tabla se muestran los valores que puede tomar, para cualquiercriatura, la propiedad «state», así como su significado y el uso de la propiedad«target» para ese estado. Nótese que gran parte de los estados, aunque no todos,están relacionados con el combate y se tratarán en más detalle en la sección 3.6Combate y armas de esta documentación.

1Por motivos puramente históricos, el identificador contenido en «target» es un númeroentero en lugar del nombre único de la sala. Sin embargo, se puede utilizar igual que si fuera elnombre único (es decir, funcionarán cosas como room(fulano.get("target")). Probablemente lapropiedad «target» pase a contener nombres únicos, en lugar de dichos identifiadores enteros,en futuras versiones de AGE.

Page 97: Documentacion age

Estados de las criaturas 97

Valor de «state» Significado de «state» Significado de«target»

Mobile.IDLE Estado por defecto. Ojo: el nombre (IDLE,ocioso) está mal puesto, pues no tiene por quésignificar que la criatura esté ociosa. Se puedeutilizar para denotar que la criatura está lle-vando a cabo cualquier acción no contempladaen los otros estados. Por ejemplo, es el esta-do que tiene una criatura que está cogiendo odejando un objeto.

Mobile.MOVING La criatura está moviendose de una habita-ción a otra. El temporizador de «state» indicacuántas unidades de tiempo quedan hasta quese complete el movimiento. En el momento enque se completa es cuando la criatura cam-bia realmente de habitación, y se imprimen losmensajes como «Fulano se va al norte» o «Fu-lano llega desde el sur».

Habitación destino

Mobile.ATTACKING La criatura está lanzando un ataque contraotra criatura. Cuando el temporizador lleguea cero, el ataque impactará al enemigo (si noha sucedido antes algo que lo impida, como quela criatura reciba un golpe).

Criatura que reci-birá el ataque

Mobile.BLOCKING La criatura está preparando un bloqueo.Cuando el temporizador llegue a cero,la criatura pasará a estar «en guardia»(Mobile.READY_TO_BLOCK) y si le llega unataque, será bloqueado.

Mobile.READY_TO_BLOCK La criatura está bloqueándose, con un escudoo arma alzada para parar un ataque enemigo.

Mobile.DODGING La criatura está echándose a un lado para in-tentar esquivar un ataque. Si el temporizadorllega a cero antes de que se reciba el ataque, seconsiderará que le ha dado tiempo a esquivar.

Mobile.READY_TO_DODGE La criatura está echándose a un lado para in-tentar esquivar un ataque, y ha pasado el tiem-po suficiente para considerar que le da tiempoa esquivar, sin que el ataque haya llegado.

Mobile.ATTACK_RECOVER La criatura está recuperándose después de ha-ber lanzado una estocada o golpe. Hasta queel temporizador del «state» llegue a cero, nopodrá emprender otra acción.

Mobile.BLOCK_RECOVER La criatura está recuperándose después de ha-ber parado un golpe. Hasta que el temporiza-dor del «state» llegue a cero, no podrá empren-der otra acción.

Mobile.DAMAGE_RECOVER La criatura está recuperándose después de ha-ber recibido un golpe. Hasta que el tempori-zador del «state» llegue a cero, no podrá em-prender otra acción.

Page 98: Documentacion age

98 Aspectos avanzados del modelo de mundo

Valor de «state» Significado de «state» Significado de«target»

Mobile.DODGE_RECOVER La criatura está recuperándose después de ha-ber llevado a cabo una esquivada con éxito.Hasta que el temporizador del «state» llegue acero, no podrá emprender otra acción.

Mobile.DYING La criatura está cayendo muerta. Este estadosiempre dura una unidad de tiempo nada más,y luego se pasa al estado Mobile.DEAD.

Mobile.DEAD La criatura está muerta.Mobile.SURPRISE_RECOVER La criatura está recuperándose de una sorpresa

o interrupción.Mobile.DISABLED La criatura está desactivada y no reaccionará

ante nada, ni recibirá entradas si es un juga-dor. Este estado se usa, por ejemplo, cuandoel jugador que controlaba un personaje en unaaventura multijugador se ha caído o desconec-tado.

Mobile.CASTING La criatura está lanzando un conjuro. Entidad a la que sele lanza el hechizo,si la hay

3.5.2. Cambios de estado

He aquí lo que sucede por defecto en AGE cuando el temporizador del es-tado de una criatura llega a cero, a no ser que nosotros como programadoresredefinamos el método update para que haga algo distinto con la propiedad«state»:

Valor de «state» Reacción de las criaturasMobile.IDLE Tomar decisiónMobile.MOVING Tomar decisiónMobile.ATTACKING Se lleva a cabo el ataque y se pasa a Mobi-

le.ATTACK_RECOVERMobile.BLOCKING Se pasa a Mobile.READY_TO_BLOCKMobile.READY_TO_BLOCK Se refresca el estado hasta que llegue el ataque enemigo,

y en ese momento se pasa a BLOCK_RECOVER o DAMA-GE_RECOVER según el resultado

Mobile.DODGING Se pasa a Mobile.READY_TO_DODGEMobile.READY_TO_DODGE Se refresca el estado hasta que llegue el ataque enemigo,

y en ese momento se pasa a DODGE_RECOVER o DAMA-GE_RECOVER según el resultado

Mobile.ATTACK_RECOVER Tomar decisiónMobile.BLOCK_RECOVER Tomar decisiónMobile.DAMAGE_RECOVER Tomar decisiónMobile.DODGE_RECOVER Tomar decisiónMobile.DYING Se pasa a Mobile.DEADMobile.DEAD Se mantiene el estadoMobile.SURPRISE_RECOVER Tomar decisiónMobile.DISABLED Se mantiene el estadoMobile.CASTING Se pasa a IDLE

Los casos en los que esta tabla dice «Tomar decisión» quiere decir que lacriatura tiene la iniciativa y puede en ese momento emprender una nueva acción.Esto tiene un significado distinto según si la criatura es un jugador o no:

Si se trata de un jugador, cuando llegue el momento de «Tomar decisión»se esperará una orden por parte del cliente del jugador, y se procesarádicha orden, ejecutando los comportamientos correspondientes.

Page 99: Documentacion age

Combate y armas 99

Si se trata de una criatura que no es jugador, cuando llegue el momentode «Tomar decisión» se hará lo siguiente:

• Si tiene enemigos presentes, se llama a la IA de combate que decidequé acción de combate tomará la criatura (atacar, bloquear, etc.)

• Si no tiene enemigos presentes, la criatura simplemente no hará nada,quedando indefinidamente en estado Mobile.IDLE.

Por supuesto, podemos sobreescribir el método update de los personajes nojugadores definiendo comportamientos más complejos, y así obtener criaturasque se muevan, sigan rutas, cojan objetos, desempeñen tareas, etc.

3.5.3. Programación con estadosComo programadores de aventuras, el uso más evidente de la propiedad

«state» es el de comprobar lo que está haciendo una criatura. Por ejemplo, siqueremos saber si un personaje llamado Manolo que está en nuestra habitaciónha empezado a irse hacia otra, podríamos hacer algo como:

i f ( e qu a l s ( ge t ( mob i l e ( "Manolo" ) , " s t a t e " ) , Mobi le .MOVING ) ){

s e l f . say ( " ¡No te vayas aún , Manolo , que t od a v í a me quedan co sa sque c o n t a r t e !\ n" ) ;

}

Además, con room(get(mobile("Manolo"),"target")) podemos obtener la ha-bitación a la que se está moviendo Manolo, y con getTime(mobile("Manolo"),"state")tendremos el número de unidades de tiempo que le faltan para llegar a ella (véasela sección 2.3.2 sobre temporización).

Aparte de utilizar la propiedad «state» para comprobar el estado de unacriatura, los programadores más osados pueden querer redefinir lo que sucede enel método update para la propiedad «state» y de ese modo cambiar la manera enla que se comporta una criatura en los cambios de estado. Estos cambios puedenpotencialmente ir desde modificaciones puntuales hasta cambios radicales que,por ejemplo, modifiquen totalmente la manera en la que se resuelven los cambiosde estado tipo «Tomar decisión» cambiando todo el sistema de parseado deAGE por otro sistema totalmente distinto. Por supuesto, sólo los programadoresavanzados o con mucha práctica en AGE y con interés en personalizarlo deberíanutilizar estas características, que no son para nada necesarias para hacer un buenjuego con AGE.

3.6. Combate y armasAetheria Game Engine fue pensado inicialmente como sistema para crear

juegos de rol de texto mono y multijugador, y por ello cuenta con un completosistema para simular combates. Por supuesto, este sistema se puede obviar yno utilizar, de ahí que AGE también sirva perfectamente para crear juegosde aventura sin elementos de rol y que, de hecho, la mayoría de los juegoscreados hasta hoy en AGE sean de este último tipo. Sin embargo, para quienesté interesado en hacer que su mundo permita emocionantes duelos a espadacontra orcos, será interesante leer esta sección.

Page 100: Documentacion age

100 Aspectos avanzados del modelo de mundo

El sistema de combate de AGE está pensado fundamentalmente para com-bate estilo medieval y de fantasía, con armas cuerpo a cuerpo y magia. Sinembargo, al tratarse de un sistema muy genérico, también es posible adaptarloa otras situaciones como pueden ser tiroteos con armas de fuego.

El combate en AGE está fuertemente basado en el sistema de temporización(ver sección 2.3.2), así como en los estados de las criaturas (ver sección 3.5). Sino recuerdas bien el contenido de esas dos secciones, es recomendable echarlesun vistazo antes de seguir con ésta.

3.6.1. Elementos de rol básicos

Puntos de vida y daño

Como la inmensa mayoría de los juegos de rol por ordenador (CRPG’s), elsistema de combate de AGE utiliza el modelo de recuento del daño por el cualuna criatura cuenta con un determinado número de puntos de golpe o puntos devida (abreviados como HP, del inglés «hit points»). Cuando la criatura recibedaño, por ejemplo por ser golpeada con un arma, pierde puntos de vida (y a lospuntos de vida que se restan de su cifra total se les llama puntos de daño). Lospuntos de vida, pues, miden el nivel de salud que le queda a una criatura, y sien algún momento llegan a cero o menos, la criatura muere.

Por ejemplo, supongamos que un jugador tiene diez puntos de vida. Si unorco le ataca con un hacha infligiéndole seis puntos de daño, estos puntos dedaño se restan de sus diez puntos de vida, y por lo tanto el jugador se quedacon sólo cuatro puntos de vida. Si a continuación el orco vuelve a atacarle y estavez le inflige siete puntos de daño, el jugador se queda con 4 − 7 = −3 puntosde vida. Como esta cantidad de puntos de vida es menor o igual (en este casomenor) que cero, el jugador muere.

En AGE, podemos definir cuántos puntos de vida tiene una criatura medianteel panel de criatura de PUCK:

Si hacemos click en una criatura en el mapa de PUCK, en la pestaña«General» hay un campo del formulario etiquetado «HP». Este camponos permite definir la cantidad de puntos de vida con la que empieza lacriatura.

El campo etiquetado «HP máx», situado al lado de «HP», nos permitedefinir la cantidad de puntos de vida máximos de la criatura (es decir, lacantidad de puntos de vida que tiene la criatura cuando está saludable yno ha recibido ningún daño).

Así pues, si ponemos la misma cifra en el campo «HP» que en el campo «HPmáx», esto significará que la criatura está inicialmente saludable. Si ponemosuna cifra menor en «HP» que en «HP máx», significará que la criatura yacomienza con algún daño.

Tanto «HP» como «HP máx» deben tener inicialmente un valor mayor quecero. No hay límite superior (al menos mientras no te pases de un par de miles demillones), así que puedes usar distintas escalas: puedes preferir un mundo dondelas criaturas tengan pocos puntos de vida (que un hombre fuerte tenga diez, y ungolpe impresionante haga cinco puntos de daño) o donde tengan muchos (que unhombre tenga mil puntos de vida, y un golpe pueda hacer fácilmente doscientos

Page 101: Documentacion age

Combate y armas 101

de daño). Lo importante para el programador será equilibrar su juego de formaque el combate resulte realista, es decir, que el daño que hacen las armas guardeuna buena proporción con la vida que tienen las criaturas.

Los puntos de vida también se pueden obtener y modificar dinámicamentemediante código BeanShell:

/∗ c l a s e Mobi le ∗/ i n t getHP ( )

Devuelve la cantidad de puntos de vida que tiene actualmente la criaturasobre la que se invoca.

/∗ c l a s e Mobi le ∗/ vo id setHP ( i n t newHP )

Cambia la cantidad de puntos de vida que tiene la criatura sobre la que seinvoca a la cantidad newHP.

/∗ c l a s e Mobi le ∗/ i n t getMaxHP ( )

Devuelve la cantidad de puntos de vida máximos de la criatura sobre la quese invoca.

/∗ c l a s e Mobi le ∗/ vo id setMaxHP ( i n t newMaxHP )

Cambia la cantidad de puntos de vida máximos que tiene la criatura sobre laque se invoca a la cantidad newMaxHP. Esto se puede utilizar, por ejemplo, paraimplementar subidas de nivel en juegos de rol basados en niveles. Este métodoestará disponible a partir de la versión 1.1.7 de AGE.

Atributos y habilidades

Otro elemento típico en muchos juegos de rol son los atributos y las habili-dades.

Los atributos son valores numéricos que describen características físicas eintelectuales genéricas de un personaje. Por ejemplo, atributos que se suelenutilizar a menudo en juegos de rol son «fuerza», «constitución», «destreza»,«inteligencia» o «carisma».

Las habilidades, por otra parte, son valores numéricos que describen la prác-tica o el talento que un personaje tiene para realizar alguna actividad específica.Ejemplos de posibles habilidades serían «espada», «trepar», «pescar», «violín»o «diplomacia».

AGE proporciona, por un lado, un sistema genérico para gestionar los valoresde los atributos y habilidades, que es muy sencillo y prácticamente no hace nadamás que almacenar los valores y permitirnos cambiarlos. Por otra parte, AGEtambién proporciona mecánicas específicas asociadas a habilidades de combatey magia.

Esto quiere decir que AGE nos proporciona toda la infraestructura necesariapara utilizar las habilidades y atributos en el combate: por ejemplo, el hecho deque la probabilidad de acertar en un ataque con un arma y el tiempo que nosconsume el ataque dependan de nuestra habilidad con esa arma, o el hecho de queel utilizar repetidamente el arma haga que nuestra habilidad con ella aumentecon el tiempo. Veremos más detalles sobre esto en las siguientes secciones.

Por otra parte, si queremos utilizar las habilidades y atributos para funcio-nalidad no relacionada con el combate (como podría ser usar una habilidad de

Page 102: Documentacion age

102 Aspectos avanzados del modelo de mundo

«violín» para determinar si un personaje cautiva a su audiencia en un conciertoo no); AGE sólo nos proporciona la infraestructura básica para almacenar y ob-tener los valores de esas habilidades y atributos, el resto (por ejemplo, el códigopara decidir si el personaje consigue tocar una pieza determinada o no segúnsu habilidad con el violín) tendremos que ponerlo nosotros como programadoresde aventuras.

La mencionada infraestructura básica para almacenar y obtener valores dehabilidades y atributos consiste en un formulario de PUCK y una serie de mé-todos invocables desde código BeanShell.

El formulario es la ficha de «Características» del panel asociado a un perso-naje, y nos permite fijar los valores iniciales de habilidades y atributos para esepersonaje. Para ello, escribimos el nombre de la habilidad o atributo (por ejem-plo, «violín») en el campo «Nombre» y su valor numérico (por ejemplo, 30) en elcampo «Valor», y pulsamos el botón «Añadir». Si queremos rectificar el nombrey valor de un atributo, podemos hacerlo seleccionándolo en la lista, modificandolos datos en los campos «Nombre» y «Valor», y pulsando «Cambiar».

Adicionalmente, AGE proporciona los siguientes métodos en la clase Mobilepara manipular atributos y habilidades:

/∗ c l a s e Mobi le ∗/ i n t g e tS t a t ( S t r i n g name )

Devuelve el valor numérico del atributo de nombre dado, por ejemplo mobi-le("manolo").getStat(ïnteligencia") devolverá la inteligencia de Manolo. Nóteseque, si no hemos asignado ningún valor a un atributo, este método devolverápor defecto un valor de 12 (el tradicional valor por defecto de los atributos enjuegos basados en Dungeons & Dragons).

/∗ c l a s e Mobi le ∗/ vo id s e t S t a t ( S t r i n g name , i n t v a l u e )

Cambia el valor numérico del atributo de nombre name por el nuevo valorvalue.

/∗ c l a s e Mobi le ∗/ long g e t S k i l l ( S t r i n g name )

Devuelve el valor numérico de la habilidad de nombre dado, por ejemplomobile("manolo").getStat("violín") devolverá la habilidad de Manolo para tocarel violín. En este caso, si no se había fijado el valor de esa habilidad, el valordevuelto por defecto será 0 (¡nadie sabe tocar el violín sin aprender!)

/∗ c l a s e Mobi le ∗/ vo id s e t S k i l l ( S t r i n g name , long v a l u e )

Cambia el valor numérico de la habilidad de nombre name por el nuevo valorvalue.

Nótese que, aunque los atributos y habilidades no están implementados co-mo propiedades, se comportan de manera muy similar a éstas. De hecho, paraimplementar una habilidad que afecte a alguna actividad externa al combate, sepodría implementar igualmente con una propiedad. Para las relacionadas con elcombate, sin embargo, es necesario emplear este sistema de habilidades y atri-butos si se quiere aprovechar toda la funcionalidad de combate que se describirámás abajo.

Page 103: Documentacion age

Combate y armas 103

3.6.2. Mecánica de combateEl sistema de combate en AGE funciona, grosso modo, de la siguiente ma-

nera:

Por defecto, existen tres acciones de combate en AGE: ataques, bloqueosy esquivadas. Por supuesto, esto no excluye que durante un combate sepuedan perfectamente llevar a cabo otras acciones, como coger cosas, usarobjetos, lanzar hechizos, hablar con el adversario, o cualquier otra cosaque esté definida en la aventura.

En un ataque, un personaje A ataca con un arma a un personaje B queestá en su misma habitación. Nótese que cada ataque es, pues, individualde un personaje a otro, pero eso no quiere decir que los combates tenganque ser uno contra uno (podría haber dos personajes A y C atacando aB a la vez, por ejemplo). El ataque consume una cantidad de unidadesde tiempo (durante las cuales el arma está moviéndose para golpear alpersonaje B), y cuando transcurren esas unidades de tiempo, se resuelve.

En un bloqueo, un personaje B intenta defenderse con un arma (nótese quelos escudos cuentan como armas) de un personaje A que lo está atacando.Para ello, es necesario que A esté lanzando un ataque que todavía no sehaya resuelto (el arma está moviéndose para golpear a B). El bloqueotambién consume una determinada cantidad de unidades de tiempo (laque tarda B en poner su arma en posición de bloquear el arma de A). SiB es capaz de hacer esto antes de que llegue el arma de A, se resolveráel ataque con bloqueo, dando oportunidad a B de defenderse (según suhabilidad para hacerlo). Por otra parte, si el arma de A llega antes deque B sea capaz de colocarse en posición de bloqueo, el ataque de A seresolverá como si B no hubiera bloqueado en absoluto.

En una esquivada, un personaje B intenta quitarse del medio ante unataque de un personaje A. De nuevo, es necesario que A esté lanzandoun ataque que todavía no se haya resuelto; y la esquivada consumirá unadeterminada cantidad de unidades de tiempo (la que tarda B en apartarsedel paso). Si B es capaz de esquivar antes de que llegue el ataque de A,se resolverá el ataque con esquivada, dando oportunidad a B de esquivar(según su habilidad para ello). Por otra parte, si el arma de A llega antes,el ataque de A se resolverá como si B no hubiera esquivado en absoluto.

Así, cuando se resuelve un ataque pueden pasar las siguientes cosas:

1. Que el golpe de A sea fallido, y por lo tanto no impacte a B.2. Que el golpe de A sea bueno, pero B llegue a tiempo para esquivar y

esquive con éxito, por lo tanto el ataque no impactará a B y éste norecibirá daño.

3. Que el golpe de A sea bueno, y B llegue a tiempo para esquivar perosu esquivada sea fallida (se lanzó hacia el lado equivocado o lo hizomal), con lo cual el ataque le impactará igual y B recibirá daño.

4. Que el golpe de A sea bueno, pero B llegue a tiempo para bloqueary bloquee con éxito, con lo cual el ataque le impacta pero el bloqueoabsorbe todo o parte del daño.

Page 104: Documentacion age

104 Aspectos avanzados del modelo de mundo

5. Que el golpe de A sea bueno, y B llegue a tiempo para bloquear, perosu bloqueo sea fallido (no colocó el arma o escudo bien para detenerel golpe de A), con lo cual el ataque le impactará como si no hubierabloqueado y recibirá todo el daño.

Todas estas acciones tienen además un «tiempo de recuperación» despuésde que se produzcan: por ejemplo, después de lanzar un ataque, el per-sonaje que ha atacado estará unos instantes sin poder hacer otra cosa,porque lógicamente volver a posicionar el brazo y el arma para lanzar otraestocada o bloqueo consume un tiempo. Asimismo, el bloqueo y la esqui-vada tienen un tiempo de recuperación, y también existe otro tiempo derecuperación (que normalmente será mayor) al recibir un golpe, ya quelo normal es que alguien que sufre un ataque en combate quede aturdidodurante unos instantes hasta que pueda reaccionar de nuevo.

A la luz de esta descripción, si quieres (aunque no es realmente necesariopara implementar combates, si no quieres hacer cosas avanzadas) puedes volvera mirar los estados relacionados con el combate presentes en la tabla de lasección 3.5.1 sobre Las propiedades «state» y «target» de las criaturas. Comocomprobarás, dichos estados se usan para implementar lo que acabamos dedescribir:

Cuando el personaje A lanza su ataque con un arma, se pone en estadoMobile.ATTACKING, y su propiedad «target» apunta al personaje B.

Si el personaje B decide bloquear, se pondrá en estado Mobile.BLOCKING.

Si B consigue ponerse en posición de bloqueo antes de que llegue el ataquede A, se pondrá en estado Mobile.READY_TO_BLOCK.

Si el personaje B decide esquivar, se pondrá en estado Mobile.DODGING.

Si a B le da tiempo a eludir el golpe antes de que llegue el ataque de A,se pondrá en estado Mobile.READY_TO_DODGE.

Cuando se resuelva el ataque de A (es decir, el temporizador de su propie-dad «state» llegue a 0), se resolverá de forma diferente si el estado de Bes Mobile.READY_TO_BLOCK, Mobile.READY_TO_DODGE u otra co-sa. En los dos primeros casos, habrá que hacer «tiradas de dados» con lashabilidades para ver si el personaje B es capaz de bloquear o esquivar.En el tercer caso, sólo se resolverá el ataque de A sin ningún movimientomitigador por parte de B.

Los estados Mobile.ATTACK_RECOVER, Mobile.BLOCK_RECOVER, Mo-bile.DODGE_RECOVER yMobile.DAMAGE_RECOVER corresponden a lostiempos de recuperación descritos anteriormente para después de atacar,bloquear, esquivar y recibir un golpe, respectivamente. Hasta que terminenestos estados, los personajes no podrán ejecutar una nueva acción.

Pero en esta descripción todavía faltan varios cabos sueltos por explicar,concretamente:

Cómo funciona eso de las armas,

Page 105: Documentacion age

Combate y armas 105

Cómo se decide cuántas unidades de tiempo exactamente consume ca-da acción (ataques, bloqueos, esquivadas) así como cuántas unidades detiempo consumen los tiempos de recuperación,

Cómo se decide cuándo un ataque, un bloqueo y una esquivada tienenéxito y cuándo no,

Cómo se decide cuánto daño se hace, y cuánto absorbe un bloqueo.

Una respuesta rápida y resumida a los tres últimos cabos sueltos es que AGEcalcula estos tres resultados usando una combinación de tres tipos de factores:factores dados por el arma, factores dados por las habilidades y atributos deljugador que maneja el arma, y por último factores aleatorios que sirven paradotar de un cierto grado de incertidumbre e imprevisibilidad al combate. Todosestos factores se explican en la siguiente sección, que trata de las armas.

3.6.3. Armas

Un arma es cualquier cosa (de la clase Item) que un personaje puede utilizarpara lanzar y/o bloquear ataques. Para crear un arma en PUCK, empezaremoscreando una cosa de forma normal, con sus nombres, descripciones y demásvalores que ya conocemos. Para indicar que se trata de un arma, seleccionaremosla pestaña «Arma» de su panel de entidad, y activaremos la casilla etiquetada«Es arma», que indica que la cosa es un arma. El resto del formulario de lapestaña «Arma» nos permitirá configurar en detalle el arma, como explicaremosen el resto de esta sección.

Usar y blandir armas

Una primera cosa a tener en cuenta es que existen dos tipos de armas segúnla manera en que funcionan: armas naturales y armas externas. Las armas na-turales son partes del cuerpo de una criatura que ésta puede usar como arma(como por ejemplo un puño, que sirve para dar puñetazos). Las armas externas,las más comunes, son objetos o herramientas que se usan como armas, comopuede ser una espada, un hacha o un escudo.

Para que una cosa funcione como arma natural, necesitaremos definirla comomiembro de una criatura (véase la sección sobre miembros para recordar cómose hacía esto). AGE interpreta automáticamente que cualquier cosa que estémarcada como arma (casilla «Es arma») y sea un miembro de una criatura esun arma natural. Por otra parte, cualquier cosa que esté marcada como armapero no sea un miembro de una criatura funcionará como arma externa.

Para utilizar un arma externa, un personaje debe blandirla, cosa que ocuparáuno o varios de sus miembros: por ejemplo, una daga se puede blandir en unamano, mientras que una espada de dos manos se podrá blandir –como su nombreindica– ocupando ambas manos. El sistema para blandir y enfundar armas estotalmente análogo al sistema visto en la sección 3.4.2 de prendas para vestir ydesvestir prendas, es decir:

Las armas se blanden y enfundan con las órdenes blandir arma y enfundararma, que funcionan de manera análoga a vestir prenda y desvestir prenda.

Page 106: Documentacion age

106 Aspectos avanzados del modelo de mundo

Al igual que las prendas, las armas ocupan miembros, de forma que nopuede haber varias armas ocupando a la vez el mismo miembro (si blan-dimos una espada en la mano derecha, no podemos blandir a la vez unhacha en la misma mano).

El formulario «Miembros requeridos» de la pestaña Arma de PUCK fun-ciona exactamente igual que el de la pestaña Prenda, sólo que en este casose refiere a miembros requeridos para blandir un arma en lugar de paravestir una prenda. Por supuesto, esto incluye la posibilidad de que un ar-ma ocupe varios miembros, y la de que un arma pueda ocupar distintostipos de miembros.

Si queremos que una criatura comience el juego blandiendo un arma, sehace de forma análoga a cuando hacíamos que comenzara con una pren-da puesta, pero en este caso utilizamos una relación «wields» en vez de«wears». Es decir, debemos crear una relación «wields» que vaya del miem-bro en el que se blande el arma al arma en sí, con valor true y temporizador−1 (además de meter el arma en el inventario).

Podemos comprobar si una criatura está blandiendo algo en un miembrocomprobando el valor de esta relación «wields», y hacer que blanda o dejede blandir un arma modificándolo con los métodos de AGE para manipularrelaciones. Como en el caso de las prendas, AGE también nos proporcionauna serie de métodos que podemos invocar desde BeanShell para saberqué está blandiendo una criatura de una forma más directa que usandorelaciones:

/∗ c l a s e Mobi le ∗/ I n v e n t o r y getWieldedWeapons ( )

Este método devuelve una lista de todas las armas que está blandiendo lacriatura sobre la que se invoca. El inventario devuelto es de sólo lectura, esdecir, quitarle y ponerle cosas no tendrá ningún efecto sobre lo que lleva puestala criatura (para esto, debemos usar las relaciones).

/∗ c l a s e Mobi le ∗/ I tem getWie lded I t em ( Item l imb )

Devuelve el arma que lleva blandida la criatura sobre la que se invoca en elmiembro dado por limb o, si no lleva ningún arma ahí, devuelve null.

/∗ c l a s e Mobi le ∗/ boolean w i e l d s I t em ( Item item )

Sirve para comprobar si la criatura sobre la que se invoca está blandiendoel arma dada como parámetro. Devuelve true si la lleva blandida (en cualquierade sus miembros) y false de lo contrario.

Hasta aquí, hemos visto cómo hacer que los jugadores y criaturas puedanblandir y enfundar armas. Pero, por supuesto, lo más importante de las armases... ¡combatir con ellas!

Parámetros de combate de las armas

Los parámetros de combate de las armas se definen en la mitad inferior dela pestaña «Arma», donde hay un formulario dividido a su vez en pestañas«Ataque» y «Bloqueo». Como su nombre indica, la pestaña «Ataque» sirve

Page 107: Documentacion age

Combate y armas 107

para definir cómo se comportará el arma al lanzar ataques, mientras que en lapestaña «Bloqueo» se puede definir su comportamiento al bloquear con ella.

A continuación se explica brevemente lo que significa cada campo del for-mulario de «Ataque»:

Uso mínimo: número entero que representa el valor mínimo de habilidadcon el arma que debe de tener el personaje para ser capaz de usarla con unmínimo de éxito. Un arma con un valor de «uso mínimo» de cero puedeutilizarla cualquier personaje o criatura que pueda blandirla (otra cosaes que la utilice bien o falle la mayoría de los ataques). Un arma con unvalor de «uso mínimo» mayor que cero no podrá ser usada en absolutopor personajes cuya habilidad con el arma sea menor que ese valor (véasemás abajo para saber a qué nos referimos exactamente al decir «habilidadcon el arma»).

Pendiente de probabilidad: valor numérico (tipo double, es decir, con deci-males) que define la dificultad de la curva de aprendizaje de usar el armacon éxito, es decir, lo rápido o lento que se aprende a lanzar ataques cer-teros. Un valor de 0 correspondería a una dificultad media o moderada,valores positivos hacen el aprendizaje más fácil, y negativos más difícil.Más abajo se explicará con más detalle cómo funcionan estos valores; pe-ro si no quieres complicarte, puedes poner siempre 0, o usar siempre losvalores −1, 0 y 1 para difícil/normal/fácil.

Tiempo de ataque base: número entero que indica el tiempo (expresadoen unidades de tiempo) que tardará en lanzar un ataque alguien que tienejusto el nivel de habilidad mínimo para utilizar el arma.

Pendiente del tiempo de ataque: valor numérico (tipo double) que definela dificultad de la curva de aprendizaje del arma en cuanto a tiempo, esdecir, lo rápido o lento que se aprende a utilizarla para atacar con másrapidez a medida que se va usando. De nuevo, 0 es el valor normal, valorespositivos significan fácil y negativos difícil.

Tiempo de recuperación en ataque (base y pendiente): como los anteriores,pero afectando al tiempo que se tarda en recuperarse de un ataque con elarma (reposicionar el arma después de lanzar una estocada o golpe).

Daño de ataque (tipo de daño y fórmula): tipo de daño que inflige el arma(por ejemplo daño de golpe, cortante, de fuego, etc.) y fórmula que seutiliza para calcular el daño.

El tipo de daño puede ser cualquier cadena que nosotros usemos en eljuego para identificar una forma de hacer daño, y su propósito es quepodamos hacer que distintas armas o armaduras sean mejores o peorespara defenderse de diferentes tipos de ataque (por ejemplo, un traje igní-fugo puede ser muy bueno para defenderse del daño de fuego hecho conun lanzallamas, pero muy malo para defenderse del daño físico hecho poruna maza). Si no queremos llegar a este nivel de detalle en el modeladosino que queremos tratar todo el daño de la misma manera, simplementepodemos poner la misma cadena en el campo «tipo de daño» de todas lasarmas y armaduras.

Page 108: Documentacion age

108 Aspectos avanzados del modelo de mundo

La fórmula se utiliza para determinar cuánto daño de cada tipo se hace,y es una cadena de sumas y restas cuyos términos pueden contener:

• Un número entero, por ejemplo, 16.

• Una cantidad multiplicada por un atributo que puede ser FUE, CON,INT, SAB, DES, CHA o POD (fuerza, constitución, inteligencia, sa-biduría, destreza, carisma o poder), por ejemplo, 2FUE.2

• Una tirada de dados, formada por dos números separados por unaletra D. Por ejemplo, 4D6 significa «tirar cuatro dados de seis carasy sumar su valor». Esto añade un factor aleatorio al daño.

Así, por ejemplo, una fórmula de daño válida podría ser: 6+2D4+2FUE,significando «seis, más dos dados de cuatro, más dos veces la fuerza delpersonaje».

Habilidades: conjunto de habilidades que condicionan lo bien que un per-sonaje utilizará el arma, lo que antes denominábamos «habilidad con elarma». El «valor» en este caso es tipo double e indica en qué medida influ-ye cada una de las habilidades, de forma que la habilidad de un personajepara usar el arma será la suma del producto de la puntuación que tieneen cada una de estas habilidades por el campo «valor».

Así, a modo de ejemplo, supongamos que tenemos una espada de dosmanos, y en el formulario «Habilidades» de la pestaña «Ataque» de lapestaña «Arma» de esa espada hemos puesto: «espadas», con un valor de2.0, y «armas de dos manos», con un valor de 1.0. Esto tiene las siguientesconsecuencias:

• La habilidad del personaje para manejar esa espada se calcula su-mando dos veces su habilidad en «espadas» y una vez su habilidaden «armas de dos manos». Por ejemplo, si el personaje tiene una ha-bilidad en espadas de 100, y una habilidad en armas de dos manosde 50, su habilidad para manejar esta espada en particular sería 250(pero otras espadas podrían requerir una combinación distinta de ha-bilidades). Esta cantidad de 250 es la que se utiliza para determinarsi el personaje llega al uso mínimo para poder utilizar esa espada,y además para calcular (según los valores de «base» y «pendiente»comentados) la probabilidad de éxito de los ataques con la espada ylos tiempos de ataque y recuperación.

• Cada vez que el personaje lance un ataque con la espada, su habilidadcon «espadas» se incrementará en 2, y su habilidad con «armas dedos manos» se incrementará en 1. Nótese que, por lo tanto, en AGEel valor de las habilidades de combate es un contador de uso que cre-ce linealmente (si usas la espada el doble de veces, tendrás el doblede habilidad). Por supuesto, esto no quiere decir que la probabilidadde éxito al usar la espada o el tiempo de ataque evolucionen lineal-mente, ya que eso no sería realista. A continuación veremos con más

2Esto es poco genérico, porque AGE permite definir los atributos que se quiera pero sólopermite incluir estos atributos específicos en las fórmulas de daño. Es algo a mejorar enversiones futuras de AGE.

Page 109: Documentacion age

Combate y armas 109

detalle cómo evolucionan, para los interesados en las matemáticas delcombate.

El formulario de bloqueo permite definir los mismos valores que el de ataque,sólo que referidos a la acción de bloquear con el arma. Así, el «uso mínimo»y la «pendiente de probabilidad» del bloqueo determinarán la probabilidad debloquear con éxito. El «tiempo de bloqueo» y «tiempo de recuperación del blo-queo» definen el tiempo que se tarda en preparar un bloqueo y en volver a tomarla iniciativa tras llevarlo a cabo. El «daño bloqueado» nos permite especificarfórmulas para el daño que absorbemos con el bloqueo (que se resta al del ataqueque nos hayan hecho, quedando en cero si el daño absorbido es mayor que eldaño que ha hecho el ataque). Por último, el formulario de «habilidades» nospermite definir qué habilidades son relevantes para (y se entrenan al) bloquearcon el arma, que podrían o no ser las mismas que las correspondientes al ataque.

3.6.4. Matemática de las armasSi bien con lo dicho en la sección anterior es suficiente para definir armas que

se puedan usar en aventuras con combates, sin duda algunos usuarios estaráninteresados en los detalles de las fórmulas matemáticas que AGE utiliza paracalcular tiempos de acciones de combate y probabilidades de éxito.

Como se ha dicho anteriormente, se supone que el valor de una habilidades una estimación lineal del tiempo de experiencia que tiene el personaje uti-lizando esa habilidad. Para obtener estimaciones realistas de tiempos y proba-bilidades de éxito de ataques a partir de ese valor, necesitaremos aplicar unafunción creciente pero de derivada negativa, de acuerdo con el concepto intui-tivo de «ganancias decrecientes» que tenemos en la vida real: inicialmente elaprendizaje de una nueva habilidad suele ser muy rápido; pero con el tiempo elperfeccionamiento es mucho más lento y laborioso.

La fórmula que utiliza AGE para esto en el caso de los tiempos de ataque,bloqueo y otras acciones de combate es la siguiente:

tiempo =tiempobase

(habilidad−uso minimo100 + 1)

ependiente (3.1)

que, para hacerse una idea, tiene la forma que se ve en la figura 3.1 (paratiempos base de 25 y 50, y pendientes de -1, 0 y 1).

Como vemos, el jugador no tardará en cogerle el truco al arma y reducirsignificativamente el tiempo que le lleva usarla, pero más adelante el aprendizajeirá siendo cada vez más paulatino.

Sin embargo, esa fórmula no lo es todo: para introducir un cierto grado devariabilidad e incertidumbre en el combate, al tiempo obtenido de la fórmula sele aplica cada vez una variación aleatoria, sacada de una distribución gaussianade media 0 y desviación típica 1/3 t, siendo t el tiempo obtenido de la fórmula.Con lo cual, el realidad, el comportamiento tendría más bien este aspecto quemuestra la figura 3.2.

Como podemos ver, la incertidumbre que añade la variación gaussiana haceque, aunque los combatientes experimentados de un arma ataquen más rápidoen general, puede haber casos en los que por una cuestión de suerte un novatoconsiga un ataque más rápido que un veterano. Esto añade imprevisibilidad alcombate.

Page 110: Documentacion age

110 Aspectos avanzados del modelo de mundo

Figura 3.1: Relación entre el tiempo de ataque/bloqueo y la habilidad con elarma menos el uso mínimo.

Page 111: Documentacion age

Combate y armas 111

Figura 3.2: Relación entre el tiempo de ataque/bloqueo y la habilidad con elarma menos el uso mínimo con factores aleatorios.

Page 112: Documentacion age

112 Aspectos avanzados del modelo de mundo

Figura 3.3: Probabilidad de éxito en el combate con armas.

Para la probabilidad de éxito de los ataques y bloqueos se utiliza una fórmulabasada en el mismo principio, que es:

probabilidad = 1− 1

(habilidad−uso minimo100 + 1)

ependiente (3.2)

Dándonos unas gráficas como las de la figura 3.3 (para pendientes de −1,−0,5, 0, 0,5 y 1).

Como vemos, la probabilidad tiende rápidamente a 1 (tener éxito siempre)en el caso de pendiente 1, pero no tan rápido con pendientes más difíciles.

En el caso de la probabilidad, ya que ella misma funciona como factor paraañadir aleatoriedad e incertidumbre, no añadimos ninguna variación gaussiana.

3.6.5. Esquivadas

Con lo visto en la sección sobre armas, ya sabemos cómo se calculan lasprobabilidades de éxito y tiempos de realización de ataques y bloqueos; pero nohemos visto cómo se calculan estos parámetros para las esquivadas.

Por el momento, la implementación de las esquivadas en AGE aún no estámuy perfeccionada, así que es sencillo: la probabilidad de realizar una esquiva-

Page 113: Documentacion age

Combate y armas 113

da con éxito es siempre del 20%, y esquivar consume siempre 15 unidades detiempo.

En futuras versiones, es posible que esto se complique un poco más.

3.6.6. Armaduras

Las armaduras no son más que prendas que tienen la capacidad especial dereducir el daño que quien las viste recibe de ataques dirigidos al miembro dondelas lleva.

Para crear una armadura en PUCK, no tenemos más que hacer lo siguiente:

Crear una prenda, tal como hemos visto anteriormente en la sección sobreprendas.

En la pestaña «Prenda», cubrir el formulario etiquetado como «Valor deprotección». Este formulario funciona exactamente igual que aquéllos enlos que introducimos las fórmulas de daño de las armas, es decir: tendremosque poner tipos de daño de los que nos protegerá la armadura, y fórmulaspara calcular cuánto daño absorberá, en el mismo formato que en el casode las armas (por ejemplo, 2+2D6).

Una armadura entra en acción cuando un ataque impacta en el miembrodonde el personaje la lleva. Si bien en AGE no existen por defecto comandosespecíficos para atacar a un miembro dado («golpea a Fulano en la cabeza»,etc.); se supone que cada ataque se dirige a un miembro determinado. Estemiembro se escoge de forma aleatoria, pero de modo que la probabilidad deelegir cada uno de los miembros de un personaje es proporcional al volumendel miembro. Se pueden definir miembros de volumen 0 si se quiere que nuncareciban ataques (por ejemplo, si queremos poder llevar un anillo en el dedo, peroque nunca nos lancen un ataque al dedo, cosa que sonaría bastante rara).

Esto quiere decir que, por ejemplo, si en un personaje definimos los miembros«cabeza» con volumen 10, «cuerpo» con volumen 15, «brazo izquierdo» convolumen 5 y «brazo derecho» con volumen 5, entonces de cada 35 ataques quereciba el personaje irán como promedio 10 dirigidos a la cabeza, 15 dirigidosal cuerpo, 5 al brazo izquierdo y 5 al brazo derecho. Si ese personaje lleva uncasco puesto en la cabeza, ese casco le protegerá de esa proporción de ataquesque van dirigidos a la cabeza.

La reducción de daño otorgada por una armadura se añade a la reducciónotorgada por el bloqueo, si el personaje ha bloqueado.

3.6.7. Entrando en combate

Ahora que sabemos cómo funciona exactamente el combate, vamos a vercómo podemos utilizarlo, es decir, cómo hacemos que un personaje de un juegoen AGE luche contra malvados monstruos.

Cuando esté en la misma habitación que otra criatura, un personaje jugadorsiempre puede poner el comando atacar criatura para lanzar un ataque. Pordefecto, las criaturas de AGE están hechas de manera que, si tienen medios paradefenderse (algún arma), siempre entrarán en combate si las atacan, lanzandoataques, bloqueando o esquivando según consideren oportuno.

Page 114: Documentacion age

114 Aspectos avanzados del modelo de mundo

Nótese que las órdenes por defecto para bloquear y esquivar son bloquearcriatura y esquivar (no hace falta especificar una criatura concreta para esquivar,porque se supone que nos apartamos de cualquier ataque que nos estén lanzandoen ese momento).

Si queremos que sea una criatura no jugadora la que empiece la pelea, po-demos hacerlo agregándole enemigos. Toda criatura en AGE tiene una lista deenemigos con los que entrará en combate si los ve. Podemos manipular esta listamediante los siguientes métodos:

/∗ c l a s e Mobi le ∗/ vo id addEnemy ( Mobi le nuevo )

Añade a nuevo como enemigo del Mobile sobre el que invocamos este método.

/∗ c l a s e Mobi le ∗/ boolean removeEnemy ( Mobi le v i e j o )

Quita viejo de la lista de enemigos del Mobile sobre el que invocamos estemétodo y devuelve true, si estaba en la lista. En caso de que no estuviese en lalista, este método no hace nada y devuelve false.

Así, por ejemplo, si tenemos un orco y hacemos

mob i l e ( " o rco " ) . addEnemy ( mob i l e ( "manolo" ) ) ;

el orco atacará automáticamente a Manolo cuando éste aparezca en su loca-lidad.

Por supuesto, utilizando los métodos asociados a las criaturas y los métodosupdate en conjunto con la propiedad «state», es posible definir comportamientosmás complejos de los monstruos, como que persigan a su enemigo o patrullenuna zona buscándolo.

3.7. Entidades abstractasEn las secciones preliminares de esta documentación, hemos visto que los

objetos del mundo que aparecen representados en el mapa de PUCK (habita-ciones, cosas, etc.) se denominan entidades. Las entidades son objetos de unaclase llamada Entity, que tiene diferentes subclases (como Room, Mobile o Item)para representar entidades de distintas características y que juegan diferentespapeles en el mundo.

Sin embargo, todas estas entidades tienen unas características en común (queson precisamente lo que hace que sean entidades). Estas características han idoapareciendo poco a poco a lo largo de esta documentación; pero nunca las hemoslistado explícitamente. Son las siguientes:

1. Toda entidad tiene un nombre único, que no puede coincidir con el deninguna otra entidad. Podemos obtener una entidad del mundo a partir desu nombre único con entity("nombre único") (aunque las entidades de cadaclase también se puedan obtener llamando a una una función específica,como en item("mesa") o mobile(.orco")).

2. Toda entidad puede tener definido código BeanShell.

3. Toda entidad puede tener propiedades (con un valor y un temporizador) yrelaciones con cualquier otra entidad (también con un valor y un tempori-zador). El sistema AGE decrementa los temporizadores de cada propiedad

Page 115: Documentacion age

Entidades abstractas 115

cada vez que pasa una unidad de tiempo, y cuando uno de estos tempo-rizadores llega a cero, llama a un método de actualización (update) si lohemos definido en BeanShell, que podemos usar para definir lo que sucedeal «expirar» la propiedad.

Hasta ahora, hemos utilizado todas estas características en entidades comolas habitaciones, cosas o criaturas; que además representan objetos «palpables»en el mundo con los que se puede interactuar. Pero en ocasiones, nos puede in-teresar tener un objeto que también soporte estas características (código BeanS-hell, propiedades, relaciones y temporizadores) pero que no represente ningúnobjeto concreto o «palpable» del mundo. Ésta es la función que tienen en AGElas entidades abstractas.

Una entidad abstracta es un objeto de la clase AbstractEntity, la cual, aligual que Room o Item, es una subclase de la clase Entity. Podemos agregarentidades abstractas al mapa desde PUCK mediante el botón «Añadir entidadabstracta», estas entidades tienen un icono y se pueden colocar en el mapa comoel resto, y también cuentan con formularios en PUCK al igual que el resto. Sinembargo, el formulario de una entidad abstracta es más sencillo que el de otrostipos de entidad, ya que permite introducir poco más que nombre único, códigoBeanShell y propiedades.

Así, funcionalmente una entidad abstracta no es más que un contenedor decódigo BeanShell, propiedades y relaciones; que nos proporciona toda la funcio-nalidad que nos dan estas características de AGE sin necesidad de traducirseen un objeto concreto y material del mundo que se simula. Sin embargo, co-mo través del código BeanShell de una entidad abstracta podemos referirnos aotras entidades y llamar a métodos que las afecten, una entidad abstracta puedeimplementar comportamientos que tengan efectos visibles en el mundo.

Algunos ejemplos de comportamientos que se han implementado con enti-dades abstractas son los siguientes:

1. Una entidad abstracta «tiempo atmosférico» que controle cómo va cam-biando el tiempo en las distintas habitaciones del mundo, propagandolos cambios de unas a otras mediante cambios de una propiedad en esashabitaciones. Esos cambios de tiempo atmosférico se reflejan en las corres-pondientes descripciones dinámicas.

2. Una entidad abstracta «incendio» que haga que las llamas de un incendiose vayan propagando por las habitaciones, esto se utilizó en la aventura«Fuego».

3. Una entidad abstracta «guión» que haga que ciertos eventos de una his-toria vayan sucediendo en momentos prefijados en distintas partes delmundo, esto se utilizó en la aventura «15 meses y un día».

Éstos son sólo ejemplos; pero los comportamientos que se pueden implemen-tar con entidades abstractas sólo están limitados por el ingenio del programador.

Page 116: Documentacion age

116 Aspectos avanzados del modelo de mundo

3.8. Hechizos

Entre las características de Aetheria Game Engine dirigidas a la creación dejuegos de rol de texto mono y multijugador, se cuenta un completo sistema dehechizos o conjuros. Al igual que las características relacionadas con combate yarmas (ver sección 3.6), los conjuros seguramente sólo serán útiles a los autoresque quieran crear mundos con elementos de rol, pudiendo ignorarlos el resto delos creadores.

El sistema de hechizos en AGE, como el sistema de combate, está fuertementebasado en el sistema de temporización (ver sección 2.3.2) y en los estados de lascriaturas (ver sección 3.5). Es recomendable familiarizarse con esas secciones dela documentación antes de hacer uso de los hechizos.

Los hechizos se modelan en AGE como entidades de una clase específica(clase Spell), que tienen las siguientes características:

Las criaturas del mundo (jugadores o no) pueden conocer un hechizo dado,en cuyo caso sabrán cómo conjurarlo.

Conjurar un hechizo supone que la criatura tenga que gastar una cantidadde puntos mágicos (MP). Si la criatura no tiene suficientes puntos mágicos,no podrá conjurar el hechizo.

Cuando se conjura con éxito un hechizo, éste produce un efecto. El efectose modela como una entidad abstracta separada del hechizo.

Un hechizo puede (o no) conjurarse sobre un objetivo, que es un Itemo Mobile sobre el que se aplicará el hechizo. Por ejemplo, un conjurode bola de fuego típicamente tendrá un objetivo (la criatura u objeto ala que le lanzamos la bola de fuego). Un conjuro de invocar monstruopuede definirse sin objetivo, simplemente aparecerá un monstruo cerca dela criatura que lo ha lanzado.

Por defecto, los jugadores pueden conjurar un hechizo escribiendo «conjurar<nombre de hechizo>(sobre <objetivo>)». Por defecto, «invocar», «convocar»y «ejecutar» son sinónimos de «conjurar».

3.8.1. Creación de hechizos en PUCK

Para crear un hechizo en PUCK, lo primero que haremos será hacer clicksobre la herramienta «Añadir Hechizo» en la barra de herramientas, y luegohacer click sobre el punto del mapa en el que queramos representar el hechizo.Como siempre, aparecerá un formulario con información que debemos rellenar.

El campo de «Nombre único», en el formulario «General», juega en los he-chizos el mismo papel que en el resto de entidades. Más abajo, tenemos unaserie de campos específicos de los hechizos, cuyo significado es el que sigue:

Pendiente de probabilidad de éxito: valor numérico (tipo double, es decir,con decimales) que define la dificultad de la curva de aprendizaje de lanzarel hechizo con éxito, es decir, lo rápido o lento que se aprende a lanzarataques certeros. Un valor de 0 correspondería a una dificultad media omoderada, valores positivos hacen el aprendizaje más fácil, y negativos

Page 117: Documentacion age

Hechizos 117

más difícil. Más en detalle, su funcionamiento es idéntico al de la pendien-te de probabilidad de éxito en los ataques con armas, que se explica enlas subsecciones sobre parámetros de combate de las armas (ver 3.6.3) ymatemática de las armas (ver 3.6.4) de la sección de combate y armas.

Tiempo de lanzamiento: nos permite personalizar el número de unidadesde tiempo que le llevará a una criatura conjurar este hechizo (es decir, eltiempo que pasará desde que la criatura comience a conjurar hasta que elconjuro haga efecto). Para ello, nos permite introducir un valor de «Base»y una «Pendiente». De nuevo, el significado de estos valores es exactamenteel mismo que para los valores de base y pendiente que rigen los tiemposde ataque, bloqueo, recuperación, etc. de las armas; y está explicado condetalle en las subsecciones sobre parámetros de combate de las armas (ver3.6.3) y matemática de las armas (ver 3.6.4) de la sección de combate yarmas.

Duración: en el caso de hechizos que tengan una duración sostenida en eltiempo (por ejemplo, un encantamiento que imbuya un arma con un aurade fuego durante unos minutos), este campo nos permite personalizar elnúmero de unidades de tiempo que se mantendrá activo el hechizo. Denuevo, el significado de «Base» y «Pendiente» es como en las armas. Paralos interesados en los detalles, es importante tener en cuenta que, como laduración es una característica «positiva» (a un conjurador experto le in-teresa que el tiempo de lanzamiento disminuya; pero en cambio le interesaque la duración aumente), la fórmula que sigue no es la correspondientea tiempos de la sección sobre matemática de las armas (ver 3.6.4), sino lacorrespondiente a probabilidades multiplicada por la duración base. Estoquiere decir que la duración base representa realmente la duración máxi-ma que obtendría del hechizo un conjurador muy experto, mientras que unmago menos experto obtendrá tiempos que se acercarán progresivamentea la duración base a medida que vaya aprendiendo. Para los hechizos quesean instantáneos, y que por lo tanto no tengan una duración sostenidaen el tiempo, debe ponerse el valor base de duración a cero.

Coste de lanzamiento: es la cantidad de puntos mágicos (MP) que le cos-tará a una criatura ejecutar el hechizo. Dado que se trata de una caracte-rística «negativa» (interesa reducirla), sigue la fórmula de los tiempos.

Intensidad: representa con cuánta fuerza es capaz el mago de lanzar elhechizo. Más adelante veremos cómo se hace para que, en el mundo deljuego, hechizos con mayor intensidad se traduzcan realmente en hechi-zos más poderosos. Al ser una característica «positiva», sigue la mismafórmula que la duración.

Habilidades relevantes: nos permite introducir el conjunto de habilidadesque condicionan lo bien que un personaje dado utilizará el conjuro. Amedida que el personaje entrene esas habilidades, irá disminuyendo eltiempo y coste de lanzamiento del conjuro, y aumentando su duración,intensidad y probabilidad de éxito. De nuevo, los detalles de cómo rellenareste formulario son los mismos que en la sección de armas, y para losdetalles de cómo se calcula el efecto que tienen las habilidades sobre los

Page 118: Documentacion age

118 Aspectos avanzados del modelo de mundo

parámetros del conjuro, remitimos a la sección de matemática de las armas(ver 3.6.4).

3.8.2. Uso de hechizos por los jugadores

La pestaña «Nombres» del formulario de un hechizo es más sencilla que lade otros tipos de entidad como las cosas o las criaturas, dado que los conju-ros no tienen nombres para mostrar, sino solamente nombres de referencia. Losnombres de referencia pueden ser tecleados por los jugadores para referirse a loshechizos, como sucedía con otras entidades. A este respecto, es importante teneren cuenta que, por el momento, los jugadores sólo pueden referirse a un hechizomediante la orden «conjurar» descrita anteriormente: no hay implementado pordefecto ningún otro comando sobre hechizo, y los hechizos no cuentan con méto-dos parseCommand que el creador de mundos pueda redefinir para implementarcomandos sobre hechizos, ni tampoco pueden participar como parámetros enlos métodos parseCommand definidos en el jugador o en el mundo. Sin embargo,esta funcionalidad podría añadirse a AGE en el futuro, si algún autor tuvieseinterés en utilizarla.

Por el momento, por lo tanto, la utilidad de los nombres de referencia es quenos permiten definir los nombres que un jugador puede utilizar para referirsea un hechizo. Así, si ponemos como nombres de referencia de nuestro conjuro«bola de fuego» y «fuego», el jugador podrá teclear cosas como «conjurar fuegosobre dragón» o «invocar fuego sobre oso» para utilizar el conjuro, suponiendoque lo conozca. Un jugador nunca se podrá referir a conjuros que no conoce.

Para indicarle a PUCK qué hechizos conoce un jugador o personaje da-do, podemos utilizar la relación estructural «conoce». Usando la herramientade «Añadir relación estructural» de PUCK, y haciendo click primero sobre elpersonaje y a continuación sobre el hechizo, crearemos automáticamente dicharelación «conoce», con lo cual el hechizo estará disponible para el jugador.

Dado que en muchos juegos puede interesar que un personaje aprenda he-chizos que antes no conocía, los hechizos que conoce un personaje también sepueden modificar directamente con código BeanShell, utilizando los métodosaddSpell() y removeSpell() de la clase Mobile:

/∗ c l a s e Mobi le ∗/ vo id addSpe l l ( S p e l l nuevo )

Hace que el Mobile sobre el que invocamos este método aprenda el hechizonuevo.

/∗ c l a s e Mobi le ∗/ boolean r emoveSpe l l ( S p e l l v i e j o )

Quita viejo de la lista de hechizos conocidos por el Mobile sobre el queinvocamos este método (es decir, hace que el personaje olvide el conjuro viejoy no lo pueda utilizar más) y devuelve true, en el caso de que efectivamente elpersonaje conociese el conjuro. En el caso de que ya no lo conociese, este métodono hace nada y devuelve false.

Para obtener un objeto de la clase Spell a partir de un hechizo que hayamoscreado en PUCK, podemos utilizar la función spell() (análoga a las funcionesroom(), item() y mobile() que veíamos en los primeros pasos con beanshell, 2.1),o bien las funciones genéricas entity() o get() que permiten obtener cualquierentidad. Por lo tanto, cualquiera de las tres siguientes líneas se pueden utilizar

Page 119: Documentacion age

Hechizos 119

para obtener el objeto de la clase Spell que representa un conjuro de «Bola defuego»:

S p e l l c on j u r o ;c on j u r o = s p e l l ( "Bola de fuego " ) ;c on j u r o = e n t i t y ( "Bola de fuego " ) ;c on j u r o = get ( "Bola de fuego " ) ;

Así, por ejemplo, el siguiente código en un libro de magia hace que quienlo lea aprenda el conjuro de «Bola de fuego» y adquiera habilidad en magia deataque (ejemplo adaptado de «Wizard’s Quest: Morluck’s Lair»):

vo id parseCommand ( Mobi le aC r ea tu r e , S t r i n g ve rb , S t r i n g a r g s ){

i f ( e qu a l s ( ve rb , " l e e r " ) ){

i f ( ! aC r ea tu r e . has I tem ( s e l f ) ){

aC r ea tu r e . w r i t eD e n i a l ( " Pr imero t e n d r í a s que t e n e r l o entu s manos . . . \ n" ) ;

end ( ) ;}

i f ( ! ge t ( s e l f , " r ead " ) ){

s e t ( s e l f , " r ead " , t rue ) ;

aC r ea tu r e . w r i t eA c t i o n ( "Leyendo e l l i b r o de magia ,ap r ende s e l c on j u r o de Bola de Fuego . \ n" ) ;

aC r ea tu r e . a ddSpe l l ( ge t ( "Bola de fuego " ) ) ;aC r ea tu r e . s e t S k i l l ( "magiaAtaque" ,10) ;end ( ) ;

}e l s e{

aCrea tu r e . w r i t e I n f o rma t i o n ( "Ya has ap r end i do e l c on j u r ode Bola de Fuego , e s t e l i b r o no t i e n e nada más quee n s e ñ a r t e . \ n" ) ;

end ( ) ;}

}}

Para saber qué hechizos conoce un personaje en un momento dado, puedeutilizarse el método getSpells() de la clase Mobile:

/∗ c l a s e Mobi le ∗/ S p e l l L i s t g e t S p e l l s ( )

Con lo que hemos visto por el momento, sabemos cómo crear un hechizo yhacer que un jugador pueda utilizarlo: para ello hará falta por un lado que elpersonaje jugador conozca el hechizo (cosa que podemos conseguir, como hemosvisto, desde el PUCK o con código BeanShell); y por otro lado que el hechizotenga algún nombre de referencia para que el jugador pueda referirse a él.

Sin embargo, todavía no hemos visto cómo hacer que los hechizos realmentefuncionen: sabemos crear un conjuro de bola de fuego y que los jugadores puedanejecutarlo, pero ¡no sabemos cómo hacer que realmente produzca fuego! Esto eslo que veremos en la siguiente subsección.

Page 120: Documentacion age

120 Aspectos avanzados del modelo de mundo

3.8.3. Funcionamiento de los hechizos

Para saber cómo programar hechizos para que hagan lo que queramos, pri-mero debemos saber cómo funciona un hechizo. El diagrama 3.4 muestra todoel proceso que se sigue para lanzar un hechizo, desde que un personaje se pro-pone lanzarlo (en el caso de un jugador, esto correspondería a haber tecleado laorden de conjurar el hechizo) hasta que el hechizo ha terminado, sea en éxito oen fracaso.

Para entender el diagrama, es importante saber que para que un hechizofuncione, son necesarias por lo menos dos entidades (sin contar el personaje quelo lanza ni el posible objetivo): una entidad de la clase Spell que representa elhechizo en sí, que es la que hemos aprendido a crear y manejar en las seccio-nes anteriores, y una entidad abstracta que representará el efecto (Effect) delhechizo.

Para crear una entidad abstracta que describa un efecto, basta con crear unaentidad abstracta de forma normal, y teclear la palabra «effect» en el campo de«Tipo» de la pestaña «General» de su formulario. Para asociar un efecto a unhechizo, usamos la herramienta «Añadir relación estructural» de PUCK paracrear una relación del hechizo al efecto: automáticamente, se creará una relaciónestructural «Tiene efecto» indicando que se trata de un efecto del hechizo.

El motivo de esta separación entre hechizo y efecto, en lugar de definirlo tododentro de la propia entidad hechizo, es que nos proporciona mayor flexibilidad:de esta manera podemos crear hechizos que tengan varios efectos (como podríaser un conjuro de escudo de fuego que nos protegiese pero que también hiciesedaño a los oponentes cercanos), o bien poder crear varios hechizos que utilicenel mismo efecto de diferentes maneras (por ejemplo, los conjuros de «escudo defuego» y «escudo de fuego mejorado» podrían utilizar el mismo efecto; pero condistintas duraciones).

Preparación del hechizo y tiempo de lanzamiento

Como se puede ver en el diagrama, lo primero que sucede cuando se pretendelanzar un hechizo es que se calculan los puntos mágicos que consumirá. Estose hace utilizando los valores base y pendiente del «Coste de lanzamiento»comentados con anterioridad. En caso de que el mago no disponga de esos puntosmágicos, no podrá realizar el conjuro, mostrándosele el mensaje por defectocorrespondiente.

En el caso de que el jugador sí tenga los puntos mágicos requeridos, se lerestan de sus puntos mágicos y se ejecuta, si se ha definido, el método BeanShellprepare() del hechizo. Este método debe definirse con cabecera

/∗ c l a s e S p e l l ∗/ vo id p r epa r e ( Mobi le c a s t e r , E n t i t y t a r g e t )

para definir lo que sucederá cuando la criatura caster comience a ejecutar elconjuro sobre el objetivo target. En el caso de que el conjuro no tenga objetivo,target tomará el valor null. El método prepare() sirve para describir cómo unpersonaje se dispone a lanzar un hechizo antes de que realmente lo haga: estopuede ser particularmente útil en juegos en los que interactúen varios persona-jes, y en particular en los que tengan combates, para dar tiempo a que otrosjugadores y criaturas se den cuenta de que alguien está preparando un hechizo

Page 121: Documentacion age

Hechizos 121

Figura 3.4: Diagrama del proceso de funcionamiento de un hechizo.

Page 122: Documentacion age

122 Aspectos avanzados del modelo de mundo

y así puedan reaccionar antes de que termine (por ejemplo intentando huir, oclavarle rápidamente una daga al mago antes de que termine el conjuro).

En el método prepare() también puede utilizarse la función de BeanShellend() para interrumpir el proceso de ejecución del hechizo, de modo que nollegue a ejecutarse. Esto se puede utilizar para validar situaciones como unconjuro que requiere un objetivo pero el jugador no lo ha tecleado, o un conjuroque sólo se puede ejecutar sobre objetivos de tipo Mobile pero el jugador loestá haciendo sobre un Item. Si bien, como veremos, la ejecución del conjurotambién se puede terminar más adelante, el método prepare() es el mejor puntopara hacerlo si queremos que el personaje no pierda unidades de tiempo.

Así, por ejemplo, el siguiente método prepare() hace que el personaje pronun-cie unas palabras arcanas, además de validar que el conjuro tiene un objetivode la clase Mobile:

vo id p r epa r e ( Mobi le c a s t e r , E n t i t y t a r g e t ){

i f ( t a r g e t == nu l l | | ! ( t a r g e t i n s t anceo f Mobi le ) ){

c a s t e r . w r i t e ( "Ese con j u r o debe e j e c u t a r s e s ob r e una c r i a t u r a . \ n" ) ;

end ( ) ;}e l s e{

c a s t e r . say ( "An k h a l i ghorum thunkys . . . " ) ;}

}

A continuación del método prepare(), y suponiendo que no se haya terminadocon end(), transcurrirán unas unidades de tiempo hasta que el conjuro realmentese ejecute. Se puede imaginar este tiempo como el que le lleva al mago concen-trarse, pronunciar palabras mágicas, focalizar energía, o lo que sea necesariopara lanzar un conjuro según la ambientación del mundo en que se encuentre.Dicho tiempo se calcula a partir de los valores de base y pendiente de «Tiempode lanzamiento» que hayamos especificado en el formulario del hechizo. Si nose quiere que ejecutar un hechizo consuma tiempo, sino que sea instantáneo, sepuede conseguir simplemente poniendo el tiempo de lanzamiento base a cero.

Durante el tiempo de lanzamiento, el mago que va a lanzar el conjuro seencontrará en el estado Mobile.CASTING. Por lo tanto, se puede emplear esteestado en código BeanShell para comprobar si un personaje está o no lanzandoun hechizo en un momento dado.

Transcurrido el tiempo de lanzamiento, se procederá inmediatamente a lan-zar el hechizo. Pero el lanzamiento de un hechizo puede ser exitoso o fallido,de acuerdo con la habilidad del personaje y con los valores de base y pendienteespecificados para la «Probabilidad de éxito» en el formulario del hechizo. Deacuerdo con esa probabilidad, se sorteará aleatoriamente si el conjurador tieneéxito o no, y según el resultado se llamará a unos métodos BeanShell o a otros,para permitirnos definir por separado cómo se comporta un hechizo cuando esexitoso y cuando fracasa.

Page 123: Documentacion age

Hechizos 123

Lanzamiento fallido

En el caso de que el conjuro fracase, se ejecutarán, por este orden, los si-guientes métodos BeanShell (si están definidos):

1. El método beforeFail() del conjuro, que es de la forma:

/∗ c l a s e S p e l l ∗/ vo id b e f o r e F a i l ( Mobi le c a s t e r , E n t i t yt a r g e t )

donde el parámetro caster nos da el personaje que intenta (sin éxito) lanzarel hechizo, y target nos proporciona la entidad objetivo. En este métodopodemos, opcionalmente, utilizar la función end() para interrumpir el pro-ceso y que no lleguen a ejecutarse los dos siguientes.

2. El método fail() de cada uno de los efectos del conjuro, que es de la forma:

/∗ c l a s e E f f e c t ∗/ vo id f a i l ( Mobi le c a s t e r , E n t i t y t a r g e t )

siendo de nuevo caster el mago que ha fallado lanzando un conjuro con eseefecto, y target el objetivo.

3. El método afterFail() del conjuro, que es de la forma:

/∗ c l a s e S p e l l ∗/ vo id a f t e r F a i l ( Mobi le c a s t e r , E n t i t yt a r g e t )

donde los parámetros significan lo mismo que en el método beforeFail().

Si bien los tres métodos se ejecutan en la misma unidad de tiempo, porsu orden de ejecución y las clases a las que están asociados nos proporcionanfuncionalidades ligeramente distintas. Por ejemplo, el método beforeFail() podríautilizarse para mostrar un mensaje diciendo que el hechizo falla, mientras quelos métodos fail() de los efectos podrían usarse para programar consecuenciasdel fallo para cada uno de los efectos (podríamos querer hacer, por ejemplo, quesi conjuramos mal una bola de fuego la bola se genere, pero nos estalle en lasmanos en lugar de golpear a nuestro enemigo). El método afterFail() podemosemplearlo para cualquier cosa que deba suceder después de las consecuenciasdel fallo de los efectos (por ejemplo, tal vez queramos mostrar algún mensajede fallo después de dichas consecuencias y no antes, o bien hacer que el magoquede aturdido un rato después de haber fallado el conjuro).

Lanzamiento con éxito

Si en lugar de fracasar, el conjuro tiene éxito, el comportamiento es análogo,ejecutándose los siguientes métodos BeanShell (si están definidos):

1. El método beforeCast() del conjuro, que es de la forma:

/∗ c l a s e S p e l l ∗/ vo id be f o r eCa s t ( Mobi le c a s t e r , E n t i t yt a r g e t )

donde el parámetro caster es el personaje que está a punto de conseguirlanzar con éxito el hechizo, y target nos proporciona la entidad objetivo.En este método se puede emplear la función end() para interrumpir elproceso y que no se ejecuten los dos siguientes.

Page 124: Documentacion age

124 Aspectos avanzados del modelo de mundo

2. El método cast() de cada uno de los efectos del conjuro, que es de la forma:

/∗ c l a s e E f f e c t ∗/ vo id c a s t ( Mobi le c a s t e r , E n t i t y t a r g e t ,i n t i n t e n s i t y )

donde caster es el mago que ha conseguido lanzar un conjuro con ese efecto,target es el objetivo e intensity es la intensidad del efecto. Este valor deintensidad se calcula a partir de los valores base y pendiente de intensidadintroducidos en el formulario del hechizo, y su uso será el que le quiera darel programador: por ejemplo, si estamos creando el efecto de una bola defuego, seguramente nos interesará programarlo para que haga más dañocuanta mayor sea la intensidad (y tal vez mostrar mensajes distintos segúnsi la bola es modesta o muy grande).

3. El método afterCast() del conjuro, que es de la forma:

/∗ c l a s e S p e l l ∗/ vo id a f t e r C a s t ( Mobi le c a s t e r , E n t i t yt a r g e t )

y donde los parámetros significan lo mismo que en el método beforeCast().

De este modo, el método beforeCast() puede utilizarse para cualquier com-portamiento que se quiera definir justo antes de que haga efecto el hechizo. Porejemplo, podríamos mostrar un mensaje para indicar que la preparación delconjuro ha terminado y que éste va a actuar ya. Además, cabe destacar que elmétodo beforeCast() puede ser útil para validaciones de última hora: por ejem-plo, puede que el jugador haya tecleado «conjurar bola de fuego sobre troll» yque hayamos comprobado en prepare() que el hechizo tenía un objetivo válido(el troll); pero que durante las unidades de tiempo transcurridas entre prepare()y beforeCast() (es decir, durante el tiempo de lanzamiento del conjuro) el trollse haya ido de la habitación. Para comprobar ese extremo, podríamos poner eneste método algo como:

vo id be f o r eCa s t ( Mobi le c a s t e r , E n t i t y t a r g e t ){

i f ( ! c a s t e r . getRoom ( ) . hasMob i l e ( t a r g e t ) ){

c a s t e r . w r i t e ( "Vaya , ¡m i e n t r a s p r epa r aba s e l hech i zo , tu enemigose ha escapado !\ n" ) ;

end ( ) ;}e l s e{

c a s t e r . say ( " . . . k h a l i mazth i bey ! " ) ;}

}

Los métodos cast() de cada efecto serán los que deban implementar los efectosde cada conjuro cuando tiene éxito: abrir una puerta si se trata de un hechizode apertura, causar daño de fuego si es un hechizo de bola de fuego, etc.

Por último, el método afterCast() del conjuro será el que se encargue derealizar cualquier procesado inmediatamente posterior a que el hechizo tengaefecto, como mostrar algún mensaje adicional o tal vez hacer que el mago tengaque pasar unos momentos recuperándose del esfuerzo de conjurar el hechizo.

Page 125: Documentacion age

Hechizos 125

Hechizos de efectos duraderos

Los métodos anteriormente descritos son suficientes si se quiere implementarhechizos de efecto instantáneo, es decir, aquéllos que producen algún cambio enel momento en el que se lanzan pero no hacen nada más después de eso.

Sin embargo, en un sistema de magia también resulta interesante podertener hechizos de efecto duradero: aquéllos que provocan algún cambio que tieneuna determinada duración en el tiempo y, transcurrida ésta, el cambio expira,pudiendo ser renovado si se lanza de nuevo el hechizo. Ejemplos de conjurosque suelen funcionar de esta manera en muchos juegos son los conjuros queproporcionan un escudo protector al jugador, le dan la capacidad de volar oencantan un arma para que haga más daño durante un período de tiempo.

Aunque estos conjuros duraderos podrían implementarse a mano a partirde los métodos anteriores y usando temporizadores, AGE proporciona soportenativo para crearlos, permitiendo hacerlo con más facilidad, y que además laduración de sus efectos se integre con el sistema de habilidades y aprendizajede AGE (es decir, que los magos más capaces consigan efectos más duraderos).

Para utilizar este soporte y crear un conjuro de efectos duraderos en AGE,lo primero que debemos hacer es poner la duración base del formulario generaldel hechizo a un valor mayor que cero. Esto indicará a AGE que se trata deun hechizo con efectos sostenidos en el tiempo, y además permitirá a AGEcalcular la duración de dichos efectos mediante la fórmula que se mencionó conanterioridad.

Además de esto, necesitaremos indicarle de alguna manera a AGE lo quetiene que hacer no sólo cuando se lanza el conjuro y entran en vigor sus efectos(cosa que ya indicábamos en el resto de hechizos); sino también cuando éstosexpiran y deben deshacerse. Para ello, utilizaremos el método fade() de cadauno de los efectos del conjuro, que AGE llamará automáticamente cuando ésteexpire y sea necesario deshacer sus efectos. Dicho método es de la forma

/∗ c l a s e E f f e c t ∗/ vo id f ade ( En t i t y t a r g e t )

donde target es el objetivo en el que expira el efecto. Por lo tanto, paraimplementar el hechizo duradero, tendremos que proceder igual que con loshechizos instantáneos, poner en el método cast() de cada efecto el código queactiva el efecto, y poner en el método fade() de cada efecto el código que desactivael efecto, y AGE se encargará del resto.

Algunas veces, dependiendo de la naturaleza del efecto, hará falta tener encuenta en el método cast() si éste está todavía activo o no. Para que podamossaber esto, AGE crea automáticamente, inmediatamente después de ejecutarcast(), una relación bidireccional de nombre «cast» y valor true entre el efectoy su objetivo (en el caso de que el conjuro no tenga objetivo, la relación se creaentre el efecto y la habitación donde se haya llamado). Esta relación se poneautomáticamente a false cuando el efecto expira, justo antes de la ejecución delmétodo fade(). Así, si por ejemplo queremos que un efecto de «aturdir» no seaaplicable a enemigos que ya están aturdidos mediante el mismo efecto, podemoshacer algo como lo que sigue (ejemplo adaptado de «Wizard’s Quest: Morluck’sLair»):

vo id c a s t ( Mobi le c a s t e r , E n t i t y t a r g e t , i n t i n t e n s i t y ){

i f ( ge t ( s e l f , " c a s t " , t a r g e t ) )

Page 126: Documentacion age

126 Aspectos avanzados del modelo de mundo

{c a s t e r . w r i t e ( " ¡Ese enemigo ya e s t á ba jo l o s e f e c t o s de un

con j u r o de a t u r d im i e n t o !\ n" ) ;}e l s e{

// l a c i f r a 10000 no t i e n e r e l e v a n c i a porque fade ( ) v o l v e r á acambiar e l e s t ado

t a r g e t . setNewState ( Mobi le . SURPRISE_RECOVER,10000) ;c a s t e r . getRoom ( ) . i n f o rmAc t i on ( c a s t e r , t a r g e t , nu l l , "$2 queda

a t u r d i d o por e l h e ch i z o de $1 . . . \ n" ,"Quedas a t u r d i d o por e l h e ch i z o de $1 . \ n" ,"Tu hech i z o a tu rde a $2 . \ n" , t rue ) ;

}}

vo id f ade ( En t i t y t a r g e t ){

// e l i f e s porque pod r í a no t e n e r ya e s t e estado , t a l vez porquea l go ( un go l pe . . . ) l o s a c a r a de su a t u r d im i e n t o .

i f ( t a r g e t . g e t S t a t e ( ) == Mobi le . SURPRISE_RECOVER )t a r g e t . setNewState ( Mobi le . IDLE , 1 ) ;

}

Nótese que esta relación «cast» sólo toma valores booleanos (true o false) ypor lo tanto no sirve para distinguir casos más complejos como que un conjuroduradero se pueda aplicar varias veces de manera acumulativa, o que un mismoobjetivo pueda ser objeto del mismo efecto varias veces por parte de diferentespersonajes o jugadores. Si se quieren soportar este tipo de cosas, se puede hacercreando otras relaciones más complejas a mano en los métodos cast() y fade().

3.8.4. Gestión de los puntos mágicos

Como vimos al hablar sobre la preparación de los hechizos, cada vez que unpersonaje conjura un hechizo deberá gastar cierta cantidad de puntos mágicos(MP); a no ser que hayamos puesto el valor base del «Coste de lanzamiento»a cero. Normalmente, los juegos de rol que utilizan puntos mágicos disponende algún mecanismo para regenerarlos, bien sea automáticamente con el pasodel tiempo, o mediante objetos como pociones de maná, o permitiendo ambascosas.

Dado que los mecanismos para regenerar puntos mágicos pueden ser muydiversos y depender de cada juego en particular, AGE no implementa ningúnmecanismo de regeneración específico, sino que lo deja al albedrío del programa-dor, proporcionando métodos para modificar los puntos mágicos de una criatura(MP) iguales que los análogos para modificar los puntos de vida (HP).

De este modo, podemos especificar los puntos mágicos máximos y los pun-tos mágicos iniciales de una criatura en los campos «MP» y «MP máx» de lapestaña «General» de su formulario de PUCK, y podemos obtener y modifi-car dinámicamente los puntos mágicos de una criatura mediante los siguientesmétodos BeanShell (análogos a los correspondientes a los puntos de vida o HP):

/∗ c l a s e Mobi le ∗/ i n t getMP ( )

Devuelve la cantidad de puntos mágicos que tiene actualmente la criaturasobre la que se invoca.

Page 127: Documentacion age

Hechizos 127

/∗ c l a s e Mobi le ∗/ vo id setMP ( i n t newMP )

Cambia la cantidad de puntos mágicos que tiene la criatura sobre la que seinvoca a la cantidad newMP.

/∗ c l a s e Mobi le ∗/ i n t getMaxMP ( )

Devuelve la cantidad de puntos mágicos máximos de la criatura sobre la quese invoca.

/∗ c l a s e Mobi le ∗/ vo id setMaxMP ( i n t newMaxMP )

Cambia la cantidad de puntos mágicos máximos que tiene la criatura sobrela que se invoca a la cantidad newMaxMP. Esto se puede utilizar, por ejemplo,para implementar subidas de nivel en juegos de rol basados en niveles.

3.8.5. Uso de hechizos por los personajes no jugadoresDespués de haber visto todos los detalles del funcionamiento de los hechizos

en AGE, y de saber cómo pueden utilizar los hechizos los personajes jugadores,concluimos la sección explicando cómo podemos hacer que un personaje nojugador lance un conjuro, cosa que puede ser útil para programar enemigos queutilicen la magia contra el jugador. Conseguirlo es muy sencillo, simplementeinvocaremos el siguiente método de la clase Mobile:

/∗ c l a s e Mobi le ∗/ vo id c a s t ( S p e l l s p e l l , E n t i t y t a r g e t )

donde como parámetro spell pasaremos el conjuro que la criatura va a lanzar,y como parámetro target pasaremos el objetivo de dicho conjuro, si lo hay, o biennull si se trata de un hechizo sin objetivo.

El proceso de que un personaje no jugador lance un hechizo pasa por lasmismas etapas y funciona exactamente igual que cuando lo hace un jugador.

Page 128: Documentacion age

128 Aspectos avanzados del modelo de mundo

3.9. Mensajes por defecto

Muchos de los textos que una aventura de AGE muestra al jugador sontextos que el autor del juego no necesita escribir explícitamente, porque yaestán programados en el AGE. Éstos son lo que llamamos mensajes por defecto.Por ejemplo, en la siguiente interacción:

> coger el asdfasdf¿Qué pretendes coger?> inventarioNo tienes nada.> coger la piedraCoges la piedra.

Los tres mensajes que muestra como salida el AGE son mensajes por defecto.En el caso de los dos primeros, todo el texto que se muestra («¿Qué pretendescoger?» y «No tienes nada.») es texto puesto por el sistema sin necesidad de queel jugador lo especifique en ninguna parte. En el tercer caso, el texto «Coges»proviene asimismo del sistema, mientras que el sintagma «la piedra» se generadinámicamente porque le hemos dado a esa entidad el nombre para mostrar«piedra» y el género femenino que implica el artículo «la».

3.9.1. Cambiar los mensajes por defecto

En ciertos mundos puede resultar conveniente cambiar los mensajes por de-fecto, si es que los que vienen de base con el AGE no nos gustan por algúnmotivo. Por ejemplo, en un juego de ambientación medieval podríamos quererque el sistema se dirigiera al jugador con un tratamiento más arcaico: «¿Quépretende coger vuesa merced?» o «Cogéis la piedra.» Por supuesto, el AGEproporciona la posibilidad de hacer esto, para lo cual existen varias maneras.

En primer lugar, observemos el fichero «lang/messages.lan» que está dentrode «AgeCore.jar» (este fichero .jar se puede abrir como si fuera un .zip para verdicho fichero .lan). En él, podemos ver entradas como:

#Cuando intentas coger algo que no está en la habitaciónget.what=¿Qué pretendes coger?\n#Mostrar inventario (vacío)you.have.nothing=No tienes nada.\n#Cuando coges el objeto $itemyou.get.item=Coges $item.\n

Estas entradas describen mensajes por defecto de AGE, en concreto, losvistos en el ejemplo anterior. La línea que empieza con # en cada entrada essimplemente un comentario que explica de qué trata el mensaje. El texto anterioral signo «=» en cada línea sin comentario es el nombre que identifica ese mensajepor defecto. Lo que hay a la derecha del signo «=» es el mensaje en sí, donde losidentificadores que tienen el símbolo del dólar se van a sustituir por nombres deentidades (normalmente debería ser obvio por el contexto cuáles: por ejemplo,en el último de los mensajes de ejemplo, el identificador $item se sustituye por elnombre de la cosa que el jugador esté cogiendo, con su artículo correspondiente).

Page 129: Documentacion age

Mensajes por defecto 129

Si cambiásemos los mensajes de este fichero, estaríamos modificando los men-sajes por defecto de AGE. Pero no es esto lo que queremos hacer, sino modificar-los para un mundo en particular. Sin embargo, abrir el fichero «messages.lan»es útil para esta tarea porque para cambiar mensajes para un mundo concretonecesitaremos saber el nombre de los mensajes que queremos redefinir (lo queviene antes del signo =, como you.get.item) así como los parámetros que so-portan (en este caso, $item). Sabido esto, existen dos maneras de cambiar losmensajes:

Cambiar mensaje por mensaje

Para cambiar un mensaje individual, por ejemplo, si quisiéramos que el par-ser nos tratara de «vos» al coger una cosa, hacemos lo siguiente:

wor ld . getMessages ( ) . se tMessage ( "you . ge t . i tem" , " Cogé i s $ i tem" ) ;

El método getMessages() de la clase World nos proporciona un objeto de laclase Messages que encapsula los mensajes por defecto, y el método setMessagenos permite cambiar cada uno de ellos.

Este cambio de mensaje será permanente, es decir, a partir de la llamada aeste método se imprimirá «Cogéis ...» cada vez que un jugador coja una cosa.Sin embargo, en ocasiones también querremos que se imprima un mensaje pordefecto en particular sólo en una ocasión. Para ello, también existe la posibilidadde cambiar el mensaje sólo para la próxima vez que se imprima. Así, si porejemplo quisiéramos que al coger un cubito de hielo se nos mostrara un mensajediciendo que está frío, podríamos poner en el código para la orden «coger» sobreel cubito se hiciese algo como:

wor ld . getMessages ( ) . se tNextMessage ( "you . ge t . i tem" , "Coges $ i tem .¡Buf , qué f r í o e s t á ! " ) ;

De esta manera conseguiríamos que se mostrase un mensaje distinto sólo alcoger el cubito; pero no con el resto de las cosas del mundo.

Cambiar todos los mensajes de una sola vez

Si queremos personalizar una aventura completa, es posible que queramoscambiar gran parte de los mensajes por defecto, o incluso todos. En ese caso, esmás sencillo utilizar este método:

1. Crear un fichero con el formato del messages.lan (por ejemplo, haciendouna copia de éste y modificándola).

2. Ponerlo en el directorio del mundo.

3. Hacer que el mundo ejecute el siguiente código:

wor ld . l oadMessages ( wor ld . g e tRe sou r ce ( " nombre f i c h e r o . l a n " ) ) ;

Si no se van a cambiar los mensajes a lo largo de toda la aventura, como seríalo más común, lo normal sería ejecutar uno de estos códigos en el método intro(ya que se ejecuta al principio). Si se quieren tener diferentes mensajes o juegosde mensajes en distintas partes de la historia, estos métodos se pueden ejecutar

Page 130: Documentacion age

130 Aspectos avanzados del modelo de mundo

en cualquier punto de la misma para cambiarlos a partir de ese momento (sepueden tener varios ficheros de mensajes, o se pueden cambiar los mensajes unopor uno varias veces sin problema).

3.9.2. Generar dinámicamente los mensajes por defectoSi en una aventura interesa que los mensajes por defecto sean muy dinámicos

y cambiantes (por ejemplo, que cada vez que cojamos un objeto se elija un men-saje diferente de un conjunto de alternativas de forma aleatoria, o alguna otracosa que requiera procesado); es posible controlar mediante código BeanShell elmostrado del mensaje, pasando por encima tanto de los mensajes por defectodefinidos por AGE como de los establecidos para el mundo según lo visto en lasección anterior. Para ello, en el objeto mundo, redefinimos el método

S t r i n g getMessage ( S t r i n g messageName , Object [ ] arguments )

donde como parámetro se nos pasa el nombre del mensaje que AGE quieremostrar (por ejemplo, you.get.item) y un array con todas las entidades u otrosobjetos relevantes al mensaje (en este caso, el array tendría longitud 1 y sólocontendría la cosa que se está cogiendo y que se utilizaría por defecto parasustituir $item).

El método debe devolver el mensaje que queremos que se muestre en esecaso, o bien null si no queremos alterar el comportamiento para ese mensaje yobjetos sino delegar en el mecanismo por defecto de mensajes del mundo o deAGE.

Page 131: Documentacion age

Capítulo 4

Métodos redefinibles

4.1. Eventos

4.2. Otros métodos redefinibles

4.2.1. Métodos de Mobile (para PSIs)

Métodos de combate

• void addEnemy ( Mobile m ): añadirle a un enemigo, al que ataca-rá automáticamente en cuanto lo vea.

• void attack ( Mobile m , Weapon w ): atacar a otra criatura conel arma dada.

• void block ( Mobile m , Weapon w ): bloquear.

• void cast ( Spell s , Entity e ): conjurar el hechizo dado so-bre la entidad dada.

• void die(): morir.

• void dodge ( Mobile m ): esquivar.

Métodos de movimiento

• boolean goTo ( Room r ): ir a la habitación dada (contigua a laactual).

• boolean makeRandomValidMove(): moverse aleatoriamente hacia al-guna habitación accesible desde la actual.

Métodos para hablar y reaccionar

• void say ( String s ): decir el texto dado.

• void say ( String s , String style ): decir el texto dado, yque los mensajes de notificación correspondientes se impriman enel estilo dado.

• void onSay ( Mobile aCreature , String text ): quién ha ha-blado y que ha dicho. ej: decir «hola»

Page 132: Documentacion age

132 Métodos redefinibles

• void onSayTo ( Mobile speaker , String text , Mobile hearer ):quién ha hablado, que ha dicho y a quien. ej: decir «hola» a Gandalf.

• void onRoomText ( String text ): para que el PSI pueda reaccio-nar al texto que se envía a la habitación.

4.2.2. Ejemplo de usoEstos ejemplos (si no se dice lo contrario) van en la pestaña Código y pro-

piedades del PSI.Ejemplo de detección de texto enviado a la habitación. Cogemos una pelota,

y el PSI se enfada, puesto que es suya.

vo id onRoomText ( S t r i n g t e x t ){

i f ( t e x t . c o n t a i n s ( " coge l a p e l o t a " ) ){

s e l f . say ( " ¡Eh ! ¡Esa p e l o t a e s mía !\ n¡Ya no me a j un to con t i g o!\ n" ) ;

}}

Page 133: Documentacion age

Capítulo 5

El análisis de la entrada

Un componente clave de todo sistema de creación y ejecución de aventuras detexto es el analizador sintáctico que permite interpretar las órdenes introducidaspor los jugadores.

El analizador sintáctico de AGE funciona de una forma muy sencilla, basán-dose en el principio de que menos es más. Se trata de un analizador diseñadopara ser robusto: el analizador de AGE no utiliza un modelo estricto de cómodeben ser las oraciones de entrada ni intenta encajar el sentido de cada unade las palabras que introduce el jugador; sino que se basa en las palabras queconoce e ignora las que no conoce. Esto hace que sea posible conseguir juegosque reconozcan una amplia gama de órdenes y de posibles variaciones en la ma-nera de expresarla, sin necesidad de que el creador del juego invierta tiempo enconstruir una complicada gramática que de todos modos se quedaría corta antela complejidad de las órdenes que podrían llegar a teclear los jugadores.

De hecho, el analizador de AGE es tan sencillo para el programador dejuegos que realmente ni siquiera es necesario saber nada de cómo funciona paraprogramar una aventura. En realidad, con saber usar los métodos parseCommandque hemos descrito en la sección 2.2 manipulación básica de entidades bastarápara que nuestra aventura pueda comprender y ejecutar órdenes complejas,pues el analizador se encarga automáticamente de traducir cosas como «cogeel plátano y cómetelo» a «coger el plátano» por un lado y «comer plátano»por otro. Sin embargo, avanzados ya en nuestro conocimiento de AGE, siemprevendrá bien saber cómo funciona el analizador para saber exactamente quépodemos esperar y qué no.

A continuación describiremos cómo AGE lleva a cabo el análisis de la entra-da, y cómo se puede modificar ese comportamiento. En particular, en la secciónsobre métodos de análisis de la entrada (parseCommand) haremos una descrip-ción detallada de la manera en que se ejecutan estos métodos que ya llevamosutilizando desde las secciones introductorias, y además, una descripción paso apaso del análisis sintáctico de AGE en general. Más adelante, en la sección sobrepreprocesado de la entrada, veremos cómo podemos analizar nosotros directa-mente la entrada si queremos saltarnos el análisis que hace AGE. Por último,en la sección sobre gestión de verbos veremos detalles avanzados sobre cómomanipular la lista de verbos que el analizador de AGE reconoce como tales.

Page 134: Documentacion age

134 El análisis de la entrada

5.1. Métodos de análisis de la entrada (parseCommand)

5.1.1. Los métodos de análisis de la entrada

En el funcionamiento típico de los juegos basados en texto, los jugadoresintroducen lo que quieren que sus personajes hagan en forma de órdenes dadascomo cadenas de texto. El juego lee entonces estas cadenas de texto y, si consigueinterpretar su significado, ejecuta las acciones desencadenadas por la orden deljugador.

Los métodos de análisis de la entrada (o métodos parseCommand) son, proba-blemente, los métodos redefinibles más importantes del Aetheria Game Engine,ya que permiten al creador de juegos inyectar código para cambiar la mane-ra en que se analiza la entrada, y por lo tanto también las acciones que éstadesencadena.

Algunas de las cosas más comunes que se pueden hacer con los métodosparseCommand son:

Definir nuevos verbos y órdenes que no estén definidos por defecto en elAGE.

Cambiar el comportamiento por defecto de las órdenes que sí existen en elAGE, haciendo que en nuestro juego hagan otra cosa en lugar de (o apartede) ejecutar el comportamiento por defecto.

Definir o cambiar el comportamiento de órdenes situacionalmente; es decir,hacer que una orden tenga normalmente el comportamiento por defecto,pero actúe de forma distinta en determinadas situaciones (cuando se estáen una habitación dada, se ejecuta sobre un objeto determinado, etc.)

Aparte de estos usos, que son los más comunes, la versatilidad de los méto-dos parseCommand hace que realmente sean pocas las cosas que no se puedenhacer con ellos. Al dejarnos inyectar código que se ejecuta durante el procesode análisis de la entrada, un programador avanzado puede llegar a cambiar porcompleto todo el funcionamiento de un juego en AGE, sustituyendo todos loscomportamientos por defecto por código propio.

5.1.2. Ejemplo de uso

Como es habitual en los sistemas de creación de juegos basados en texto,Aetheria Game Engine es capaz de interpretar y procesar por defecto las órdenesmás comunes. Por ejemplo, si un jugador está en una habitación que contiene unplátano y teclea «cojo el plátano», el AGE se encargará de hacer que la sabrosafruta pase a las pertenencias del jugador, mostrándole además el mensaje detexto correspondiente. Esto es porque la acción «coger» es algo común a lamayoría de los juegos de texto: lo normal es que si una cosa no pesa demasiado,y no está fija en el sitio (como lo estaría un adoquín del suelo), se pueda coger.

Muchas de las acciones que se llevan a cabo en un juego de texto típico sonacciones estándar comunes a todos los juegos, como la de coger el plátano. Sinembargo, si queremos implementar un buen juego, lo normal es que en algúnmomento necesitemos crear acciones personalizadas, que implementen compor-tamientos específicos de nuestro juego. Por ejemplo, podemos querer que el

Page 135: Documentacion age

Métodos de análisis de la entrada (parseCommand) 135

personaje se pueda comer el plátano, saciando su hambre y quedándose con unapiel de plátano utilizable para hacer resbalar a algún malvado. Como esto no esun comportamiento genérico que venga en el sistema por defecto, lo tendremosque implementar nosotros en el objeto plátano, lo cual se puede hacer medianteun método de análisis de la entrada:

vo id parseCommand ( Mobi le aC r ea tu r e , S t r i n g ve rb , S t r i n g a r g s ){

i f ( e qu a l s ( verb , "comer" ) ){

aC r ea tu r e . w r i t eA c t i o n ( "Te comes e l p l á t ano . ¡Ñam , ñam ! ¡Quér i c o !\ n" ) ;

aC r ea tu r e . removeItem ( i tem ( " p l á t ano " ) ) ;aC r ea tu r e . addItem ( i tem ( " p i e l de p l á t ano " ) ) ;end ( ) ;

}}

5.1.3. Tipos de métodos de análisis de la entrada

Aquí se muestran los diferentes métodos de análisis de la entrada que sepueden redefinir, así como el orden en que AGE los ejecuta. Nótese que estatabla parece muy complicada; pero no hay ninguna necesidad de saberla en lapráctica, se muestra sólo como referencia de consulta para usuarios avanzados.En realidad, muchos de los métodos no son necesarios salvo para usos muyavanzados y específicos. En concreto, los métodos que contienen la subcadenaOnContents sólo hacen falta para usos avanzados de contenedores, que les gustaponer a algunos autores pero no son para nada necesarios en una aventura. Mu-chos de los métodos restantes son alternativos unos a otros, de modo que unosautores pueden preferir por comodidad usar unos y otros autores usar otros:por ejemplo, un método parseCommandGeneric hace todo lo que pueden hacerparseCommandObj1, parseCommandObj2, parseCommandTwoObjects y parseCom-mand; pero unos autores pueden preferir utilizar un sólo método más complejopara todas las situaciones, y otros usar varios métodos más sencillos y adap-tados a cada situación específica. También cabe destacar que no es necesariopara nada saber los nombres, parámetros y función de estos métodos, ya quelos menús de PUCK nos permiten encontrar el método que queremos y generarsu declaración y explicación de parámetros automáticamente (en la columnaderecha se muestra el menú del PUCK que conduce a cada método).

Page 136: Documentacion age

136 El análisis de la entrada

Orden Objeto Signatura del método Nomenclatura en menús de PUCK1 Player String parseCommand( String verb , String args

)Método de análisis de la entrada (es-tándar) – Introducida por este jugador

2 Mobile/Item void parseCommandOnContentsObj1 ( MobileaCreature , String verb , String args1 , Stringargs2 , Vector path1 , Vector path2 , Entity obj2)

Método de análisis de la entrada (pa-ra contenedores y objetos contenidos)– Referente a ésta y otra cosa, en eseorden

2 Mobile/Item void parseCommandOnContentsObj2 ( MobileaCreature , String verb , String args1 , Stringargs2 , Vector path1 , Vector path2 , Entity obj1)

Método de análisis de la entrada (pa-ra contenedores y objetos contenidos)– Referente a otra cosa y ésta, en eseorden

3 Mobile/Item void parseCommandOnContentsTwoObjects (Mobile aCreature , String verb , String args1 ,String args2 , Vector path1 , Vector path2 , En-tity otherEnt )

Método de análisis de la entrada (pa-ra contenedores y objetos contenidos)– Referente a ésta y otra cosa, en cual-quier orden

3,6* Mobile/Item void parseCommandOnContentsGeneric ( MobileaCreature , String verb , String args1 , Stringargs2 , Vector path1 , Vector path2 , Entity obj1, Entity obj2 , boolean goesFirst )

Método de análisis de la entrada (paracontenedores y objetos contenidos) –Referente a ésta y, opcionalmente, otracosa

4 Mobile/Item void parseCommandObj1 ( Mobile aCreature ,String verb , String args1 , String args2 , Entityobj2 )

Método de análisis de la entrada (es-tándar) – Referente a ésta y otra cosa,en ese orden

4 Mobile/Item void parseCommandObj2 ( Mobile aCreature ,String verb , String args1 , String args2 , Entityobj1 )

Método de análisis de la entrada (es-tándar) – Referente a otra cosa y ésta,en ese orden

5 Mobile/Item void parseCommandTwoObjects ( Mobile aCrea-ture , String verb , String args1 , String args2 ,Entity otherEnt )

Método de análisis de la entrada (es-tándar) – Referente a ésta y otra cosa,en cualquier orden

5,7* Mobile/Item void parseCommandGeneric ( Mobile aCreature ,String verb , String args1 , String args2 , Entityobj1 , Entity obj2 , boolean goesFirst )

Método de análisis de la entrada (es-tándar) – Referente a ésta y, opcional-mente, otra cosa

6 Mobile/Item void parseCommandOnContents ( Mobile aCrea-ture , String verb , String args , Vector path )

Método de análisis de la entrada (paracontenedores y objetos contenidos) –Referente a esta cosa

7 Mobile/Item void parseCommand ( Mobile aCreature , Stringverb , String args )

Método de análisis de la entrada (es-tándar) – Referente a esta cosa

8 Room void parseCommand ( Player aPlayer , Stringverb , String args )

Método de análisis de la entrada

9 World void parseCommandOnContentsTwoObjects (Mobile aCreature , String verb , String args1 ,String args2 , Vector path1 , Vector path2 , En-tity obj1 , Entity obj2 )

Método de análisis de la entrada (paracontenedores y objetos contenidos) –Referente a dos cosas

9,11* World void parseCommandOnContentsGeneric ( MobileaCreature , String verb , String args1 , Stringargs2 , Vector path1 , Vector path2 , Entity obj1, Entity obj2 )

Método de análisis de la entrada (paracontenedores y objetos contenidos) –Referente a una o dos cosas

10 World void parseCommandTwoObjects ( Mobile aCrea-ture , String verb , String args1 , String args2 ,Entity obj1 , Entity obj2 )

Método de análisis de la entrada (es-tándar) – Referente a dos cosas

10,12* World void parseCommandGeneric ( Mobile aCreature ,String verb , String args1 , String args2 , Entityobj1 , Entity obj2 )

Método de análisis de la entrada (es-tándar) – Referente a una o dos cosas

11 World void parseCommandOnContents ( Mobile aCrea-ture , String verb , String args , Vector path ,Entity target )

Método de análisis de la entrada (paracontenedores y objetos contenidos) –Referente a una cosa

12 World void parseCommand ( Mobile aCreature , Stringverb , String args , Entity target )

Método de análisis de la entrada (es-tándar) – Referente a una cosa

13 World void parseCommand ( Mobile aCreature , Stringverb , String args )

Método de análisis de la entrada (es-tándar) – Para cualquier entrada

* Los métodos de tipo «generic» se muestran con dos órdenes de ejecuciónporque pueden ejecutarse tanto para procesar comandos que se refieren a dos

Page 137: Documentacion age

Métodos de análisis de la entrada (parseCommand) 137

entidades del mundo como para los que se refieren a uno solo. Para el caso dedos entidades, se ejecutan junto a los métodos análogos que procesan comandosreferidos a dos entidades (en el primer orden que aparece en la tabla). Para elcaso de una entidad, se ejecutan junto a los demás métodos para una entidad(en el segundo orden).

5.1.4. El proceso de análisis de AGE

Para ahorrar trabajo al creador de juegos y que no tenga que encargarsede interpretar las frases que introduce el jugador, sino sólo de definir qué cosaspuede hacer su juego; el AGE lleva a cabo por sí solo un análisis sintáctico de laentrada, pasándole al creador de aventuras (a través de los métodos parseCom-mand) una entrada ya preprocesada. Por ejemplo, si el jugador teclease «cogeel plátano y cómelo», AGE rompería el comando en sus dos partes y se encar-garía de los pronombres y los tiempos verbales, traduciéndolo como «coger elplátano», por un lado, y «comer el plátano», por otro. De este modo, el métodoparseCommand que vimos en el ejemplo anterior funcionará para «coge el plá-tano y cómelo»; aunque el verbo no esté en infinitivo como aparece en el código,y aunque el jugador haya tecleado «cómelo» en lugar de poner explícitamente«comer el plátano».

Nota: Si en algún caso se hace necesario analizar toda la entrada a mano,sin que AGE lleve a cabo este preprocesado, también es posible; pero esto debehacerse mediante el método preprocessCommand (véase 5.2 preprocesado de laentrada) en lugar de parseCommand. Los métodos parseCommand siempre nosdan la entrada preprocesada.

Más en detalle, los pasos del procesado de textos que realiza AGE son lossiguientes (no debería ser necesario saber esto salvo para usos muy avanzados):

1. Si la orden es compuesta, romperla en órdenes simples: así, «coge el plá-tano y cómelo» se romperá en las dos órdenes «coge el plátano» y «cóme-lo». Cada una de ellas se tratará como una orden independiente de cara atodos los métodos parseCommand.

2. Sustituir los pronombres: cada uno de los pronombres enclíticos se susti-tuye por el nombre que referencia, que se supone que es el último nombrereferenciado que coincide en género con el pronombre. Así, el «lo» de «có-melo» se sustituiría por «plátano», dando lugar a «cóme plátano» (sic).Nótese que la sustitución de pronombres funciona también entre distintasórdenes, es decir, si el jugador primero teclea «coge el plátano», y mástarde en una línea separada teclea «cómelo», el pronombre será sustituidoigualmente.

3. Corregir automáticamente (sólo desde versión 1.0.2): desde la versión 1.0.2de AGE, si la primera palabra de la orden es algo que se parece mucho a unverbo conocido pero no lo es (por ejemplo, «cgoer»), se cambia por dichoverbo. A partir de la versión 1.0.3, las siguientes palabras también se corri-gen si se parecen mucho a un nombre de referencia de un objeto del mundopero no lo son (por ejemplo, «trajje» por «traje»). Estas funcionalidades sepueden desactivar poniendo la propiedad booleana «noVerbSpellChecking»del jugador a true.

Page 138: Documentacion age

138 El análisis de la entrada

4. Sustituir las formas verbales por infinitivos: las formas que aparezcan enimperativo o segunda persona son cambiadas por infinitivos, para evitaral programador de juegos el tener que reconocer distintas formas. Así, lasórdenes del ejemplo quedarían cambiadas por «coger el plátano» y «comerplátano».

5. Sustituir alias: hay algunos verbos que están definidos como sinónimosde otros, para que el programador de aventuras no tenga que tratar in-dividualmente con cada sinónimo. Por ejemplo, «subir» se sustituye por«ir arriba», y «tomar» se sustituye por «coger», de modo que el códigodefinido antes para el parseCommand funcionaría también para «toma elplátano».

6. Detectar a qué objetos se refiere la orden: la palabra «plátano» no es unsustantivo cualquiera, es un sustantivo que se refiere a una entidad delmundo (siempre que la hayamos puesto como nombre de referencia delplátano, claro). El AGE detecta nombres de referencia en las órdenes ytoma nota de a qué entidad hacen referencia.

7. Ejecutar los parseCommand: se ejecutan los diferentes métodos parseCom-mand, en el orden que se muestra en la tabla de más arriba. Los parseCom-mand en concreto que se ejecuten dependerán de las entidades a las quehaga referencia la orden: por ejemplo, como «comer plátano» se refiere auna entidad (el plátano), se ejecutará el método parseCommand ( MobileaCreature, String verb , String args ) de la entidad plátano; pero no se eje-cutarán los métodos para órdenes que se refieren a dos entidades (comoparseCommandTwoObjects) ya que en la orden no aparecen dos entidades.item Llevar a cabo el comportamiento por defecto, si es posible: si en losparseCommand ejecutados en el paso anterior no se llamó a end(), hay dosposibilidades:

a) Que la frase empiece por un verbo: en cuyo caso, si hay un compor-tamiento por defecto para ese verbo (como sucede con «coger») seejecuta; mientras que si no lo hay (como en «comer») se muestra unmensaje para indicar al jugador que la aventura no entiende lo quequiere hacer.

b) Que la frase no empice por un verbo: en este caso, se repite todoel procesado de la entrada suponiendo que la frase va precedida delúltimo verbo que se ha utilizado; esto es el llamado modo «secondchance». Esto puede servir para interpretar algunos comandos dondese omite el verbo: por ejemplo, si el jugador teclea «comer el plátano»y después sigue con «ahora la manzana», refiriéndose a que quierecomer la manzana. Si después del modo «second chance» se llega alpunto 7.a; es señal de que la frase introducida no era inteligible enabsoluto y se muestra el mensaje de que no se ha entendido.

Como programadores de aventuras, lo que nos interesa saber es que en elparseCommand, el AGE siempre nos va a dar órdenes simples (con un solo verbo),con el verbo en infinitivo, y con una serie de sinónimos sustituidos por verbosestándar. Verbos estándar son (lista incompleta) coger, dejar, mirar, ir, poner,decir, vestir, desvestir, atacar, bloquear, esquivar. Algunos verbos no estándar

Page 139: Documentacion age

Preprocesado de la entrada 139

típicos son examinar (se convierte en mirar), tomar (se convierte en coger),quitar (en coger), sacar (en coger), entrar (se convierte en ir dentro), etc.

5.2. Preprocesado de la entrada

Los métodos de análisis de la entrada (parseCommand) que hemos ido utili-zando a lo largo de esta documentación, y detallado más en la sección 5.1 sobremétodos de análisis de la entrada, son el mecanismo principal para definir o mo-dificar cómo un mundo de AGE procesa las entradas del jugador. Como hemosvisto, estos métodos reciben la entrada preprocesada, es decir, no trabajan di-rectamente con el texto que teclea el jugador, sino que AGE le hace a dicho textouna serie de transformaciones (dividirlo en oraciones simples, cambiar los verbosa infinitivo, hacer correcciones en palabras mal escritas, sustituir los pronombrespor los objetos a los que se refieren, etc.).1 En general, dichas transformacionesson útiles porque le quitan trabajo al programador de aventuras, que no tieneque preocuparse de problemas como tratar con diferentes formas verbales o pro-nombres. Sin embargo, puede haber situaciones donde, por cualquier motivo, elprogramador quiera acceder directamente al texto tecleado por el usuario, sinque AGE interfiera. Esto se puede hacer mediante el método de preprocesadode la entrada.

Para definir este método en PUCK, vamos al panel de código de mundo,hacemos click derecho, y seleccionamos Insertar código – Redefinir métodos demundo – Método de preprocesado de la entrada.

Nos aparecerá algo así:

/∗Método de p r ep ro c e s ado de l a en t r ada . Con é l podemos ob t ene r y

p r o c e s a r d i r e c t amen t e l a en t r adaque i n t r o d u c e un jugado r o c r i a t u r a , an t e s de que e n t r e en juego e l

p a r s e r de AGE.La cadena que devo lvamos desde e s t e método s e r á l a que se pase a l

p a r s e r .∗/S t r i n g preprocessCommand ( Mobi le aC r ea tu r e , S t r i n g i npu tTex t ){

/∗ Man ipu l ac i ón d e l comando ∗/

re tu rn i npu tTex t ;}

El método de preprocesado de la entrada se llama preprocessCommand ytoma dos parámetros: el jugador que ha introducido una determinada entrada(Mobile aCreature) y el texto que ha escrito (String inputText). Como se hamencionado, este último parámetro nos proporciona el texto tal cual ha sidoescrito, sin ninguna modificación hecha por AGE.

El método debe devolver un resultado de tipo String, que será la cadena quese pase al parser de AGE (que realiza todo el preprocesado mencionado ante-riormente, y después llama a los métodos parseCommand y realiza el procesadopor defecto). Utilizar este valor de retorno nos permite emplear el método pre-processCommand no sólo para conocer y procesar la entrada del jugador; sino

1Estas transformaciones están documentadas en detalle en la sección 5.1 sobre métodos deanálisis de la entrada.

Page 140: Documentacion age

140 El análisis de la entrada

también para modificarla. Por ejemplo, si quisiéramos que una aventura exigieraque el jugador le pidiese todo por favor para funcionar, podríamos hacer algocomo:

S t r i n g preprocessCommand ( Mobi le aC r ea tu r e , S t r i n g i npu tTex t ){

i f ( i npu tTex t . toLowerCase ( ) . s t a r t sW i t h ( " por f a v o r " ) ){

S t r i n gTok e n i z e r s t = new S t r i n gTok e n i z e r ( i npu tTex t ) ;s t . nextToken ( ) ; // consume l a pa l a b r a pors t . nextToken ( ) ; // consume l a pa l a b r a f a v o rre tu rn s t . nextToken ( "" ) . t r im ( ) ; // de vu e l v e e l r e s t o de l a s

p a l a b r a s}e l s e{

aCrea tu r e . w r i t e ( " Ere s un maleducado . No haré nada s i no me l op i d e s con l a p a l a b r a mágica . \ n" ) ;

end ( ) ;}

}

En este ejemplo, si la entrada que nos ponen no empieza por «por favor»,mostramos un mensaje de protesta interrumpimos el procesado con end(), quefunciona de la misma forma que en los métodos parseCommand, interrumpiendoel procesado de la orden. Nótese que si se interrumpe una orden a este nivel,nunca llegará a ser procesada por los métodos parseCommand, ya que prepro-cessCommand va antes.

En el caso de que la entrada sí empiece por «por favor», le quitamos el «porfavor» del principio dejando que AGE procese el resto de la oración de formanormal. Así pues, será la oración sin «por favor», que es lo que devolvemos, laque sea procesada por los métodos parseCommand.

Produciendo salidas como ésta:

> ve al norteEres un maleducado. No haré nada si no me lo pides con la palabra mágica.> por favor, ve al norteMe dirijo hacia el norte.Estoy en un camino de tierra, desde aquí puedo ir al norte, al sur o al oeste.> ve al surEres un maleducado. No haré nada si no me lo pides con la palabra mágica.

Nótese que, en el caso de no interrumpir el procesado con un end(), devolverun valor de tipo String es obligatorio. En el caso de no querer hacer ningúncambio a la cadena de entrada antes de que AGE la procese, simplemente de-volveríamos el propio parámetro inputText.

5.3. Gestión de verbosPara analizar correctamente las órdenes introducidas por el jugador, AGE

necesita identificar los verbos que se utilizan en los juegos, distinguiéndolos depalabras que no sean verbos. Esto es necesario por dos motivos:

Para que funcione correctamente la corrección automática de verbos quese describió en la sección 5.1 sobre métodos de análisis de la entrada. Por

Page 141: Documentacion age

Gestión de verbos 141

ejemplo, si AGE no supiese que «comer» es un verbo, podría confundirlocon el verbo «coger» mal escrito e intentar corregirlo.

Para que funcione correctamente el modo «second chance», también des-crito en la sección 5.1 sobre métodos de análisis de la entrada. El funcio-namiento de este modo se basa en suponer que, si la primera palabra de laorden no es un verbo, será porque el jugador quiere aplicar el verbo ante-rior. Así, por ejemplo, la oración «comer manzana y plátano» se divide endos órdenes: «comer manzana» por un lado, y «plátano» por el otro. Laprimera orden se ejecuta sin problemas, mientras que en la segunda ordenentra en juego el modo second chance, se inserta el verbo anterior y de esemodo se ejecuta «comer plátano». Pero para que esto funcione, necesita-mos saber que «plátano» no es un verbo, distinguiéndolo de palabras quesí lo sean (si la oración fuese «comer manzana y saludar», el modo secondchance no entraría en acción, porque saludar es un verbo).

Para que funcione correctamente la conversión de imperativos y formasen primera persona a infinitivo, y así se pueda usar indistintamente en lasórdenes cualquiera de esas tres formas verbales.

En la gran mayoría de los casos, el programador de aventuras no se tienepor qué preocupar de estas cuestiones, dado que AGE cuenta con una lista deverbos que le permiten distinguir las palabras que pueden ser verbos de las queno lo son. Sin embargo, en algunos casos específicos puede ser útil manipularesta lista de verbos:

Si por algún motivo la lista está incompleta y falta algún verbo que sequiere utilizar en alguna aventura. Esto debería pasar muy rara vez por-que se ha intentado que la lista (al menos para idioma español) sea muycompleta, pero al fin y al cabo el idioma es algo muy grande y ademásdinámico, con palabras creándose todos los días, así que nunca se puededescartar que falte algún verbo.

Si el creador de una aventura quiere que se utilice como posible orden unapalabra que normalmente no sería un verbo en el habla común (por ejemplo«estado» para comprobar el estado del personaje, o cosas similares).

Si se quieren aceptar formas verbales que AGE no reconoce por defecto:por ejemplo, si se quiere que una aventura pueda aceptar órdenes dadasen pasado («fui hacia el norte», «cogí la manzana»).

En esta sección veremos cómo se puede consultar la lista de verbos de AGE,así como editarla para una aventura concreta si nos encontramos en uno de estoscasos.

5.3.1. Visualización de la lista de verbos por defectoLa lista de verbos por defecto de AGE se puede ver accediendo a la opción

«Ver lista de verbos» que se encuentra en el menú «Herramientas» de PUCK.Nótese que esta ventana de PUCK no permite modificar la lista, sino sólo verlaa efectos informativos. En ella podemos comprobar, por ejemplo, si falta al-gún verbo que necesitemos, para así añadirlo a la aventura mediante códigoBeanShell.

Page 142: Documentacion age

142 El análisis de la entrada

Utilizando esta opción, podremos comprobar que las entradas de la lista deverbos contienen dos formas verbales: una es un imperativo o primera persona,y la otra es el infinitivo al que corresponde. El motivo es que AGE puede com-prender órdenes tanto en infinitivo como en imperativo o primera persona, y(como se vio en la sección 5.1 sobre métodos de análisis de la entrada) lo haceconvirtiendo todas esas formas al infinitivo. La lista de verbos es lo que usaAGE para hacer dicha conversión.

Por este motivo, si añadimos un nuevo verbo a la lista, deberíamos añadirdos entradas: una que indique cómo pasar de imperativo a infinitivo, y otra queindique cómo pasar de primera persona a infinitivo.

5.3.2. Añadir y quitar verbosPara añadir un verbo a la lista de verbos de nuestro mundo, podemos utilizar

el siguiente código BeanShell:

wor ld . getLanguage ( ) . addVerbEntry ( " e n l a d r i l l a " , " e n l a d r i l l a r " ) ; //formas impe r a t i v o e i n f i n i t i v o

wor ld . getLanguage ( ) . addVerbEntry ( " e n l a d r i l l o " , " e n l a d r i l l a r " ) ; //formas p r ime ra pe r sona e i n f i n i t i v o

El método getLanguage() de la clase World nos proporciona un objeto dela clase NaturalLanguage que representa el idioma en el que acepta órdenes elmundo, y que contiene métodos que trabajan con ese idioma como es en estecaso el método addVerbEntry() para añadir formas verbales.

Las dos líneas del ejemplo actúan como si se añadieran dos filas a la tablaque hemos visto más arriba: una diciendo que «enladrilla» es el imperativo delverbo «enladrillar», y otra diciendo que «enladrillo» es su primera persona (enrealidad AGE no distingue entre imperativos y primeras personas, simplementele estamos diciendo que ambas son formas verbales aceptadas para el verbo «en-ladrillar». La propia forma «enladrillar», al aparecer como infinitivo, tambiénserá aceptada automáticamente).

Nótese que el verbo se añadirá de forma dinámica al ejecutarse el códigoBeanShell, así que no se verá en la ventana de PUCK de la lista de verbos, quemuestra siempre la lista que hay por defecto al comenzar el juego.

Para quitar un verbo de la lista de verbos, se utiliza un método removeVer-bEntry() que hace lo opuesto al método addVerbEntry():

wor ld . getLanguage ( ) . removeVerbEntry ( " e n l a d r i l l a " , " e n l a d r i l l a r " ) ; //formas impe r a t i v o e i n f i n i t i v o

wor ld . getLanguage ( ) . removeVerbEntry ( " e n l a d r i l l o " , " e n l a d r i l l a r " ) ; //formas p r ime ra pe r sona e i n f i n i t i v o

Las ejecuciones de removeVerbEntry() del este ejemplo quitarán los verbosañadidos por las ejecuciones de addVerbEntry() del ejemplo anterior. Por su-puesto, mediante este método también se pueden quitar de la aventura verbosque no hayamos añadido nosotros, sino que estén en la lista por defecto quemuestra PUCK.

5.3.3. Verbos adivinables y no adivinablesNOTA: En opinión del creador de AGE, el contenido de esta subsección no

es necesario, y de hecho no debería utilizarse nunca porque no se gana nada

Page 143: Documentacion age

Gestión de verbos 143

con ello y en cambio sí hay posibilidades de perder o degradar funcionalidad.Sin embargo, se incluye en esta documentación por completitud, dado que esfuncionalidad que existe y que algunos autores han utilizado.

Por defecto, todos los verbos de la lista pueden ser «adivinados» por AGEen el modo «second chance» descrito en la sección de métodos de análisis de laentrada. Sin embargo, AGE también proporciona la opción de desactivar estemodo para todos los verbos o para algún verbo en concreto.

Desactivar el modo «second chance» para todos los verbos no está recomen-dado bajo ningún concepto, dado que es un componente fundamental para queAGE analice correctamente las órdenes del jugador. Sin embargo, en alguna oca-sión podría interesar a algunos autores desactivar este modo para algún verboconcreto. En particular, puede interesar desactivarlo para verbos intransitivosque tengamos definidos de manera que se ignore toda palabra que se teclee des-pués del verbo. Por ejemplo, si hemos definido un verbo «estornudar» con elparseCommand del jugador de esta manera:

S t r i n g parseCommand ( S t r i n g ve rb , S t r i n g a r g s ){

i f ( e qu a l s ( verb , " e s t o r nuda r " ) ){

s e l f . w r i t e ( " Es to rnudas con f u e r z a . \ n" ) ;end ( ) ;

}}

Este verbo nos aceptaría entradas como:

> es t o r nuda rEs to rnudas con f u e r z a .> e s t o r nuda r Pep i toEs to rnudas con f u e r z a .> e s t o r nuda r con muchís imo cu idado de no d e s p e r t a r a JuanEs to rnudas con f u e r z a .> e s t o r nuda r a d f a i f j a d fEs to rnudas con f u e r z a .

Pero si utilizáramos una oración compuesta en la que la segunda orden notuviese un verbo reconocido ni una palabra que se le pareciese, como

> es t o r nuda r y a s d f a d f

Veríamos como salida

Es to rnudas con f u e r z a .Es to rnudas con f u e r z a .

debido a que después del primer estornudo, al procesarse la orden «asdfadf»,salta el modo «second chance» y adivina el verbo «estornudar» (la oración seinterpretaría como dos órdenes: «estornudar» y «estornudar asdfadf»).

Una situación así nunca se dará en una partida real, sino sólo en sesiones detesting dirigidas específicamente a abusar del parser. Sin embargo, algún autorpodría querer evitarla. Esto se hace quitando el verbo «estornudar» de la listade verbos «adivinables» con el modo «second chance», de la siguiente manera:

wor ld . getLanguage ( ) . s e tUngue s s ab l e ( " e s t o r nuda r " ) ;

Page 144: Documentacion age

144 El análisis de la entrada

Nótese que en este método setUnguessable sólo hace falta especificar el verboen infinitivo para desactivar el modo «second chance» con ese verbo.

También cabe destacar que si hubiésemos programado el verbo «estornu-dar» para que sólo funcionase si se introduce con argumentos, el modo «secondchance» nunca tendría efecto con este verbo aunque no utilizásemos setUngues-sable():

S t r i n g parseCommand ( S t r i n g ve rb , S t r i n g a r g s ){

i f ( e qu a l s ( verb , " e s t o r n uda r " ) && equa l s ( args , "" ) ){

s e l f . w r i t e ( " Es to rnudas con f u e r z a . \ n" ) ;end ( ) ;

}}

En el ejemplo de arriba, esto haría que «estornudar asdfadf» no respondiese:

> es t o r nuda r y a s d f a d fEs to rnudas con f u e r z a .No en t i e ndo . . .

Más en general, los siguientes métodos de la clase NaturalLanguagemanipulanla lista de verbos que son adivinables y no adivinables con el modo «secondchance»:

/∗ c l a s e Natura lLanguage ∗/ vo id s e t A l l G u e s s a b l e ( )

Activa el modo «second chance» para todos los verbos. Éste es el comporta-miento por defecto de AGE.

/∗ c l a s e Natura lLanguage ∗/ vo id s e tA l l U n g u e s s a b l e ( )

Desactiva el modo «second chance» para todos los verbos. Se recomienda nollamar a este método nunca, salvo tal vez en el caso especial en el que se quieradegradar a propósito la funcionalidad de análisis de AGE (tal vez para simularla respuesta de algún sistema retro).

/∗ c l a s e Natura lLanguage ∗/ vo id s e tGu e s s a b l e ( S t r i n g ve rb )

Desactiva el modo «second chance» para el verbo verb.

/∗ c l a s e Natura lLanguage ∗/ vo id s e tUngue s s ab l e ( S t r i n g ve rb )

Activa el modo «second chance» para el verbo verb.

Page 145: Documentacion age

Capítulo 6

Presentación del mundo

En las secciones anteriores nos hemos centrado en cómo modelar un mundocon AGE y hacer que funcione; pero no nos hemos detenido mucho en aspectosde presentación, sino sólo en el texto puro. Si bien el texto es la base de cual-quier mundo en AGE, el sistema proporciona al programador de aventuras undetallado control sobre cómo presentar el mundo y acompañarlo de multimedia,incluyendo colores de texto, tipografías, configuración de los prompts, uso deimágenes y animaciones, sonido, etc.

Al utilizar estas características, es importante tener en cuenta que AGEes un sistema donde una aventura se puede jugar en diferentes clientes condistintas características. En particular, se pueden jugar aventuras de AGE delas siguientes maneras (y el diseño de AGE deja abierta la posibilidad de queaparezcan más en el futuro):

1. Mediante cliente gráfico:

Modo SDI (simpleage.bat, simpleage.sh, simpleage.command): unaventana de escritorio por mundo, para partidas locales.

Modo MDI (aetheria.bat, aetheria.sh, aetheria.command): una solaventana de escritorio con subventanas para cada mundo, para parti-das locales y remotas (por internet).

Online mediante applet: una página web donde se muestra el mundo,para partidas online.

2. Mediante cliente en modo consola (cheapage.bat, cheapage.sh, cheapa-ge.command): se juega en la terminal/consola del sistema, para partidaslocales.

3. A través de un cliente telnet, para partidas remotas.

4. A través de un cliente IRC, para partidas remotas.

Debido a que estas formas de jugar son muy diferentes, las posibilidades depresentación de aventuras cambiarán entre unas y otras: por ejemplo, evidente-mente no se podrán mostrar imágenes en un cliente de consola, igual que no sepodrán tocar sonidos en un cliente IRC. En particular, en la actualidad:

Page 146: Documentacion age

146 Presentación del mundo

1. En el cliente gráfico (sea en modo SDI, MDI o como applet) están dispo-nibles todas las opciones de presentación.

2. En modos consola y telnet no está disponible actualmente ninguna opciónde presentación; se muestra el texto sin más.

3. A través de IRC está disponible la posibilidad de colorear textos (restrin-gida, pues sólo hay 16 colores en el IRC), pero no el resto de las opciones.

En todo caso, el programador de aventuras no necesita saber exactamentequé soporta y qué no cada uno de los clientes a la hora de programar (cosaque además podría cambiar en el futuro, si se añadiese funcionalidad a algúncliente). El programador no necesita preocuparse de la variedad de clientes quehaya porque todas las características de presentación o bien se ignoran automá-ticamente para clientes que no las soportan (caso de los colores de texto), o bienexisten métodos con los que le puede preguntar al cliente de forma genérica silas soportan o no (caso de los sonidos). Al explicar cada una de las característi-cas veremos en cuál de estos dos casos se encuadra, y si es o no necesario haceralguna comprobación antes de utilizarlas para ver si el cliente las soporta.

6.1. Estilos de texto

6.2. Prompt

6.2.1. Métodos de manipulación del promptCambiar colores

• void setInputFieldForeground( String color ): color del texto del prompt.• void setInputFieldBackground( String color ): color del fondo del texto

del prompt.• void setOutputAreaBackground( String color ): color del fondo.

Cambiar texto del prompt

• void setPrompts ( String leftPrompt , String rightPrompt ): modificarla parte izquierda (anterior al texto introducido por el usuario) yderecha (posterior al texto introducido por el usuario) del prompt.

Cambiar márgenes del texto

• void setMargins ( int top , int left , int bottom , int right ): margenessuperior, izquierdo, inferior y derecho del area de texto.

6.2.2. Ejemplos de códigoNota: Todos estos métodos deben comprobar que el cliente que usa el jugador

es un ColoredSwingClient:

i f ( aP l a y e r . get IO ( ) i n s t anceo f Co l o r e dSw i ngC l i e n t ){

u sa r l o s métodos ;}

Page 147: Documentacion age

Tipografías en AGE 147

Cambiar los colores del prompt.

i f ( aP l a y e r . get IO ( ) i n s t anceo f Co l o r e dSw i ngC l i e n t ){

aP l a y e r . get IO ( ) . s e t I n p u t F i e l d F o r e g r o u n d ( "FF0000" ) ;aP l a y e r . get IO ( ) . s e t I n pu tF i e l dBa ckg r ound ( "00FF00" ) ;aP l a y e r . get IO ( ) . s e tOutputF i e l dBackg round ( "0000FF" ) ;

}

Cambiar el texto del prompt al clásico ‘>’.

aP l a y e r . get IO ( ) . se tPrompts ( ">" , "" ) ;

Cambiar el texto del prompt: Si el jugador teclea «hola», en el promptaparecería «Tu texto aquí: (hola)».

aP l a y e r . get IO ( ) . se tPrompts ( "Tu t e x t o aqu í : ( " , " ) " ) ;

6.3. Tipografías en AGEAGE incluye diferentes opciones que permiten cambiar las tipografías (fuen-

tes) con las que se muestra tanto el campo de entrada de la aventura (Prompt)como el texto de salida.

6.3.1. Control básico de la tipografíaEl entorno de desarrollo integrado PUCK permite cambiar la tipografía ge-

neral de una aventura de forma sencilla y sin tocar una línea de código, medianteformularios. Para ello, hacemos click en alguna parte del mapa que esté vacía (esdecir, que no contenga ninguna entidad ni relación) para seleccionar el objetomundo. Hecho esto, tendremos el panel de mundo a mano derecha. Seleccionan-do la pestaña «Presentación», en la parte de abajo del panel hay una sección de«Tipografía» con tres campos de formulario, que nos permiten especificar «Fuen-te», «Fichero» o «Tamaño». Estos campos se utilizan para cambiar la fuentepor defecto en la que se mostrarán todos los textos de la aventura (también sepuede cambiar la fuente puntualmente para mostrar textos con diferentes tipo-grafías en la misma aventura; pero esto no se puede hacer mediante formulariossino sólo programando, véase «Control avanzado de la tipografía»).

El campo «Fuente» se utiliza para especificar el nombre de una fuente quese suponga instalada en el sistema operativo del jugador. Por ejemplo, podemosteclear Courier New y, si el usuario tiene instalada una fuente con ese nombre,los textos de la aventura se le mostrarán con dicha fuente. En el caso de que elusuario no tenga instalada la fuente, este campo no tendrá efecto, y simplementese utilizará la fuente por defecto de AGE.

El campo «Fichero» se usa si, en lugar de llamar a una fuente instalada enel sistema, se quiere incluir un fichero de fuente TrueType con la aventura. Porejemplo, podemos incluir un fichero de fuente TrueType «Biergarten.ttf» en eldirectorio de mundo, y en este campo teclearíamos Biergarten.ttf para utilizaresa fuente.

Nota: Los tipos de fuentes soportados con Java pueden variar un poco segúnla versión de la máquina virtual Java y del sistema operativo. Por lo que he visto,muchas fuentes TrueType funcionan en todas las VM’s (Windows, Linux y Mac);

Page 148: Documentacion age

148 Presentación del mundo

aunque hay una minoría que no funcionan (¿tal vez dependiendo de la versióndel estándar?) En cualquier caso, si por un motivo u otro la fuente especificadaen el fichero no funciona, no se producirá ningún error desagradable sino quesimplemente el cambio de fuente no tendrá efecto.

Por último, el campo «Tamaño» se utiliza para especificar el tamaño de laletra, tanto en las fuentes dadas mediante el campo «Fuente» como en las dadasmediante el campo «Fichero».

En el caso de que se teclee algo tanto en el campo «Fichero» como en elcampo «Fuente», tomará prioridad el campo «Fichero». Si no se puede obteneruna fuente a partir del fichero (sea porque éste no existe o porque su formato noes válido), se utilizará el campo «Fuente». Si éste tampoco funciona, se usarála fuente por defecto de AGE.

6.3.2. Control avanzado de la tipografía

La clase ColoredSwingClient cuenta con los siguientes métodos para cambiarde forma dinámica la tipografía:

pub l i c vo id se tOutputAreaFont ( URL u , i n t f o n t S i z e )pub l i c vo id s e t I n p u t F i e l d F o n t ( URL u , i n t f o n t S i z e )pub l i c vo id s e tCur r en tOutputFont ( URL u , i n t f o n t S i z e )

pub l i c vo id se tOutputAreaFont ( Font f )pub l i c vo id s e t I n p u t F i e l d F o n t ( Font f )pub l i c vo id s e tCur r en tOutputFont ( Font f )

pub l i c vo id se tOutputAreaFont ( InputSt ream i s , i n t f o n t S i z e )pub l i c vo id s e t I n p u t F i e l d F o n t ( InputSt ream i s , i n t f o n t S i z e )pub l i c vo id s e tCur r en tOutputFont ( InputSt ream i s , i n t f o n t S i z e )

Estos métodos permiten cambiar la tipografía de tres maneras:

Los métodos llamados setInputFieldFont cambian la tipografía con la quese escribe en el campo de entrada de texto.

Los métodos llamados setOutputAreaFont cambian la tipografía de todael área de salida de texto (es decir, todo el texto existente en dicha áreapasa a dibujarse con la fuente dada).

Los métodos llamados setCurrentOutputFont cambian la tipografía actualdel área de salida de texto. Esto quiere decir que el texto que haya sidomostrado hasta ahora en dicha área no cambia; pero a partir de ahoralos nuevos textos que se muestren tendrán la nueva tipografía. Se puedeutilizar este método para mezclar diferentes tipografías en el mismo juego.

Cada uno de los métodos tiene tres variantes diferentes según el tipo de losparámetros que le pasemos.

A la primera variante le pasamos una URL al fichero de fuente junto con el ta-maño de la fuente. La URL puede especificar la localización del fichero de fuenteen el disco duro local, en una página web, en un archivo zip, etc. Esta variante esla más sencilla para cargar un fichero de fuente que incluyamos junto con nuestromundo, dado que podemos utilizar el método world.getResource("nombreFichero.ttf")para obtener su URL. Así, podemos hacer:

Page 149: Documentacion age

Métodos gráficos 149

c l i e n t . s e tCur r en tOutputFont ( wor ld . g e tRe sou r ce ( " f u en t e3 . t t f " ) ,16) ;

Y se usará un fichero de fuente «fuente3.ttf» almacenado en el directorio delmundo.

Las otras dos variantes, más complejas, son:

Uno al que le pasamos un objeto de la clase Font, el objeto que en Javase utiliza para representar una fuente (incluyendo tipo de letra, tamaño yotras características como negrita o cursiva). La API de Java nos permiteobtener objetos Font de muchas maneras. Por ejemplo:

Font f = Font . c r e a t eFon t ( Font .TRUETYPE_FONT , new F i l e ( "C:\\Fuentes \\ miFuente . t t f " ) ;

Uno al que le pasamos un objeto de la clase InputStream con los datos dela fuente y un tamaño de fuente. El InputStream se puede obtener de unfichero de fuente, de una URL, de un fichero zip, etc.

Estas dos últimas variantes serán útiles para los usuarios avanzados quenecesiten crear o manipular fuentes directamente, utilizando la API de Javapara ello.

Es importante recordar que los métodos mencionados en esta sección sóloexisten en la clase ColoredSwingClient, que implementa el cliente de ventanasSwing para AGE. Otros clientes (como el que se utiliza para jugar por IRC opor telnet) no soportan cambio de fuentes. Así pues, es importante asegurarsede que el cliente que usa el jugador es realmente un ColoredSwingClient antes dellamar a cualquiera de estos métodos:

I n pu tOu tpu tC l i e n t c l i e n t = jugado r . get IO ( ) ;i f ( c l i e n t i n s t anceo f Co l o r e dSw i ngC l i e n t )

c l i e n t . se tOutputAreaFont ( wor ld . g e tRe sou r c e ( " f u en t e3 . t t f " ) ,16) ;

6.4. Métodos gráficos

Son aquellos métodos que nos permiten añadir gráficos, frames, etc.

6.4.1. Imágenes

Soporte de imágenes en AGE

Los mundos de Aetheria Game Engine pueden enriquecerse añadiéndoles grá-ficos. En particular, AGE permite mostrar fácilmente imágenes en una variedadde formatos, tanto raster como vectoriales.

Los gráficos raster son aquéllos que están definidos mediante una cuadrículade pequeños puntos (pixels), como es el caso de las fotografías digitales. Losgráficos vectoriales son aquellos que se definen mediante líneas y curvas, y segeneran con programas como Inkscape o Corel Draw.

AGE soporta los siguientes formatos de imagen:

Gráficos raster estáticos: JPG, GIF, PNG, BMP.

Page 150: Documentacion age

150 Presentación del mundo

Gráficos raster animados: GIF animado.

Gráficos vectoriales: SVG.

Además, cada imagen se puede mostrar en la ventana de juego de tres ma-neras diferentes:

Integrados en el texto (como si fuera una imagen en una página web).

Como fondo del área de texto (es decir, por detrás del texto).

En una zona independiente de la ventana, especialmente pensada paramostrar gráficos, llamada frame.

Los métodos que muestran imágenes se invocan sobre un objeto de la claseInputOutputClient, que representa el cliente que está utilizando un jugador parajugar su partida. Se puede obtener el cliente de un jugador dado de la siguientemanera:

I n pu tOu tpu tC l i e n t t h e C l i e n t = jugado r . get IO ( ) ;

Es importante tener en cuenta que no todos los clientes de AGE soportanimágenes: por ejemplo, se pueden mostrar imágenes en el cliente gráfico deventanas; pero no pueden mostrarse cuando se juega una partida por telnet opor IRC (véase 6 presentación del mundo para más información). Esto obligaal programador de aventuras a comprobar que el cliente tiene la posibilidad demostrar imágenes. Para ello, tenemos que comprobar que el cliente sea de lasubclase MultimediaInputOutputClient (clientes que tienen soporte multimedia)y que permita mostrar imágenes, de la siguiente manera:

i f ( t h e C l i e n t i n s t anceo f Mul t imed i a I npu tOu tpu tC l i e n t ) &&t h eC l i e n t . i sG r a ph i c sEn ab l e d ( ) )

{// e l cód igo para most ra r imágenes i r í a den t ro d e l i f .

}

Si invocáramos los métodos para mostrar imágenes sin hacer esta comproba-ción, nuestro juego funcionaría (y mostraría las imágenes) al jugarlo en clientesgráficos; pero no funcionaría (y daría mensajes de error) al jugarlo en clientessin soporte de imágenes como cheapAGE o el bot IRC de AGE. Por ello, esmuy recomendable hacer siempre la comprobación para que nuestro mundo nodependa de un cliente en concreto y pueda ser accesible al mayor número degente posible.

Métodos para imágenes

Una vez obtenido el cliente usado por el jugador y comprobado que soportamultimedia (tal y como se ha descrito más arriba), podemos invocar los siguien-tes métodos para mostrarle imágenes al jugador:

/∗ c l a s e I npu tOu tpu tC l i e n t ∗/ i n s e r t C e n t e r e d I c o n ( URL imageUr l )

Mostrar una imagen centrada integrada en el texto. Es una forma rápida demostrar una imagen ocasionalmente, si no queremos preocuparnos de hacer unaconfiguración detallada de dónde y cómo queremos que aparezca.

Page 151: Documentacion age

Métodos gráficos 151

/∗ c l a s e I npu tOu tpu tC l i e n t ∗/ useImage ( URL imageUr l , i n t mode ,i n t p o s i t i o n , i n t s c a l i n g )

Muestra la imagen que está en el fichero imageFile. Este método puede usarsetanto para mostrar imágenes en medio del texto, como en marcos o como fondo.

El parámetro mode indica cómo se mostrará la imagen. Sus valores puedenser ImageConstants.INLINE (mostrar con el texto), ImageConstants.FRAME(mostrar en un marco), o ImageConstants.BACKGROUND (mostrar comofondo). Nótese que para mostrar imágenes en marcos con el parámetroImageConstants.FRAME es necesario haber creado el marco primero, tal ycomo se describe en la sección sobre frames.

El parámetro position indica en qué posición se mostrará la imagen. Si elmodo es ImageConstants.INLINE, el parámetro position puede valer Ima-geConstants.CENTER (imagen centrada) o ImageConstants.LEFT (imagenalineada a la derecha). Si el modo es ImageConstants.FRAME, el parámetroposition puede valer ImageConstants.TOP (arriba), ImageConstants.BOTTOM(abajo), ImageConstants.LEFT (izquierda) o ImageConstants.RIGHT (dere-cha), según en qué frame se quiere mostrar la imagen. Si el modo es Ima-geConstants.BACKGROUND, el parámetro position no tiene de momentoningún efecto (en este caso se puede pasar cualquier valor, por ejemplo 0).

El parámetro scaling indica qué tipo de escalado se aplicará a la imagen.Este parámetro, al menos de momento, sólo tiene sentido en el modo Ima-geConstants.FRAME (ya que por ejemplo una imagen que se muestra enel medio del texto no necesita escalado). En el resto de los modos se pue-de pasar cualquier valor, por ejemplo 0. En modo ImageConstants.FRAME,tenemos los siguientes valores permitidos para el parámetro scaling: Image-Constants.NO_SCALING (la imagen no se escalará, sino que se mostraráa su tamaño natural), ImageConstants.FIT_WIDTH (la imagen se esca-lará para ocupar toda la anchura del frame, manteniendo la proporciónde aspecto original, o sea, sin estirar ni encoger la imagen), ImageCons-tants.FIT_HEIGHT (la imagen se escalará para ocupar toda la altura delframe, manteniendo la proporción de aspecto original), y por último Ima-geConstants.FIT_BOTH (la imagen se escalará para ocupar toda la alturay anchura del frame, pudiendo para ello modificar la proporción de aspec-to).

Ejemplos de código

Suponemos que hemos obtenido el cliente del jugador tal y como se explicabaarriba, que lo tenemos almacenado en una variable llamada theClient y sabemosque soporta multimedia.

Podemos obtener la URL de una imagen que hayamos puesto en el directoriodel mundo mediante el método getResource() del mundo, de la siguiente manera:

imagen = wor ld . g e tRe sou r ce ( " f i c h e r o imag en . png" ) ;

Hecho esto, podemos usar este código para mostrar la imagen como fondo:

t h e C l i e n t . use Image ( imagen , ImageConstants .BACKGROUND , 0 , 0 ) ;

Page 152: Documentacion age

152 Presentación del mundo

Para mostrar la imagen centrada e integrada en el texto:t h e C l i e n t . use Image ( imagen , ImageConstants . INLINE , ImageConstants

.CENTER , 0 ) ;

Otra forma de mostrar la imagen centrada e integrada en el texto:t h e C l i e n t . i n s e r t C e n t e r e d I c o n ( imagen ) ;

En la sección 6.4.2 específica sobre frames se muestra un ejemplo que muestrauna imagen en un frame.

6.4.2. FramesUtilidad de los frames

Los frames son áreas de tamaño fijo que se pueden añadir a la ventanade juego para mostrar imágenes. Los frames se pueden añadir o quitar de laventana dinámicamente, además de ir cambiando la imagen que muestran encada momento. Esto permite, por ejemplo, implementar el clásico interfaz quemuestra una imagen de la habitación en la que se encuentra el jugador en laparte superior de la ventana y el texto de la descripción debajo, entre otrasfuncionalidades.

Métodos para crear y borrar frames

Los siguientes métodos se pueden ejecutar sobre un objeto de la clase Mul-timediaInputOutputClient:1

/∗ c l a s e I npu tOu tpu tC l i e n t ∗/ vo id addFrame ( i n t p o s i t i o n , i n t s i z e)

Este método crea un marco en la posición dada, y del tamaño en pixels dado.

La posición puede ser ImageConstants.TOP (arriba), ImageConstants.BOTTOM(abajo), ImageConstants.LEFT (izquierda) o ImageConstants.RIGHT (dere-cha).

El tamaño se refiere al alto si el frame se crea arriba o abajo, o al anchosi se crea a la derecha o a la izquierda (la otra dimensión variará según eltamaño de la ventana de AGE).

Puede haber varios marcos a la vez, aunque de momento sólo uno porposición (o sea, por ejemplo, no puede haber dos marcos encima del textoa la vez).

/∗ c l a s e I npu tOu tpu tC l i e n t ∗/ vo id removeFrames ( )

Este método quita todos los marcos que se hayan creado. Esto es útil siuna aventura tiene partes que muestran imágenes y otras que no. Más tarde sepueden volver a crear los marcos con addFrame().

1el método getIO() de la clase Player nos devuelve una instancia de MultimediaInputOut-putClient cuando el jugador esté utilizando un cliente de juego con soporte multimedia, comolo es el interfaz de ventanas de Aetheria Game Engine. Nótese que otros clientes, como el delínea de comandos cheapAGE, pueden no soportar multimedia y por lo tanto no permitir eluso de frames. Hay más información sobre esto (incluyendo cómo se comprueba si un clientesoporta multimedia) en la sección 6.4.1 sobre imágenes.

Page 153: Documentacion age

sonido 153

Métodos para mostrar imágenes en frames

Para mostrar una imagen en un frame, puede utilizarse el método useImageespecificando como modo de mostrado ImageConstants.FRAME, tal y como semuestra en la sección 6.4.1 Imágenes.

Manipulación avanzada de frames

Solamente si eres un programador avanzado de Java y quieres ir más allá demostrar imágenes en los frames, podría interesarte este método:

/∗ c l a s e I npu tOu tpu tC l i e n t ∗/ JPane l getFrame ( i n t p o s i t i o n )

Dicho método devuelve el panel que se utiliza internamente para representarel frame, y puede ser utilizado por programadores que conozcan la API de Swingy que quieran dibujar directamente sobre él. Por supuesto, no hace falta estemétodo para mostrar imágenes en frames, basta con los anteriores, que seránlos que interesen a la gran mayoría de usuarios.

Ejemplos de código

Creamos un frame en la parte superior de la pantalla de 300 píxeles de alto,y que incluye la imagen «titulo.png» sin escalar.

i f ( j ugado r . get IO ( ) i n s t anceo f Mul t imed i a I npu tOu tpu tC l i e n t &&jugado r . get IO ( ) . i sG r a ph i c sEn ab l e d ( ) )

{Mu l t imed i a I npu tOu tpu tC l i e n t t h e C l i e n t = jugado r . get IO ( ) ;URL imageURL = wor ld . ge tRe sou r ce ( " t i t u l o . png" ) ;t h e C l i e n t . addFrame ( ImageConstants .TOP , 300 ) ;t h e C l i e n t . use Image ( imageURL , ImageConstants .FRAME ,

ImageConstants .TOP , ImageConstants .NO_SCALING ) ;}

6.5. sonido

Una buena forma de enriquecer un mundo es añadirle efectos sonoros ymúsica ambiente. En Aetheria Game Engine es sencillo conseguir esto, mediantecódigo BeanShell. AGE soporta una gran variedad de formatos de audio y músicapara este propósito.

6.5.1. Audio

Un fichero de audio es aquél que guarda una grabación digital de sonido.Son la manera más general y más utilizada de representar sonidos, porque enellos podemos tener almacenados efectos, música, voces, o cualquier otro tipode sonido.

El soporte de audio de AGE permite utilizar mayor variedad de formatosde audio que otros sistemas, y permite tocar sonidos, detenerlos, repetirlos enbucle y cambiar dinámicamente el volúmen de forma muy sencilla.

Los formatos de audio actualmente soportados por AGE son: WAV, AIFF,MP3, OGG y SPX (nótese que AGE también soporta música en MIDI y MOD;

Page 154: Documentacion age

154 Presentación del mundo

pero éstos no son formatos de audio y su manejo se hace con otros métodosdistintos, que se explican más adelante).

Los sonidos se pueden reproducir y gestionar directamente desde métodosde la clase Mobile, de forma que para que un jugador escuche un sonido en sucliente, invocaremos los métodos directamente sobre ese jugador.

Reproducir un sonido

/∗ c l a s e Mobi le ∗/ boolean p l a y A u d i o I f A v a i l a b l e ( URL sound )

m.playAudioIfAvailable( sound ) toca el sonido referenciado por sound en elcliente asociado con la criatura m (que normalmente será un jugador), si estoes posible.

En el caso de que sea posible y el cliente del jugador tenga el volumenactivado, comenzará a sonar el audio y el método devolverá inmediatamentetrue (sin esperar a que la reproducción termine).

En el caso de que no sea posible reproducir el sonido, no sonará nada y elmétodo devolverá false. Esto puede suceder por las siguientes razones:

La criatura es un personaje no jugador y por lo tanto no puede escucharsonidos,

La criatura es un jugador pero está jugando a través de un cliente que nosoporta sonidos (por ejemplo, conectado por medio de telnet),

La URL pasada como parámetro es errónea, o apunta a un fichero queno existe, o apunta a un fichero que no es de ninguno de los formatos deaudio reconocidos por AGE.

Cabe recordar que para obtener el objeto de la clase URL que apunta a unfichero que incluimos con el mundo, podemos utilizar el método getResource()de la clase World. Así, para reproducir un sonido llamado «musica.ogg» queincluimos con el mundo, haríamos:

j u gado r . p l a y A u d i o I f A v a i l a b l e ( wor ld . g e tRe sou r ce ( "musica . ogg" ) ) ;

Nótese que en este caso estamos ignorando el valor de retorno (true o false)porque no nos importa: simplemente queremos que si el jugador está jugando enun cliente que soporta sonido, pueda oírlo, y si no, tampoco pasa nada. El valorde retorno sólo será importante si queremos programar algún comportamientoalternativo para clientes que no tengan sonido.

/∗ c l a s e Mobi le ∗/ boolean p l a y A u d i o I f A v a i l a b l e ( URL sound , i n tloopTimes )

Este método hace lo mismo que boolean playAudioIfAvailable ( URL sound );pero nos permite especificar a mayores un parámetro entero loopTimes que sirvepara hacer que un sonido se repita un determinado número de veces:

Si pasamos loopT imes = 1, el sonido se reproducirá una vez (es decir,igual que con el método sin parámetro),

Si pasamos loopT imes > 1, el sonido se reproducirá loopTimes veces,

Page 155: Documentacion age

sonido 155

Si pasamos loopT imes < 1, el sonido se repetirá indefinidamente (pero sepuede parar usando stopAudioIfAvailable(), que se verá más abajo).

/∗ c l a s e Mobi le ∗/ boolean p l a y A u d i o I f A v a i l a b l e ( URL sound , i n tloopTimes , boolean f ade )

Este método hace lo mismo que boolean playAudioIfAvailable ( URL sound ,int loopTimes ); pero nos permite especificar un parámetro booleano adicionalfade. Si este parámetro se pone a true, el sonido se reproducirá con un «fade in»(transición de inicio). Si se pone a false, el método se comportará igual que laversión sin este parámetro.

Detener un sonido

/∗ c l a s e Mobi le ∗/ boolean s t o pA u d i o I f A v a i l a b l e ( URL sound )

m.stopAudioIfAvailable( sound ) para la reproducción del sonido referenciadopor sound en el cliente asociado con la criatura m (que normalmente será unjugador), si este sonido se encuentra sonando.

En el caso de que el sonido esté sonando, además de detenerlo, el métododevuelve true. Si no está sonando (bien porque no ha llegado a reproducirse oporque ya ha terminado su reproducción), el método no tiene efecto y devuelvefalse.

/∗ c l a s e Mobi le ∗/ boolean s t o pA u d i o I f A v a i l a b l e ( URL sound , booleanf ade )

Este método hace lo mismo que boolean stopAudioIfAvailable ( URL sound); pero nos permite especificar un parámetro booleano adicional fade. Si esteparámetro se pone a true, el sonido se reproducirá con un «fade out» (transiciónde final). Si se pone a false, el método se comportará igual que la versión sineste parámetro.

Cambiar el volumen de un sonido

/∗ c l a s e Mobi le ∗/ boolean s e t A u d i oG a i n I f A v a i l a b l e ( URL sound ,double ga i n )

m.setAudioGainIfAvailable ( sound , gain ) permite cambiar el volumen delsonido referenciado por sound que esté sonando en el cliente asociado con lacriatura m (que normalmente será un jugador), si esto es posible. El volumentoma el valor gain, que ha de estar entre 0.0 y 1.0, siendo 0.0 el volumen nulo(sin sonido) y 1.0 el volumen máximo. El método devolverá true si el volumense ha podido cambiar, y false de lo contrario.

Nótese que para cambiar el volumen, el sonido tiene que estar sonando. Sinembargo, podemos cambiar el volumen de un sonido desde el principio llamandoinmediatamente a setAudioGainIfAvailable después de playAudioIfAvailable:

j u gado r . p l a y A u d i o I f A v a i l a b l e ( wor ld . g e tRe sou r ce ( "musica . ogg" ) ) ;j ugado r . s e t A u d i oG a i n I f A v a i l a b l e ( wor ld . g e tRe sou r ce ( "musica . ogg" ) ,

0 . 2 ) ;

Page 156: Documentacion age

156 Presentación del mundo

Como nada más lanzar el sonido ya llamamos al cambio de volumen, el efectode este código será que el audio «musica.ogg» se reproduzca con volumen 0.2.

Si en lugar de llamar a setAudioGainIfAvailable inmediatamente lo llamamosmás adelante, con el sonido a medias, el volumen cambiará dinámicamente.

Ejemplo

Supongamos que queremos parar un sonido que estaba sonando (sonido1.mp3)y comenzar a reproducir otro (sonido2.mp3) con volumen 0.5. Entonces, haría-mos lo siguiente:

// pa r a r un son i do s i e s t á tocandoj u gado r . s t o pA u d i o I f A v a i l a b l e ( wor ld . g e tRe sou r ce ( " son ido1 .mp3" ) ) ;

// t o c a r un son ido , s i e x i s t e e l f i c h e r o y e l j ugado r usa un c l i e n t eque s opo r t a aud io

j u gado r . p l a y A u d i o I f A v a i l a b l e ( wor ld . g e tRe sou r ce ( " son ido2 .mp3" ) ) ;j ugado r . s e t A u d i oG a i n I f A v a i l a b l e ( wor ld . g e tRe sou r ce ( " son ido2 .mp3" ) ,

0 . 5 ) ;

Cabe destacar que también es posible reproducir varios sonidos a la vez. Estopuede ser interesante si, por ejemplo, tenemos un sonido de música ambientepero queremos que además se produzcan efectos sonoros al llevar a cabo acciones.

6.5.2. Música MIDI

Los ficheros de música MIDI (con extensión .mid) son archivos que, en lugarde almacenar una grabación de sonido (como los ficheros de audio), guardanmúsica en forma de una partitura que el ordenador puede reproducir.

Esta manera de representar la música es mucho más limitada que los ficherosde audio, porque no se guarda una representación fidedigna de los sonidos sinosólo notas y orientaciones sobre a qué instrumento corresponden, y suele suce-der que la interpretación de esos instrumentos suena de manera muy distintadependiendo de la tarjeta de sonido, sistema operativo y configuración de cadamáquina. Por otra parte, la ventaja que tienen los ficheros MIDI es que ocupanmucho menos espacio que los ficheros de audio.

Los métodos para reproducir y detener música MIDI en AGE son similaresa los correspondientes al audio, aunque proporcionan menos funcionalidad:

No se soportan funciones como los «fade–in» y «fade–out» porque el vo-lumen de los ficheros MIDI se comporta de una manera muy distinta encada sistema operativo y configuración y es difícil garantizar un compor-tamiento fidedigno de ese tipo de funciones.

Sólo se puede reproducir un fichero MIDI a la vez, al contrario que con losficheros de audio, que pueden sonar varios a la vez. Por lo tanto, antes dereproducir una nueva música en MIDI se debe siempre llamar al métodoque detiene la anterior, que veremos más abajo. Nótese que esto no afectaa la combinación de MIDI y audio: no se puede tener varios MIDI sonandoa la vez, pero sí un solo MIDI sonando a la vez que uno o varios ficherosde audio.

Page 157: Documentacion age

sonido 157

Por todo esto, en general sólo es recomendable utilizar música en formatoMIDI en AGE en casos en los que sea necesario que el mundo ocupe poco espacioen disco. De lo contrario, es mucho más recomendable usar formatos de audio:en caso de duda, siempre audio, ya que es más flexible, suena mejor y es másfiable.

Reproducir un fichero MIDI

/∗ c l a s e Mobi le ∗/ boolean p l a y M i d i I f A v a i l a b l e ( URL sound )

m.playMidiIfAvailable( sound ) toca el fichero MIDI por sound en el clienteasociado con la criatura m (que normalmente será un jugador), si esto es posible.

En el caso de que no sea posible reproducir la música, no sonará nada y elmétodo devolverá false. Esto puede suceder por las siguientes razones:

La criatura es un personaje no jugador y por lo tanto no puede escucharsonidos,

La criatura es un jugador pero está jugando a través de un cliente que nosoporta sonidos (por ejemplo, conectado por medio de telnet),

La URL pasada como parámetro es errónea, o apunta a un fichero que noexiste, o apunta a un fichero que no es un fichero MIDI válido.

También puede suceder que no se pueda reproducir la música porque estésonando otra (como ya se ha dicho, sólo se puede reproducir un fichero MIDIa la vez). En este caso, el comportamiento depende del sistema operativo yde la configuración: puede ser que el método se niegue a reproducir el sonidoy devuelva false, o bien que se pare el sonido anterior y empiece a sonar elnuevo, o bien incluso que se paren los dos. Por lo tanto, lo más razonable es nointentar reproducir nunca dos MIDIs a la vez, ya que no hay garantía de que elcomportamiento vaya a ser el mismo en una máquina que en otra.

/∗ c l a s e Mobi le ∗/ boolean p l a y M i d i I f A v a i l a b l e ( URL sound , i n tloopTimes )

Este método hace lo mismo que boolean playMidiIfAvailable ( URL sound );pero nos permite especificar a mayores un parámetro entero loopTimes que sirvepara hacer que una música en MIDI se repita un determinado número de veces:

Si pasamos loopT imes = 1, el MIDI se reproducirá una vez (es decir, igualque con el método sin parámetro),

Si pasamos loopT imes > 1, el MIDI se reproducirá loopTimes veces,

Si pasamos loopT imes < 1, el MIDI se repetirá indefinidamente (pero sepuede parar usando stopMidiIfAvailable(), que se verá más abajo).

Detener un fichero MIDI

/∗ c l a s e Mobi le ∗/ boolean s t o p M i d i I f A v a i l a b l e ( )

Page 158: Documentacion age

158 Presentación del mundo

m.stopMidiIfAvailable( sound ) para la música MIDI que esté sonando en elcliente asociado con la criatura m (que normalmente será un jugador).

En el caso de que algún MIDI esté sonando, además de detenerlo, el métododevuelve true. Si no está sonando (bien porque no ha llegado a reproducirse oporque ya ha terminado su reproducción), el método no tiene efecto y devuelvefalse.

6.6. Otros aspectos de la presentación

Page 159: Documentacion age

Capítulo 7

Referencia de métodosinvocables

7.1. Manipulación del modelo de mundo

7.2. Obtención de nombres de cosas y criaturas

7.2.1. Obtención de nombres para mostrar

Al escribir mensajes por pantalla, son comunes las situaciones en las quequeremos mostrar el nombre de alguna cosa o criatura. Por ejemplo, recordemosel mendigo que habíamos definido en la sección 2.2 sobre manipulación básicade entidades. Este mendigo aceptaba que le entregásemos una moneda:

vo id parseCommandObj2 ( Mobi le aC r ea tu r e , S t r i n g ve rb , S t r i n ga rg s1 , S t r i n g a rg s2 , E n t i t y ob j1 )

{i f ( e qu a l s ( verb , " dar " ) && equa l s ( obj1 , i tem ( "moneda" ) ) ){

aC r ea tu r e . w r i t e ( " O f r e c e s l a moneda a l mendigo . \ n" ) ;aC r ea tu r e . w r i t e ( " E l mendigo acepta l a moneda y se l a mete en

e l b o l s i l l o . \ n" ) ;aC r ea tu r e . removeItem ( ob j1 ) ;s e l f . addItem ( ob j1 ) ;s e l f . say ( " ¡Muchas g r a c i a s , e x t r a n j e r o ! E re s muy amable . " ) ;end ( ) ;

}}

Podríamos querer que, aparte de aceptar la moneda, el mendigo rechazaracualquier otra cosa que le diésemos, respondiendo que no la quiere:

vo id parseCommandObj2 ( Mobi le aC r ea tu r e , S t r i n g ve rb , S t r i n ga rg s1 , S t r i n g a rg s2 , E n t i t y ob j1 )

{i f ( e qu a l s ( verb , " dar " ) ){

i f ( e qu a l s ( obj1 , i tem ( "moneda" ) ) ){

aC r ea tu r e . w r i t e ( " O f r e c e s l a moneda a l mendigo . \ n" ) ;

Page 160: Documentacion age

160 Referencia de métodos invocables

aCr ea tu r e . w r i t e ( " E l mendigo acepta l a moneda y se l a mete ene l b o l s i l l o . \ n" ) ;

aC r ea tu r e . removeItem ( ob j1 ) ;s e l f . addItem ( ob j1 ) ;s e l f . say ( " ¡Muchas g r a c i a s , e x t r a n j e r o ! E re s muy amable . " ) ;end ( ) ;

}e l s e i f ( ob j1 i n s t anceo f I tem ){

aCr ea tu r e . w r i t e ( " E l mendigo r e chaza l o que l e o f r e c e s ,moviendo l a cabeza . \ n" ) ;

s e l f . say ( "No n e c e s i t o eso , l o que n e c e s i t o e s d i n e r o . . . " ) ;end ( ) ;

}}

}

Con este código, conseguimos que el mendigo acepte la moneda y rechacetodo lo demás, respondiendo así:

> dar chorizo a mendigoEl mendigo rechaza lo que le ofreces, moviendo la cabeza.El mendigo dice: "No necesito esto, lo que necesito es dinero..."> dar cuchara a mendigoEl mendigo rechaza lo que le ofreces, moviendo la cabeza.El mendigo dice: "No necesito esto, lo que necesito es dinero..."> dar moneda a mendigoOfreces la moneda al mendigo.El mendigo acepta la moneda y se la mete en el bolsillo.El mendigo dice: "¡Muchas gracias, extanjero! Eres muy amable.

Dependiendo de cómo queramos que responda nuestro juego, tal vez estonos resulte suficiente. Pero también puede ser que queramos una respuesta máspersonalizada, en la que el mendigo se refiera a lo que le hemos dado, como eneste ejemplo:

> dar chorizo a mendigoOfreces el chorizo al mendigo.El mendigo rechaza el chorizo, moviendo la cabeza.El mendigo dice: "No necesito un chorizo, lo que necesito es dinero..."> dar cuchara a mendigoOfreces la cuchara al mendigo.El mendigo rechaza la cuchara, moviendo la cabeza.El mendigo dice: "No necesito una cuchara, lo que necesito es dinero..."> dar moneda a mendigoOfreces la moneda al mendigo.El mendigo acepta la moneda y se la mete en el bolsillo.El mendigo dice: "¡Muchas gracias, extanjero! Eres muy amable."

Para conseguir esto, necesitaremos obtener de alguna manera el nombre paramostrar de la cosa que le damos al mendigo (en este caso, obj1). Asimismo, tam-bién necesitaremos conseguir que se muestre el artículo adecuado al género quetenga la cosa (EL chorizo, masculino, frente a LA cuchara, femenino). Por suer-te, existen métodos en AGE que se encargan de proporcionar esta informaciónde manera muy sencilla.

Page 161: Documentacion age

Obtención de nombres de cosas y criaturas 161

Estos métodos, presentes tanto en la clase Item como en la clase Mobile, sonlos siguientes:

S t r i n g getOutputNameOnly ( )S t r i n g getOutputNameThe ( )S t r i n g getOutputNameA ( )S t r i n g getOutputNameOnly ( i n t nI tems )S t r i n g getOutputNameThe ( i n t nI tems )S t r i n g getOutputNameA ( i n t nI tems )S t r i n g getOutputNameOnly ( i n t nItems , E n t i t y v i ewe r )S t r i n g getOutputNameThe ( i n t nItems , E n t i t y v i ewe r )S t r i n g getOutputNameA ( i n t nItems , E n t i t y v i ewe r )

Los métodos getOutputNameOnly(), getOutputNameThe() y getOutputNa-meA() sin parámetros son los más sencillos; pero bastarán en la mayoría delos casos. Lo que hacen es devolver el nombre singular para mostrar actual de lacosa o criatura sobre la que los llamamos. Recuérdese que los nombres para mos-trar son dinámicos, con lo cual devolver el nombre actual quiere decir evaluarlas condiciones BeanShell de dichos nombres (si las hay) para determinar cuáles y obtenerlo. Nótese que si utilizamos este método, la variable viewer en di-chas condiciones BeanShell valdrá null (es decir, estos métodos no nos permitenparametrizar los nombres según la entidad que los ve).

La diferencia entre getOutputNameOnly(), getOutputNameThe() y getOut-putNameA() es que el primero devuelve sólo el nombre (por ejemplo, «cho-rizo»), el segundo lo prefija con el artículo determinado correspondiente («elchorizo», «la cuchara») y el tercero usa en su lugar el artículo indetermina-do («un chorizo», «una cuchara»). La excepción a esta regla se da cuando elnombre en cuestión está marcado como nombre propio, en cuyo caso nunca seañadirá ningún artículo sea cual sea el método que utilicemos. Por ejemplo,item(.excalibur").getOutputNameThe() devolverá «Excalibur» a secas, si ése essu nombre para mostrar y está marcado como propio, mientras que si no fue-se un nombre propio devolvería «el Excalibur», si la cosa es masculina, o «laExcalibur» si es femenina.

Los métodos getOutputNameOnly(int nItems), getOutputNameThe(int nItems)y getOutputNameA(int nItems) funcionan como los anteriores, pero además nospermiten especificar un número de criaturas o cosas para mostrar. Es decir, sitenemos una cosa moneda, y hacemos moneda.getOutputNameOnly(3), se nosdevolverá la cadena «tres monedas». Si pasamos como parámetro 1, estos méto-dos se comportarán como los anteriores sin parámetro, mostrando el nombre ensingular (para una sola cosa) con los artículos correspondientes según la versiónque usemos.

Por último, los métodos getOutputNameOnly(int nItems,Entity viewer), ge-tOutputNameThe(int nItems,Entity viewer) y getOutputNameA(int nItems,Entityviewer) funcionan como los anteriores pero nos permiten especificar la entidadque va a ver el mensaje, y serán los que tendremos que utilizar en el caso de quetengamos nombres para mostrar dinámicos cuyas condiciones utilicen la variableviewer. En caso contrario, los métodos anteriores serán suficientes.

Como ejemplo de uso de estos métodos, el comportamiento del mendigopuesto antes como ejemplo se podría conseguir de esta forma:

vo id parseCommandObj2 ( Mobi le aC r ea tu r e , S t r i n g ve rb , S t r i n ga rg s1 , S t r i n g a rg s2 , E n t i t y ob j1 )

{

Page 162: Documentacion age

162 Referencia de métodos invocables

i f ( e qu a l s ( verb , " dar " ) ){

i f ( e qu a l s ( obj1 , i tem ( "moneda" ) ) ){

aC r ea tu r e . w r i t e ( " O f r e c e s l a moneda a l mendigo . \ n" ) ;aC r ea tu r e . w r i t e ( " E l mendigo acepta l a moneda y se l a mete en

e l b o l s i l l o . \ n" ) ;aC r ea tu r e . removeItem ( ob j1 ) ;s e l f . addItem ( ob j1 ) ;s e l f . say ( " ¡Muchas g r a c i a s , e x t r a n j e r o ! E re s muy amable . " ) ;end ( ) ;

}e l s e i f ( ob j1 i n s t anceo f I tem ){

aCr ea tu r e . w r i t e ( " O f r e c e s " + ob j1 . getOutputNameThe ( ) + " a lmendigo . \ n" ) ;

aC r ea tu r e . w r i t e ( " E l mendigo r e chaza " + ob j1 . getOutputNameThe( ) + " , moviendo l a cabeza . \ n" ) ;

s e l f . say ( "No n e c e s i t o " + ob j1 . getOutputNameA ( ) + " , l o quen e c e s i t o e s d i n e r o . . . " ) ;

end ( ) ;}

}}

Nótese en el ejemplo cómo se utilizan los métodos obj1.getOutputNameThe()y obj1.getOutputNameA() según si se quiere utilizar el artículo determinado(el/la) o indeterminado (un/una), respectivamente.

7.3. Notificación de acciones y sucesos

7.3.1. Notificar sobre algo que ha ocurrido en una habita-ción

En la clase Room tenemos los siguientes métodos:

vo id r e p o r tA c t i o n ( En t i t y s ou r c e /∗$1∗/ , E n t i t y t a r g e t /∗$2∗/ ,E n t i t y [ ] o b j e c t s /∗$3 . . $n∗/ , S t r i n g th i r dPe r s onDe s , S t r i n gs u f f e rD e s , S t r i n g execDes , boolean s e l f_ i n c l u d e d )

vo id r e p o r tA c t i o n ( En t i t y s ou r c e /∗$1∗/ , E n t i t y t a r g e t /∗$2∗/ ,E n t i t y [ ] o b j e c t s /∗$3 . . $n∗/ , S t r i n g th i r dPe r s onDe s , S t r i n gs u f f e rD e s , S t r i n g execDes , S t r i n g s t y l e , booleans e l f_ i n c l u d e d )

Informa a todos los jugadores presentes en la habitación de un suceso que seha producido, donde:

source es el sujeto del suceso (por ejemplo, Juan),

target es el objeto del suceso (por ejemplo, un goblin),

objects son otras entidades que hayan intervenido en el suceso (por ejem-plo, una espada),

thirdPersonDes es la descripción en tercera persona con los objetos para-metrizados con $1 (sujeto), $2 (objeto), $3...$n (resto). Por ejemplo: «$1ataca a $2 con $3».

Page 163: Documentacion age

Notificación de acciones y sucesos 163

sufferDes es la descripción que se mostrará al objeto del suceso («$1 teataca con $3»).

execDes es la descripción que es mostrará al sujeto del suceso («Atacas a$1 con $3»).

style permite especificar un estilo (color) para mostrar el texto a los juga-dores (puede ser «story», «description», «action», etc.)

self_included: si es true, se muestra el texto también para el sujeto, si no,sólo para el resto de jugadores/criaturas.

vo id r e po r tAc t i onAu to ( En t i t y s ou r c e /∗$1∗/ , E n t i t y t a r g e t /∗$2∗/, E n t i t y [ ] o b j e c t s /∗$3 . . $n∗/ , S t r i n g th i r dPe r s onDe s ,

boolean s e l f_ i n c l u d e d )vo id r e po r tAc t i onAu to ( En t i t y s ou r c e /∗$1∗/ , E n t i t y t a r g e t /∗$2∗/

, E n t i t y [ ] o b j e c t s /∗$3 . . $n∗/ , S t r i n g th i r dPe r s onDe s , S t r i n gs t y l e , boolean s e l f_ i n c l u d e d )

Hace lo mismo que reportAction; pero sólo proporcionamos la descripción entercera persona y AGE intenta generar las otras dos mediante sus tablas de«tercera a segunda». Utilizar sólo en los dos casos siguientes:

Si realmente no es necesario el grado de detalle que da reportAction, puesno hay que emitir descripciones distintas en primera, segunda y tercerapersona (por ejemplo, en juegos monojugador).

Si sí es necesario emitir descripciones distintas en primera, segunda ytercera persona; pero la oración es lo suficientemente sencilla como paraque AGE pueda convertirla (en el futuro se darán más detalles de cuálesse pueden convertir automáticamente y cuáles no, de momento se puedesaber por prueba y error).

7.3.2. Notificar sobre algo que ha ocurrido con una cosa

En la clase Item tenemos los siguientes métodos:

vo id r e p o r tA c t i o n ( En t i t y s ou r c e /∗$1∗/ , E n t i t y t a r g e t /∗$2∗/ ,E n t i t y [ ] o b j e c t s /∗$3 . . $n∗/ , S t r i n g th i r dPe r s onDe s , S t r i n gs u f f e rD e s , S t r i n g execDes , boolean s e l f_ i n c l u d e d )

vo id r e p o r tA c t i o n ( En t i t y s ou r c e /∗$1∗/ , E n t i t y t a r g e t /∗$2∗/ ,E n t i t y [ ] o b j e c t s /∗$3 . . $n∗/ , S t r i n g th i r dPe r s onDe s , S t r i n gs u f f e rD e s , S t r i n g execDes , S t r i n g s t y l e , booleans e l f_ i n c l u d e d )

vo id r e po r tAc t i onAu to ( En t i t y s ou r c e /∗$1∗/ , E n t i t y t a r g e t /∗$2∗/, E n t i t y [ ] o b j e c t s /∗$3 . . $n∗/ , S t r i n g th i r dPe r s onDe s ,

boolean s e l f_ i n c l u d e d )vo id r e po r tAc t i onAu to ( En t i t y s ou r c e /∗$1∗/ , E n t i t y t a r g e t /∗$2∗/

, E n t i t y [ ] o b j e c t s /∗$3 . . $n∗/ , S t r i n g th i r dPe r s onDe s , S t r i n gs t y l e , boolean s e l f_ i n c l u d e d )

Funcionan exactamente igual que los de la clase Room, y lo que hacen esmostrar la notificación en todas las habitaciones en las que esté el Item dado.

Page 164: Documentacion age

164 Referencia de métodos invocables

7.3.3. Notificar sobre algo que ha ocurrido con una cria-tura

No hay métodos específicos en la clase Mobile, ya que una criatura siempreestá en una sola habitación, fácilmente accesible mediante getRoom(). Por lotanto, podemos hacer estas notificaciones fácilmente así:

e lMob i l e . getRoom ( ) . i n f o rmAc t i on ( . . . )e lMob i l e . getRoom ( ) . i n fo rmAct ionAuto ( . . . )

7.4. Ejecución automática de órdenes

En un mundo de Aetheria Game Engine, tanto los personajes jugadores (clasePlayer) como los no jugadores (clase Mobile) pueden interactuar con el mundoque los rodea. Así, cualquier criatura en AGE puede manipular objetos, vestirropa, moverse por el mundo, combatir, o ejecutar cualquier otro comportamientoque hayamos programado.

Por defecto, lo que hace el jugador en el mundo está determinado por lasórdenes que teclea (sean las que provocan comportamientos por defecto, como«coger espada» o «ir al norte», o sean las que provocan comportamientos defini-dos por el creador del mundo en los métodos de análisis de la entrada (ver 5.1).Por otra parte, las criaturas que no son jugadores no hacen nada por defectosalvo combatir una vez que han entrado en combate, ya que AGE incluye unaIA (inteligencia artificial) para que puedan decidir cuándo atacar, bloquear, etc.Para que tomen la iniciativa en otras cosas aparte del combate (ver 3.6.7), hayque decírselo explícitamente mediante código BeanShell.

Para conseguir esto, hay varias maneras en AGE de hacer que las criaturashagan cosas. Por un lado, la clase Mobile tiene una serie de métodos (ver 4.2.1)que podemos usar para mandarle a una criatura (sea un jugador o no) que ejecu-te un comportamiento por defecto, como puede ser ponerse una prenda o deciralgo. Por otro lado, si lo que queremos que haga la criatura no es exactamente uncomportamiento por defecto, siempre se puede simular la interacción «a mano»:por ejemplo, ya en la sección sobre manipulación básica de entidades (2.2) veía-mos un ejemplo donde un mendigo aceptaba una moneda si un jugador se laentregaba (ver 2.2.5): para que se quedara con la moneda, simplemente la po-níamos en su inventario y la quitábamos del del jugador directamente mediantecódigo BeanShell.

En esta sección veremos una tercera manera de hacer que una criatura hagacosas, que es pasar órdenes directamente a la criatura en cuestión para que lasejecute. Con esto, podemos hacer que cualquier criatura reciba una orden en for-mato texto del mismo modo que lo haría el personaje jugador: por ejemplo, comoveremos en detalle más abajo, podemos poner mobile(”Juan”).forceCommand(”iral norte”) para que Juan vaya al norte, sin necesidad de utilizar un método es-pecífico de movimiento como goTo(room(”Sala norte”)). Así, aunque la ejecuciónautomática de comandos por parte de criaturas no añade nueva funcionalidadque no se pueda implementar de otro modo, sí que nos permite a menudo con-seguir cosas con mayor comodidad, especialmente si queremos que un personajeno jugador realice acciones personalizadas implementadas mediante método deanálisis de la entrada.

Page 165: Documentacion age

Ejecución automática de órdenes 165

La ejecución automática de comandos también se puede utilizar para perso-najes jugadores: por ejemplo, si se quiere que cuando un jugador coja una espadala empuñe automáticamente, podemos mandarle ejecutar la orden «blandir es-pada» después de cogerla. Además de para ejecutar una orden a continuaciónde otra, también podemos usar esta característica para sustituir una orden porotra: por ejemplo, si queremos que «leer cartel» sea equivalente a «mirar cartel»,podemos hacer que cuando el jugador teclee «leer cartel» se ejecute de formatransparente la orden «mirar cartel» en su lugar.

Para todo ello, existen dos métodos de la clase Mobile que nos permitenejecutar órdenes automáticamente: uno que añade la orden dada a una cola deórdenes, y otro que la ejecuta lo antes posible.

7.4.1. Añadir una orden a la cola de órdenesTodas las criaturas, jugadores o no, tienen una cola de órdenes pendientes

de ejecutar.En el caso de los jugadores, se guardan por defecto en esta cola las partes

constituyentes pendientes dentro de una orden compuesta tecleada por el juga-dor: por ejemplo, si el jugador teclea «coger la manzana y la pera y mirar lapuerta», la orden «coger la manzana» se ejecutará de inmediato; mientras quelas órdenes «coger la pera» y «mirar la puerta» quedan en la cola de órdenesesperando su turno para ser ejecutadas. De este modo, sólo se cogerá la pera unavez que se haya terminado de coger la manzana y se mirará la puerta después dehaber cogido la pera. Cada vez que se comienza a ejecutar una orden, se eliminade la cola de órdenes pendientes, de modo que cuando estemos cogiendo la perala cola sólo contendrá «mirar la puerta».

En el caso de las criaturas que no son jugadores, AGE no almacena nadapor defecto en la cola de órdenes pendientes; pero igualmente está disponiblepara que podamos almacenar órdenes nosotros si queremos que la criatura lasejecute.

Así, para añadir una nueva orden al final de la cola de órdenes de unacriatura, jugador o no, utilizamos el siguiente método:

/∗ c l a s e Mobi le ∗/ vo id enqueueCommand ( S t r i n g command )

que incluye la orden dada (command) al final de la cola de órdenes. Nóteseque se puede dar a las criaturas cualquier orden que pueda teclear un jugador,incluso órdenes compuestas, en cuyo caso se añadirán a la cola de órdenes suspartes constituyentes.

La orden añadida a la cola se ejecutará en cuanto hayan terminado de proce-sarse el resto de órdenes que pueda haber en la cola, y la criatura esté disponiblepara ejecutar una acción.1 Las órdenes encoladas tienen prioridad sobre la pe-tición de órdenes por teclado a los jugadores (es decir, si un jugador tiene unaorden encolada, se ejecutará ésta en lugar de recibir una orden tecleada); y tam-bién sobre las órdenes que emite la IA de combate para los Mobile no jugadores.

De este modo, si en el código de una espada ponemos lo siguiente:1Una criatura está disponible para ejecutar una acción cuando llega al final de un estado

de los etiquetados con «Tomar decisión» en la tabla de cambios de estado – es decir, si noestá muerta, en posición de guardia para bloquear un ataque, recuperándose de un golpe, etc.Los momentos de disponibilidad coinciden con los momentos en los que, si la criatura es unjugador, se lee un comando tecleado.

Page 166: Documentacion age

166 Referencia de métodos invocables

vo id parseCommand ( Mobi le aC r ea tu r e , S t r i n g ve rb , S t r i n g a r g s ){

i f ( e qu a l s ( verb , " coge r " ) && aCrea tu r e . getRoom ( ) . has I tem ( s e l f ))

{aC r ea tu r e . enqueueCommand ( " d e c i r Con e s t o podré d e r r o t a r "+ "a e so s s u c i o s v i l l a n o s " ) ;

}}

El resultado de coger la espada será:

> coger la espadaCoges la espada.Se trata de una espada funcional, ligera pero bien equilibrada.Dices "Con esto podré derrotar a esos sucios villanos".

Por otra parte, si cogemos la espada como parte de una orden compuesta,sucedería lo siguiente:

> coger espada y dejar el escudoCoges la espada.Se trata de una espada funcional, ligera pero bien equilibrada.Dejas el escudo.Dices "Con esto podré derrotar a esos sucios villanos".

Las órdenes encoladas de este modo siguen exactamente el mismo procesoque una orden tecleada normalmente por un jugador, incluyendo su paso porlos métodos preprocessCommand() y parseCommand().

Nótese que, en cualquier momento, podemos vaciar la cola de órdenes de unacriatura con el siguiente método:

/∗ c l a s e Mobi le ∗/ vo id cance lPend ing ( )

Esto hará que cualquier orden que en ese momento esté pendiente en lacola no llegue a ejecutarse, tanto si ha sido introducida por nosotros medianteenqueueCommand() como si formaba parte de una orden compuesta.

7.4.2. Ejecutar una orden lo antes posibleEn el ejemplo anterior, la orden «decir» se ejecuta después de dejar el escudo,

dado que se pone al final de la cola de órdenes pendientes, y por lo tanto tieneque esperar a que «dejar el escudo» termine.

Sin embargo, a veces nos interesará más ejecutar una orden lo antes posible:en el primer momento en que la criatura esté disponible para ejecutar acciones,independientemente del resto de órdenes que puedan quedar en la cola (que que-dan en ella para su ejecución posterior). Para este propósito, existe el siguientemétodo:

/∗ c l a s e Mobi le ∗/ vo id forceCommand ( S t r i n g command )

que pone la orden dada (command) a ejecutarse la siguiente vez que la cria-tura pueda llevar a cabo una acción, independientemente de la cola de órdenes.Así, si modificáramos el ejemplo anterior para hacer lo siguiente:

Page 167: Documentacion age

Ejecución automática de órdenes 167

vo id parseCommand ( Mobi le aC r ea tu r e , S t r i n g ve rb , S t r i n g a r g s ){

i f ( e qu a l s ( verb , " coge r " ) && aCrea tu r e . getRoom ( ) . has I tem ( s e l f ))

{aC r ea tu r e . forceCommand ( " d e c i r Con e s t o podré d e r r o t a r "+ "a e so s s u c i o s v i l l a n o s " ) ;

}}

El resultado de coger la espada y dejar el escudo será:

> coger espada y dejar el escudoCoges la espada.Se trata de una espada funcional, ligera pero bien equilibrada.Dices "Con esto podré derrotar a esos sucios villanos".Dejas el escudo.

Como en el caso de enqueueCommand(), las órdenes ejecutadas medianteforceCommand() siguen exactamente el mismo proceso que una orden tecleadanormalmente por un jugador, incluyendo su paso por los métodos preprocess-Command() y parseCommand().

El método forceCommand() es especialmente útil para «redirigir» unos co-mandos a otros. Por ejemplo, supongamos que tenemos un cartel, y queremosconseguir que teclear «leer cartel» haga exactamente lo mismo que si hubiése-mos tecleado «mirar cartel». Podemos hacerlo poniendo lo siguiente en el códigodel cartel:

vo id parseCommand ( Mobi le aC r ea tu r e , S t r i n g ve rb , S t r i n g a r g s ){

i f ( e qu a l s ( verb , " l e e r " ) ){

aC r ea tu r e . forceCommand ( " m i r a r c a r t e l " ) ;end ( ) ;

}}

De este modo, si la descripción del cartel es «El cartel dice que la distanciaa Madrid es 22 km.», el jugador podría teclear:

> mirar el cartelEl cartel dice que la distancia a Madrid es 22 km.> leer el cartelEl cartel dice que la distancia a Madrid es 22 km.> leer el cartel y dejar el escudoEl cartel dice que la distancia a Madrid es 22 km.Dejas el escudo.

Nótese que las dos primeras órdenes de este ejemplo habrían funcionado igualde bien con enqueueCommand() en lugar de forceCommand(); pero la tercera no,pues habría dejado el escudo antes de mostrar la descripción del cartel.

Algo importante a tener en cuenta a la hora de usar forceCommand() es quesólo puede haber una acción pendiente de ejecutar con este método, y llamarotra vez a forceCommand() quita la última para sustituirla por una nueva. Esdecir, mientras que si hacemos algo como

Page 168: Documentacion age

168 Referencia de métodos invocables

mob i l e ( " o rco " ) . enqueueCommand ( " i r a l n o r t e " ) ;mob i l e ( " o rco " ) . enqueueCommand ( " i r a l e s t e " ) ;mob i l e ( " o rco " ) . enqueueCommand ( " i r a l e s t e " ) ;

los tres comandos se añadirán a la cola del orco y por lo tanto éste irá primeroal norte y dos veces al este; sin embargo, si en lugar de eso pusiéramos

mob i l e ( " o rco " ) . forceCommand ( " i r a l n o r t e " ) ;mob i l e ( " o rco " ) . forceCommand ( " i r a l e s t e " ) ;mob i l e ( " o rco " ) . forceCommand ( " i r a l e s t e " ) ;

el resultado sería que el orco sólo se movería una vez, para ir al este, que esel último comando que hemos forzado.

Otra cosa a tener en cuenta, tanto con forceCommand() como con enqueue-Command(), es que si como parte del procesado de una orden forzamos o enco-lamos esa misma orden en la misma criatura, caeremos en un bucle infinito.

7.5. Presentación general

7.6. Métodos útiles de la API de Java

Page 169: Documentacion age

Capítulo 8

Distribución como juegoonline

Las aventuras de AGE se pueden jugar online, directamente desde el nave-gador, a través de un applet que viene con el propio AGE.

Para distribuir una aventura de AGE de esta manera, es requisito indispen-sable que los accesos a ficheros que haga la aventura (imágenes, música, etc.) sehagan mediante el método getResource() de la clase World.

Cumplido este requisito, distribuir una aventura de AGE para jugar onlinees muy fácil. Sólo hay que hacer lo siguiente:

1. Copiar en un directorio el fichero AgeCore.jar y el directorio lib del AGE.

2. Con el directorio worlds que contenga la aventura se pueden tomar dosalternativas diferentes: o bien copiarlo también junto al AgeCore.jar y aldirectorio lib; o bien meterlo dentro del fichero AgeCore.jar. Si se quiereusar esta segunda alternativa, se hace así:

a) Abrir el fichero AgeCore.jar con un programa de manejo de ficheroszip (el formato .jar es lo mismo que el formato .zip, pero renombrado.Si el programa descompresor de ficheros zip no quiere abrirlo, puedescambiarle la extensión temporalmente a zip).

b) Meter el directorio worlds que contenga la aventura dentro del Age-Core.jar.

3. Crear un fichero .html basado en el ejemplo que sigue:

<html><body><center></ center></body></html>

<html><body><d iv a l i g n=" c en t e r " s t y l e =’ p o s i t i o n : r e l a t i v e ; min−he ight :

95 %; min−width : 95%’><app let code = "eu . i r r e a l i t y . age . swing . a p p l e t . SwingSDIApplet "

a rch i ve = "AgeCore . j a r , l i b /bsh −2.0b4 . j a r , l i b /commons−c l i−1.2 . j a r " ,

Page 170: Documentacion age

170 Distribución como juego online

width = "750" ,he ight = "95%"a l i g n = " c en t e r "><param name=" wo r l dU r l " va lue=" wor l d s /Morluck / wor ld . xml"/><param name=" java_arguments " va lue="−Xmx300M"/>

</ app let></ d iv></body></html>

Haciendo los cambios siguientes:

4. Donde pone «worlds/Morluck/world.xml», debes cambiarlo a la ruta co-rrespondiente al fichero world.xml de tu aventura dentro del archivo .jar.

5. Donde pone «AgeCore.jar,lib/bsh-2.0b4.jar,lib/commons-cli-1.2.jar», de-bes añadir (separadas por comas) las rutas a las bibliotecas adicionales queutilice tu aventura. Por ejemplo, si tu aventura tiene música en formatoogg, tendrás que añadir la biblioteca que toca música en formato ogg. Heaquí una tabla de las bibliotecas que necesitas añadir en cada caso:

biblioteca cuándo hace faltalib/bsh-2.0b4.jar Necesaria siemprelib/commons-cli-1.2.jar Necesaria siemprelib/basicplayer3.0.jar Aventuras con audio mp3, ogg y/o speexlib/commons-logging-api.jar Aventuras con audio mp3, ogg y/o speexlib/jl1.0.jar Aventuras con audio mp3, ogg y/o speexlib/tritonus_share.jar Aventuras con audio mp3, ogg y/o speexlib/jogg-0.0.7.jar Aventuras con audio ogglib/jorbis-0.0.15.jar Aventuras con audio ogglib/vorbisspi1.0.2.jar Aventuras con audio ogglib/jspeex0.9.7.jar Aventuras con audio speexlib/micromod.jar Aventuras con audio en formato modlib/mp3spi1.9.4.jar Aventuras con audio en formato mp3lib/svgSalamander.jar Aventuras con gráficos vectoriales svg

Una vez completados estos pasos, deberías poder jugar la aventura local-mente, abriendo en tu navegador la página HTML que has creado. Sólo falta eldirectorio completo (incluyendo la página web, el fichero AgeCore.jar y el sub-directorio lib) a tu servidor web, y ya se podrá jugar a tu aventura a través deinternet, accediendo a la URL de la página HTML.

Nótese que si quieres crear páginas para jugar varios mundos distintos, nonecesitas subir al servidor distintos directorios con copias del fichero AGECore.jary el subirectorio lib. Basta con subir una sola copia, que todos los mundos a jugarestén en el directorio worlds (esté éste metido dentro de AgeCore.jar o fuera), ycrear una página HTML para cada mundo que especifique la ruta al mismo enel parámetro worldUrl.

La diferencia entre poner el directorio worlds dentro del AgeCore.jar o fueraestá en los tiempos de carga. Si se pone dentro, el fichero .jar, que es lo que hayque descargarse al empezar a jugar la aventura, será más grande. Esto suponeuna espera inicial más larga; pero a cambio, los posibles elementos multimediaque tenga el juego se mostrarán más rápido una vez superada esta espera inicial,

Page 171: Documentacion age

171

ya que están todos dentro del .jar. Si se pone fuera, sucederá lo contrario: laespera inicial será más corta; pero a cambio, cada vez que se utilice un nuevofichero multimedia habrá que bajarlo del servidor por lo que se producirá unaespera adicional.