Separação de estado mundial e animação em um jogo baseado em turnos

9

Como você lida com a separação da animação do estado mundial em um jogo baseado em turnos? Atualmente, estou trabalhando em um jogo baseado em grade 2D. O código abaixo é simplificado para melhor explicar.

Quando um ator se move, quero pausar o fluxo de voltas enquanto a criatura anima e se move para a nova posição. Caso contrário, a tela poderá ficar significativamente atrás do estado mundial, o que causaria uma aparência visual estranha. Eu também quero ter animações que não bloqueiem o fluxo do jogo - um efeito de partícula pode se desdobrar em vários turnos sem afetar a jogabilidade.

O que eu fiz foi introduzir dois tipos de animações, as quais eu chamo de animações de bloqueio e de não-bloqueio. Quando o jogador quer se mover, o código que é executado é

class Actor {
    void move(PositionInfo oldPosition, PositionInfo newPosition) {
        if(isValidMove(oldPosition, newPosition) {
             getGame().pushBlockingAnimation(new MoveAnimation(this, oldPosition, newPosition, ....));
             player.setPosition(newPosition);
        }
    }
}

Em seguida, o loop principal de atualização:

class Game {
    void update(float dt) {
        updateNonBlockingAnimations(dt); //Effects like particle explosions, damage numbers, etc. Things that don't block the flow of turns.
        if(!mBlockingAnimations.empty()) {
            mBlockingAnimations.get(0).update(dt);
        } else {
             //.. handle the next turn. This is where new animations will be enqueued..//
        }
        cleanUpAnimations(); // remove finished animations
    }
}

... onde a animação atualiza a posição da tela do ator.

Uma outra idéia que estou implementando é ter uma animação de bloqueio simultânea, na qual várias animações de bloqueio serão atualizadas simultaneamente, mas a próxima etapa não acontecerá até que todas elas estejam concluídas.

Parece uma maneira sensata de fazer as coisas? Alguém tem alguma sugestão ou mesmo referência a como outros jogos semelhantes fazem uma coisa dessas.

mdkess
fonte

Respostas:

2

Se o seu sistema funcionar para você, não vejo motivo para fazê-lo.

Bem, um motivo: pode ficar confuso quando você não consegue julgar facilmente as consequências de "player.setPosition (newPosition);" não mais.

Exemplo (apenas inventando as coisas): imagine que você move o jogador com sua setPosition para cima de alguma armadilha. A chamada para "player.setPosition" acionará outra chamada para ... digamos um código de ouvinte de interceptação que, por sua vez, acionará uma animação de bloqueio por si só que exibirá um "golpe doloroso" em cima de si.

Você vê o problema: o splash é exibido simultaneamente com o movimento do jogador em direção à armadilha. (Vamos supor aqui que você não deseja isso, mas deseja que a animação da armadilha seja reproduzida logo após a conclusão da movimentação;)).

Desde que você consiga manter em mente todas as conseqüências de suas ações, após as chamadas "pushBlockingAnimation", tudo ficará bem.

Pode ser necessário começar a agrupar a lógica do jogo em classes "bloqueando algo" para que elas possam ser colocadas em fila para serem executadas após o término da animação. Ou você passa um retorno de chamada "callThisAfterAnimationFinishes" para pushBlockingAnimation e move o setPosition para a função de retorno de chamada.

Outra abordagem seria a linguagem de script. Por exemplo, Lua possui "coroutine.yield", que retorna a função com um estado especial para que possa ser chamada posteriormente para continuar na próxima instrução. Dessa forma, você pode esperar facilmente o final da animação executar a lógica do jogo sem sobrecarregar a aparência "normal" do fluxo de programa do seu código. (significa que seu código ainda pareceria um pouco com "playAnimation (); setPosition ();" em vez de passar retornos de chamada e sobrecarregar a lógica do jogo em várias funções).

O Unity3D (e pelo menos um outro sistema de jogo que eu conheço) apresenta a instrução C # "yield return" para simular isso diretamente no código C #.

// instead of simple call to move(), you have to "iterate" it in a foreach-like loop
IEnumerable<Updatable> move(...) {
    // playAnimation returns an object that contain an update() and finished() method.
    // the caller will not iterate over move() again until the object is "finished"
    yield return getGame().playAnimation(...)
    // the next statement will be executed *after* the animation finished
    player.setPosition(newPosition);
}

Obviamente, nesse esquema, a chamada para mover () recebe todo o trabalho sujo agora. Você provavelmente aplicaria essa técnica apenas para funções específicas nas quais você sabe exatamente quem as chama de onde. Portanto, tome isso apenas como uma idéia básica de como os outros mecanismos são organizados - provavelmente é muito complicado para o seu problema específico atual.

Eu recomendo que você mantenha sua ideia de pushBlockingAnimation até ter problemas no mundo real. Então modifique-o. ;)

Imi
fonte
Muito obrigado por dedicar um tempo para escrever esta resposta, eu realmente aprecio isso.
Mdkess