Rede para jogos de estratégia em tempo real


16

Estou desenvolvendo um jogo de estratégia em tempo real para um curso de ciência da computação que estou fazendo. Um dos aspectos mais difíceis parece ser a sincronização e rede cliente-servidor. Eu li esse tópico (incluindo 1500 arqueiros ), mas decidi adotar uma abordagem cliente-servidor em oposição a outros modelos (na LAN, por exemplo).

Este jogo de estratégia em tempo real vem com alguns problemas. Felizmente, todas as ações que o jogador executa são determinísticas. No entanto, existem eventos que acontecem em intervalos agendados. Por exemplo, o jogo é composto de peças, e quando um jogador pega uma peça, o 'nível de energia', um valor nessa peça, deve crescer um a cada segundo após ser colhido. Esta é uma explicação muito rápida que deve justificar meu caso de uso.

No momento, estou fazendo thin clients, que apenas enviam pacotes ao servidor e aguardam uma resposta. No entanto, existem vários problemas.

Quando os jogos entre os jogadores se tornam final, geralmente há mais de 50 eventos por segundo (devido aos eventos agendados, explicados anteriormente, acumulando-se), e os erros de sincronização começam a aparecer. Meu maior problema é que mesmo um pequeno desvio de estado entre os clientes pode significar decisões diferentes que os clientes tomam, que se transformam em jogos totalmente separados. Outro problema (que não é tão importante no momento) é que há latência e é preciso esperar alguns milissegundos, mesmo segundos depois que eles se movem para ver o resultado.

Estou imaginando quais estratégias e algoritmos eu poderia usar para tornar isso mais fácil, rápido e agradável para o usuário final. Isso é especialmente interessante, dada a grande quantidade de eventos por segundo, juntamente com vários jogadores por jogo.

TL; DR fazendo um RTS com> 50 eventos por segundo, como sincronizo clientes?


Talvez você possa implementar o que o Eve-online faz e "diminuir a velocidade" para permitir que tudo processe corretamente.
Ryan Erb

3
Aqui está um link obrigatório para o modelo de cliente / servidor da Aniquilação Planetária: forrestthewoods.ghost.io/… Essa é uma alternativa ao modelo de passo em etapas que parece estar funcionando muito bem para eles.
DallonF

Considere reduzir o número de eventos enviando uma única atualização para todos os blocos obtidos em vez de atualizações para cada bloco ou, conforme respondido por Ilmari, descentralizando as ações que não são de jogadores.
Lilienthal

Respostas:


12

Seu objetivo de sincronizar 50 eventos por segundo em tempo real parece-me que não é realista. É por isso que a abordagem da etapa de bloqueio mencionada no mencionada no artigo de 1500 arqueiros é, bem, comentada!

Em uma frase: A única maneira de sincronizar muitos itens em um tempo muito curto em uma rede muito lenta é NÃO sincronizar muitos itens em um período de tempo muito curto em uma rede muito lenta, mas, em vez disso, progrida de forma determinística em todos os clientes e sincronize apenas o necessidades básicas (entrada do usuário).


6

toda ação que o jogador executa é determinística; no entanto, existem eventos que acontecem em intervalos agendados

Eu acho que há seu problema; seu jogo deve ter apenas uma linha do tempo (para coisas que afetam a jogabilidade). Você diz que certas coisas crescem a uma taxa de X por segundo ; descubra quantas etapas do jogo há em um segundo e converta isso em uma taxa de X por Y . Então, embora o jogo possa desacelerar, tudo permanece determinístico.

A execução independente do jogo em tempo real tem outras vantagens:

  • você pode avaliar comparando-o o mais rápido possível
  • você pode depurar abrandando o jogo para ver eventos fugazes e, como mencionado
  • o jogo permanece determinístico, o que é muito, muito importante para o multiplayer.

Você também mencionou que enfrenta problemas quando há> 50 eventos ou atrasos de até segundos. Isso é muito menor em escala do que o cenário descrito em 1500 arqueiros , então veja se você pode criar um perfil do seu jogo e descobrir onde está a desaceleração.


1
+1: Baseado em quadros é a escolha certa, não baseada em tempo. Você pode tentar manter N quadros por segundo, é claro. Um leve engate é melhor que um dessincronização total.
PatrickB

@PatrickB: Vejo que muitos jogos usam um tempo "simulado" que não está vinculado aos quadros de vídeo. World of Warcraft atualiza apenas coisas como mana a cada 100ms, e o Dwarf Fortress assume o padrão de 10 ticks por quadro de vídeo.
Mooing Duck

@ Duck Duck: Meu comentário foi específico para RTSs. Para algo em que pequenos erros podem ser tolerados e corrigidos posteriormente (por exemplo, MMORPGs, FPSs), o uso de valores contínuos não é apenas bom, mas crítico. No entanto, simulações determinísticas que precisam ser sincronizadas em várias máquinas? Atenha-se aos quadros.
PatrickB

4

Primeiro, para resolver o problema com eventos agendados, não transmita os eventos quando eles acontecerem , mas quando forem agendados inicialmente. Ou seja, em vez de enviar uma mensagem "incrementar a energia do bloco ( x , y )" a cada segundo, envie uma única mensagem dizendo "incrementar a energia do bloco ( x , y ) uma vez por segundo até que esteja cheio ou até interrompido". Cada cliente é responsável por agendar as atualizações localmente.

De fato, você pode levar esse princípio adiante e transmitir apenas as ações do jogador : todo o resto pode ser calculado localmente por cada cliente (e pelo servidor, conforme necessário).

(Obviamente, você provavelmente também deve ocasionalmente transmitir somas de verificação do estado do jogo, para detectar qualquer dessincronização acidental e ter algum mecanismo para sincronizar novamente os clientes, se isso acontecer, por exemplo, reenviando todos os dados do jogo da cópia autorizada do servidor para os clientes. Esperemos que seja um evento raro, encontrado apenas em testes ou durante raros problemas de funcionamento.)


Segundo, para manter os clientes sincronizados, verifique se o seu jogo é determinístico. Outras respostas já forneceram bons conselhos para isso, mas deixe-me incluir um breve resumo do que fazer:

  • Torne seu jogo internamente baseado em turnos, com cada turno ou "tick" levando, digamos, 1/50 segundos. (De fato, você provavelmente poderia se safar com 1/10 de segundo turnos ou mais.) Quaisquer ações de jogador que ocorram durante um único turno devem ser tratadas como simultâneas. Todas as mensagens, pelo menos do servidor para os clientes, devem ser marcadas com o número do turno, para que cada cliente saiba em qual turno cada evento acontece.

    Como seu jogo está usando uma arquitetura cliente-servidor, você pode fazer com que o servidor atue como árbitro final do que acontece durante cada turno, o que simplifica algumas coisas. Observe, no entanto, que isso significa que os clientes também devem reconfirmar suas próprias ações do servidor: se um cliente enviar uma mensagem dizendo "Movo a unidade X um bloco à esquerda" e a resposta do servidor não diz nada sobre a movimentação da unidade X, o cliente deve assumir que isso não aconteceu e, possivelmente, cancelar qualquer animação de movimento preditivo que eles já tenham começado a tocar.

  • Defina uma ordem consistente para eventos "simultâneos" que ocorrem no mesmo turno, para que cada cliente os execute na mesma ordem. Esse pedido pode ser qualquer coisa, desde que seja determinístico e o mesmo para todos os clientes (e o servidor).

    Por exemplo, você pode primeiro incrementar todos os recursos (o que pode ser feito de uma só vez, se o crescimento de recursos em um bloco não puder interferir com o de outro), mover as unidades de cada jogador em uma sequência predeterminada e mover as unidades de NPC. Para ser justo com os jogadores, você pode variar a ordem do movimento da unidade entre os turnos, para que cada jogador comece primeiro com a mesma frequência; isso é bom, desde que seja feito deterministicamente (por exemplo, com base no número do turno).

  • Se você estiver usando matemática de ponto flutuante, verifique se está usando no modo IEEE estrito. Isso pode atrasar um pouco as coisas, mas esse é um preço pequeno a pagar pela consistência entre os clientes. Além disso, verifique se nenhum arredondamento acidental ocorre durante as comunicações (por exemplo, um cliente transmitindo um valor arredondado ao servidor, mas ainda usando o valor não arredondado internamente). Como observado acima, ter um protocolo para detectar e recuperar da dessincronização também é uma boa ideia, apenas por precaução.


1
Além disso, sincronize o RNG para iniciar e puxe apenas o RNG sincronizado quando o servidor solicitar. O Starcraft1 teve um bug por um longo tempo em que a semente do RNG não foi salva durante os replays, portanto os replays se desviavam lentamente dos jogos reais.
Mooing Duck

1
@MooingDuck: Bom ponto. De fato, eu sugiro transmitir a semente RNG atual a cada passo, para que a dessincronização RNG seja detectada imediatamente. Além disso, se seu código de interface do usuário precisar de qualquer aleatoriedade, não o puxe da mesma instância RNG usada para a lógica do jogo.
Ilmari Karonen

3

Você deve tornar a lógica dos seus jogos completamente independente do tempo real e essencialmente torná-la baseada em turnos. Dessa forma, você sabe exatamente a vez em que "a mudança de energia dos ladrilhos acontece". No seu caso, cada turno é apenas 1/50 de segundo.

Dessa forma, você precisa se preocupar apenas com as entradas dos jogadores, tudo o mais é gerenciado pela lógica dos jogos e completamente idêntico em todos os clientes. Mesmo que o jogo pare por um momento, devido ao atraso na rede ou cálculo extremamente complicado, os eventos ainda estão acontecendo em sincronia para todos.


1

Antes de tudo, você precisa entender que a matemática float / double do PC NÃO É determinística, a menos que você especifique o uso estrito do IEEE-754 para o seu cálculo (será lento)

Então é assim que eu o implementaria: o cliente se conectaria ao servidor e sincronizaria o tempo (cuide da latência do ping!) (Para uma jogabilidade longa, pode ser necessário ressincronizar o carimbo de data / hora)

agora, toda vez que um cliente executa uma ação, ele inclui um carimbo de data / hora e cabe ao servidor rejeitar o carimbo de data / hora incorreto. Em seguida, o servidor envia de volta a ação aos clientes, e toda vez que uma volta é "fechada" (o servidor não aceita turno / carimbo de data / hora tão antigo), o servidor envia e ação de volta aos clientes.

Os clientes terão 2 "mundo": um está sincronizado com a curva final, o outro é calculado a partir da curva final, somando a ação que chegou na fila, até a curva / carimbo de data / hora do cliente atual.

como o servidor aceita uma ação um pouco antiga, o cliente pode adicionar sua própria ação diretamente na fila, para que o tempo de ida e volta pela rede fique oculto, pelo menos para sua própria ação.

a última coisa é enfileirar mais ações para que você possa preencher o pacote MTU, causando menos sobrecarga de protocoll; uma boa idéia é fazer isso no servidor, para que todos os eventos de final de turno contenham ação na fila.

eu uso esse algoritmo em um jogo de tiro em tempo real e funciona bem (com e sem cliente executando sua própria ação, mas com ping de servidor baixo como 20 / 50ms), também todo servidor X final envia um "tudo pacote "mapa do cliente", para corrigir valores desviados.


Questões matemáticas de ponto flutuante geralmente podem ser evitadas como tal - em um RTS, você geralmente pode fazer facilmente a simulação e o movimento com número inteiro / ponto fixo e usar ponto flutuante apenas para a camada de exibição que não afeta o comportamento do jogo.
Peteris 26/05

Com número inteiro é difícil fazer ladrilhos horizontais, a menos que seja um quadro octogonal. Não há acceleretion hw para ponto fixo, por isso pode ser mais lento do que flutuante IEEE754
Lesto
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.