c# - Codificación de interacciones en una aventura de texto
.net adventure (7)
EDITAR: Si no puede molestarse en leer esta pregunta gigantesca, he puesto un resumen en la parte inferior.
Actualmente estoy trabajando en una especie de "marco" para una aventura de texto que voy a hacer en C #, como un ejercicio de codificación. En este marco, las posibles acciones se definen mediante una clase de "Interacción".
Los posibles objetos "procesables" son elementos de inventario (palo, arma, espada), elementos ambientales (pared, puerta, ventana) y personajes (personas, animales). Cada uno de estos tiene una propiedad que es una Lista de Interacciones. Por el momento, una Interacción es básicamente un par de nombre y nombre de "acción / respuesta". Cuando escribe "ventana aplastante", examina todos los elementos procesables posibles que el Reproductor tiene disponibles y coincide con el tema (en este caso, "Ventana"). Luego se resuelve que la acción es "Smash" y busca en la Lista de interacciones en la ventana (Elemento ambiental) para obtener una respuesta para la acción Smash y luego la escribe en la consola.
Todo está hecho, pero aquí está el punto de que estoy atascado:
Una acción tiene varias consecuencias potenciales, que difieren según cada interacción potencial. Estos son:
- Devuelve una respuesta que describe el resultado de la acción buscándolo en la interacción, posiblemente con una segunda asignatura
O BIEN - El sujeto de la acción (elemento de inventario, elemento ambiental o personaje) cambia su descripción EG. "golpe de pared" podría cambiar la descripción de la pared para describir una abolladura en la pared O - El sujeto de la acción es reemplazado por otro elemento EG. "botella smash" da como resultado que "botella" cambie a "botella rota" o "mate a John" y el personaje John sea reemplazado por el elemento ambiental "cadáver de John".
- Devuelve una respuesta que describe el cambio anterior EG. "Los pedazos rotos de la botella están esparcidos por el piso".
- La descripción de un área ha cambiado. P.EJ. "Smash lightbulb" hace que la descripción de la sala cambie para describir una sala de tono negro
- Los elementos se agregan / eliminan del inventario o del entorno EG. "recoger la botella". Ahora tiene una botella en su inventario y la botella se retira del medio ambiente.
- Se cambian las direcciones disponibles para el movimiento y las áreas a las que conducen. "desbloquear la puerta con la llave" te permite moverte al este hacia otra habitación
- El jugador se mueve a una nueva área EG. "ir al norte" te lleva a otra área.
De alguna manera, debo determinar de manera genérica cuál de estas consecuencias debe invocar una Interacción particular e invocarlas. Una acción podría potencialmente usar varias de estas consecuencias, o solo una.
Por ejemplo, si el artículo es una botella:
" llenar botella con agua " primero devolvería una respuesta que describa que ha llenado la botella con agua. Luego reemplazaría el elemento "botella" con un elemento "botella de agua". Eso es dos consecuencias, devolver una respuesta y reemplazar un artículo.
Digamos que tenías que hacer " arrojar una botella de agua a la ventana ". Esto es más complejo. Primero devolvería una respuesta que describa los eventos que tienen lugar, la botella y la ventana se romperían y el agua iría a todas partes. La botella se eliminará del inventario del jugador. Luego, la " botella de agua " sería reemplazada por la "botella rota" y la "Ventana" se reemplazaría por "Ventana rota". La descripción del área también cambiaría para reflejar esto. Eso es cinco consecuencias, devolver una respuesta, eliminar un elemento del inventario, reemplazar dos elementos y actualizar la descripción del área actual.
Como puede ver, necesito una forma genérica de poder definir en base a "Interacción", cuáles serán las consecuencias de esa acción y actualizar otros objetos como Artículo, Jugador (para inventario) y Área de manera apropiada.
Lo siento si esto no está claro, y haré todo lo posible para aclarar si alguien tiene alguna pregunta.
EDITAR: ¿Hay alguna manera de definir un método en una interacción que pueda pasar una cantidad de métodos para llamar (y sus parámetros) a? La respuesta inicial devuelta sería la consecuencia predeterminada y obligatoria, y luego podría haber otras si se especifica.
Por ejemplo, en los ejemplos anteriores, para la primera interacción, "llenar con agua", le diría que devuelva una respuesta ("Ha llenado la botella con agua") y también que llame a un método ReplaceItem que reemplazaría al " botella "sujeto con una" botella de agua ".
Para la segunda interacción, le diría que devuelva una respuesta ("La botella se precipita en el aire en ..."), llame a RemoveFromInventory sobre el tema de la acción, llame a UpdateStatus en la botella ("la botella está rota") y la ventana ("la ventana está rota") y llama a UpdateAreaDescription para cambiar la descripción del área actual ("Estás parado en una habitación con una sola ventana, el vidrio roto en pedazos").
¿Eso suena factible? Intento mantener esto tan genérico como sea posible, por el bien de todas las diferentes interacciones posibles.
EDIT 2: Para aclarar más e intentar resumir el problema:
En mi juego, hay objetos accionables (una botella, una pared, John). Cada objeto accionable tiene una lista de objetos de Interacción que describen cómo un jugador puede interactuar con ellos. Por el momento, una Interacción tiene una propiedad de "Nombre" ("lanzar", "golpear", "romper") y devuelve una Respuesta ("Lanzas la").
El problema que trato de resolver es que una Interacción también necesita hacer otras cosas, variando según cada Interacción particular. Tomemos el ejemplo de una botella de vidrio.
"lanzar una botella de vidrio"
- Se devuelve una respuesta ("Arrojaste la botella de vidrio").
- La "botella", se elimina del inventario del jugador.
- Se reemplaza con una nueva para reflejar el cambio. ("Botella" reemplazada por "Botella rota").
- Se devuelve una segunda respuesta ("Las piezas de la botella de vidrio están esparcidas por el piso").
"arrojar una botella de vidrio a la ventana"
- Se devuelve una respuesta ("Arrojaste la botella de vidrio en la ventana").
- El objeto "Botella", se elimina del inventario del Jugador.
- El objeto se reemplaza con un nuevo objeto para reflejar el cambio. ("Botella" reemplazada por "Botella rota").
- Un segundo objeto opcional se reemplaza por uno nuevo para reflejar el cambio. ("Ventana" reemplazada por "Ventana rota").
- Se actualiza la propiedad "Descripción" del Área actual. ("Estás parado en una habitación, con una sola ventana rota").
Cuando creo las Interacciones, ¿cómo puedo variar las acciones adicionales que realizan, como cambios de estado en el tema o cambios en la descripción del Área actual?
Si necesita más ejemplos de acciones como las anteriores, hágamelo saber y haré algunas más.
Creo que debes decidir sobre un número determinado de verbos que reconocerás, y luego, para cada objeto, decidir a cuál de esos verbos responde.
Bloquear verbos reconocidos por objetos
- Mira
- UseItemOn (Key001, LockPicks, Sledgehammer, ...)
- Puñetazo
De esta forma, puede manejar genéricamente verbos que no reconoce con una respuesta como "No puede <verbo> el <objeto>, y manejar verbos que reconoce con eventos o lo que sea.
Editar
Según su comentario, obviamente, simplemente escaneé su pregunta (demasiado tiempo para mí). Aún así, no veo la diferencia, de verdad. El punto es que un objeto participa en un evento. Desde la perspectiva de la botella, es golpeada por una pared. Desde la perspectiva del Muro, es golpeado por una botella. Ambos objetos tendrán una lista de verbos a los que responderán de cierta manera.
Entonces, si planea que el muro responda a CUALQUIER objeto lanzado, entonces deberá agregar un verbo colisionar a su lista. Deberá especificar con qué objetos debería interesar colisionar, y quizás para cada uno de ellos, cómo debería responder a las magnitudes de fuerza particulares, etc.
Pero el principio es el mismo. Para cualquier evento hay un número de participantes, y cada participante tendrá ciertos estímulos que le interesen, y para esos estímulos tendrá ciertos objetos de origen de estímulo que le interesan. Si se trata de un verbo que le interesa, pero su origen no es un objeto que le interese, entonces lo ignorará de manera efectiva, o responderá de alguna manera.
La botella participa en una colisión con el muro. La botella tiene en su lista de verbos el tipo de interacción Collide. Puede tener un solo objeto con el que se preocupe por colisionar, o puede tener un valor de Cualquiera, o AnySolid, o lo que sea. Hay un millón de formas de diseñar eso. En cualquier caso, el Muro también participa y también puede tener en su lista de verbos el tipo de interacción Collide. Pero solo le importa colisionar con el objeto Sledgehammer, o tal vez AnySolid que tiene una masa de 10 o más ...
También podría hacer esto con interfaces. Puede tener un LootableObject que implemente la interfaz ICollidible, o lo que sea. Cuando cualquier ICollidible (por ejemplo, una botella) ejecuta su método Collide, necesitará ciertos parámetros: qué tan frágil es, cuánta fuerza recibió, si el objeto en colisión es algo que le importa, etc.
Puede estar lleno de líquido, por lo que implementará una interfaz IContainer que tiene un método de Derrame, y también una interfaz IConsableable que tiene un método de Bebida. Puede ser un bloqueo que implemente una interfaz ILockable que tenga un método Unlock (obj Key) y un método Pick (int PickSkill). Cada uno de estos métodos puede producir ciertos cambios de estado en el objeto y el otro participante en la interacción. Puede hacer esto con Eventos si lo desea.
Básicamente debe decidir qué nivel de (des) predecibilidad desea y luego componer una matriz de interacciones (no necesariamente físicas, sino cualquier tipo de interacción en la que planee operar: un evento de bloqueo, un evento de colisión, un evento de bebida) que implican ciertas propiedades predecibles.
Hah, estoy trabajando en algo similar también. Me pregunto si tu framework termina convirtiéndose en un creador de aventuras de texto, que es mi proyecto.
Mi enfoque es tener un tipo de API que consiste en métodos que representan todas las acciones más básicas en el juego. Luego use ''scripts'', que son básicamente métodos que contienen una combinación de estas acciones básicas. Estas acciones básicas pueden involucrar:
- Imprimir un mensaje
- Cambiar la descripción de un objeto / sala
- "Bloquear" o "desbloquear" un objeto. Esto significa que "examinar el cinturón" dirá "No se ve ningún cinturón aquí" HASTA que se haya "examinado el cadáver" para saber que "el cadáver tiene un cinturón brillante alrededor de la cintura".
- Bloquear o desbloquear salidas de una habitación
- Mueva al jugador a alguna habitación
- Agregar / eliminar algo del inventario del jugador
- Establecer / Cambiar algunas variables del juego, por ejemplo. "movedGlowingRock = true" o "numBedroomVisits = 13", etc.
y así sucesivamente ... Esto es lo que tengo actualmente en mente. Estos son todos los métodos en tal vez una clase API y toman varios parámetros según sea necesario.
Ahora, hay habitaciones. Las habitaciones tienen objetos. Ciertos comandos son válidos para cada objeto. Una forma simple es hacer que cada objeto de sala contenga un Diccionario de comandos permitidos. Script es un delegado que apunta a su script de acción. Medita esto:
delegate void Script();
class GameObject
{
public Dictionary<string, Script> Scripts {get; set;}
public string Name {get; set;}
//etc...
}
Y sus scripts, almacenados en la instancia de la habitación relevante:
//In my project, I plan to have such an abstract class, and since it is a game _creator_, the app will generate a C# file that contains derived types containing info that users will specify using a GUI Editor.
abstract class Room
{
protected Dictionary<string, GameObject> objects;
public GameObject GetObject(string objName) {...//get relevant object from dictionary}
}
class FrontYard : Room
{
public FrontYard()
{
GameObject bottle;
bottle.Name = "Bottle";
bottle.Scripts["FillWithWater"] = Room1_Fill_Bottle_With_Water;
bottle.Scripts["ThrowAtWindow"] = Room1_Throw_Bottle_At_Window;
//etc...
}
void void Room1_Fill_Bottle_With_Water()
{
API.Print("You fill the bottle with water from the pond");
API.SetVar("bottleFull", "true");
}
void Room1_Throw_Bottle_At_Window()
{
API.Print("With all your might, you hurl the bottle at the house''s window");
API.RemoveFromInventory("bottle");
API.UnlockExit("north");
API.SetVar("windowBroken", "true");
//etc...
}
}
Todo esto es una especie de visión esquemática de lo que tengo en mente (hay muchas sutilezas que he notado, pero esto es suficiente para un ejemplo). Lamentablemente, ni siquiera he codificado una palabra para mi proyecto, jeje. Todo en papel.
Entonces ... todo esto podría darte algunas ideas para jugar en tu propio proyecto. Si algo no está claro, pregunta. Espero no haberme desviado de tu pregunta o algo así.
Creo que pasé demasiado tiempo escribiendo todo esto> _>
EDITAR: PD: Mi ejemplo esqueleto no muestra exactamente cómo administrar comandos que involucran múltiples objetos del juego (Esta es solo una de las muchas sutilezas que insinué). Para cosas como "lanzar una botella en la ventana", debe pensar cómo administrar dicha sintaxis, por ej. el sabor de mi solución a esto es analizar y descubrir qué comando se está emitiendo ... "lanzar IR a IR". Averigüe cuáles son los objetos del juego, luego vea si la habitación actual los tiene. etcétera etcétera.
Lo que es más importante, esto también le impide tener secuencias de comandos dentro de una instancia de objeto de juego, ya que un comando implica más de un objeto de juego. Probablemente sea mejor almacenar el diccionario en la instancia de la sala ahora. (Este es el lugar donde estoy con mi proyecto).
Perdonen mis divagaciones ...> _>
Tienes dos cosas: el jugador y el entorno (es posible que también tengas otros jugadores).
Pase ambos a cada interacción:
interaction.ActOn(environment, player);
//eg:
smash.ActOn(currentRoom, hero);
Luego, deje que cada interacción resuelva qué hacer:
environment.ReplaceObject("window", new Window("This window is broken. Watch out for the glass!");
player.Inventory.RemoveObject("bottle");
player.Hears("The window smashes. There is glass all over the floor! If only John McLane were here...").
Con las comprobaciones habituales para asegurarse de que el entorno tiene realmente una ventana, el jugador tiene la botella, etc.
player.Inventory.ReplaceObject("bottle", new BottleOfWater());
La interacción se convierte en una interfaz común que puede adjuntarse a cualquier elemento del sistema, ya sea un entorno, reproductor, botella, etc. Probablemente pueda resolver algunos tipos particulares de interacción que puede utilizar para eliminar la duplicación, pero yo comenzaría simple e ir de allí.
Ver también Despacho doble .
Todas las acciones que ha descrito consisten en lo siguiente:
- Un verbo (por ejemplo, "lanzar")
- un objeto (por ejemplo, "botella")
- un parámetro adicional opcional que describe más la acción (por ejemplo, "en la ventana")
¿Qué hay de modelar cada objeto accionable como una clase derivada de un ancestro común y hacer que esa clase maneje la acción en sí misma? Algo como
public interface IObjectBase
{
bool HandleAction(string verb,string [] params)
}
public class Bottle: IObjectBase
{
bool HandleAction(string verb,string [] params)
{
//analyze verb and params to look for appropriate actions
//handle action and return true if a match has been found
}
}
La interacción se puede definir como "Verbo + {Lista de filtros} + {Lista de respuestas}"
Para su ejemplo de "llenar la botella con agua", la Interacción sería:
- Verbo: Fill ({"fill", "pour"})
- Lista de filtros:
Have(player, "bottle")
,Have(currentRoom, "water tap")
- Lista de respuestas:
Print("You filled the bottle with water")
,Remove(player, "bottle")
,Add(player, "bottle of water")
- alternativamente, la Lista de respuestas puede ser:
SetAttribute(player.findInventory("bottle"), "fill", "water")
Entonces, si necesita "arrojar una botella de agua a las ventanas":
- Verbo: lanzar ({"lanzar", "aplastar"})
- Lista de filtros:
Have(player, "bottle of water")
,Have(currentRoom, "windows")
- Lista de respuestas:
Print("The bottle smashed with the windows, and both of them are broken")
,Remove(player, "bottle of water")
,Add(curentRoom, "broken bottle")
,Remove(currentRoom, "window")
,Add(currentRoom, "broken window")
,SetAttribute(currentRoom, "description", "There is water on the floor")
Al ingresar a una Sala, el Marco buscará en todos los objetos de la sala una lista de Verbos válidos y los enumerará. Cuando el jugador ingresa un comando, el marco busca un Verbo que coincida con el comando; luego verificará la lista de filtros, y si todos son verdaderos, repita la lista de respuestas para ejecutarlos en orden.
Las respuestas serían un objeto de función que implementa la interfaz IResponse que tiene algunos constructores, y un método IResponse.do (). Los filtros serán objeto de función que implementa la interfaz IFilter, de nuevo con algunos constructores, y el método IFilter.check () devuelve un booleano. Incluso puede tener el filtro E (), O () y No () para realizar consultas más complejas.
Puede hacer que las cosas sean aún más legibles teniendo algunos métodos de conveniencia, un jugador puede tener el método de conveniencia Player.have (accionable) para que pueda escribir player.have ("botella de agua"), que devuelve no el objeto de botella en sí, sino un Objeto IFilter que verificará si el jugador tiene "botella de agua" cuando se invoca su método .check (). Básicamente, haz los objetos perezosos.
Parece que tu problema es gestionar la propagación de eventos. Microsoft maneja este problema (para propósitos menos coloridos) usando el patrón / eventos Observer.
Creo que la combinación de los patrones de diseño Observer y Mediator del libro de Gamma, etc. "Patrones de diseño" sería muy útil para usted. El libro tiene una clase de ChangeManager de ejemplo que podría ser útil, pero he adjuntado algunos otros enlaces que deberían servirle bien.
Una sugerencia de implementación que tengo sería usar una clase estática o singleton que actúa como mediador y también almacena referencias a todos los objetos procesables en la memoria activa, así como a todas las acciones invocadas. Esta clase puede procesar los algoritmos para determinar todas las respuestas y el orden cronológico de las respuestas de una acción determinada. (Si considera que los efectos colaterales de una acción principal, A, pueden afectar las consecuencias de esa acción, A, antes de completar la acción, sería evidente que la secuencia cronológica correcta es necesaria, y debe actualizarse antes de invocar cada acción colateral)
Artículo de Microsoft sobre el patrón del observador: http://msdn.microsoft.com/en-us/library/ee817669.aspx
Patrón DoFactory en Mediator (con diagramas UML): http://www.dofactory.com/Patterns/PatternMediator.aspx
Patrón DoFactory on Observer (con diagramas UML): http://www.dofactory.com/Patterns/PatternObserver.aspx
Documentación de la interfaz IObserver en .Net 4, http://msdn.microsoft.com/en-us/library/dd783449.aspx
otro artículo sobre el patrón del observador. http://www.devx.com/cplus/Article/28013/1954
Bien chicos, así es como lo manejé. Era la forma más genérica en que podía pensar y creo que se adapta a lo que intento lograr.
Agregué un método "Invoke ()" a la clase Interaction, una nueva interfaz llamada IActionResult que definía un método "Initiate ()" y varios tipos diferentes de ActionResult para cada posible consecuencia. También agregué una Lista de resultados de acción a una interacción. El método Invoke simplemente recorrerá todos los objetos IActionResult y llamará al método Initiate ().
Cuando defina una interacción en un elemento, pasará una lista de verbos para esa interacción y luego agregará un número de objetos ActionResult según las consecuencias de esa interacción.
También agregué una GlobalActionReference, que se actualizaría cada vez que se realiza una acción, y un ActionResult tendría acceso apropiado a los objetos que necesita para actualizar a través de esto.
Realmente aprecio todas sus sugerencias, y lo siento si no estaba clara con mi pregunta o mis comentarios (o incluso esta respuesta). Gracias por tu ayuda.