Escrevendo um mecanismo de aventura de texto multijogador no Node.js (parte 1)

Publicados: 2022-03-10
Resumo rápido ↬ Já ouviu falar em aventura de texto? Nesta série de artigos, Fernando Doglio explica o processo de como criar um motor completo capaz de permitir que você jogue qualquer aventura de texto que você e seus amigos gostem. Isso mesmo, vamos apimentar um pouco adicionando multiplayer ao gênero de aventura de texto!

As aventuras de texto foram uma das primeiras formas de RPG digital, quando os jogos não tinham gráficos e tudo o que você tinha era sua própria imaginação e a descrição que você lia na tela preta do seu monitor CRT.

Se quisermos ficar nostálgicos, talvez o nome Colossal Cave Adventure (ou apenas Adventure, como foi originalmente chamado) soa um sino. Esse foi o primeiro jogo de aventura de texto já feito.

Uma imagem de uma aventura de texto real de antigamente
Uma imagem de uma aventura de texto real de volta ao dia. (Visualização grande)

A imagem acima é como você realmente veria o jogo, muito longe dos nossos principais jogos de aventura AAA atuais. Dito isto, eles eram divertidos de jogar e roubavam centenas de horas do seu tempo, enquanto você sentava na frente daquele texto, sozinho, tentando descobrir como vencê-lo.

Compreensivelmente, as aventuras de texto foram substituídas ao longo dos anos por jogos que apresentam melhores visuais (embora, pode-se argumentar que muitos deles sacrificaram a história por gráficos) e, especialmente nos últimos anos, a crescente capacidade de colaboração com outros amigos e jogar juntos. Esse recurso em particular é aquele que faltava nas aventuras de texto original e que eu quero trazer de volta neste artigo.

Outras partes desta série

  • Parte 2: Design do servidor do Game Engine
  • Parte 3: Criando o Terminal Client
  • Parte 4: Adicionando o bate-papo ao nosso jogo

Mais depois do salto! Continue lendo abaixo ↓

Nosso objetivo

O objetivo desse esforço, como você provavelmente já deve ter adivinhado pelo título deste artigo, é criar um mecanismo de aventura de texto que permita compartilhar a aventura com amigos, permitindo que você colabore com eles da mesma forma que faria durante um jogo de Dungeons & Dragons (no qual, assim como nas boas e velhas aventuras de texto, não há gráficos para ver).

Na criação do mecanismo, do servidor de bate-papo e do cliente é bastante trabalhoso. Neste artigo, mostrarei a fase de design, explicando coisas como a arquitetura por trás do mecanismo, como o cliente irá interagir com os servidores e quais serão as regras deste jogo.

Apenas para dar uma ajuda visual de como isso vai ficar, aqui está o meu objetivo:

Wireframe geral para a interface do usuário final do cliente do jogo
Wireframe geral para a interface do usuário final do cliente do jogo (visualização grande)

Esse é o nosso objetivo. Quando chegarmos lá, você terá capturas de tela em vez de maquetes rápidas e sujas. Então, vamos começar com o processo. A primeira coisa que abordaremos é o design da coisa toda. Em seguida, abordaremos as ferramentas mais relevantes que usarei para codificar isso. Por fim, mostrarei alguns dos trechos de código mais relevantes (com um link para o repositório completo, é claro).

Espero que, no final, você se encontre criando novas aventuras de texto para experimentá-las com os amigos!

Fase de desenho

Para a fase de design, vou cobrir nosso plano geral. Vou tentar o meu melhor para não entediar você até a morte, mas, ao mesmo tempo, acho importante mostrar algumas das coisas dos bastidores que precisam acontecer antes de estabelecer sua primeira linha de código.

Os quatro componentes que quero abordar aqui com uma quantidade razoável de detalhes são:

  • O motor
    Este será o servidor principal do jogo. As regras do jogo serão implementadas aqui e fornecerão uma interface tecnologicamente agnóstica para qualquer tipo de cliente consumir. Implementaremos um cliente de terminal, mas você pode fazer o mesmo com um cliente de navegador da Web ou qualquer outro tipo que desejar.
  • O servidor de bate-papo
    Por ser complexo o suficiente para ter seu próprio artigo, esse serviço também terá seu próprio módulo. O servidor de bate-papo cuidará de permitir que os jogadores se comuniquem durante o jogo.
  • O cliente
    Como dito anteriormente, este será um cliente de terminal, que, idealmente, será semelhante ao modelo anterior. Ele fará uso dos serviços fornecidos pelo mecanismo e pelo servidor de bate-papo.
  • Jogos (arquivos JSON)
    Finalmente, vou falar sobre a definição dos jogos reais. O objetivo disso é criar um mecanismo que possa executar qualquer jogo, desde que o arquivo do jogo esteja em conformidade com os requisitos do mecanismo. Então, mesmo que isso não exija codificação, vou explicar como vou estruturar os arquivos de aventura para escrever nossas próprias aventuras no futuro.

O motor

O mecanismo de jogo, ou servidor de jogo, será uma API REST e fornecerá todas as funcionalidades necessárias.

Optei por uma API REST simplesmente porque — para esse tipo de jogo — o atraso adicionado pelo HTTP e sua natureza assíncrona não causarão nenhum problema. Teremos, no entanto, que seguir um caminho diferente para o servidor de bate-papo. Mas antes de começarmos a definir endpoints para nossa API, precisamos definir do que o mecanismo será capaz. Então vamos fazer isso.

Funcionalidade Descrição
Junte-se a um jogo Um jogador poderá entrar em um jogo especificando o ID do jogo.
Criar um novo jogo Um jogador também pode criar uma nova instância do jogo. O mecanismo deve retornar um ID, para que outros possam usá-lo para ingressar.
Cena de retorno Este recurso deve retornar a cena atual onde a festa está localizada. Basicamente, ele retornará a descrição, com todas as informações associadas (possíveis ações, objetos nela, etc.).
Interagir com a cena Este será um dos mais complexos, porque receberá um comando do cliente e executará essa ação - coisas como mover, empurrar, pegar, olhar, ler, para citar apenas alguns.
Verificar inventário Embora esta seja uma forma de interagir com o jogo, ela não se relaciona diretamente com a cena. Portanto, verificar o inventário de cada jogador será considerado uma ação diferente.

Uma palavra sobre o movimento

Precisamos de uma maneira de medir as distâncias no jogo porque mover-se pela aventura é uma das principais ações que um jogador pode realizar. Usaremos esse número como medida de tempo, apenas para simplificar a jogabilidade. Medir o tempo com um relógio real pode não ser o melhor, considerando que esses tipos de jogos têm ações baseadas em turnos, como combate. Em vez disso, usaremos a distância para medir o tempo (o que significa que uma distância de 8 exigirá mais tempo para percorrer do que uma de 2, permitindo-nos fazer coisas como adicionar efeitos a jogadores que duram uma quantidade definida de "pontos de distância" ).

Outro aspecto importante a considerar sobre o movimento é que não estamos jogando sozinhos. Para simplificar, o mecanismo não permitirá que os jogadores dividam o grupo (embora isso possa ser uma melhoria interessante para o futuro). A versão inicial deste módulo só permitirá que todos se movam para onde a maioria do partido decidir. Portanto, a movimentação terá que ser feita por consenso, o que significa que cada ação de movimentação aguardará que a maioria da parte a solicite antes de ocorrer.

Combate

O combate é outro aspecto muito importante desses tipos de jogos, e que teremos que considerar adicionar ao nosso motor; caso contrário, acabaremos perdendo um pouco da diversão.

Isso não é algo que precisa ser reinventado, para ser honesto. O combate em grupo por turnos existe há décadas, então vamos implementar apenas uma versão dessa mecânica. Vamos misturar isso com o conceito de “iniciativa” de Dungeons & Dragons, rolando um número aleatório para manter o combate um pouco mais dinâmico.

Em outras palavras, a ordem em que todos os envolvidos em uma luta escolhem sua ação será aleatória, e isso inclui os inimigos.

Finalmente (embora eu fale sobre isso com mais detalhes abaixo), você terá itens que você pode pegar com um número de “dano” definido. Estes são os itens que você poderá usar durante o combate; qualquer coisa que não tenha essa propriedade causará 0 de dano aos seus inimigos. Provavelmente adicionaremos uma mensagem quando você tentar usar esses objetos para lutar, para que você saiba que o que está tentando fazer não faz sentido.

Interação cliente-servidor

Vamos ver agora como um determinado cliente interagiria com nosso servidor usando a funcionalidade definida anteriormente (sem pensar em endpoints ainda, mas chegaremos lá em um segundo):

(Visualização grande)

A interação inicial entre o cliente e o servidor (do ponto de vista do servidor) é o início de um novo jogo, e os passos para isso são os seguintes:

  1. Crie um novo jogo .
    O cliente solicita a criação de um novo jogo do servidor.
  2. Crie uma sala de bate-papo .
    Embora o nome não o especifique, o servidor não está apenas criando uma sala de bate-papo no servidor de bate-papo, mas também configurando tudo o que precisa para permitir que um conjunto de jogadores jogue uma aventura.
  3. Devolva os metadados do jogo .
    Depois que o jogo for criado pelo servidor e a sala de bate-papo estiver pronta para os jogadores, o cliente precisará dessas informações para solicitações subsequentes. Isso será principalmente um conjunto de IDs que os clientes podem usar para identificar a si mesmos e ao jogo atual em que desejam ingressar (mais sobre isso em um segundo).
  4. Compartilhe manualmente o ID do jogo .
    Esta etapa terá que ser feita pelos próprios jogadores. Poderíamos criar algum tipo de mecanismo de compartilhamento, mas deixarei isso na lista de desejos para melhorias futuras.
  5. Junte-se ao jogo .
    Este é bem direto. Uma vez que todos tenham o ID do jogo, eles entrarão na aventura usando seus aplicativos clientes.
  6. Junte-se a sua sala de bate-papo .
    Por fim, os aplicativos clientes dos jogadores usarão os metadados do jogo para entrar na sala de bate-papo de sua aventura. Esta é a última etapa necessária pré-jogo. Feito isso, os jogadores estão prontos para começar a se aventurar!
Ordem de ação para um jogo existente
Ordem de ação para um jogo existente (visualização grande)

Uma vez que todos os pré-requisitos tenham sido atendidos, os jogadores podem começar a jogar a aventura, compartilhando seus pensamentos através do bate-papo do grupo e avançando na história. O diagrama acima mostra as quatro etapas necessárias para isso.

As etapas a seguir serão executadas como parte do loop do jogo, o que significa que elas serão repetidas constantemente até que o jogo termine.

  1. Cena do pedido .
    O aplicativo cliente solicitará os metadados da cena atual. Este é o primeiro passo em cada iteração do loop.
  2. Retorne os metadados .
    O servidor, por sua vez, enviará de volta os metadados da cena atual. Essas informações incluirão coisas como uma descrição geral, os objetos encontrados dentro dela e como eles se relacionam entre si.
  3. Enviar comando .
    Isto é onde a diversão começa. Esta é a entrada principal do player. Ele conterá a ação que eles desejam realizar e, opcionalmente, o alvo dessa ação (por exemplo, soprar vela, pegar pedra e assim por diante).
  4. Retorne a reação ao comando enviado .
    Isso poderia ser simplesmente o passo dois, mas para maior clareza, adicionei-o como um passo extra. A principal diferença é que o passo dois pode ser considerado o início deste loop, enquanto este leva em conta que você já está jogando e, portanto, o servidor precisa entender quem esta ação vai afetar (seja um único jogador ou todos os jogadores).

Como uma etapa extra, embora não faça realmente parte do fluxo, o servidor notificará os clientes sobre as atualizações de status relevantes para eles.

A razão para esta etapa extra recorrente é por causa das atualizações que um jogador pode receber das ações de outros jogadores. Lembre-se do requisito para se mudar de um lugar para outro; como eu disse antes, uma vez que a maioria dos jogadores tenha escolhido uma direção, todos os jogadores se moverão (nenhuma entrada de todos os jogadores é necessária).

O interessante aqui é que o HTTP (já mencionamos que o servidor será uma API REST) ​​não permite esse tipo de comportamento. Então, nossas opções são:

  1. realizar polling a cada X segundos do cliente,
  2. use algum tipo de sistema de notificação que funcione em paralelo com a conexão cliente-servidor.

Na minha experiência, costumo preferir a opção 2. Na verdade, eu usaria (e usarei para este artigo) o Redis para esse tipo de comportamento.

O diagrama a seguir demonstra as dependências entre serviços.

Interações entre um aplicativo cliente e o mecanismo de jogo
Interações entre um aplicativo cliente e o mecanismo de jogo (visualização grande)

O servidor de bate-papo

Vou deixar os detalhes do design deste módulo para a fase de desenvolvimento (que não faz parte deste artigo). Dito isto, há coisas que podemos decidir.

Uma coisa que podemos definir é o conjunto de restrições para o servidor, o que simplificará nosso trabalho no futuro. E se jogarmos bem, podemos acabar com um serviço que fornece uma interface robusta, permitindo, eventualmente, estender ou até alterar a implementação para fornecer menos restrições sem afetar o jogo.

  • Haverá apenas um quarto por festa.
    Não permitiremos que subgrupos sejam criados. Isso anda de mãos dadas com não deixar o partido se dividir. Talvez uma vez que implementemos esse aprimoramento, permitir a criação de subgrupos e salas de bate-papo personalizadas seja uma boa ideia.
  • Não haverá mensagens privadas.
    Isso é puramente para fins de simplificação, mas ter um bate-papo em grupo já é bom o suficiente; não precisamos de mensagens privadas agora. Lembre-se de que sempre que estiver trabalhando em seu produto mínimo viável, tente evitar cair na toca do coelho de recursos desnecessários; é um caminho perigoso e difícil de sair.
  • Não vamos persistir mensagens.
    Em outras palavras, se você sair da festa, perderá as mensagens. Isso simplificará enormemente nossa tarefa, pois não teremos que lidar com nenhum tipo de armazenamento de dados, nem teremos que perder tempo decidindo qual a melhor estrutura de dados para armazenar e recuperar mensagens antigas. Tudo ficará na memória e permanecerá lá enquanto a sala de bate-papo estiver ativa. Assim que estiver fechado, simplesmente nos despediremos deles!
  • A comunicação será feita por sockets .
    Infelizmente, nosso cliente terá que lidar com um canal de comunicação duplo: um RESTful para a engine do jogo e um socket para o servidor de chat. Isso pode aumentar um pouco a complexidade do cliente, mas, ao mesmo tempo, usará os melhores métodos de comunicação para cada módulo. (Não faz sentido forçar REST em nosso servidor de bate-papo ou soquetes em nosso servidor de jogo. Essa abordagem aumentaria a complexidade do código do lado do servidor, que também lida com a lógica de negócios, então vamos nos concentrar nesse lado por enquanto.)

Isso é tudo para o servidor de bate-papo. Afinal, não será complexo, pelo menos não inicialmente. Há mais a fazer quando é hora de começar a codificá-lo, mas para este artigo, são informações mais que suficientes.

O cliente

Este é o módulo final que requer codificação, e será o mais estúpido do lote. Como regra geral, prefiro ter meus clientes burros e meus servidores inteligentes. Dessa forma, criar novos clientes para o servidor fica muito mais fácil.

Só para estarmos na mesma página, aqui está a arquitetura de alto nível com a qual devemos terminar.

Arquitetura final de alto nível de todo o desenvolvimento
Arquitetura final de alto nível de todo o desenvolvimento (visualização grande)

Nosso cliente CLI simples não implementará nada muito complexo. Na verdade, a parte mais complicada que teremos que enfrentar é a UI real, porque é uma interface baseada em texto.

Dito isto, a funcionalidade que a aplicação cliente terá que implementar é a seguinte:

  1. Crie um novo jogo .
    Como quero manter as coisas o mais simples possível, isso só será feito através da interface CLI. A interface do usuário real só será usada após ingressar em um jogo, o que nos leva ao próximo ponto.
  2. Junte-se a um jogo existente .
    Dado o código do jogo retornado do ponto anterior, os jogadores podem usá-lo para participar. Novamente, isso é algo que você deve poder fazer sem uma interface do usuário, portanto, essa funcionalidade fará parte do processo necessário para começar a usar a interface do usuário de texto.
  3. Analisar arquivos de definição de jogo .
    Discutiremos isso daqui a pouco, mas o cliente deve ser capaz de entender esses arquivos para saber o que mostrar e saber como usar esses dados.
  4. Interaja com a aventura.
    Basicamente, isso dá ao jogador a capacidade de interagir com o ambiente descrito a qualquer momento.
  5. Mantenha um inventário para cada jogador .
    Cada instância do cliente conterá uma lista de itens na memória. Esta lista será copiada.
  6. Bate-papo de suporte .
    O aplicativo cliente também precisa se conectar ao servidor de bate-papo e registrar o usuário na sala de bate-papo da parte.

Mais sobre a estrutura interna e design do cliente posteriormente. Enquanto isso, vamos terminar o estágio de design com a última preparação: os arquivos do jogo.

O jogo: arquivos JSON

É aí que fica interessante porque até agora eu cobri as definições básicas de microsserviços. Alguns deles podem falar REST e outros podem trabalhar com sockets, mas em essência, eles são todos iguais: você os define, você os codifica e eles fornecem um serviço.

Para este componente em particular, não estou planejando codificar nada, mas precisamos projetá-lo. Basicamente, estamos implementando uma espécie de protocolo para definir nosso jogo, as cenas dentro dele e tudo dentro deles.

Se você pensar bem, uma aventura de texto é, em sua essência, basicamente um conjunto de salas conectadas umas às outras, e dentro delas há “coisas” com as quais você pode interagir, todas ligadas a uma história, esperançosamente, decente. Agora, nosso motor não cuidará dessa última parte; essa parte será com você. Mas de resto, há esperança.

Agora, voltando ao conjunto de salas interconectadas, isso para mim soa como um gráfico, e se também adicionarmos o conceito de distância ou velocidade de movimento que mencionei anteriormente, temos um gráfico ponderado. E isso é apenas um conjunto de nós que tem um peso (ou apenas um número – não se preocupe com o nome) que representa esse caminho entre eles. Aqui está um visual (eu adoro aprender vendo, então olhe para a imagem, ok?):

Um exemplo de gráfico ponderado
Um exemplo de gráfico ponderado (visualização grande)

Isso é um gráfico ponderado - é isso. E tenho certeza de que você já descobriu, mas para completar, deixe-me mostrar como você faria quando nosso motor estivesse pronto.

Depois de começar a configurar a aventura, você criará seu mapa (como você vê à esquerda da imagem abaixo). E então você traduzirá isso em um gráfico ponderado, como você pode ver à direita da imagem. Nosso motor será capaz de pegá-lo e deixá-lo percorrer na ordem certa.

Exemplo de gráfico para uma determinada masmorra
Exemplo de gráfico para uma determinada masmorra (visualização grande)

Com o gráfico ponderado acima, podemos garantir que os jogadores não possam ir da entrada até a ala esquerda. Eles teriam que passar pelos nós entre esses dois, e isso consumiria tempo, que podemos medir usando o peso das conexões.

Agora, para a parte “divertida”. Vamos ver como ficaria o gráfico no formato JSON. Tenha paciência comigo aqui; este JSON conterá muitas informações, mas passarei o máximo que puder:

 { "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 } } ] } } }

Eu sei que parece muito, mas se você resumir a uma simples descrição do jogo, você tem uma masmorra composta por seis salas, cada uma interconectada com as outras, conforme mostrado no diagrama acima.

Sua tarefa é percorrê-lo e explorá-lo. Você descobrirá que existem dois lugares diferentes onde você pode encontrar uma arma (na cozinha ou no quarto escuro, quebrando a cadeira). Você também será confrontado com uma porta trancada; então, uma vez que você encontrar a chave (localizada dentro da sala de escritório), você poderá abri-la e lutar contra o chefe com qualquer arma que você coletou.

Você ganhará matando-o ou perderá sendo morto por ele.

Vamos agora ter uma visão geral mais detalhada de toda a estrutura JSON e suas três seções.

Gráfico

Este conterá o relacionamento entre os nós. Basicamente, esta seção se traduz diretamente no gráfico que vimos antes.

A estrutura para esta seção é bastante simples. É uma lista de nós, onde cada nó compreende os seguintes atributos:

  • um ID que identifica exclusivamente o nó entre todos os outros no jogo;
  • um nome, que é basicamente uma versão legível do ID;
  • um conjunto de links para os outros nós. Isso é evidenciado pela existência de quatro chaves possíveis: norte”, sul, leste e oeste. Poderíamos eventualmente adicionar outras direções adicionando combinações dessas quatro. Cada link contém o ID do nó relacionado e a distância (ou peso) dessa relação.

Jogo

Esta seção conterá as configurações e condições gerais. Em particular, no exemplo acima, esta seção contém as condições de vitória e derrota. Em outras palavras, com essas duas condições, informaremos ao mecanismo quando o jogo pode terminar.

Para manter as coisas simples, adicionei apenas duas condições:

  • você ganha matando o chefe,
  • ou perder sendo morto.

quartos

Aqui é de onde vem a maioria das 163 linhas, e é a mais complexa das seções. É aqui que descreveremos todas as salas da nossa aventura e tudo dentro delas.

Haverá uma chave para cada quarto, usando o ID que definimos anteriormente. E cada sala terá uma descrição, uma lista de itens, uma lista de saídas (ou portas) e uma lista de personagens não jogáveis ​​(NPCs). Dessas propriedades, a única que deve ser obrigatória é a descrição, porque essa é necessária para que o mecanismo informe o que está vendo. O resto deles só estará lá se houver algo para mostrar.

Vamos ver o que essas propriedades podem fazer pelo nosso jogo.

A descrição

Este item não é tão simples quanto se possa pensar, pois sua visão de uma sala pode mudar dependendo de diferentes circunstâncias. Se, por exemplo, você olhar para a descrição da primeira sala, você notará que, por padrão, você não pode ver nada, a menos, é claro, que você tenha uma tocha acesa com você.

Portanto, pegar itens e usá-los pode desencadear condições globais que afetarão outras partes do jogo.

Os itens

Estes representam todas as coisas” que você pode encontrar dentro de uma sala. Cada item compartilha o mesmo ID e nome que os nós na seção do gráfico tinham.

Eles também terão uma propriedade “destino”, que indica onde aquele item deve ser armazenado, uma vez retirado. Isso é relevante porque você poderá ter apenas um item em suas mãos, enquanto poderá ter quantos quiser em seu inventário.

Por fim, alguns desses itens podem desencadear outras ações ou atualizações de status, dependendo do que o jogador decidir fazer com eles. Um exemplo disso são as tochas acesas da entrada. Se você pegar um deles, acionará uma atualização de status no jogo, que por sua vez fará com que o jogo mostre uma descrição diferente da próxima sala.

Os itens também podem ter “subitens”, que entram em jogo assim que o item original é destruído (através da ação “quebrar”, por exemplo). Um item pode ser dividido em vários, e isso é definido no elemento “subitens”.

Essencialmente, esse elemento é apenas uma matriz de novos itens, que também contém o conjunto de ações que podem desencadear sua criação. Isso basicamente abre a possibilidade de criar diferentes subitens com base nas ações que você executa no item original.

Finalmente, alguns itens terão uma propriedade de “dano”. Então, se você usar um item para acertar um NPC, esse valor será usado para subtrair a vida deles.

As saídas

Este é simplesmente um conjunto de propriedades indicando a direção da saída e as propriedades da mesma (uma descrição, caso você queira inspecioná-la, seu nome e, em alguns casos, seu status).

As saídas são uma entidade separada dos itens porque o mecanismo precisará entender se você pode realmente percorrê-las com base em seu status. As saídas bloqueadas não permitirão que você passe por elas, a menos que você descubra como alterar seu status para desbloqueado.

Os NPCs

Por fim, os NPCs farão parte de outra lista. São basicamente itens com estatísticas que o motor usará para entender como cada um deve se comportar. Os que definimos em nosso exemplo são “hp”, que significa pontos de saúde, e “dano”, que, assim como as armas, é o número que cada acerto subtrairá da saúde do jogador.

Isso é tudo para a masmorra que eu criei. É muito, sim, e no futuro posso considerar a criação de uma espécie de editor de níveis, para simplificar a criação dos arquivos JSON. Mas, por enquanto, isso não será necessário.

Caso você ainda não tenha percebido, o principal benefício de ter nosso jogo definido em um arquivo como este é que poderemos trocar arquivos JSON como você fazia cartuchos na era Super Nintendo. Basta carregar um novo arquivo e começar uma nova aventura. Fácil!

Considerações finais

Obrigado por ler até agora. Espero que tenham gostado do processo de design pelo qual passei para dar vida a uma ideia. Lembre-se, porém, que estou inventando isso à medida que vou, para que possamos perceber mais tarde que algo que definimos hoje não vai funcionar, e nesse caso teremos que voltar atrás e corrigi-lo.

Tenho certeza de que há muitas maneiras de melhorar as ideias apresentadas aqui e fazer um motor incrível. Mas isso exigiria muito mais palavras do que eu posso colocar em um artigo sem torná-lo chato para todos, então vamos deixar assim por enquanto.

Outras partes desta série

  • Parte 2: Design do servidor do Game Engine
  • Parte 3: Criando o Terminal Client
  • Parte 4: Adicionando o bate-papo ao nosso jogo