9. Floating origin: por que o mundo treme longe de casa
Um número de 32 bits não consegue ser grande e preciso ao mesmo tempo.
Os motores guardam posição em float de 32 bits. Um float tem 24 bits para os dígitos e o resto para a "vírgula". Isso significa que a menor diferença que ele consegue representar cresce com o tamanho do número. Perto de zero, ele distingue milionésimos. A 100 mil metros da origem, só distingue quase um centímetro. A 10 milhões, só um metro.
Espaçamento entre floats vizinhos (o "passo mínimo") em cada distância
Qualquer movimento menor que o passo mínimo é arredondado para zero ou para o passo inteiro. Um personagem andando a 0,02 m por frame a 1 milhão de metros da origem só se move quando o acumulado passa de 0,06 m: ele anda aos pulos. Física, sombras e animações sofrem o mesmo.
Veja tremer
O jogador anda em linha reta, devagar, e a câmera o acompanha. As posições passam por float de 32 bits (Math.fround), como num motor. Arraste o slider de distância devagar, de 1 metro até 10 milhões, e observe três coisas: as pedras do chão, o rastro (um ponto por frame) e o gráfico da posição.
Perto da origem o rastro é uma fileira de pontos igualmente espaçados e o gráfico é uma reta. Conforme a distância sobe, os pontos do rastro se agrupam (o jogador fica parado uns frames e pula) e o gráfico vira escada. Bem longe, os degraus viram metros inteiros. Aumentar a velocidade encurta o intervalo entre degraus, mas o tamanho de cada degrau só depende da distância.
Aqui a posição verdadeira avança em double e só o valor guardado na cena é arredondado para float. Se o motor também somar em float (posição += velocidade × dt), é pior: quando o movimento de um frame cai abaixo de metade do passo mínimo, a soma devolve o mesmo número e o jogador trava de vez.
A solução: mover o mundo, não o jogador
Quando o jogador se afasta mais que um limite da origem (por exemplo 1000 m), o jogo desloca tudo pela posição dele: cada objeto da cena recebe posição −= posição_do_jogador, incluindo o próprio jogador, que volta para (0, 0, 0). O mundo "escorrega" por baixo dos pés dele. Ninguém percebe, porque tudo se moveu junto.
// a cada frame, ou quando o jogador cruza a borda de um chunk
se |jogador.posição| > LIMITE:
desloc = jogador.posição
para cada objeto raiz na cena: // chunks, itens, inimigos, partículas, o próprio jogador
objeto.posição -= desloc
origem_do_mundo += desloc // quantas vezes já deslocamos, para saber a posição "verdadeira"
| Nome | O que é |
|---|---|
LIMITE | distância que dispara o deslocamento. 1000 a 5000 m é comum: longe o bastante para não disparar toda hora, perto o bastante para o float ainda estar preciso. |
desloc | o vetor subtraído de todo mundo. Costuma ser arredondado para múltiplo do tamanho do chunk, para os chunks continuarem alinhados. |
origem_do_mundo | acumulado dos deslocamentos. Posição verdadeira de qualquer coisa = posição na cena + origem_do_mundo. É isso que entra no ruído e nas chaves dos itens. |
Rigidbody em movimento pode dar um solavanco se você mover o transform no meio do passo de física; use Physics.SyncTransforms ou faça o deslocamento no FixedUpdate), câmera com suavização (ela tenta "alcançar" a posição antiga e dá um tranco; desloque a câmera junto e zere o histórico), e trilhas/linhas renderizadas (TrailRenderer).
posição + origem_do_mundo) para decidir as coordenadas de chunk. A malha de cada chunk fica em coordenadas locais pequenas. O momento mais limpo de deslocar é exatamente quando o jogador troca de chunk: os chunks já vão ser rearranjados mesmo.