Implementando comportamento em um jogo de aventura simples

11

Ultimamente, tenho me divertido programando um jogo de aventura simples baseado em texto e estou preso ao que parece ser uma questão de design muito simples.

Para dar uma breve visão geral: o jogo é dividido em Roomobjetos. Cada Roomum tem uma lista de Entityobjetos que estão naquela sala. Cada Entityum possui um estado de evento, que é um mapa simples de string-> booleano e uma lista de ações, que é um mapa de string-> função.

A entrada do usuário assume o formulário [action] [entity]. Ele Roomusa o nome da entidade para retornar o Entityobjeto apropriado , que usa o nome da ação para encontrar a função correta e a executa.

Para gerar a descrição da sala, cada Roomobjeto exibe sua própria string de descrição e, em seguida, anexa as strings de descrição de cada Entity. A Entitydescrição pode mudar com base no seu estado ("A porta está aberta", "A porta está fechada", "A porta está trancada", etc).

Aqui está o problema: usando esse método, o número de funções de descrição e ação que preciso implementar rapidamente fica fora de controle. Somente minha sala de partida possui cerca de 20 funções entre 5 entidades.

Posso combinar todas as ações em uma única função e alternar entre elas, mas ainda são duas funções por entidade. Também posso criar Entitysubclasses específicas para objetos comuns / genéricos, como portas e chaves, mas isso só me leva até agora.

EDIT 1: Conforme solicitado, exemplos de pseudo-código dessas funções de ação.

string outsideDungeonBushesSearch(currentRoom, thisEntity, player)
    if thisEntity["is_searched"] then
        return "There was nothing more in the bushes."
    else
        thisEntity["is_searched"] := true
        currentRoom.setEntity("dungeonDoorKey")
        return "You found a key in the bushes."
    end if

string dungeonDoorKeyUse(currentRoom, thisEntity, player)
    if getEntity("outsideDungeonDoor")["is_locked"] then
        getEntity("outsideDungeonDoor")["is_locked"] := false
        return "You unlocked the door."
    else
        return "The door is already unlocked."
    end if

As funções de descrição agem da mesma maneira, verificando o estado e retornando a string apropriada.

EDIT 2: Revisou a redação da minha pergunta. Suponha que possa haver um número significativo de objetos no jogo que não compartilhem comportamento comum (respostas baseadas em estado a ações específicas) com outros objetos. Existe uma maneira de definir esses comportamentos exclusivos de maneira mais limpa e sustentável do que escrever uma função personalizada para cada ação específica da entidade?

Eric
fonte
1
Eu acho que você precisa explicar o que essas "funções de ação" fazem e talvez postar algum código, porque não tenho certeza do que você está falando lá.
Jhocking 31/01/12
Adicionado o código.
Eric

Respostas:

5

Em vez de criar uma função separada para cada combinação de substantivos e verbos, você deve configurar uma arquitetura em que exista uma interface comum que todos os objetos do jogo implementem.

Uma abordagem em cima da minha cabeça seria definir um objeto de entidade que todos os objetos específicos do seu jogo estendem. Cada entidade terá uma tabela (qualquer estrutura de dados que seu idioma use para matrizes associativas) que associe ações diferentes a resultados diferentes. As ações na tabela provavelmente serão Strings (por exemplo, "open") enquanto o resultado associado pode até ser uma função privada no objeto se o seu idioma suportar funções de primeira classe.

Da mesma forma, o estado do objeto é armazenado em vários campos do objeto. Assim, por exemplo, você pode ter uma variedade de coisas em um Bush, e a função associada à "pesquisa" atuará nessa matriz, retornando o objeto encontrado ou a string "Não havia mais nada nos arbustos".

Enquanto isso, um dos métodos públicos é algo como Entity.actOn (ação String). Nesse método, compare a ação transmitida com a tabela de ações para esse objeto; se essa ação estiver na tabela, retorne o resultado.

Agora, todas as diferentes funções necessárias para cada objeto estarão contidas no objeto, facilitando a repetição desse objeto em outras salas (por exemplo, instanciar o objeto Door em todas as salas que possuem uma porta)

Por fim, defina todas as salas em XML ou JSON ou o que for, para que você possa ter várias salas exclusivas sem precisar escrever código separado para cada sala. Carregue esse arquivo de dados quando o jogo iniciar e analise os dados para instanciar os objetos que preenchem seu jogo. Algo como:

<rooms>
  <room id="room1">
    <description>Outside the dungeon you see some bushes and a heavy door over the entrance.</description>
    <entities>
      <bush>
        <description>The bushes are thick and leafy.</description>
        <contains>
          <key />
        </contains>
      </bush>
      <door connection="room2" isLocked="true">
        <description>It's an oak door with stout iron clasps.</description>
      </door>
    </entities>
  </room>

  <room id="room2">
    etc.

ADICIONAL: aha, acabei de ler a resposta do FxIII e este pouco perto do fim saltou para mim:

(no things like <item triggerFlamesOnPicking="true"> that you will use just once)

Embora eu discorde que uma armadilha de chamas sendo acionada é algo que só aconteceria uma vez (eu podia ver essa armadilha sendo reutilizada para muitos objetos diferentes), acho que finalmente entendi o que você quis dizer sobre entidades que reagem exclusivamente à entrada do usuário. Eu provavelmente abordaria coisas como fazer uma porta em sua masmorra ter uma armadilha de bola de fogo, construindo todas as minhas entidades com uma arquitetura de componentes (explicada em detalhes em outro lugar).

Dessa forma, cada entidade do Door é construída como um pacote de componentes, e posso misturar e combinar componentes de maneira flexível entre diferentes entidades. Por exemplo, a maioria das portas teria configurações semelhantes a

<entity name="door">
  <description>It's an oak door with stout iron clasps.</description>
  <components>
    <lock isLocked="true" />
    <portal connection="room2" />
  </components>
</entity>

mas a única porta com uma armadilha de bola de fogo seria

<entity name="door">
  <description>There are strange runes etched into the wood.</description>
  <components>
    <lock isLocked="true" />
    <portal connection="room7" />
    <fireballTrap />
  </components>
</entity>

e então o único código único que eu teria que escrever para essa porta é o componente FireballTrap. Ele usaria os mesmos componentes de bloqueio e portal que todas as outras portas, e se mais tarde eu decidisse usar o FireballTrap em uma arca do tesouro ou algo tão simples quanto adicionar o componente FireballTrap a essa arca.

Se você define ou não todos os componentes no código compilado ou em uma linguagem de script separada não é uma grande distinção em minha mente (de qualquer forma, você escreverá o código em algum lugar ), mas o importante é que você pode reduzir significativamente o quantidade de código exclusivo que você precisa escrever. Heck, se você não está preocupado com a flexibilidade para designers / modders de nível (você está escrevendo este jogo sozinho), você pode até mesmo fazer com que todas as entidades sejam herdadas da Entity e adicione componentes no construtor em vez de um arquivo ou script de configuração ou tanto faz:

Door extends Entity {
  public Door() {
    addComponent(new LockComponent());
    addComponent(new PortalComponent());
  }
}

TrappedDoor extends Entity {
  public TrappedDoor() {
    addComponent(new LockComponent());
    addComponent(new PortalComponent());
    addComponent(new FireballTrap());
  }
}
jhocking
fonte
1
Isso funciona para itens comuns e repetíveis. Mas e as entidades que respondem exclusivamente à entrada do usuário? Criar uma subclasse Entityapenas para um único objeto agrupa o código, mas não reduz a quantidade de código que tenho que escrever. Ou isso é uma armadilha inevitável a esse respeito?
Eric
1
Abordei os exemplos que você deu. Eu não consigo ler sua mente; Quais objetos e entradas você deseja ter?
Jhocking
Editou minha postagem para explicar melhor minhas intenções. Se entendi seu exemplo corretamente, parece que cada tag de entidade corresponde a alguma subclasse de Entitye os atributos definem seu estado inicial. Suponho que as tags filho da entidade atuem como parâmetros para qualquer ação à qual a tag esteja associada, certo?
Eric
Sim, essa é a ideia.
Jhocking
Eu deveria ter imaginado que os componentes teriam sido parte da solução. Obrigado pela ajuda.
Eric
1

O problema dimensional que você aborda é bastante normal e quase inevitável. Você deseja encontrar uma maneira de expressar suas entidades que seja coincidente e flexível .

Um "contêiner" (o arbusto na resposta jhocking) é uma maneira coincidente , mas você percebe que não é suficientemente flexível .

Eu não sugiro que você tente encontrar uma interface genérica e use arquivos de configuração para especificar os comportamentos, porque você sempre terá a sensação desagradável de estar entre uma rocha (entidades padrão e chatas, fáceis de descrever) e um local difícil ( entidades fantásticas únicas, mas muito longas para serem implementadas).

Minha sugestão é usar uma linguagem interpretada para codificar comportamentos.

Pense no exemplo do mato: é um contêiner, mas nosso mato precisa ter itens específicos; o objeto contêiner pode ter:

  • um método para o contador de histórias adicionar itens,
  • um método para o mecanismo mostrar o item que ele contém,
  • um método para o jogador escolher um item.

Um desses itens tem uma corda que aciona uma engenhoca que, por sua vez, dispara uma chama que queima o mato ... (você vê, eu posso ler sua mente para saber o que você gosta).

Você pode usar um script para descrever esse arbusto, em vez de um arquivo de configuração, colocando o código extra relevante em um gancho que você executa no seu programa principal sempre que alguém escolhe um item de um contêiner.

Agora você tem muitas opções de arquitetura: pode definir ferramentas comportamentais como classes base usando sua linguagem de código ou a linguagem de script (coisas como contêineres, como portas e assim por diante). O objetivo dessas coisas é permitir que você descreva as entidades que agregam facilmente comportamentos simples e os configuram usando ligações em uma linguagem de script .

Todas as entidades devem estar acessíveis ao script: você pode associar um identificador a cada entidade e colocá-los em um contêiner que é exportado no estendido do script da linguagem de script.

O uso de estratégias de script permite manter sua configuração simples (nada <item triggerFlamesOnPicking="true">disso você usará apenas uma vez), ao mesmo tempo em que expressa beaviours estranhos (os divertidos), adicionando alguma linha de código

Em poucas palavras: scripts como arquivo de configuração que pode executar código.

FxIII
fonte