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:
- Meça o tempo: Sim
- Use o tempo na simulação: Não
Agora, reordenação de instruções.
O compilador pode decidir que f * a + b
é o mesmo que b + 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:
- Mantenha os valores pequenos: Sim
- Arredondamento cuidadoso: Sim
- Números de pontos fixos quando possível: Sim
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 :
A idéia é que os jogadores possam salvar suas máquinas em algum formato (como xml ou json), para que a posição e a rotação de cada peça sejam registradas. Esse arquivo xml ou json será usado para reproduzir a máquina no computador de outro jogador.
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