Escribir un motor de aventuras de texto multijugador en Node.js (Parte 1)
Publicado: 2022-03-10Las aventuras de texto fueron una de las primeras formas de juegos de rol digitales que existieron, cuando los juegos no tenían gráficos y todo lo que tenías era tu propia imaginación y la descripción que leías en la pantalla negra de tu monitor CRT.
Si queremos ponernos nostálgicos, tal vez el nombre Colossal Cave Adventure (o simplemente Adventure, como se llamó originalmente) te suene. Ese fue el primer juego de aventuras de texto jamás creado.
La imagen de arriba es cómo verías realmente el juego, muy lejos de nuestros principales juegos de aventuras AAA actuales. Dicho esto, era divertido jugarlos y te robarían cientos de horas de tu tiempo, mientras te sentabas frente a ese texto, solo, tratando de descubrir cómo superarlo.
Comprensiblemente, las aventuras de texto han sido reemplazadas a lo largo de los años por juegos que presentan mejores gráficos (aunque se podría argumentar que muchos de ellos han sacrificado la historia por los gráficos) y, especialmente en los últimos años, la creciente capacidad de colaborar con otros amigos y jugar juntos. Esta característica particular es una de la que carecían las aventuras de texto originales, y una que quiero traer de vuelta en este artículo.
Otras partes de esta serie
- Parte 2: Diseño del servidor de Game Engine
- Parte 3: Creando el Cliente Terminal
- Parte 4: Agregar chat a nuestro juego
Nuestra meta
El objetivo de este esfuerzo, como probablemente ya hayas adivinado por el título de este artículo, es crear un motor de aventuras de texto que te permita compartir la aventura con amigos, permitiéndote colaborar con ellos de manera similar a como lo harías durante un juego de Dungeons & Dragons (en el que, al igual que con las buenas aventuras de texto, no hay gráficos para mirar).
En la creación del motor, el servidor de chat y el cliente es bastante trabajo. En este artículo, les mostraré la fase de diseño, explicando cosas como la arquitectura detrás del motor, cómo interactuará el cliente con los servidores y cuáles serán las reglas de este juego.
Solo para brindarle una ayuda visual de cómo se verá esto, este es mi objetivo:
Ese es nuestro objetivo. Una vez que lleguemos allí, tendrás capturas de pantalla en lugar de maquetas rápidas y sucias. Entonces, empecemos con el proceso. Lo primero que cubriremos es el diseño de todo el asunto. Luego, cubriremos las herramientas más relevantes que usaré para codificar esto. Finalmente, le mostraré algunos de los fragmentos de código más relevantes (con un enlace al repositorio completo, por supuesto).
¡Con suerte, al final, te encontrarás creando nuevas aventuras de texto para probarlas con amigos!
Fase de diseño
Para la fase de diseño, voy a cubrir nuestro plano general. Haré todo lo posible para no aburrirte hasta la muerte, pero al mismo tiempo, creo que es importante mostrar algunas de las cosas detrás de escena que deben suceder antes de establecer tu primera línea de código.
Los cuatro componentes que quiero cubrir aquí con una cantidad decente de detalles son:
- El motor
Este va a ser el servidor principal del juego. Las reglas del juego se implementarán aquí y proporcionará una interfaz tecnológicamente independiente para que la consuma cualquier tipo de cliente. Implementaremos un cliente de terminal, pero puede hacer lo mismo con un cliente de navegador web o cualquier otro tipo que desee. - el servidor de chat
Debido a que es lo suficientemente complejo como para tener su propio artículo, este servicio también tendrá su propio módulo. El servidor de chat se encargará de permitir que los jugadores se comuniquen entre sí durante el juego. - El cliente
Como se indicó anteriormente, este será un cliente de terminal, uno que, idealmente, se verá similar a la maqueta anterior. Hará uso de los servicios proporcionados tanto por el motor como por el servidor de chat. - Juegos (archivos JSON)
Finalmente, repasaré la definición de los juegos reales. El objetivo de esto es crear un motor que pueda ejecutar cualquier juego, siempre que el archivo del juego cumpla con los requisitos del motor. Entonces, aunque esto no requerirá codificación, explicaré cómo estructuraré los archivos de aventuras para escribir nuestras propias aventuras en el futuro.
El motor
El motor del juego, o el servidor del juego, será una API REST y proporcionará toda la funcionalidad requerida.
Opté por una API REST simplemente porque, para este tipo de juego, el retraso agregado por HTTP y su naturaleza asíncrona no causarán ningún problema. Sin embargo, tendremos que tomar una ruta diferente para el servidor de chat. Pero antes de comenzar a definir puntos finales para nuestra API, debemos definir de qué será capaz el motor. Vamos a por ello.
Rasgo | Descripción |
---|---|
Únete a un juego | Un jugador podrá unirse a un juego especificando la ID del juego. |
Crear un nuevo juego | Un jugador también puede crear una nueva instancia de juego. El motor debe devolver una ID, para que otros puedan usarla para unirse. |
escena de regreso | Esta función debería devolver la escena actual donde se encuentra la fiesta. Básicamente, devolverá la descripción, con toda la información asociada (posibles acciones, objetos en ella, etc.). |
Interactuar con la escena | Este va a ser uno de los más complejos, porque tomará un comando del cliente y realizará esa acción, como mover, empujar, tomar, mirar, leer, por nombrar solo algunos. |
Verificar inventario | Aunque esta es una forma de interactuar con el juego, no se relaciona directamente con la escena. Por lo tanto, revisar el inventario de cada jugador se considerará una acción diferente. |
Una palabra sobre el movimiento
Necesitamos una forma de medir distancias en el juego porque moverse a través de la aventura es una de las acciones principales que un jugador puede realizar. Usaremos este número como una medida de tiempo, solo para simplificar el juego. Medir el tiempo con un reloj real podría no ser lo mejor, considerando que este tipo de juegos tienen acciones por turnos, como el combate. En cambio, usaremos la distancia para medir el tiempo (lo que significa que una distancia de 8 requerirá más tiempo para atravesar que una de 2, lo que nos permitirá hacer cosas como agregar efectos a los jugadores que duran una cantidad determinada de "puntos de distancia" ).
Otro aspecto importante a considerar sobre el movimiento es que no estamos jugando solos. En aras de la simplicidad, el motor no permitirá que los jugadores se dividan el grupo (aunque eso podría ser una mejora interesante para el futuro). La versión inicial de este módulo solo permitirá que todos se muevan donde lo decida la mayoría del grupo. Por lo tanto, el movimiento tendrá que hacerse por consenso, lo que significa que cada acción de movimiento esperará a que la mayoría del partido lo solicite antes de llevarse a cabo.
Combate
El combate es otro aspecto muy importante de este tipo de juegos, y que tendremos que considerar agregar a nuestro motor; de lo contrario, nos perderemos parte de la diversión.
Esto no es algo que necesite ser reinventado, para ser honesto. El combate en grupo por turnos existe desde hace décadas, así que solo implementaremos una versión de esa mecánica. Lo combinaremos con el concepto de "iniciativa" de Dungeons & Dragons, lanzando un número aleatorio para mantener el combate un poco más dinámico.
En otras palabras, el orden en que todos los involucrados en una pelea pueden elegir su acción será aleatorio, y eso incluye a los enemigos.
Por último (aunque hablaré de esto con más detalle a continuación), tendrás artículos que puedes recoger con un número de "daño" establecido. Estos son los elementos que podrás usar durante el combate; cualquier cosa que no tenga esa propiedad causará 0 daños a tus enemigos. Probablemente agregaremos un mensaje cuando intentes usar esos objetos para pelear, para que sepas que lo que estás tratando de hacer no tiene sentido.
Interacción cliente-servidor
Veamos ahora cómo un cliente determinado interactuaría con nuestro servidor utilizando la funcionalidad definida anteriormente (sin pensar en los puntos finales todavía, pero llegaremos allí en un segundo):
La interacción inicial entre el cliente y el servidor (desde el punto de vista del servidor) es el comienzo de un nuevo juego, y los pasos para ello son los siguientes:
- Crear un nuevo juego .
El cliente solicita la creación de un nuevo juego desde el servidor. - Crear sala de chat .
Aunque el nombre no lo especifica, el servidor no solo crea una sala de chat en el servidor de chat, sino que también configura todo lo que necesita para permitir que un grupo de jugadores juegue una aventura. - Devuelve los metadatos del juego .
Una vez que el juego ha sido creado por el servidor y la sala de chat está disponible para los jugadores, el cliente necesitará esa información para solicitudes posteriores. En su mayoría, será un conjunto de ID que los clientes pueden usar para identificarse a sí mismos y al juego actual al que quieren unirse (más sobre eso en un segundo). - Comparte manualmente la ID del juego .
Este paso lo tendrán que hacer los propios jugadores. Podríamos idear algún tipo de mecanismo para compartir, pero lo dejaré en la lista de deseos para futuras mejoras. - Únete al juego .
Este es bastante sencillo. Una vez que todos tengan la ID del juego, se unirán a la aventura utilizando sus aplicaciones cliente. - Únase a su sala de chat .
Finalmente, las aplicaciones cliente de los jugadores usarán los metadatos del juego para unirse a la sala de chat de su aventura. Este es el último paso requerido antes del juego. Una vez que todo esto esté hecho, ¡los jugadores estarán listos para comenzar la aventura!
Una vez que se hayan cumplido todos los requisitos previos, los jugadores pueden comenzar a jugar la aventura, compartir sus pensamientos a través del chat de la fiesta y avanzar en la historia. El diagrama anterior muestra los cuatro pasos necesarios para ello.
Los siguientes pasos se ejecutarán como parte del ciclo del juego, lo que significa que se repetirán constantemente hasta que finalice el juego.
- Solicitar escena .
La aplicación cliente solicitará los metadatos de la escena actual. Este es el primer paso en cada iteración del bucle. - Devolver los metadatos .
El servidor, a su vez, devolverá los metadatos de la escena actual. Esta información incluirá cosas como una descripción general, los objetos que se encuentran dentro y cómo se relacionan entre sí. - Enviar comando .
Aquí es donde la diversión comienza. Esta es la entrada principal del reproductor. Contendrá la acción que quieren realizar y, opcionalmente, el objetivo de esa acción (por ejemplo, soplar una vela, agarrar una piedra, etc.). - Devuelve la reacción al comando enviado .
Esto podría ser simplemente el paso dos, pero para mayor claridad, lo agregué como un paso adicional. La principal diferencia es que el paso dos podría considerarse el comienzo de este ciclo, mientras que este tiene en cuenta que ya estás jugando y, por lo tanto, el servidor debe comprender a quién afectará esta acción (ya sea un solo jugador o todos los jugadores).
Como paso adicional, aunque en realidad no forma parte del flujo, el servidor notificará a los clientes sobre las actualizaciones de estado que sean relevantes para ellos.
El motivo de este paso recurrente adicional se debe a las actualizaciones que un jugador puede recibir de las acciones de otros jugadores. Recuérdese el requisito para trasladarse de un lugar a otro; como dije antes, una vez que la mayoría de los jugadores hayan elegido una dirección, todos los jugadores se moverán (no se requiere la entrada de todos los jugadores).
Lo interesante aquí es que HTTP (ya mencionamos que el servidor será una API REST) no permite este tipo de comportamiento. Entonces, nuestras opciones son:
- realizar sondeos cada X cantidad de segundos desde el cliente,
- utilice algún tipo de sistema de notificación que funcione en paralelo con la conexión cliente-servidor.
En mi experiencia, tiendo a preferir la opción 2. De hecho, usaría (y lo haré para este artículo) Redis para este tipo de comportamiento.
El siguiente diagrama demuestra las dependencias entre servicios.
El servidor de chat
Dejaré los detalles del diseño de este módulo para la fase de desarrollo (que no forma parte de este artículo). Dicho esto, hay cosas que podemos decidir.
Una cosa que podemos definir es el conjunto de restricciones para el servidor, lo que simplificará nuestro trabajo en el futuro. Y si jugamos bien nuestras cartas, podríamos terminar con un servicio que proporciona una interfaz sólida, lo que nos permite, eventualmente, extender o incluso cambiar la implementación para proporcionar menos restricciones sin afectar el juego en absoluto.
- Solo habrá una sala por fiesta.
No dejaremos que se creen subgrupos. Esto va de la mano con no dejar que el partido se divida. Tal vez una vez que implementemos esa mejora, sería una buena idea permitir la creación de subgrupos y salas de chat personalizadas. - No habrá mensajes privados.
Esto es puramente con fines de simplificación, pero tener un chat grupal ya es lo suficientemente bueno; No necesitamos mensajes privados en este momento. Recuerde que cada vez que esté trabajando en su producto mínimo viable, trate de evitar caer en la madriguera del conejo de características innecesarias; es un camino peligroso y del que es difícil salir. - No persistiremos los mensajes.
En otras palabras, si sales de la fiesta, perderás los mensajes. Esto simplificará enormemente nuestra tarea, ya que no tendremos que lidiar con ningún tipo de almacenamiento de datos, ni perder tiempo decidiendo cuál es la mejor estructura de datos para almacenar y recuperar mensajes antiguos. Todo vivirá en la memoria y permanecerá allí mientras la sala de chat esté activa. Una vez que esté cerrado, ¡simplemente nos despediremos de ellos! - La comunicación se realizará a través de sockets .
Lamentablemente, nuestro cliente deberá manejar un doble canal de comunicación: uno RESTful para el motor del juego y un socket para el servidor de chat. Esto puede aumentar un poco la complejidad del cliente, pero al mismo tiempo utilizará los mejores métodos de comunicación para cada módulo. (No tiene sentido forzar REST en nuestro servidor de chat o forzar sockets en nuestro servidor de juegos. Ese enfoque aumentaría la complejidad del código del lado del servidor, que es el que también maneja la lógica comercial, así que concentrémonos en ese lado por ahora.)
Eso es todo para el servidor de chat. Después de todo, no será complejo, al menos no inicialmente. Hay más que hacer cuando llega el momento de empezar a codificarlo, pero para este artículo, es información más que suficiente.
El cliente
Este es el módulo final que requiere codificación, y será el más tonto de todos. Como regla general, prefiero que mis clientes sean tontos y mis servidores inteligentes. De esa manera, la creación de nuevos clientes para el servidor se vuelve mucho más fácil.
Solo para que estemos en la misma página, aquí está la arquitectura de alto nivel con la que deberíamos terminar.
Nuestro cliente CLI simple no implementará nada muy complejo. De hecho, la parte más complicada que tendremos que abordar es la interfaz de usuario real, porque es una interfaz basada en texto.
Dicho esto, la funcionalidad que tendrá que implementar la aplicación cliente es la siguiente:
- Crear un nuevo juego .
Como quiero mantener las cosas lo más simples posible, esto solo se hará a través de la interfaz CLI. La interfaz de usuario real solo se usará después de unirse a un juego, lo que nos lleva al siguiente punto. - Únete a un juego existente .
Dado el código del juego devuelto desde el punto anterior, los jugadores pueden usarlo para unirse. Nuevamente, esto es algo que debería poder hacer sin una IU, por lo que esta funcionalidad será parte del proceso requerido para comenzar a usar la IU de texto. - Analizar archivos de definición de juegos .
Discutiremos esto en un momento, pero el cliente debe poder comprender estos archivos para saber qué mostrar y saber cómo usar esos datos. - Interactuar con la aventura.
Básicamente, esto le da al jugador la capacidad de interactuar con el entorno descrito en un momento dado. - Mantenga un inventario para cada jugador .
Cada instancia del cliente contendrá una lista de elementos en memoria. Esta lista va a ser respaldada. - Charla de soporte .
La aplicación del cliente también debe conectarse al servidor de chat e iniciar la sesión del usuario en la sala de chat del grupo.
Más sobre la estructura interna y el diseño del cliente más adelante. Mientras tanto, terminemos la etapa de diseño con el último paso de preparación: los archivos del juego.
El juego: archivos JSON
Aquí es donde se pone interesante porque hasta ahora he cubierto definiciones básicas de microservicios. Algunos de ellos pueden hablar REST y otros pueden trabajar con sockets, pero en esencia, todos son iguales: los defines, los codificas y brindan un servicio.
Para este componente en particular, no planeo codificar nada, pero necesitamos diseñarlo. Básicamente, estamos implementando una especie de protocolo para definir nuestro juego, las escenas dentro de él y todo lo que hay dentro de ellas.
Si lo piensas, una aventura de texto es, en esencia, básicamente un conjunto de habitaciones conectadas entre sí, y dentro de ellas hay "cosas" con las que puedes interactuar, todas unidas con una historia, con suerte, decente. Ahora bien, nuestro motor no se encargará de esa última parte; esa parte depende de ti. Pero para el resto, hay esperanza.
Ahora, volviendo al conjunto de habitaciones interconectadas, eso a mí me suena a grafo, y si además le sumamos el concepto de distancia o velocidad de movimiento que mencioné antes, tenemos un grafo ponderado. Y eso es solo un conjunto de nodos que tienen un peso (o solo un número, no se preocupen por cómo se llama) que representa ese camino entre ellos. Aquí hay una imagen (me encanta aprender viendo, así que solo mire la imagen, ¿de acuerdo?):
Eso es un gráfico ponderado, eso es todo. Y estoy seguro de que ya lo ha descubierto, pero en aras de la exhaustividad, déjeme mostrarle cómo lo haría una vez que nuestro motor esté listo.
Una vez que comience a configurar la aventura, creará su mapa (como se ve a la izquierda de la imagen a continuación). Y luego traducirá eso en un gráfico ponderado, como puede ver a la derecha de la imagen. Nuestro motor podrá recogerlo y permitirle recorrerlo en el orden correcto.
Con el gráfico ponderado de arriba, podemos asegurarnos de que los jugadores no puedan ir desde la entrada hasta el ala izquierda. Tendrían que pasar por los nodos entre esos dos, y hacerlo consumirá tiempo, que podemos medir usando el peso de las conexiones.
Ahora, en la parte "divertida". Veamos cómo se vería el gráfico en formato JSON. Ten paciencia conmigo aquí; este JSON contendrá mucha información, pero revisaré la mayor cantidad posible:
{ "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } }
{ "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } }
{ "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } }
{ "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } }
Sé que parece mucho, pero si lo reduce a una simple descripción del juego, tiene una mazmorra que consta de seis habitaciones, cada una interconectada con otras, como se muestra en el diagrama de arriba.
Su tarea es moverse a través de él y explorarlo. Encontrarás que hay dos lugares diferentes donde puedes encontrar un arma (ya sea en la cocina o en el cuarto oscuro, rompiendo la silla). También se enfrentará a una puerta cerrada; entonces, una vez que encuentre la llave (ubicada dentro de la habitación similar a una oficina), podrá abrirla y luchar contra el jefe con cualquier arma que haya recolectado.
Ganarás matándolo o perderás si te mata.
Ahora entremos en una descripción más detallada de toda la estructura JSON y sus tres secciones.
Grafico
Éste contendrá la relación entre los nodos. Básicamente, esta sección se traduce directamente en el gráfico que vimos antes.
La estructura de esta sección es bastante sencilla. Es una lista de nodos, donde cada nodo comprende los siguientes atributos:
- una ID que identifica de forma única el nodo entre todos los demás en el juego;
- un nombre, que es básicamente una versión legible por humanos de la identificación;
- un conjunto de enlaces a los otros nodos. Esto se evidencia por la existencia de cuatro posibles claves: norte, sur, este y oeste. Eventualmente podríamos agregar más direcciones agregando combinaciones de estos cuatro. Cada enlace contiene el ID del nodo relacionado y la distancia (o peso) de esa relación.
Juego
Esta sección contendrá las configuraciones y condiciones generales. En particular, en el ejemplo anterior, esta sección contiene las condiciones de ganar y perder. En otras palabras, con esas dos condiciones, le haremos saber al motor cuándo puede terminar el juego.
Para simplificar las cosas, he agregado solo dos condiciones:
- o ganas matando al jefe,
- o perder al ser asesinado.
Habitaciones
Aquí es de donde provienen la mayoría de las 163 líneas, y es la más compleja de las secciones. Aquí es donde describiremos todas las habitaciones de nuestra aventura y todo lo que hay dentro de ellas.
Habrá una llave para cada habitación, usando el ID que definimos antes. Y cada habitación tendrá una descripción, una lista de elementos, una lista de salidas (o puertas) y una lista de personajes no jugables (NPC). De esas propiedades, la única que debería ser obligatoria es la descripción, porque esa es necesaria para que el motor le permita saber lo que está viendo. El resto de ellos solo estará allí si hay algo que mostrar.
Veamos qué pueden hacer estas propiedades por nuestro juego.
La descripción
Este artículo no es tan simple como podría pensarse, porque la vista de una habitación puede cambiar dependiendo de las diferentes circunstancias. Si, por ejemplo, miras la descripción de la primera habitación, notarás que, por defecto, no puedes ver nada, a menos, por supuesto, que tengas una antorcha encendida contigo.
Por lo tanto, recoger elementos y usarlos puede desencadenar condiciones globales que afectarán otras partes del juego.
Los artículos
Estos representan todas las cosas que puedes encontrar dentro de una habitación. Cada elemento comparte el mismo ID y nombre que tenían los nodos en la sección del gráfico.
También tendrán una propiedad de "destino", que indica dónde se debe almacenar ese artículo, una vez recogido. Esto es relevante porque solo podrá tener un artículo en sus manos, mientras que podrá tener tantos como desee en su inventario.
Finalmente, algunos de estos elementos pueden desencadenar otras acciones o actualizaciones de estado, según lo que el jugador decida hacer con ellos. Un ejemplo de ello son las antorchas encendidas desde la entrada. Si toma uno de ellos, activará una actualización de estado en el juego, lo que a su vez hará que el juego le muestre una descripción diferente de la siguiente habitación.
Los elementos también pueden tener "subelementos", que entran en juego una vez que se destruye el elemento original (a través de la acción "romper", por ejemplo). Un ítem se puede descomponer en varios, y eso se define en el elemento “subitems”.
Esencialmente, este elemento es solo una matriz de elementos nuevos, uno que también contiene el conjunto de acciones que pueden desencadenar su creación. Básicamente, esto abre la posibilidad de crear diferentes subelementos en función de las acciones que realice en el elemento original.
Finalmente, algunos elementos tendrán una propiedad de "daño". Entonces, si usa un elemento para golpear a un NPC, ese valor se usará para restarle vida.
las salidas
Este es simplemente un conjunto de propiedades que indican la dirección de la salida y las propiedades de la misma (una descripción, en caso de querer inspeccionarla, su nombre y, en algunos casos, su estado).
Las salidas son una entidad separada de los elementos porque el motor deberá comprender si realmente puede atravesarlos en función de su estado. Las salidas que están bloqueadas no le permitirán pasar a menos que averigüe cómo cambiar su estado a desbloqueadas.
los PNJ
Finalmente, los NPC serán parte de otra lista. Básicamente son ítems con estadísticas que el motor usará para entender cómo debe comportarse cada uno. Los que hemos definido en nuestro ejemplo son “hp”, que significa puntos de salud, y “daño”, que, al igual que las armas, es el número que cada golpe restará a la salud del jugador.
Eso es todo por la mazmorra que creé. Es mucho, sí, y en el futuro podría considerar crear una especie de editor de niveles para simplificar la creación de los archivos JSON. Pero por ahora, eso no será necesario.
En caso de que aún no te hayas dado cuenta, el principal beneficio de tener nuestro juego definido en un archivo como este es que podremos cambiar archivos JSON como lo hacías con los cartuchos en la era de Super Nintendo. Simplemente cargue un nuevo archivo y comience una nueva aventura. ¡Fácil!
Pensamientos finales
Gracias por leer hasta ahora. Espero que hayas disfrutado el proceso de diseño por el que paso para dar vida a una idea. Recuerde, sin embargo, que estoy inventando esto a medida que avanzo, por lo que podríamos darnos cuenta más adelante de que algo que definimos hoy no va a funcionar, en cuyo caso tendremos que dar marcha atrás y arreglarlo.
Estoy seguro de que hay un montón de maneras de mejorar las ideas presentadas aquí y hacer un gran motor. Pero eso requeriría muchas más palabras de las que puedo poner en un artículo sin que sea aburrido para todos, así que lo dejaremos así por ahora.
Otras partes de esta serie
- Parte 2: Diseño del servidor de Game Engine
- Parte 3: Creando el Cliente Terminal
- Parte 4: Agregar chat a nuestro juego