Estou criando um jogo de física envolvendo corpos rígidos nos quais os jogadores movem peças e peças para resolver um quebra-cabeça / mapa. Um aspecto extremamente importante do jogo é que, quando os jogadores iniciam uma simulação, ela é executada da mesma maneira em todos os lugares , independentemente do sistema operacional, processador, etc.
Há espaço para muita complexidade, e as simulações podem ser executadas por um longo tempo, por isso é importante que o mecanismo de física seja completamente determinístico em relação às operações de ponto flutuante; caso contrário, uma solução pode parecer "resolver" na máquina de um jogador e "falhar" em outro.
Como posso obter esse determinismo no meu jogo? Estou disposto a usar uma variedade de estruturas e linguagens, incluindo Javascript, C ++, Java, Python e C #.
Fui tentado pelo Box2D (C ++) e seus equivalentes em outras linguagens, pois parece atender às minhas necessidades, mas falta um determinismo de ponto flutuante, principalmente com funções trigonométricas.
A melhor opção que vi até agora foi o equivalente em Java do Box2D (JBox2D). Parece fazer uma tentativa de determinismo de ponto flutuante usando StrictMath
mais do que Math
para muitas operações, mas não está claro se esse mecanismo garantirá tudo o que preciso, pois ainda não construí o jogo.
É possível usar ou modificar um mecanismo existente para atender às minhas necessidades? Ou precisarei construir um mecanismo por conta própria?
EDIT: ignore o restante desta postagem se você não se importa com o motivo de alguém precisar de tanta precisão. Pessoas em comentários e respostas parecem acreditar que estou procurando algo que não deveria e, como tal, explicarei melhor como o jogo deve funcionar.
O jogador recebe um quebra-cabeça ou nível, que contém obstáculos e um objetivo. Inicialmente, uma simulação não está sendo executada. Eles podem usar peças ou ferramentas fornecidas para construir uma máquina. Depois que eles pressionam start, a simulação começa e eles não podem mais editar sua máquina. Se a máquina resolver o mapa, o jogador venceu o nível. Caso contrário, eles terão que pressionar parar, alterar sua máquina e tentar novamente.
A razão pela qual tudo precisa ser determinista é porque planejo produzir código que mapeie todas as máquinas (um conjunto de peças e ferramentas que tentam resolver um nível) para um arquivo xml ou json, registrando a posição, o tamanho e a rotação de cada peça. Será possível, então, que os jogadores compartilhem soluções (representadas por esses arquivos) para que possam verificar soluções, aprender uns com os outros, realizar competições, colaborar, etc ... É claro que a maioria das soluções, principalmente as simples ou rápidas os, não serão afetados pela falta de determinismo. Mas os projetos lentos ou complexos que resolvem níveis realmente difíceis podem, e esses são os que provavelmente serão os mais interessantes e que vale a pena compartilhar.
fonte
Respostas:
Manipulando números de ponto flutuante de maneira determinística
O ponto flutuante é determinístico. Bem, deveria ser. É complicado.
Há muita literatura sobre números de ponto flutuante:
E como eles são problemáticos:
Para resumo. Pelo menos, em um único encadeamento, as mesmas operações, com os mesmos dados, ocorrendo na mesma ordem, devem ser determinísticas. Assim, podemos começar nos preocupando com insumos e reordenando.
Uma dessas entradas que causa problemas é o tempo.
Primeiro de tudo, você deve sempre calcular o mesmo timestep. Não estou dizendo para não medir o tempo, estou dizendo que você não passará o tempo para a simulação da física, porque variações no tempo são uma fonte de ruído na simulação.
Por que você mede o tempo se não está passando para a simulação de física? Você deseja medir o tempo decorrido para saber quando uma etapa de simulação deve ser chamada e - supondo que você esteja usando o sono - quanto tempo dormir.
Portanto:
Agora, reordenação de instruções.
O compilador pode decidir que
f * a + b
é o mesmo queb + f * a
, no entanto, que pode ter um resultado diferente. Também pode ser compilado para o fmadd , ou pode-se decidir pegar várias linhas como essa que acontecem juntas e escrevê-las com o SIMD , ou alguma outra otimização em que não consigo pensar agora. E lembre-se de que queremos que as mesmas operações ocorram na mesma ordem, é lógico que queremos controlar quais operações acontecem.E não, usar o dobro não o salvará.
Você precisa se preocupar com o compilador e sua configuração, em particular para sincronizar números de ponto flutuante na rede. Você precisa obter as compilações para concordar em fazer a mesma coisa.
Indiscutivelmente, escrever montagem seria o ideal. Dessa forma, você decide qual operação fazer. No entanto, isso pode ser um problema para suportar várias plataformas.
Portanto:
O caso dos números de pontos fixos
Devido à maneira como os carros alegóricos são representados na memória, valores grandes perdem a precisão. É lógico que manter seus valores pequenos (grampo) atenua o problema. Assim, não há velocidades enormes nem salas grandes. O que também significa que você pode usar física discreta porque tem menos risco de tunelamento.
Por outro lado, pequenos erros serão acumulados. Então, trunque. Quero dizer, dimensionar e converter para um tipo inteiro. Dessa forma, você sabe que nada está se acumulando. Haverá operações que você pode fazer permanecendo com o tipo inteiro. Quando você precisa voltar ao ponto flutuante, lança e desfaz a escala.
Nota eu digo escala. A ideia é que 1 unidade seja realmente representada como uma potência de duas (16384, por exemplo). Seja o que for, faça uma constante e use-a. Você está basicamente usando-o como número de ponto fixo. De fato, se você puder usar números de pontos fixos adequados de uma biblioteca confiável, é muito melhor.
Eu estou dizendo truncado. Sobre o problema do arredondamento, isso significa que você não pode confiar no último bit de qualquer valor que tenha após o lançamento. Portanto, antes da escala de elenco obter um pouco mais do que você precisa, e trunque depois.
Portanto:
Espere, por que você precisa de ponto flutuante? Você não poderia trabalhar apenas com um tipo inteiro? Oh, certo. Trigonometria e radicação. Você pode calcular tabelas para trigonometria e radicação e colocá-las em sua fonte. Ou você pode implementar os algoritmos usados para computá-los com número de ponto flutuante, exceto usando números de ponto fixo. Sim, você precisa equilibrar memória, desempenho e precisão. No entanto, você pode ficar fora dos números de ponto flutuante e permanecer determinístico.
Você sabia que eles fizeram coisas assim para o PlayStation original? Por favor, encontre o meu cão, patches .
A propósito, não estou dizendo para não usar ponto flutuante para gráficos. Apenas para a física. Quero dizer, claro, as posições dependerão da física. No entanto, como você sabe, um colisor não precisa corresponder a um modelo. Não queremos ver os resultados do truncamento dos modelos.
Assim: USE NÚMEROS DE PONTO FIXO.
Para ficar claro, se você pode usar um compilador que permite especificar como os pontos flutuantes funcionam e isso é suficiente para você, você pode fazer isso. Isso nem sempre é uma opção. Além disso, estamos fazendo isso por determinismo. Os números de pontos fixos não significam que não há erros, afinal eles têm precisão limitada.
Eu não acho que "número de ponto fixo seja difícil" é uma boa razão para não usá-los. E se você deseja um bom motivo para usá-los, é determinismo, em particular determinismo entre plataformas.
Veja também:
Adendo : Estou sugerindo que o tamanho do mundo seja pequeno. Com isso dito, o OP e o Jibb Smart levantam o ponto de que se afastar dos carros alegóricos de origem tem menos precisão. Isso terá um efeito sobre a física, algo que será visto muito mais cedo do que o limite do mundo. Números de pontos fixos, bem, têm precisão fixa, serão igualmente bons (ou ruins, se você preferir) em todos os lugares. O que é bom se queremos determinismo. Também quero mencionar que a maneira como costumamos fazer física tem a propriedade de amplificar pequenas variações. Veja O Efeito Borboleta - Física Determinística no Incredible Machine and Contraption Maker .
Outra maneira de fazer física
Eu estive pensando, a razão pela qual o pequeno erro de precisão nos números de ponto flutuante se amplifica é porque estamos fazendo iterações nesses números. A cada etapa da simulação, pegamos os resultados da última etapa da simulação e fazemos coisas neles. Acumular erros em cima de erros. Esse é o seu efeito borboleta.
Eu não acho que veremos uma única compilação usando um único thread na mesma máquina produzir uma saída diferente pela mesma entrada. No entanto, em outra máquina poderia, ou uma construção diferente poderia.
Há um argumento para testar lá. Se decidirmos exatamente como as coisas devem funcionar e pudermos testar no hardware de destino, não devemos criar builds que tenham um comportamento diferente.
No entanto, há também um argumento para não trabalhar longe que acumula tantos erros. Talvez seja uma oportunidade de fazer física de uma maneira diferente.
Como você deve saber, existe uma física contínua e discreta, ambas trabalham em quanto cada objeto avançaria no passo temporal. No entanto, a física contínua tem os meios para descobrir o instante de colisão em vez de investigar diferentes instantes possíveis para ver se ocorreu uma colisão.
Assim, proponho o seguinte: use as técnicas da física contínua para descobrir quando ocorrerá a próxima colisão de cada objeto, com um grande intervalo de tempo, muito maior que o de uma única etapa de simulação. Então você pega o instante de colisão mais próximo e descobre onde tudo estará naquele instante.
Sim, isso é muito trabalho de uma única etapa de simulação. Isso significa que a simulação não será iniciada instantaneamente ...
... No entanto, você pode simular as próximas etapas da simulação sem verificar a colisão a cada vez, porque você já sabe quando a próxima colisão ocorrerá (ou que nenhuma colisão ocorrerá no grande espaço de tempo). Além disso, os erros acumulados nessa simulação são irrelevantes porque, uma vez que a simulação atinge um grande passo no tempo, colocamos as posições que computamos anteriormente.
Agora, podemos usar o orçamento de tempo que teríamos usado para verificar colisões em cada etapa da simulação para calcular a próxima colisão após a que encontramos. Ou seja, podemos simular com antecedência usando o timestep grande. Supondo um mundo de escopo limitado (isso não funcionará para jogos enormes), deve haver uma fila de estados futuros para a simulação e, em seguida, cada quadro que você interpola do último estado para o próximo.
Eu argumentaria por interpolação. No entanto, como existem acelerações, não podemos simplesmente interpolar tudo da mesma maneira. Em vez disso, precisamos interpolar levando em consideração a aceleração de cada objeto. Por esse motivo, poderíamos apenas atualizar a posição da mesma maneira que fazemos para o timestep grande (o que também significa que é menos propenso a erros, porque não estaríamos usando duas implementações diferentes para o mesmo movimento).
Nota : Se estivermos fazendo esses números de ponto flutuante, essa abordagem não resolverá o problema de os objetos se comportarem de maneira diferente quanto mais distantes da origem. No entanto, embora seja verdade que a precisão é perdida quanto mais longe você for da origem, isso ainda é determinístico. De fato, é por isso que nem trouxe isso à tona originalmente.
Termo aditivo
Do OP no comentário :
Então, nenhum formato binário, certo? Isso significa que também precisamos nos preocupar com os números de ponto flutuante recuperados, correspondentes ou não ao original. Consulte: Precisão do flutuador revisitada: portabilidade de flutuador de nove dígitos
fonte
base64
elementos. Não é uma maneira eficiente de representar grandes quantidades desses dados, mas é incorreto sugerir que eles impedem o uso de representações binárias.Eu trabalho para uma empresa que faz um certo jogo de estratégia em tempo real bem conhecido e posso dizer que o determinismo de ponto flutuante é possível.
O uso de compiladores diferentes, ou o mesmo compilador com configurações diferentes, ou mesmo versões diferentes do mesmo compilador, pode quebrar o determinismo.
Se você precisar de crossplay entre plataformas ou versões de jogos, acho que precisará ir com um ponto fixo - a única crossplay possível que eu conheço com ponto flutuante é entre PC e XBox1, mas isso é muito louco.
Você precisará encontrar um mecanismo de física que seja totalmente determinístico, ou usar um mecanismo de código aberto e torná-lo determinístico ou executar seu próprio mecanismo. No topo da minha cabeça, tenho a sensação de que a Unity de todas as coisas adicionou um mecanismo determinístico de física, mas não tenho certeza se é apenas determinístico na mesma máquina ou determinístico em todas as máquinas.
Se você tentar criar suas próprias coisas, algumas coisas que podem ajudar:
fonte
rsqrtps
?Não tenho certeza se esse é o tipo de resposta que você está procurando, mas uma alternativa pode ser executar os cálculos em um servidor central. Faça com que os clientes enviem a configuração ao seu servidor, permitam que ele execute a simulação (ou recupere uma em cache) e envie os resultados, que são então interpretados pelo cliente e processados em gráficos.
Obviamente, isso interrompe todos os planos que você possa ter para executar o cliente no modo off-line e, dependendo de quão intensamente computacional sejam as simulações, pode ser necessário um servidor muito poderoso. Ou vários, mas pelo menos você tem a opção de garantir que eles tenham a mesma configuração de hardware e software. Uma simulação em tempo real pode ser difícil, mas não impossível (pense em transmissões de vídeo ao vivo - elas funcionam, mas com um pequeno atraso).
fonte
Vou dar uma sugestão contra-intuitiva de que, embora não seja 100% confiável, funcione bem na maior parte do tempo e é muito fácil de implementar.
Reduza a precisão.
Use um tamanho de etapa de tempo constante pré-determinado, execute a física em cada etapa no flutuador padrão de precisão dupla, mas depois quantize a resolução de todas as variáveis em precisão única (ou algo ainda pior) após cada etapa. Em seguida, a maioria dos desvios possíveis que a reordenação de ponto flutuante poderia introduzir (em comparação com uma execução de referência do mesmo programa) será cortada porque esses desvios ocorrem em dígitos que nem existem na precisão reduzida. Assim, o desvio não tem a chance de um acúmulo de Lyapunov (efeito borboleta) que acabaria se tornando notável.
Obviamente, a simulação será um pouco menos precisa do que poderia ser (em comparação com a física real), mas isso não é realmente notável desde que todas as execuções de seu programa sejam imprecisas da mesma maneira .
Agora, tecnicamente falando, é claro que é possível que uma reordenação cause um desvio que chegue a um dígito de maior significado, mas se os desvios são realmente apenas causados por flutuação e seus valores representam quantidades físicas contínuas, isso é extremamente improvável. Observe que existem meio bilhão de
double
valores entre doissingle
, portanto, pode-se esperar que a grande maioria dos intervalos de tempo na maioria das simulações seja exatamente a mesma entre as execuções da simulação. Os poucos casos em que um desvio passa pela quantização serão esperados em simulações que não duram tanto (pelo menos não com dinâmica caótica).Também recomendo que você considere a abordagem completamente oposta ao que está perguntando: aceite a incerteza ! Se o comportamento é levemente não determinístico, então isso é realmente mais próximo dos experimentos físicos reais. Então, por que não aleatoriamente deliberadamente os parâmetros de partida para cada execução de simulação e exige que a simulação seja bem - sucedida de maneira consistente em várias tentativas? Isso ensinará muito mais sobre física e sobre como projetar as máquinas para serem robustas / lineares o suficiente, em vez de super-frágeis que são apenas realistas em uma simulação.
fonte
Crie sua própria classe para armazenar números!
Você pode forçar um comportamento determinístico se souber exatamente como os cálculos serão executados. Por exemplo, se as únicas operações com as quais você lida são multiplicação, divisão, adição e subtração, seria suficiente representar todos os números como apenas um número racional. Para fazer isso, uma simples classe Rational funcionaria perfeitamente.
Mas se você quiser lidar com cálculos mais complicados (possivelmente funções trigonométricas, por exemplo), terá que escrever essas funções sozinho. Se você quisesse obter o seno de um número, seria necessário escrever uma função que se aproxima do seno de um número enquanto usa apenas as operações mencionadas acima. Tudo isso é possível e, na minha opinião, contorna muitos dos detalhes cabeludos de outras respostas. A desvantagem é que você terá que lidar com algumas contas.
fonte
*
/
+
-
, os denominadores se tornarão cada vez maiores ao longo do tempo.Há alguma confusão de terminologia aqui. Um sistema físico pode ser completamente determinístico, mas impossível de modelar por um período útil, porque seu comportamento é extremamente sensível às condições iniciais e uma mudança infinitesimalmente pequena nas condições iniciais produzirá comportamentos completamente diferentes.
Aqui está um vídeo de um dispositivo real cujo comportamento é intencionalmente imprevisível, exceto no sentido estatístico:
https://www.youtube.com/watch?v=EvHiee7gs9Y
É fácil construir sistemas matemáticos simples (usando apenas adição e multiplicação) em que o resultado após N etapas depende da n-ésima casa decimal das condições iniciais. Escrever um software para modelar esse sistema de forma consistente, em qualquer hardware e software que o usuário possa ter, é quase impossível - mesmo se você tiver um orçamento grande o suficiente para testar o aplicativo em todas as combinações prováveis de hardware e software.
A melhor maneira de corrigir isso é atacar o problema na sua origem: torne a física do seu jogo o mais determinista possível para obter resultados reproduzíveis.
A alternativa é tentar torná-lo determinístico, aprimorando o software do computador para modelar algo que não é o que a física especificou. O problema é que você introduziu várias camadas de complexidade no sistema, em comparação com a alteração explícita da física.
Como um exemplo específico, suponha que seu jogo envolva colisões de corpos rígidos. Mesmo se você ignorar o atrito, a modelagem exata de colisões entre objetos de formato arbitrário que podem estar girando enquanto se movem é na prática impossível. Mas se você mudar a situação para que os únicos objetos sejam tijolos retangulares não rotativos, a vida se tornará muito mais simples. Se os objetos no seu jogo não parecerem tijolos, oculte esse fato com alguns gráficos "não físicos" - por exemplo, oculte literalmente o instante de colisão atrás de fumaça ou chamas, ou um balão de texto de desenho animado "Ai" como queiras.
O jogador tem que descobrir a física do jogo jogando o jogo. Não importa se não é "totalmente realista" desde que seja autoconsistente e semelhante o suficiente à experiência do senso comum para ser plausível.
Se você faz a própria física se comportar de maneira estável, um modelo de computador dela também pode produzir resultados estáveis, pelo menos no sentido de que erros de arredondamento serão irrelevantes.
fonte
Use precisão de ponto flutuante duplo , em vez de precisão de ponto flutuante único . Embora não seja perfeito, é preciso o suficiente para ser considerado determinístico em sua física. Você pode enviar um foguete para a lua com precisão de ponto flutuante duplo, mas não precisão de ponto flutuante único.
Se você realmente precisa de um determinismo perfeito, use a matemática do ponto fixo . Isso fornecerá menos precisão (supondo que você use o mesmo número de bits), mas resultados determinísticos. Não tenho conhecimento de nenhum mecanismo de física que use matemática de ponto fixo; portanto, você pode precisar escrever o seu próprio se quiser seguir esse caminho. (Algo que eu desaconselharia.)
fonte
Use o padrão Memento .
Na sua execução inicial, salve os dados posicionais de cada quadro ou quaisquer benchmarks necessários. Se isso for muito ruim, faça-o apenas a cada n quadros.
Então, quando você reproduzir a simulação, siga a física arbitrária, mas atualize os dados posicionais a cada n quadros.
Pseudo-código excessivamente simplificado:
fonte