Mecanismo de reversão de tempo em jogos

10

Eu estou pensando sobre como os mecanismos de manipulação do tempo nos jogos são tipicamente projetados. Estou particularmente interessado em reverter o tempo (mais ou menos como no último SSX ou Prince of Persia).

O jogo é um shooter 2D de cima para baixo.

O mecanismo que estou tentando projetar / implementar possui os seguintes requisitos:

1) Ações de entidades além do personagem do jogador são completamente determinísticas.

  • A ação que uma entidade realiza é baseada nos quadros progredidos desde o início do nível e / ou na posição do jogador na tela
  • As entidades são geradas em um horário definido durante o nível.

2) A reversão do tempo funciona revertendo em tempo real.

  • As ações do jogador também são revertidas, repetindo inversamente o que o jogador realizou. O jogador não tem controle durante o tempo inverso.
  • Não há limite para o tempo gasto na reversão, podemos reverter todo o caminho até o início do nível, se desejado.

Como um exemplo:

Quadros 0-50: O jogador avança 20 unidades ao longo desse tempo O Inimigo 1 aparece no quadro 20 O Inimigo 1 move-se para a esquerda 10 unidades durante o quadro 30-40 O jogador dispara a bala no quadro 45 Bala viaja 5 na frente (45-50) e mata o Inimigo 1 em quadro 50

A reversão disso seria reproduzida em tempo real: o jogador se move para trás 20 unidades durante esse tempo. O inimigo 1 revive no quadro 50 Bala reaparece no quadro 50 Bala se move para trás 5 e desaparece (50-45) O inimigo se move para a esquerda 10 (40-30) O inimigo é removido quadro 20.

Olhando para o movimento, eu tinha algumas idéias sobre como conseguir isso, pensei em ter uma interface que mudasse o comportamento para quando o tempo estava avançando ou revertendo. Em vez de fazer algo assim:

void update()
{
    movement += new Vector(0,5);
}

Eu faria algo assim:

public interface movement()
{
    public void move(Vector v, Entity e);
}

public class advance() implements movement
{
    public void move(Vector v, Entity e)
    {
            e.location += v;
    }
}


public class reverse() implements movement
{
    public void move(Vector v, Entity e)
    { 
        e.location -= v;
    }
}

public void update()
{
    moveLogic.move(new vector(5,0));
}

No entanto, percebi que isso não seria o desempenho ideal e rapidamente se tornaria complicado para ações mais avançadas (como movimento suave ao longo de caminhos curvos, etc.).

Jkh2
fonte
1
Eu não assisti tudo isso (câmera instável do YouTube, 1,5 horas) , mas talvez haja algumas idéias de Jonathan Blow que tenham trabalhado isso em seu jogo Braid.
MichaelHouse
Possível duplicata de gamedev.stackexchange.com/questions/15251/…
Hackworth

Respostas:

9

Você pode dar uma olhada no padrão de comando .

Basicamente, todas as ações reversíveis que suas entidades executam são implementadas como um objeto de comando. Todos esses objetos implementam pelo menos 2 métodos: Execute () e Undo (), além do que mais você precisar, como uma propriedade de carimbo de data / hora para o tempo correto.

Sempre que sua entidade executa uma ação reversível, você cria primeiro um objeto de comando apropriado. Salve-o em uma pilha de Desfazer, depois alimente seu mecanismo de jogo e execute-o. Quando você deseja reverter o tempo, você exibe ações da parte superior da pilha e chama o método Undo (), que faz o oposto do método Execute (). Por exemplo, no caso de um salto do ponto A para o ponto B, você executa um salto do B para A.

Depois de executar uma ação, salve-a em uma pilha Refazer, se desejar avançar e retroceder à vontade, assim como a função desfazer / refazer em um editor de texto ou programa de pintura. Obviamente, suas animações também devem suportar o modo "retroceder" para reproduzi-las ao contrário.

Para mais travessuras no design de jogos, deixe cada entidade armazenar suas ações em sua própria pilha, para que você possa desfazê-las / refazê-las independentemente uma da outra.

Um padrão de comando tem outras vantagens: por exemplo, é bastante trivial criar um gravador de repetição, pois você só precisa salvar todos os objetos nas pilhas em um arquivo e, no momento da repetição, basta alimentá-lo no mecanismo de jogo um a um. 1.

Hackworth
fonte
2
Observe que a reversibilidade das ações em um jogo pode ser uma coisa muito delicada, devido a problemas de precisão de ponto flutuante, timesteps variáveis, etc; é muito mais seguro salvar esse estado do que reconstruí-lo na maioria das situações.
Steven Stadnicki
@StevenStadnicki Talvez, mas é definitivamente possível. De cabeça para baixo, os C&C Generals fazem assim. Ele tem replays de horas de até 8 jogadores, pesando algumas centenas de kB, na pior das hipóteses, e é como eu acho que a maioria, senão todos os jogos RTS, fazem o seu multiplayer: você simplesmente não pode transmitir o estado completo do jogo com potencialmente centenas de unidades em cada quadro, é necessário deixar o mecanismo fazer a atualização. Então, sim, é definitivamente viável.
Hackworth
3
A reprodução é uma coisa muito diferente de retroceder, porque operações que são consistentemente reproduzíveis para a frente (por exemplo, encontrar a posição no quadro n, x_n, iniciando com x_0 = 0 e adicionando os deltas v_n para cada etapa) não são necessariamente reproduzíveis para trás ; (x + v_n) -v_n não é consistentemente igual a x na matemática de ponto flutuante. E é fácil dizer 'contornar isso', mas você está falando de uma possível revisão completa, incluindo a impossibilidade de usar muitas bibliotecas externas.
Steven Stadnicki
1
Para alguns jogos, sua abordagem pode ser viável, mas a maioria dos jogos do AFAIK que usam inversão de tempo como mecânico estão usando algo mais próximo da abordagem Memento do OriginalDaemon, onde o estado relevante é salvo para cada quadro.
Steven Stadnicki
2
Que tal rebobinar recalculando as etapas, mas salvando um quadro-chave a cada dois segundos? É provável que erros de ponto flutuante não façam uma diferença significativa em apenas alguns segundos (dependendo da complexidade, é claro). Ele também é mostrado para trabalhar em compressão de vídeo: P
Tharwen
1

Você pode dar uma olhada no Memento Pattern; sua intenção principal é implementar operações de desfazer / refazer revertendo o estado do objeto, mas para certos tipos de jogos isso deve ser suficiente.

Para um jogo em tempo real, você pode considerar cada quadro de suas operações como uma mudança de estado e armazená-lo. Essa é uma abordagem simples de implementar. A alternativa é interceptar quando o estado de um objeto é alterado. Por exemplo, detectar quando as forças que atuam sobre um corpo rígido são alteradas. Se você estiver usando propriedades para obter e definir variáveis, isso também pode ser uma implementação relativamente direta, a parte difícil é identificar quando reverter o estado, pois esse não será o mesmo tempo para todos os objetos (você pode armazenar o tempo de reversão como uma contagem de quadros desde o início do sistema).

OriginalDaemon
fonte
0

No seu caso particular, lidar com a reversão rebobinando o movimento deve funcionar bem. Se você estiver usando qualquer forma de busca de caminho com as unidades de AI, apenas certifique-se de recalculá-lo após a reversão para evitar a sobreposição de unidades.

O problema é a maneira como você lida com o próprio movimento: um mecanismo de física decente (um shooter 2D de cima para baixo será bom com um muito simples) que rastreia informações de etapas anteriores (incluindo posição, força líquida, etc.) uma base sólida. Em seguida, ao decidir uma reversão máxima e uma granularidade para as etapas de reversão, você deverá obter o resultado desejado.

Darkwings
fonte
0

Embora essa seja uma ideia interessante. Eu desaconselharia.

A repetição do jogo adiante funciona bem, porque uma operação sempre terá o mesmo efeito no estado do jogo. Isso não significa que a operação reversa fornece o estado original. Por exemplo, avalie a seguinte expressão em qualquer linguagem de programação (desative a otimização)

(1.1 + 3 - 3) == 1.1

Em C e C ++, pelo menos, ele retorna false. Embora a diferença possa ser pequena, imagine quantos erros podem acumular a 60fps em 10s de segundos de minutos. Haverá casos em que um jogador apenas perde alguma coisa, mas a atinge enquanto o jogo é repetido para trás.

Eu recomendaria armazenar quadros-chave a cada meio segundo. Isso não ocupará muita memória. Você pode então interpolar entre quadros-chave ou, melhor ainda, simular o tempo entre dois quadros-chave e depois reproduzi-lo para trás.

Se o seu jogo não for muito complicado, armazene os quadros-chave do estado do jogo 30 vezes por segundo e jogue-o para trás. Se você tivesse 15 objetos, cada um com uma posição 2D, levaria uns bons 1,5 minutos para atingir um MB, sem compactação. Os computadores têm gigabytes de memória.

Portanto, não complique demais, não será fácil reproduzir um jogo ao contrário e causará muitos erros.

Hannesh
fonte