Codificando estados diferentes em Jogos de Aventura

12

Estou planejando um jogo de aventura e não consigo descobrir qual é a maneira correta de implementar o comportamento de um nível, dependendo do estado da progressão da história.

Meu jogo para um jogador apresenta um mundo imenso, onde o jogador precisa interagir com pessoas de uma cidade em vários pontos do jogo. No entanto, dependendo da progressão da história, coisas diferentes seriam apresentadas ao jogador, por exemplo, o Líder da Guilda mudará os locais da praça da cidade para vários locais da cidade; As portas só se destrancaram em determinados momentos do dia após o término de uma rotina específica; Diferentes eventos de tela cortada / gatilho acontecem somente depois que um determinado marco é atingido.

Pensei ingenuamente em usar uma instrução switch {} inicialmente para decidir o que o NPC deveria dizer ou em que ele poderia ser encontrado e tornar os objetivos da missão interativos somente após verificar a condição de uma variável global game_state. Mas percebi que encontraria rapidamente vários estados de jogo e casos de troca para mudar o comportamento de um objeto. Essa declaração de switch também seria extremamente difícil de depurar, e acho que também pode ser difícil de usar em um editor de níveis.

Então pensei que, em vez de ter um único objeto com vários estados, talvez devesse ter várias instâncias do mesmo objeto, com um único estado. Dessa forma, se eu usar algo como um editor de níveis, eu posso colocar uma instância do NPC em todos os locais diferentes em que ele poderia aparecer, e também uma instância para cada estado de conversa que ele tiver. Mas isso significa que haverá muitos objetos de jogo invisíveis e inativos flutuando pelo nível, o que pode ser um problema de memória ou simplesmente difícil de ver em um editor de níveis, não sei.

Ou simplesmente, crie um nível idêntico, mas separado, para cada estado do jogo. Essa é a maneira mais limpa e sem erros de fazer as coisas, mas parece um trabalho manual maciço, garantindo que cada versão do nível seja realmente idêntica uma à outra.

Todos os meus métodos parecem tão ineficientes, para recapitular minha pergunta, existe uma maneira melhor ou padronizada de implementar o comportamento de um nível, dependendo do estado da progressão da história?

PS: Ainda não tenho um editor de níveis - pensando em usar algo como o JME SDK ou criar o meu.

Cardin
fonte

Respostas:

9

Eu acho que o que você precisa neste caso é o State Design Pattern . Em vez de ter várias instâncias de cada objeto de jogo, crie uma única instância, mas encapsule seu comportamento em uma classe separada. Crie várias classes, uma para cada comportamento possível, e dê a todas as classes a mesma interface. Associe um ao seu objeto de jogo (o estado inicial) e, quando as condições mudam (um marco é atingido, a hora do dia passa etc.), você alterna o estado desse objeto (ou seja, associa-o a um objeto diferente, dependendo da lógica do jogo) e atualize suas propriedades, se aplicável.

Um exemplo de como seria uma interface de estado (totalmente composta - apenas para ilustrar o nível de controle que esse esquema fornece):

interface NPCState {
    Scene whereAmI(NPC o);
    String saySomething(NPC o);
}

E duas classes de implementação:

class Busy implements NPCState {
    Scene whereAmI(NPC o) {
        return o.getWorkScene();
    }
    String saySomething(NPC o) {
        return "Can't talk now, I'm busy!";
    }
}

class Available implements NPCState {
    Scene whereAmI(NPC o) {
        return TAVERN;
    }
    String saySomething(NPC o) {
        String[] choices = o.getRandomChat();
        return choices[RANDOM.getInt(choices.length)];
    }
}

E alternando estados:

// The time of day passed from "afternoon" to "evening"
NPCState available = new Available();
for ( NPC o : list ) {
    Scene oldScene = o.state.whereAmI(o);
    o.state = available;
    Scene newScene = o.state.whereAmI(o);
    moveGameObject(o, oldScene, newScene);
    ...

Os NPCs importantes podem ter seus estados personalizados, a lógica de escolha do estado pode ser mais personalizável e você pode ter estados diferentes para diferentes facetas do jogo (neste exemplo, usei uma única classe para informar a localização e o bate-papo, mas você pode separar eles e faça muitas combinações).

Isso também funciona bem com os editores de nível: você pode ter uma caixa de combinação simples para alternar o estado "global" de um nível e, em seguida, adicionar e reposicionar os objetos do jogo como você deseja que apareçam nesse estado. O mecanismo do jogo seria responsável por apenas "adicionar" esse objeto à cena quando ele tiver o estado correto - mas seus parâmetros ainda seriam editáveis ​​de uma maneira fácil de usar.

(Isenção de responsabilidade: tenho pouca experiência no mundo real com editores de jogos, por isso posso dizer com confiança sobre como os editores profissionais funcionam; mas meu argumento sobre o Padrão de Estado ainda é válido: organizar seu código dessa maneira deve ser limpo, sustentável e não desperdiçar sistema Recursos.)

mgibsonbr
fonte
você sabe, você poderia combinar esse padrão de design de estado com a matriz associativa que descrevi. Você pode codificar os objetos de estado conforme descrito aqui e, em seguida, escolher entre diferentes objetos de estado usando uma matriz associativa, como sugeri.
Jhocking 01/06
Eu concordo, também é bom separar o jogo de seu mecanismo, e a lógica do jogo codificada fortalece o acoplamento entre eles (reduzindo a possibilidade de reutilização). Porém, há uma desvantagem, pois, dependendo da complexidade do comportamento pretendido, tentar "codificar" tudo pode resultar em confusão desnecessária . Neste caso, uma abordagem mista pode ser desejável (ou seja, têm uma lógica de transição "genérico" do Estado, mas permitindo código personalizado para ser incorporada como bem)
mgibsonbr
Pelo que entendi, há um mapeamento individual entre o NPCState e o GameState. Então eu colocaria NPCs em uma matriz e itera através dele, atribuindo o novo NPCState quando uma mudança de estado do jogo é observada. O NPCState deve ser capaz de saber como lidar com todos os NPCs diferencial enviados a ele, portanto, essencialmente, o NPCState contém o comportamento de todos os NPCs para um determinado estado? Eu gosto que todos os comportamentos sejam armazenados de maneira limpa em um único NPCState, que mapeie de maneira limpa a implementação do editor de jogos, mas meio que torna o NPCState bastante grande.
Cardin
Ah, acho que não entendi direito. Mudei um pouco para incluir observadores. Portanto, é um NPCState diff para cada NPC diff, exceto os super genéricos, como o NPC Crowd, que podem compartilhar estado. Para cada estado do jogo, o NPC se registrará e seu NPCState com um Observer. Portanto, o Observer saberá exatamente qual NPC está registrado para mudar o comportamento em que estado do jogo e simplesmente iterará através deles. E no lado do editor de jogos, o editor de jogos apenas precisa passar um sinal para o Observer para mudar o estado de todo o nível.
Cardin
1
Sim, essa é a ideia! Os NPCs importantes terão muitos estados e a transição entre estados dependerá principalmente dos marcos concluídos. Às vezes, os NPCs genéricos também podem reagir a marcos e até ter seu estado escolhido dependente de uma propriedade interna (digamos que todos os NPCs tenham um estado inicial padrão e, quando você falar com um deles pela primeira vez, ele se apresenta, digite o ciclo normal de mudança de estado).
mgibsonbr
2

As escolhas que eu consideraria são fazer com que os objetos individuais respondam a diferentes gamestates ou atendam níveis diferentes em diferentes gamestates. A escolha entre os dois dependeria exatamente do que estou tentando fazer no jogo (quais são os diferentes estados? Como o jogo fará a transição entre estados? Etc.)

De qualquer forma, no entanto, eu não faria isso codificando os estados no código do jogo. Em vez de uma declaração de comutação massiva em objetos NPC, eu usaria os comportamentos NPC carregados em uma matriz associativa a partir de um arquivo de dados e depois usaria essa matriz associativa para executar comportamentos diferentes para os estados associados, algo como:

if (state in behaviors) {
  behaviors[state]();
}
jhocking
fonte
Esse arquivo de dados seria um tipo de linguagem de script? Eu acho que um arquivo de dados de texto simples pode não ser suficiente para descrever o comportamento. De qualquer forma, você está certo de que deve ser carregado dinamicamente. Não consigo pensar em usar um editor de jogos para gerar código Java válido; ele definitivamente precisa ser analisado de alguma forma.
Cardin
1
Bem, esse foi o meu pensamento inicial, mas depois de ver a resposta de mgibsonbr, percebi que era possível codificar os vários bevaviors como classes separadas e, no arquivo de dados, apenas dizer quais classes de comportamento combinam com qual estado. Carregue esses dados em uma matriz associativa em tempo de execução.
perfil completo de jhocking
Oh .. Sim, isso é definitivamente mais simples! : D Em comparação com o cenário de incorporar algo como Lua haha ..
Cardin
2

Que tal usar um padrão de observador para procurar mudanças de marcos? Se uma mudança acontecer, alguma classe reconhecerá isso e manipulará, por exemplo, uma mudança que deve ser feita em um npc.

Em vez do padrão de design de estado mencionado, eu usaria um padrão de estratégia.

Se um npc tem n maneiras de interagir com o personagem e m posições onde ele poderia estar, há um máximo de (m * n) +1 de classe que você precisa criar. Usando o padrão de estratégia, você terminaria com as classes n + m + 1, mas essas estratégias também poderiam ser usadas por outros npcs.

Portanto, pode haver uma classe lidando com os marcos e classes que observam essa classe e lidam com npc ou inimigos ou o que quer que deva ser alterado. Se os observadores forem atualizados, eles decidirão se precisam mudar algo para as instâncias que governam. A classe NPC por exemplo, no construtor, informa o NPC-Manager quando ele precisa ser atualizado e o que precisa ser atualizado ...

TOAOGG
fonte
O padrão Observer parece interessante. Eu acho que poderia deixar todas as responsabilidades com o NPC para se registrar com o observador do estado. O padrão Strategy se parece muito com os comportamentos Trigger e AI do Unity Engine, que são usados ​​para compartilhar comportamentos entre objetos de jogo diferentes (eu acho). Parece viável. Estou nt certo o que é as vantagens / desvantagens agora, mas que a Unidade usa o mesmo método também é um pouco reconfortante haha ..
Cardin
Eu apenas usei esses dois padrões algumas vezes, para não poder falar sobre os contras: - / Mas acho que é legal em caso de responsabilidade única e disponibilidade para testar todas as estratégias :) Pode ficar confuso se o seu usando uma estratégia em muitas classes diferentes e você deseja encontrar todas as classes que a utilizam.
TOAOGG
0

Todas as abordagens fornecidas são válidas. Depende da situação em que você está, a qualquer momento. Muitas aventuras ou MMOs usam uma combinação desses.

Por exemplo, se um evento crucial altera uma grande parte do nível (por exemplo, um cobrador de dívidas limpa seu apartamento e todos são presos), geralmente é mais fácil substituir a sala inteira por uma segunda que é parecida.

OTOH, se os personagens andam pelo mapa e fazem coisas diferentes em lugares diferentes, geralmente você tem um único ator que gira através de diferentes objetos de comportamento (por exemplo, siga em frente / sem conversas versus fique aqui / conversando sobre a morte de Mitch), que pode incluir "oculto" se o seu objetivo tiver sido cumprido.

Dito isto, geralmente ter duplicatas de um objeto que você cria manualmente não deve causar problemas. Quantos objetos você pode criar? Se você pode criar mais objetos do que o seu jogo pode passar, observe a propriedade "oculta" e pule, seu mecanismo está muito lento. Então eu não me preocuparia muito com isso. Muitos jogos online realmente fazem isso. Certos personagens ou itens estão sempre lá, mas não são exibidos para personagens que não têm a missão correspondente.

Você pode até combinar as abordagens: Tenha duas portas em seu prédio. Um leva ao apartamento "antes do cobrador de dívidas", outro ao apartamento depois. Quando você entra no corredor, apenas o que se aplica à sua progressão na história é realmente mostrado. Dessa forma, você pode apenas ter um mecanismo genérico para "o item está visível no momento atual da história" e uma porta com um único destino. Como alternativa, você pode criar portas mais complicadas que podem ter comportamentos que podem ser trocados, e uma delas é "vá para o apartamento completo", a outra "vá para o apartamento vazio". Isso pode parecer absurdo se realmente apenas o destino da porta mudar, mas se sua aparência mudar também (por exemplo, uma grande fechadura na frente da porta que você primeiro tenha que abrir),

uliwitness
fonte