Herramientas de usuario

Herramientas del sitio


manipulacion_basica_de_entidades

Manipulación básica de entidades

Ahora que ya hemos visto los Primeros pasos con BeanShell, y conocemos los elementos básicos que podemos utilizar en este lenguaje (métodos, variables, funciones, estructuras condicionales, bucles, entrada/salida…); ya podemos empezar a aplicarlos a la programación de comportamientos interesantes para nuestros mundos de AGE. Pero para eso, primero debemos ver cómo se puede utilizar 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 el plátano”, queremos que éste desaparezca de su inventario, y que en su lugar quede una piel de plátano. Una forma sencilla de conseguirlo detectar cuándo el jugador ha tecleado el verbo “comer” referido a esa entidad plátano, y a continuación deberemos quitarlo de su inventario y meter una entidad representando la piel. En esta sección veremos cómo hacer cosas como ésta.

Método de análisis de la entrada referida a una entidad

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

Por ejemplo, creemos en el PUCK una entidad plátano. Para ello, usamos la herramienta “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 “Un bonito 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 una flecha desde la habitación hasta él, y a continuación vamos a la pestaña “Código y 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 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 análisis sintáctico de la entrada referida a una cosa*/
void parseCommand( Mobile aCreature , String verb , String args )
{
	//aCreature: criatura que introduce un comando.
	//verb: comando que introduce, por ejemplo "comer"
	//args: resto de la orden que introduce, por ejemplo "la seta"
 
 
	//terminar con end(): interceptamos la frase, no se ejecuta lo que se tenga que ejecutar
	//por defecto ante ella
	//terminar normal: después de nuestro procesado, se lleva a cabo el análisis normal del
	//comando y ejecución de la acción correspondiente
}

Podemos observar que el método que se nos ha generado tiene exactamente el mismo nombre y tipo de parámetros que el que estábamos utilizando antes en el mundo. Sin embargo, al definirlo en una entidad, el método tiene un significado distinto al que tenía en el mundo. Mientras AGE invocará el método del mundo para todas las entradas que introduzca un jugador en ese mundo, el método del plá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 ponga un comando en el que mencione “plátano” (u otro nombre de referencia) y además cuente con el plátano a su alcance (es decir, en el inventario o bien en la habitación en la que está). El método no se ejecutará, por ejemplo, si el jugador escribe “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 (que quiere 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 análisis sintáctico de la entrada referida a una cosa*/
void parseCommand( Mobile aCreature , String verb , String args )
{
  if ( equals ( verb , "comer" ) )
  { 
    aCreature.write("No puedes comer plátano: tu religión te impide morder cosas alargadas y amarillas.\n");
    end();
  } 
}

Esto producirá salidas como la siguiente:

> mirar Aquí hay un plátano.
> coger el plátano
Coges el plátano.
> comer el plátano
No puedes comer plátano: tu religión te impide morder cosas alargadas y amarillas.
> come el platano
No puedes comer plátano: tu religión te impide morder cosas alargadas y amarillas.
> como la fruta
No 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 “platano” o “fruta” es porque hemos puesto todas esas palabras como nombres de referencia. Pero algo un poco más misterioso puede ser ¿por qué funciona tanto “come” como “como” o “comer”, si en la condición del if hemos puesto equals( verb , “comer” ), que debería comprobar si el verbo que ha puesto el jugador es textualmente “comer”?

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

Métodos para quitar, poner y mover entidades

Con el código que acabamos de ver, podemos proporcionar una “excusa” al jugador para no dejarle que se coma el plátano. Pero lo que realmente queríamos hacer es que se lo coma de verdad. Para ello, tendremos que quitar el plátano del inventario del jugador y, si queremos darle un mayor realismo, hacer que se quede con una piel de plátano en su lugar. El AGE proporciona una serie de métodos que sirven para llevar a cabo este tipo de operaciones que mueven entidades de un lado a otro. He aquí algunos de ellos, donde expresamos en un comentario a qué clase pertenecen (un método de una clase X se puede ejecutar haciendo obj.metodo(parametros) si obj es un objeto de esa clase X):

/*clase Mobile*/ boolean removeItem ( Item oldItem )

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

/*clase Mobile*/ void addItem ( Item newItem )

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

/*clase Room*/ boolean removeItem ( Item oldItem )

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

/*clase Room*/ void addItem ( Item newItem )

r.addItem ( newItem ) sirve para agregar la cosa newItem al inventario de la habitació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, una cosa puede estar en varios sitios a la vez (por ejemplo, en varias habitaciones, o en 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 para añ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 se encuentre en un momento dado, y la pone en uno nuevo. Estos métodos son los siguientes:

/*clase Item*/ void moveTo ( Mobile m )

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

/*clase Item*/ void moveTo ( Room r )

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

Nota: en caso de estar usando el sistema de pesos y volúmenes, hay que tener en cuenta que todos los métodos que ponen una cosa en un inventario (de habitación o de criatura) pueden tirar las excepciones VolumeLimitExceededException y WeightLimitExceededException, si ese inventario está limitado en peso o volumen y no puede contener esa cosa. Veremos detalles sobre esto cuando veamos manejo de excepciones, por ahora supondremos que no estamos limitando explícitamente los inventarios en peso (el límite por defecto son 10.000 unidades de peso y volumen, que debería llegar siempre si no ponemos objetos muy pesados).

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

/*clase Mobile*/ void setRoom ( Room newRoom )

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

Sobre este método, hay que destacar que, mientras que una cosa puede estar en cero, uno o más sitios (habitaciones e inventarios de criaturas), una criatura siempre estará en un y sólo un sitio a la vez (y ese sitio siempre tiene que ser una habitació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 a ella, basta con quitarla de todos los sitios en los que esté, mediante el método removeItem correspondiente. Esto no borrará la cosa de memoria (seguiremos pudiendo 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 momento queremos volver a poner la cosa en el mundo, basta añadirla a cualquier habitación o inventario de criatura.

Si queremos quitar una criatura (Mobile) de la circulación, podemos crear una habitación artificial llamada Limbo, a la que no se pueda acceder mediante ningún camino (es decir, que esté desconectada del resto del mapeado en el PUCK) y mover la criatura a esa habitación.

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

/*Método de análisis sintáctico de la entrada referida a una cosa*/
void parseCommand( Mobile aCreature , String verb , String args )
{
  if ( equals ( verb , "comer" ) )
  { 
    aCreature.write("Te comes el plátano. Ñam, ñam. ¡Qué rico!\n");
    aCreature.removeItem( item("plátano") ); 
    end();
  } 
}

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

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 hacer esto, podemos utilizar siempre una variable especial llamada self para referirnos a la entidad en la que estamos definiendo el código. Así, el código que definimos antes se podría reescribir como:

/*Método de análisis sintáctico de la entrada referida a una cosa*/
void parseCommand( Mobile aCreature , String verb , String args )
{
  if ( equals ( verb , "comer" ) )
  { 
    aCreature.write("Te comes el plátano. Ñam, ñam. ¡Qué rico!\n");
    aCreature.removeItem( self ); 
    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, en su lugar, lo definiésemos en el formulario correspondiente a una piedra, o al jugador; 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 en cualquier momento es world. La variable world siempre almacena el objeto de la clase World que representa el mundo. Más adelante veremos cosas que se pueden hacer con él.

Las variables especiales self y world son las únicas que están presentes y se 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 declaremos explícitamente.

Métodos para comprobar dónde están las entidades

El 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 el inventario del jugador sino en el suelo de la habitación, el método también se ejecutará (pues ya hemos dicho que este método de análisis de la entrada se ejecuta tanto cuando alguien se refiere a un objeto que lleva como a objetos de la habitación en la que está) pero no le podemos quitar el plátano al jugador porque 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 que cogerlo”).
  2. Dejar que el jugador se coma el plátano incluso si no lo lleva, pero en este caso, borrar el plátano de la habitación, en lugar de del inventario del jugador.

Para implementar cualquiera de estas dos opciones, necesitamos comprobar la 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 hacer comprobaciones sobre dónde está una entidad dada:

/*clase Mobile*/ Room getRoom()

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

/*clase Mobile*/ boolean hasItem( Item it )

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

/*clase Room*/ boolean hasItem( Item it )

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

/*clase Room*/ boolean Mobile( Mobile criatura )

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

Con estos mimbres, ya podemos mejorar el método anterior de forma que controle 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 hacer de esta manera:

/*Método de análisis sintáctico de la entrada referida a una cosa*/
void parseCommand( Mobile aCreature , String verb , String args )
{
  if ( equals ( verb , "comer" ) )
  { 
 
    if ( aCreature.hasItem( self ) )
    {
      aCreature.write("Te comes el plátano. Ñam, ñam. ¡Qué rico!\n");
      aCreature.removeItem( self ); 
    }   
    else
    {  
      aCreature.write("Para comer el plátano, necesitarías cogerlo primero.\n");
    }
    end(); 
  } 
}

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

/*Método de análisis sintáctico de la entrada referida a una cosa*/
void parseCommand( Mobile aCreature , String verb , String args )
{
  if ( equals ( verb , "comer" ) )
  { 
 
    if ( aCreature.hasItem( self ) )
    {
      aCreature.write("Te comes el plátano. Ñam, ñam. ¡Qué rico!\n");
      aCreature.removeItem( self ); 
    }   
    else if ( aCreature.getRoom().hasItem(self) )
    {  
      aCreature.write("Coges el plátano del suelo y te lo comes. Ñam, ñam. ¡Qué rico!\n");
      aCreature.getRoom().removeItem( self ); 
    }
    else
    {
      aCreature.write("Error: esto no debería suceder nunca, porque el plátano o está en tu inventario o en tu habitación.\n");
    }   
    end(); 
  } 
}

Nótese que el tercer mensaje no se debería mostrar nunca, porque nunca se entrará 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 modelos de objetos complejos (por ejemplo, se puede hacer que el plátano se componga de un interior y una piel, y que lo que se coma el jugador sea el interior y quede la piel; o bien que la misma entidad plátano pase de estar intacta a conservar sólo la piel, cambiándole las descripciones); para un comportamiento como este eso tendría una complejidad excesiva. Una solución mucho más sencilla, y que funcionará igual, es que tengamos un objeto “piel de plátano” creado de antemano pero no enlazado con el mundo, y que cuando el jugador ponga el plátano demos 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
  • 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 análisis sintáctico de la entrada referida a una cosa*/
void parseCommand( Mobile aCreature , String verb , String args )
{
  if ( equals ( verb , "comer" ) )
  { 
 
    if ( aCreature.hasItem( self ) )
    {
      aCreature.write("Te comes el plátano. Ñam, ñam. ¡Qué rico!\n");
      aCreature.removeItem( self ); 
      aCreature.write("Te quedas con la piel, por si te resulta útil.\n");
      aCreature.addItem( item("piel de plátano") );  
    }   
    else
    {  
      aCreature.write("Para comer el plátano, necesitarías cogerlo primero.\n");
    }
    end(); 
  } 
}

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

Método de análisis de la entrada referida a dos entidades

Con lo que hemos visto en las secciones anteriores, podemos conseguir muchos 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.

Con esto se puede implementar una parte significativa de los puzzles que aparecen en aventuras sencillas, tipo “Vampiro”. Pero tenemos una limitación: por el momento, sólo sabemos implementar acciones que se refieren como mucho a una entidad1): 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 con la palanca”, “atar al perro al tronco”, “echar sal al guiso” o “dar una moneda al mendigo”: estas últimas acciones se refieren a dos entidades, no a una.

Para implementar acciones como éstas con facilidad, existen métodos de análisis de la entrada específicos que AGE ejecuta sólo cuando el jugador se ha referido en una misma orden a dos entidades, y que nos proporcionan dichas entidades como parámetro. Por ejemplo, supongamos que tenemos un objeto “moneda de oro” que le podemos dar a diversos personajes de nuestra aventura mediante la orden “dar moneda a Fulanito”. Para implementarlo, haremos lo siguiente:

  • Creamos la cosa “moneda de oro”, con los nombres, descripciones y características correspondientes.
  • Creamos los personajes a los que les podremos dar la moneda. Para ello, usaremos la herramienta “Añadir personaje” (pero sin marcarlos como jugador) y rellenaremos las fichas “General” y “Nombres” del formulario exactamente igual que al crear cosas. Por ejemplo, un personaje podría ser 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 vida y 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 otra cosa (en ese orden)”. Con lo cual se nos generará la siguiente plantilla:
/*Método de análisis sintáctico de la entrada referida a dos cosas, que no están dentro de otras*/
/*Este método se ejecuta cuando el jugador invoca una orden sobre dos objetos, que no están en contenedores, y el primero de los cuales es éste.
*/
void parseCommandObj1 ( Mobile aCreature , String verb , String args1 , String args2 , Entity obj2  )
{
 
	//aCreature: criatura que introduce un comando.
	//verb: comando que introduce, por ejemplo "afilar"
	//args1: parte de la orden que se refiere a un primer objeto (que es este objeto), por ejemplo "el cuchillo".
	//args2: parte de la orden que se refiere a un segundo objeto, por ejemplo "con el afilador"
	//obj2: segundo objeto al que se refiere la acción del jugador (en el ejemplo, el objeto afilador).
 
 
	//terminar con end(): interceptamos la frase, no se ejecuta lo que se tenga que ejecutar
	//por defecto ante ella
	//terminar normal: después de nuestro procesado, se lleva a cabo el análisis normal del
	//comando y ejecución de la acción correspondiente
 
}

Como en casos anteriores, la plantilla que PUCK genera nos da una explicación de cuándo se llama este método y de qué significan sus parámetros, que ya debería ayudarnos a entender lo que hace. En concreto, este método que hemos definido sirve para capturar acciones sobre dos entidades, de las cuales aquélla en 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” pero no a otros como “forzar la cerradura con la moneda”, donde la moneda aparece en segundo lugar (hay otro método, que veremos después, que es independiente del orden).

Los primeros dos parámetros hacen la misma función que en el método de análisis de la entrada referida a una entidad, proporcionándonos el objeto que representa al jugador que ha tecleado la orden así como el verbo que ha introducido. Los siguientes dos parámetros, que aparecen como args1 y args2, nos dan el resto de la orden introducida dividida en dos partes, una con cada entidad a la 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 jugador. Así, si éste teclea “dar moneda al mendigo”, obj2 será la entidad mendigo (mobile(“mendigo”)); mientras que si teclea “dar moneda al embajador”, sería el objeto embajador. Nótese que en este ejemplo obj2 se refiere a criaturas, pero podría referirse a cosas también: tanto Item (cosas) como Mobile (criaturas) son subtipos de la clase Entity (entidades), así que nos pueden aparecer como valor de un parámetro de clase Entity.2)

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

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

void parseCommandObj1 ( Mobile aCreature , String verb , String args1 , String args2 , Entity obj2  )
{
  if ( equals ( verb , "dar" ) )
  {
     if ( equals ( obj2 , mobile("mendigo") ) )
     {
       aCreature.write("Ofreces la moneda al mendigo.\n"); 
       aCreature.write("El mendigo acepta la moneda y se la mete en el bolsillo.\n"); 
       aCreature.removeItem( self );
       obj2.addItem( self );  
       obj2.say("¡Muchas gracias, extranjero! Eres muy amable.");
       end(); 
     }  
     else if ( equals ( obj2 , mobile("embajador") ) )
     {
       aCreature.write("Ofreces la moneda al embajador.\n");  
       aCreature.write("El embajador mira la moneda con desprecio y hace un gesto de rechazo.\n");
       obj2.say("¿Qué te crees, que te puedes ganar mi favor con tu sucio dinero? ¡Vete, canalla!"); 
       end(); 
     }   
  } 	
}

Como vemos, en el caso de que el verbo sea “dar”, comparamos obj2 con distintas criaturas para programar sus reacciones. Hemos usado un método que no habíamos visto antes, el método

/*clase Mobile*/ void say ( String text )

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

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

> inventario
Tienes una moneda de oro.
> mirar
Estás en una habitación muy bonita.
Aquí está un embajador y un mendigo.
> dar la moneda al embajador
Ofreces 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!”
> inventario
Tienes una moneda de oro.
> dar la moneda al mendigo
Ofreces 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.”

Variantes del método referido a dos entidades

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

Sin embargo, muchas veces es más conveniente hacerlo al revés: definir el método en la segunda entidad, y obtener la primera como parámetro. Imaginemos que en nuestra aventura sólo hay un personaje al que podemos intentar darle diferentes cosas, y según la cosa que le demos reaccionará de una u otra manera: está claro que en este caso será más cómodo definir el comando “dar X a personaje” en el personaje, y programar allí la reacción a todas las posibles cosas 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 entidad o en la segunda también puede depender de consideraciones de reutilización de objetos: por ejemplo, incluso si el único objeto que le pudiésemos dar al mendigo fuese la moneda, en realidad la reacción del mendigo cuando se la damos es algo que lógicamente depende del mendigo, más que de la moneda. Así pues, es más útil definir este código en el mendigo: de este modo, si copiamos la entidad mendigo y la llevamos a otra aventura, podremos tener fácilmente un mendigo que acepta monedas de oro; cosa que parece tener más sentido que ponerlo en la moneda y poder copiar a otra aventura una moneda con el código para ser aceptada por mendigos (ya que en muchas aventuras con monedas no habrá mendigos).

Por supuesto, todo esto es subjetivo, y dependerá de la conveniencia de cada situación; pero el caso es que AGE nos permite definir los métodos de análisis de la entrada para dos entidades tanto en la primera entidad a la que se refieren las acciones como en la segunda. Para definirlo en la segunda, vamos al formulario 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 análisis sintáctico de la entrada referida a dos cosas, que no están dentro de otras*/
/*Este método se ejecuta cuando el jugador invoca una orden sobre dos objetos, que no están en contenedores, y el segundo de los cuales es éste.
*/
void parseCommandObj2 ( Mobile aCreature , String verb , String args1 , String args2 , Entity obj1  )
{
 
	//aCreature: criatura que introduce un comando.
	//verb: comando que introduce, por ejemplo "afilar"
	//args1: parte de la orden que se refiere a un primer objeto, por ejemplo "el cuchillo".
	//args2: parte de la orden que se refiere a un segundo objeto (que es este objeto), por ejemplo "con el afilador"
	//obj2: primer objeto al que se refiere la acción del jugador (en el ejemplo, el objeto cuchillo).
 
	//terminar con end(): interceptamos la frase, no se ejecuta lo que se tenga que ejecutar
	//por defecto ante ella
	//terminar normal: después de nuestro procesado, se lleva a cabo el análisis normal del
	//comando y ejecución de la acción correspondiente
 
}

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

void parseCommandObj2 ( Mobile aCreature , String verb , String args1 , String args2 , Entity obj1  )
{
 
  if ( equals(verb,"dar") && equals(obj1,item("moneda")) )
  {
     aCreature.write("Ofreces la moneda al mendigo.\n"); 
     aCreature.write("El mendigo acepta la moneda y se la mete en el bolsillo.\n"); 
     aCreature.removeItem( obj1 );
     self.addItem( obj1 );  
     self.say("¡Muchas gracias, extranjero! Eres 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 se digan las entidades. De hecho, el caso que estamos tratando es un ejemplo: se puede decir “dar la moneda al mendigo”, pero también podría interesarnos que la aventura entendiese “dar al mendigo la moneda”, aunque sea menos común.

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

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

/*Método de análisis sintáctico de la entrada referida a dos cosas, que no están dentro de otras*/
/*Este método se ejecuta cuando el jugador invoca una orden sobre dos objetos no contenidos en otros, uno cualquiera de los cuales es éste.
*/
void parseCommandTwoObjects ( Mobile aCreature , String verb , String args1 , String args2 ,  Entity otherEnt  )
{
 
	//aCreature: criatura que introduce un comando.
	//verb: comando que introduce, por ejemplo "atar"
	//args1: parte de la orden que se refiere a este objeto, por ejemplo "la piedra". 
	//args2: parte de la orden que se refiere al otro objeto, por ejemplo "a la tabla"
	//otherEnt: objeto al que se refiere la acción del jugador, aparte de éste (o del objeto contenido en éste).
		//si self es la piedra, otherEnt sería la tabla; si self es la tabla, otherEnt sería la piedra.
 
 
	//terminar con end(): interceptamos la frase, no se ejecuta lo que se tenga que ejecutar
	//por defecto ante ella
	//terminar normal: después de nuestro procesado, se lleva a cabo el análisis normal del
	//comando y ejecución de la acción correspondiente
 
}

Este método tiene la característica de que se llama siempre que un jugador teclea una orden referida a dos entidades, de las cuales una es la entidad en la que lo definimos, independientemente de que sea la segunda (dar la moneda al mendigo) o la primera (dar el mendigo a la moneda). El parámetro Entity otherEnt siempre contiene el objeto correspondiente a la otra entidad, es decir, aquélla donde no definimos el método (pues la entidad donde definimos el mé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 dos entidades que hemos visto; pero con la diferencia de que si el jugador teclea las 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 defecto como sigue, en el código para la entidad mendigo:

void parseCommandTwoObjects ( Mobile aCreature , String verb , String args1 , String args2 ,  Entity otherEnt  )
{
 
  if ( equals(verb,"dar") && equals(otherEnt,item("moneda")) )
  {
     aCreature.write("Ofreces la moneda al mendigo.\n"); 
     aCreature.write("El mendigo acepta la moneda y se la mete en el bolsillo.\n"); 
     aCreature.removeItem( otherEnt );
     self.addItem( otherEnt );  
     self.say("¡Muchas gracias, extranjero! Eres 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 referida a dos entidades, y los métodos anteriores que vimos, podemos cubrir razonablemente todas las variantes de órdenes que se nos puedan presentar.

1) También sabemos implementar acciones que no se refieren a ninguna entidad, con el método de análisis de la entrada del mundo que utilizábamos al principio
2) Las habitaciones (Room) también son entidades y en principio podrían aparecer como valor donde nos encontremos algo de clase Entity. Sin embargo, en este caso particular nunca nos aparecerán: las habitaciones no tienen nombre de referencia, por lo que AGE no comprueba si el jugador se ha referido a ellas y no se las pasa a los métodos de análisis de la entrada.
manipulacion_basica_de_entidades.txt · Última modificación: 2011/08/22 17:07 por aetheria