Para criar um jogo como um RTS em rede, já vi várias respostas sugerir tornar o jogo completamente determinístico; então você só precisa transferir as ações dos usuários e atrasar o que é exibido um pouco para "bloquear" a entrada de todos antes que o próximo quadro seja renderizado. Então, coisas como posições, saúde, etc. da unidade não precisam ser constantemente atualizadas na rede, porque a simulação de cada jogador será exatamente a mesma. Eu também ouvi a mesma coisa sugerida para fazer replays.
No entanto, como os cálculos de ponto flutuante não são determinísticos entre as máquinas, ou mesmo entre diferentes compilações do mesmo programa na mesma máquina, isso é realmente possível? Como podemos evitar que esse fato cause pequenas diferenças entre jogadores (ou replays) que se propagam ao longo do jogo ?
Ouvi algumas pessoas sugerindo evitar números de ponto flutuante e usar int
para representar o quociente de uma fração, mas isso não me parece prático - e se eu precisar, por exemplo, assumir o cosseno de um ângulo? Preciso seriamente reescrever uma biblioteca de matemática inteira?
Observe que eu estou interessado principalmente em C #, que, até onde eu sei, tem exatamente os mesmos problemas que o C ++ a esse respeito.
fonte
Respostas:
Os pontos flutuantes são determinísticos?
Eu li muito sobre esse assunto alguns anos atrás, quando queria escrever um RTS usando a mesma arquitetura de trava de segurança que você faz.
Minhas conclusões sobre pontos flutuantes de hardware foram:
STREFLOP
biblioteca)Concluí que é impossível usar os tipos de ponto flutuante embutidos no .net de maneira determinística.
Soluções possíveis
Portanto, eu precisava de soluções alternativas. Eu considerei:
FixedPoint32
em C #. Embora isso não seja muito difícil (eu tenho uma implementação parcialmente concluída), o intervalo muito pequeno de valores torna irritante o uso. Você deve ter cuidado o tempo todo para não transbordar nem perder muita precisão. No final, achei isso não mais fácil do que usar números inteiros diretamente.FixedPoint64
em C #. Achei isso bastante difícil de fazer. Para algumas operações, números inteiros intermediários de 128 bits seriam úteis. Mas .net não oferece esse tipo.Decimal
. Mas é lento, consome muita memória e gera facilmente exceções (divisão por 0, estouros). É muito bom para uso financeiro, mas não serve para jogos.My SoftFloat
Inspirado em sua postagem no StackOverflow , eu comecei a implementar um tipo de ponto flutuante de 32 bits em software e os resultados são promissores.
float
adição / multiplicação (thread único em um i3 de 2,66 GHz ). Se alguém tiver boas referências de ponto flutuante para .net, envie-as para mim, pois meu teste atual é muito rudimentar.Se alguém quiser contribuir com testes ou melhorar o código, entre em contato comigo ou faça uma solicitação pull no github. https://github.com/CodesInChaos/SoftFloat
Outras fontes de indeterminismo
Existem também outras fontes de indeterminismo no .net.
Dictionary<TKey,TValue>
ouHashSet<T>
retorna os elementos em uma ordem indefinida.object.GetHashCode()
difere de corrida para corrida.Random
classe incorporada não é especificada, use o seu próprio.WeakReference
s perdem, seu objetivo é indeterminado, pois o GC pode ser executado a qualquer momento.fonte
A resposta a esta pergunta é do link que você postou. Especificamente, você deve ler a citação de Gas Powered Games:
E então abaixo desse é o seguinte:
Um jogo determinístico só será determinístico ao usar os arquivos compilados de forma idêntica e rodar em sistemas que cumpram os padrões IEEE. Simulações ou replays sincronizados de rede entre plataformas não serão possíveis.
fonte
Use aritmética de ponto fixo. Ou escolha um servidor autorizado e sincronize o estado do jogo de vez em quando - é isso que o MMORTS faz. (Pelo menos, o Elements of War funciona assim. Também está escrito em C #.) Dessa forma, os erros não têm chance de se acumular.
fonte
Edit: Um link para uma classe de ponto fixo (Comprador, cuidado! - Não usei ...)
Você sempre pode recorrer à aritmética de ponto fixo. Alguém (fazendo rts, nada menos) já fez o trabalho de perna no stackoverflow .
Você pagará uma penalidade de desempenho, mas isso pode ou não ser um problema, pois o .net não será especialmente eficiente aqui, pois não usará instruções simd. Referência!
NB: Aparentemente, alguém da intel parece ter uma solução para permitir que você use a biblioteca de primitivas de desempenho da intel em c #. Isso pode ajudar a vetorizar o código de ponto fixo para compensar o desempenho mais lento.
fonte
decimal
funcionaria bem para isso também; mas isso ainda tem os mesmos problemas do usoint
- como faço para disparar com ele?Eu trabalhei em vários títulos grandes.
Resposta curta: é possível se você é incrivelmente rigoroso, mas provavelmente não vale a pena. A menos que você esteja em uma arquitetura fixa (leia-se: console), é exigente, quebradiço e vem com uma série de problemas secundários, como a associação tardia.
Se você ler alguns dos artigos mencionados, notará que, embora possa definir o modo da CPU, existem casos bizarros, como quando um driver de impressão alterna o modo da CPU porque estava no mesmo espaço de endereço. Eu tive um caso em que um aplicativo estava bloqueado por quadro em um dispositivo externo, mas um componente defeituoso estava causando a aceleração da CPU devido ao calor e relatado de maneira diferente pela manhã e à tarde, tornando-a uma máquina diferente após o almoço.
Essas diferenças de plataforma estão no silício, não no software, portanto, para responder sua pergunta, o C # também é afetado. Instruções como FMA (multiply-add fundido) vs ADD + MUL alteram o resultado porque ele arredonda internamente apenas uma vez em vez de duas vezes. C oferece a você mais controle para forçar a CPU a fazer o que você deseja basicamente excluindo operações como o FMA para tornar as coisas "padrão" - mas ao custo da velocidade. Os intrínsecos parecem ser os mais propensos a diferir. Em um projeto, tive que desenterrar um livro de 150 anos de tabelas acos para obter valores de comparação para determinar qual CPU estava "certa". Muitas CPUs usam aproximações polinomiais para funções trigonométricas, mas nem sempre com os mesmos coeficientes.
Minha recomendação:
Independentemente de você seguir passo inteiro de bloqueio ou sincronização, lide com a mecânica do jogo principal separadamente da apresentação. Torne o jogo preciso, mas não se preocupe com a precisão na camada de apresentação. Lembre-se também de que você não precisa enviar todos os dados mundiais em rede na mesma taxa de quadros. Você pode priorizar suas mensagens. Se sua simulação corresponder a 99,999%, você não precisará transmitir com tanta frequência para permanecer emparelhado. (Prevenir fraudes à parte.)
Há um bom artigo sobre o mecanismo de origem que explica uma maneira de sincronizar: https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
Lembre-se, se você estiver interessado em ingressar tarde, terá que acertar e sincronizar de qualquer maneira.
fonte
Sim, o C # tem os mesmos problemas que o C ++. Mas também tem muito mais.
Por exemplo, considere esta declaração de Shawn Hawgraves:
É "fácil o suficiente" garantir que isso aconteça em C ++. No C # , no entanto, será muito mais difícil lidar com isso. Isso é graças ao JIT.
O que você acha que aconteceria se o intérprete executasse seu código interpretado uma vez, mas o executasse na segunda vez? Ou talvez o interprete duas vezes na máquina de outra pessoa, mas o JIT é depois disso?
O JIT não é determinístico, porque você tem muito pouco controle sobre ele. Essa é uma das coisas que você desiste de usar o CLR.
E Deus o ajude se uma pessoa estiver usando o .NET 4.0 para executar seu jogo, enquanto outra pessoa estiver usando o CLR Mono (usando as bibliotecas .NET, é claro). Mesmo o .NET 4.0 vs. .NET 5.0 pode ser diferente. Você simplesmente precisa de mais controle sobre os detalhes de baixo nível de uma plataforma para garantir esse tipo de coisa.
Você deve ser capaz de se safar da matemática de ponto fixo. Mas é isso aí.
fonte
int
- como faço para disparar com ele?Depois de escrever essa resposta, percebi que ela realmente não responde à pergunta, que era especificamente sobre não-determinismo de ponto flutuante. Mas talvez isso seja útil para ele, de qualquer maneira, se ele for criar um jogo em rede dessa maneira.
Juntamente com o compartilhamento de entrada que você está transmitindo para todos os jogadores, pode ser muito útil criar e transmitir uma soma de verificação de estado importante do jogo, como posições do jogador, saúde etc. Ao processar a entrada, afirme que o estado do jogo verifica somas todos os players remotos estão sincronizados. Você tem a garantia de ter bugs fora de sincronia (OOS) para corrigir e isso tornará mais fácil - você terá um aviso prévio de que algo deu errado (o que ajudará você a descobrir as etapas de reprodução) e poderá adicionar mais estado do jogo registrando código suspeito para permitir que você suporte o que está causando o OOS.
fonte
Acho que a ideia do blog vinculada a ainda precisa de sincronização periódica para ser viável - já vi bugs suficientes em jogos RTS em rede que não adotam essa abordagem.
As redes são com perdas, lentas, têm latência e podem até poluir seus dados. "Determinismo de ponto flutuante" (que soa bastante enérgico para me deixar cético) é a menor das suas preocupações na realidade ... especialmente se você usar uma etapa de tempo fixo. com etapas de tempo variável, você precisará interpolar entre etapas de tempo fixo para evitar problemas de determinismo também. Eu acho que isso é geralmente o que se entende por comportamento não determinístico de "ponto flutuante" - apenas que as etapas de tempo variável causam divergências nas integrações - nada a ver com bibliotecas matemáticas ou funções de baixo nível.
A sincronização é a chave.
fonte
Você os torna determinísticos. Para um ótimo exemplo, consulte a Apresentação da GDC do Dungeon Siege sobre como eles tornaram os locais do mundo em rede.
Lembre-se também de que o determinismo também se aplica a eventos 'aleatórios'!
fonte