Técnicas de gerenciamento de estado de jogo?

24

Primeiro, não estou me referindo ao gerenciamento de cenas; Estou definindo o estado do jogo livremente como qualquer tipo de estado em um jogo que tenha implicações sobre se a entrada do usuário deve ou não ser ativada ou se certos atores devem ser temporariamente desativados etc.

Como um exemplo concreto, digamos que é um jogo do clássico Battlechess. Depois de fazer um movimento para pegar a peça de outro jogador, uma breve sequência de batalha é executada. Durante esta sequência, o jogador não deve ter permissão para mover peças. Então, como você acompanharia esse tipo de transição de estado? Uma máquina de estados finitos? Uma simples verificação booleana? Parece que o último só funcionaria bem para um jogo com muito poucas mudanças de estado desse tipo.

Posso pensar em várias maneiras simples de lidar com isso usando máquinas de estados finitos, mas também posso vê-las rapidamente ficando fora de controle. Só estou curioso para saber se há uma maneira mais elegante de acompanhar os estados / transições do jogo.

vargoniano
fonte
Você já conferiu gamedev.stackexchange.com/questions/1783/game-state-stack e gamedev.stackexchange.com/questions/2423/… ? É meio que tudo pulando em torno do mesmo conceito, mas não consigo pensar em nada que seria melhor do que uma máquina de estado para o estado do jogo.
226116 Michael Jackson.bartnett
Possível duplicata: gamedev.stackexchange.com/questions/12664/…
Tetrad

Respostas:

18

Uma vez me deparei com um artigo que resolve seu problema de maneira bastante elegante. É uma implementação básica do FSM, chamada no seu loop principal. Descrevi o resumo básico do artigo no restante desta resposta.

Seu estado básico do jogo é assim:

class CGameState
{
    public:
        // Setup and destroy the state
        void Init();
        void Cleanup();

        // Used when temporarily transitioning to another state
        void Pause();
        void Resume();

        // The three important actions within a game loop
        void HandleEvents();
        void Update();
        void Draw();
};

Cada estado do jogo é representado por uma implementação dessa interface. Para o seu exemplo de Battlechess, isso pode significar os seguintes estados:

  • animação de introdução
  • menu principal
  • animação de instalação do tabuleiro de xadrez
  • entrada de movimento do jogador
  • jogador mover animação
  • animação de movimento do oponente
  • menu de pausa
  • tela final

Os estados são gerenciados no seu mecanismo de estado:

class CGameEngine
{
    public:
        // Creating and destroying the state machine
        void Init();
        void Cleanup();

        // Transit between states
        void ChangeState(CGameState* state);
        void PushState(CGameState* state);
        void PopState();

        // The three important actions within a game loop
        // (these will be handled by the top state in the stack)
        void HandleEvents();
        void Update();
        void Draw();

        // ...
};

Observe que cada estado precisa de um ponteiro para o CGameEngine em algum momento, para que o próprio estado possa decidir se um novo estado deve ser inserido. O artigo sugere a passagem no CGameEngine como um parâmetro para HandleEvents, Update e Draw.

No final, seu loop principal lida apenas com o mecanismo de estado:

int main ( int argc, char *argv[] )
{
    CGameEngine game;

    // initialize the engine
    game.Init( "Engine Test v1.0" );

    // load the intro
    game.ChangeState( CIntroState::Instance() );

    // main loop
    while ( game.Running() )
    {
        game.HandleEvents();
        game.Update();
        game.Draw();
    }

    // cleanup the engine
    game.Cleanup();
    return 0;
}
fantasma
fonte
17
C para a classe? Ai credo. No entanto, esse é um bom artigo - +1.
The Duck comunista
Pelo que pude entender, esse é o tipo de coisa sobre a qual a pergunta está explicitamente não perguntando. Isso não quer dizer que você não possa lidar com isso dessa maneira, como certamente poderia, mas se tudo que você queria fazer era desativar temporariamente a entrada, acho que é exagero e ruim para a manutenção derivar uma nova subclasse de CGameState que vai seja 99% idêntico a outra subclasse.
Kylotan
Eu acho que isso depende muito de como o código foi combinado. Eu posso imaginar uma separação clara entre a seleção de uma peça e um destino (principalmente indicadores de interface do usuário e manipulação de entradas) e uma animação da peça de xadrez em direção a esse destino (uma animação de tabuleiro inteiro onde outras peças se afastam, interagem com o movimento peça etc), tornando os estados longe de serem idênticos. Isso separa a responsabilidade, permitindo fácil manutenção e até reutilização (demonstração de introdução, modo de reprodução). Eu acho que isso também responde à pergunta, mostrando que o uso de um FSM não precisa ser um aborrecimento.
fantasma
Isso é realmente ótimo, obrigado. Um ponto importante que você mencionou foi em seu último comentário: "o uso de um FSM não precisa ser um aborrecimento". Eu havia imaginado erroneamente que o uso de um FSM envolveria o uso de comandos switch, o que não é necessariamente verdade. Outra confirmação importante é que cada estado precisa de uma referência ao mecanismo do jogo; Eu me perguntava como isso funcionaria de outra maneira.
vargonian
2

Começo lidando com esse tipo de coisa da maneira mais simples possível.

bool isPieceMoving;

Depois, adicionarei as verificações contra esse sinalizador booleano nos locais relevantes.

Se mais tarde descobrir que preciso de mais casos especiais do que isso - e apenas isso -, me refiro a algo melhor. Geralmente, existem três abordagens que eu adotarei:

  • Refatorar qualquer sinalizador exclusivo representando subestado em enumerações. por exemplo. enum { PRE_MOVE, MOVE, POST_MOVE }e adicione as transições sempre que necessário. Então eu posso checar contra essa enumeração onde eu checava a bandeira booleana. Essa é uma alteração simples, mas que reduz o número de itens a serem verificados, permite que você use instruções de chave para gerenciar o comportamento com eficiência etc.
  • Desligue os subsistemas individuais conforme necessário. Se a única diferença durante a sequência de batalha é que você não pode mover peças, você pode chamar pieceSelectionManager->disable()ou similar no início da sequência e pieceSelectionManager->enable(). Você ainda tem essencialmente sinalizadores, mas agora eles são armazenados mais perto do objeto que controlam, e você não precisa manter nenhum estado extra no código do jogo.
  • A parte anterior implica a existência de um PieceSelectionManager: em geral, você pode fatorar partes do seu estado e comportamento do jogo em objetos menores que lidam com um subconjunto do estado geral de maneira coerente. Cada um desses objetos terá seu próprio estado, que determina seu comportamento, mas é fácil de gerenciar, pois está isolado dos outros objetos. Resista à tentação de permitir que seu objeto gamestate ou loop principal se torne um depósito de pseudo-globais e considere isso!

De um modo geral, nunca preciso ir além disso quando se trata de subestados de casos especiais; portanto, não acho que exista o risco de "ficar rapidamente fora de controle".

Kylotan
fonte
11
Sim, eu imagino que exista uma linha entre ir all-out com estados e apenas usar um bool / enums quando apropriado. Mas conhecendo minhas tendências pedantes, provavelmente vou acabar fazendo de quase todos os estados sua própria classe.
vargonian
Você faz parecer que uma classe é mais correta do que as alternativas, mas lembre-se de que isso é subjetivo. Se você começar a criar muitas classes pequenas para coisas que podem ser representadas mais facilmente por outras construções de linguagem, isso poderá obscurecer a intenção do código.
Kylotan
1

http://www.ai-junkie.com/architecture/state_driven/tut_state1.html é um lindo tutorial para gerenciamento de estado de jogos! Você pode usá-lo para entidades de jogos ou para um sistema de menus como acima.

Ele começa a ensinar sobre o Padrão de Design do Estado e depois implementa um State Machine, e o estende sucessivamente mais e mais. É uma leitura muito boa! Você terá uma sólida compreensão de como todo o conceito funciona e como aplicá-lo a novos tipos de problemas!

Zolomon
fonte
1

Tento não usar máquina de estado e booleanos para esse fim, porque ambos não são escaláveis. Ambos se transformam em confusão quando o número de estados cresce.

Eu costumo projetar a jogabilidade como uma sequência de ações e consequências, qualquer estado do jogo vem naturalmente sem a necessidade de defini-la separadamente.

Por exemplo, no seu caso com a desativação da entrada do player: você tem algum manipulador de entrada do usuário e alguma indicação visual no jogo de que a entrada está desativada, você deve torná-los um único objeto ou componente. Para desativar a entrada, basta desativar o objeto inteiro, sem necessidade de sincronize-os em alguma máquina de estado ou reaja a algum indicador booleano.

Filipp Keks
fonte