Como um sistema de snapshots de estado de jogo seria implementado para jogos em tempo real em rede?

12

Quero criar um simples jogo multijogador em tempo real cliente-servidor como um projeto para minha classe de rede.

Eu li muito sobre modelos de rede multiplayer em tempo real e entendo as relações entre o cliente e o servidor e as técnicas de compensação de lag.

O que eu quero fazer é algo semelhante ao modelo de rede do Quake 3: basicamente, o servidor armazena um instantâneo de todo o estado do jogo; ao receber a entrada dos clientes, o servidor cria um novo instantâneo refletindo as alterações. Em seguida, calcula as diferenças entre o novo instantâneo e o último e as envia para os clientes, para que eles possam estar sincronizados.

Essa abordagem parece realmente sólida para mim - se o cliente e o servidor tiverem uma conexão estável, apenas a quantidade mínima necessária de dados será enviada para mantê-los sincronizados. Se o cliente ficar fora de sincronia, um instantâneo completo também poderá ser solicitado.

No entanto, não consigo encontrar uma boa maneira de implementar o sistema de instantâneos. Acho realmente difícil me afastar da arquitetura de programação para um jogador e pensar em como eu poderia armazenar o estado do jogo de tal maneira que:

  • Todos os dados são separados da lógica
  • As diferenças podem ser calculadas entre o instantâneo dos estados do jogo
  • As entidades do jogo ainda podem ser facilmente manipuladas via código

Como uma classe de instantâneo é implementada? Como as entidades e seus dados são armazenados? Toda entidade cliente possui um ID que corresponda a um ID no servidor?

Como as diferenças de instantâneo são calculadas?

Em geral: como seria implementado um sistema de instantâneos no estado do jogo?

Vittorio Romeo
fonte
4
+1. Isso é um pouco amplo demais para uma única pergunta, mas o IMO é um tópico interessante que pode ser abordado aproximadamente em uma resposta.
Kromster
Por que você não armazena apenas um instantâneo (o mundo real), salva todas as alterações recebidas nesse estado mundial regular E armazena as alterações em uma lista ou algo assim. Então, quando chegar a hora de enviar as alterações para todos os clientes, basta enviar o conteúdo da lista para todos eles e limpar a lista, comece do zero (alterações). Talvez isso não seja tão bom quanto armazenar 2 snapshots, mas com essa abordagem, você não precisa se preocupar com algoritmos sobre como acelerar o diff 2 snapshots.
tkausl
Você já leu isso: fabiensanglard.net/quake3/network.php - a revisão do modelo de rede do quake 3 inclui discussões sobre implementação.
Steven
Que tipo de jogo está tentando construir? A configuração da rede depende muito do tipo de jogo que você está criando. Um RTS não se comporta como um FPS em termos de rede.
precisa

Respostas:

3

Você pode calcular o delta da captura instantânea (alterações no estado sincronizado anterior) mantendo duas instâncias da captura instantânea: uma atual e a última sincronizada.

Quando a entrada do cliente chega, você modifica o instantâneo atual. Depois, na hora de enviar o delta para os clientes, você calcula o último instantâneo sincronizado com o atual campo a campo (recursivamente) e calcula e serializa o delta. Para serialização, você pode atribuir um ID exclusivo a cada campo no escopo de sua classe (em oposição ao escopo do estado global). O cliente e o servidor devem compartilhar a mesma estrutura de dados para o estado global, para que o cliente entenda a que um ID específico é aplicado.

Então, quando o delta é calculado, você clona o estado atual e o torna o último sincronizado. Agora, você tem idêntico estado atual e último sincronizado, mas instâncias diferentes para poder modificar o estado atual e não afetar o outro.

Essa abordagem pode ser mais fácil de implementar, especialmente com a ajuda da reflexão (se você tem esse luxo), mas pode ser lenta, mesmo se você otimizar altamente a parte da reflexão (construindo seu esquema de dados para armazenar em cache a maioria das chamadas de reflexão). Principalmente porque você precisa comparar duas cópias do estado potencialmente grande. Obviamente, depende de como você implementa a comparação e seu idioma. Pode ser rápido em C ++ com o comparador codificado, mas não tão flexível: qualquer alteração na estrutura de estado global requer modificação desse comparador, e essas alterações são tão frequentes nos estágios iniciais do projeto.

Outra abordagem é usar sinalizadores sujos. Cada vez que a entrada do cliente chega, aplica-a à sua cópia única do estado global e sinaliza os campos correspondentes como sujos. Depois, na hora de sincronizar os clientes, você serializa campos sujos (recursivamente) usando os mesmos IDs exclusivos. A desvantagem (menor) é que às vezes você envia mais dados do que o estritamente necessário: por exemplo, int field1foi inicialmente 0, depois foi atribuído 1 (e marcado como sujo) e depois foi atribuído 0 novamente (mas permanece sujo). O benefício é que, com uma estrutura hierárquica enorme de dados, você não precisa analisá-la completamente para calcular delta, apenas caminhos sujos.

Em geral, essa tarefa pode ser bastante complicada, depende de quão flexível deve ser a solução final. Por exemplo, o Unity3D 5 (a ser lançado) usará atributos para especificar dados que devem ser sincronizados automaticamente com os clientes (abordagem muito flexível, você não precisa fazer nada além de adicionar um atributo aos seus campos) e gerar o código como um etapa pós-compilação. Mais detalhes aqui.

Andriy Tylychko
fonte
2

Primeiro, você precisa saber como representar seus dados relevantes de maneira compatível com o protocolo. Isso depende dos dados relevantes para o jogo. Vou usar um jogo RTS como exemplo.

Para propósitos de rede, todas as entidades do jogo são enumeradas (por exemplo, captadores, unidades, prédios, recursos naturais, destrutíveis).

Os jogadores precisam ter os dados relevantes para eles (por exemplo, todas as unidades visíveis):

  • Eles estão vivos ou mortos?
  • Que tipo são eles?
  • Quanta saúde eles têm?
  • Posição atual, rotação, velocidade (velocidade + direção), caminho no futuro próximo ...
  • Atividade: Atacar, caminhar, construir, consertar, curar, etc ...
  • efeitos de status de buff / debuff
  • e possivelmente outras estatísticas como mana, escudos e o que não?

Inicialmente, o jogador deve receber o estado completo antes de poder entrar no jogo (ou, alternativamente, todas as informações relevantes para esse jogador).

Cada unidade possui um ID inteiro. Os atributos são enumerados e, portanto, também possuem identificadores integrais. Os IDs das unidades não precisam ter 32 bits (pode ser, se não formos frugal). Poderia muito bem ser 20 bits (deixando 10 bits para os atributos). O ID das unidades deve ser único, podendo muito bem ser atribuído por um contador quando a unidade é instanciada e / ou adicionada ao mundo do jogo (edifícios e recursos são considerados uma unidade imóvel e recursos podem receber um ID quando o mapa está carregado).

O servidor armazena o estado global atual. O estado atualizado mais recente de cada jogador é representado por um ponteiro para uma listdas alterações recentes (todas as alterações após o ponteiro ainda não foram enviadas para esse jogador). As alterações são adicionadas listquando ocorrem. Quando o servidor terminar de enviar a última atualização, ele poderá começar a percorrer a lista: o servidor move o ponteiro do jogador ao longo da lista até a cauda, ​​coletando todas as alterações ao longo do caminho e colocando-as em um buffer que será enviado para a lista. o jogador (ou seja, o formato do protocolo pode ser algo como: unit_id; attr_id; new_value) Novas unidades também são consideradas alterações e são enviadas com todos os seus valores de atributo para os jogadores receptores.

Se você não estiver usando um idioma com um coletor de lixo, precisará configurar um ponteiro lento que ficará para trás e, em seguida, localizar o ponteiro de jogador mais desatualizado da lista, liberando objetos pelo caminho. Você pode se lembrar de qual jogador está mais desatualizado dentro de uma pilha prioritária ou simplesmente iterar e liberar até que o ponteiro lento seja igual (ou seja, aponta para o mesmo item que um dos ponteiros do jogador).

Algumas perguntas que você não levantou e acho interessantes são:

  1. Os clientes devem receber um instantâneo com todos os dados em primeiro lugar? E os itens fora da linha de visão? E o nevoeiro da guerra nos jogos RTS? Se você enviar todos os dados, o cliente poderá ser invadido para exibir dados que não devem estar disponíveis para o player (dependendo de outras medidas de segurança que você tomar). Se você enviar apenas dados relevantes, o problema será resolvido.
  2. Quando é vital enviar alterações em vez de enviar todas as informações? Considerando a largura de banda disponível em máquinas modernas, ganhamos algo enviando um "delta" em vez de enviar todas as informações, se sim, quando?
AturSams
fonte