O que realmente está se movendo em um corredor sem fim?

107

Por exemplo, o famoso jogo Flappy Bird, ou qualquer coisa desse tipo, é o jogador (neste caso, o pássaro ou a câmera que você preferir) avançando ou o mundo inteiro se movendo para trás (o pássaro muda apenas a posição Y e tem uma posição X constante)?

Lendrit Ibrahimi
fonte
1
Excluímos muitos comentários fora do tópico desta pergunta e de suas respostas. Também movemos várias discussões razoáveis ​​para conversar, em alguns casos repetidamente. Apesar disso, vários usuários sentiram a necessidade de ignorar o fato de que os comentários não são para discussões prolongadas e continuaram. Como tal, esta questão foi temporariamente bloqueada.
Josh

Respostas:

146

Discordo um pouco da resposta de Philipp ; ou pelo menos com a forma como ele a apresentou. Dá a impressão de que mudar o mundo ao redor do player pode ser uma idéia melhor; quando é exatamente o oposto. Então aqui está a minha própria resposta ...


Ambas as opções podem funcionar, mas geralmente é uma má idéia "inverter a física" movendo o mundo ao redor do jogador, e não o jogador ao redor do mundo.


Perda / desperdício de desempenho:

O mundo normalmente terá muitos objetos; muitos, se não a maioria, estáticos ou adormecidos. O jogador terá um ou relativamente poucos objetos. Mover o mundo inteiro ao redor do jogador significa mover tudo na cena, exceto o jogador. Objetos estáticos, objetos dinâmicos adormecidos, objetos dinâmicos ativos, fontes de luz, fontes sonoras, etc; tudo tem que ser movido.

Isso é (obviamente) consideravelmente mais caro do que mover apenas o que realmente está em movimento (o jogador e talvez mais algumas coisas).


Manutenção e extensibilidade:

Mover o mundo ao redor do jogador faz com que o mundo (e tudo nele) seja o ponto em que as coisas estão acontecendo mais ativamente. Qualquer bug ou alteração no sistema significa que, potencialmente, tudo muda. Esta não é uma boa maneira de fazer as coisas; você deseja que os bugs / alterações sejam o mais isolados possível, para que você não tenha comportamentos inesperados em algum lugar em que não tenha feito alterações.

Há também muitos outros problemas com essa abordagem. Por exemplo, ele quebra muitas suposições de como as coisas devem funcionar no mecanismo. Você não seria capaz de usar dinâmico RigidBodypara nada além do player, por exemplo; como um objeto com um anexo RigidBodynão definido como cinemático se comportará inesperadamente ao definir a posição / rotação / escala (o que você faria em todos os quadros, para cada objeto na cena, exceto o jogador 😨)


Portanto, a resposta é apenas mover o jogador!

Bem ... sim e não . Como mencionado na resposta de Philipp, em um tipo de jogo de corredor infinito (ou qualquer jogo com uma grande área explorável contínua), ir muito longe da origem acabaria por introduzir FPPEs ( Erros de precisão de ponto flutuante) visíveis e, ainda mais, eventualmente, transbordar o tipo numérico, causando o travamento do jogo ou, basicamente, fazer com que o mundo do jogo fume de crack ... Em esteróides! Because (porque a essa altura, os FPPEs já tornariam o jogo no crack "normal")


A solução real :

Não faça nem faça os dois! Você deve manter o mundo estático e mover o jogador ao seu redor. Mas "re-enraíza" tanto o jogador quanto o mundo, quando o jogador começa a ficar muito longe da raiz (posição [0, 0, 0]) da cena.

Se você mantiver a posição relativa das coisas (do jogador para o mundo ao seu redor) e fizer esse processo em uma única atualização de quadro, o jogador (real) nem perceberá!

Para fazer isso, você tem duas opções principais:

  1. Mova o jogador para a raiz da cena e mova o pedaço do mundo para sua nova posição em relação ao jogador.
  2. Pense no mundo como uma grade; mova a parte da grade em que o jogador está para a raiz e mova o jogador para sua nova posição em relação à parte da grade.

Aqui está um exemplo deste processo em ação


Mas, quão longe é longe demais?

Se você observar o código-fonte do Unity, eles usarão 1e-5( 0.00001) como base para considerar dois valores de ponto flutuante "iguais", dentro Vector2e Vector3(os tipos de dados responsáveis ​​pelas posições dos objetos, [euler-] rotações e escalas). Como a perda de precisão de ponto flutuante ocorre em ambos os sentidos do zero, é seguro assumir que qualquer coisa sob 1e+5( 100000) unidades de distância da cena raiz / origem é segura para trabalhar.

Mas! Desde a...

  1. É mais apropriado criar um sistema para lidar com esses processos de reinicialização automática.
  2. Seja qual for o seu jogo, não há necessidade de uma "seção" contígua do mundo ter 100.000 unidades (metros [?]) De largura.

... então provavelmente é uma boa ideia re-enraizar muito mais cedo / com mais frequência do que a marca de 100.000 unidades. O exemplo de vídeo que forneci parece ser feito a cada 1000 unidades, por exemplo.

XenoRo
fonte
2
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo . Por favor, use esse bate-papo em vez de postar mais comentários aqui.
Alexandre Vaillancourt
> Isso é (obviamente) consideravelmente mais caro do que mover apenas o que realmente está se movendo <É? Ao renderizar, você terá que fazer uma multiplicação de matrizes com a matriz da câmera em todos os objetos, pois a câmera fixada na identidade seria mais barata dessa maneira.
Matsemann
O momento em que o desenvolvimento do jogo torna-se o desenvolvimento de unidade ...
LJ ᛃ
@ Matsemann - Se você tivesse ido conversar, teria notado que esse não é um ponto novo. Como já explicado, renderize a cena NOT-IGUAL; eles são assuntos e pipelines diferentes e quase completamente independentes. Fazer uma inversão do quadro de referência na maneira como os objetos são posicionados na cena significa que você terá um processo mais pesado nas duas extremidades , em vez de apenas na renderização. --- Além disso, mesmo se o desempenho fosse igualmente negociado, como você argumenta, todos os outros problemas com a inversão ainda permaneceriam sem solução, tornando essa abordagem ainda pior do que a re-enraizamento.
XenoRo
@LJ ᛃ A pergunta foi feita sobre a unidade especificamente (veja as tags do OP antes das edições de D. Everhard [desfigurando o IMHO]) e , portanto, a resposta foi adaptada para refletir isso . --- Mas aqui está a melhor parte: Seja Unity, Unreal, CryEngine, Source, etc ... Os melhores e / ou motores mais populares funcionam dessa maneira (e fazem isso por uma razão), portanto a resposta não é apenas perfeitamente válido em geral, mas os argumentos apresentados ainda estão no auge da precisão . De um modo geral, se você inverter o quadro de referência da física, poderá encontrar problemas, especialmente em objetos dinâmicos.
XenoRo
89

Ambas as opções funcionam.

Mas se você quiser que o corredor interminável seja realmente interminável, precisará manter o jogador parado e mover o mundo. Caso contrário, você atingirá os limites das variáveis ​​usadas para armazenar a posição X. Um número inteiro acabaria transbordando e uma variável de ponto flutuante se tornaria cada vez menos precisa, o que tornaria a jogabilidade mais lenta depois de um tempo. Mas você pode evitar esse problema usando um tipo grande o suficiente para que ninguém encontre esses problemas dentro do intervalo de tempo que se possa reproduzir em uma sessão (quando um jogador move 1000 pixels por segundo, um número inteiro de 32 bits transborda após 49 dias).

Faça o que parecer conceitualmente mais intuitivo para você.

Philipp
fonte
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
Josh
1
Eu não acho que isso resolve nada. Em vez de manter a posição do jogador (e da câmera), você mantém a posição de rolagem do mundo - como isso é diferente?
Agent_L
1
@Agent_L Geralmente, os mundos são gerados aos poucos e cada peça tem sua posição X / Y. Quando a peça fica atrás do jogador o suficiente (por exemplo, fora da tela), ela é excluída e é gerada apenas uma certa quantia de antecedência, portanto está sempre dentro de um intervalo relativamente pequeno - um jogo que eu fiz nunca teve uma coordenada maior maior que ~ 1000 ou menor que 0, apesar de ser um corredor infinito e gerado aleatoriamente, por causa disso - acompanhei a posição do jogador em cada pedaço e o índice de cada pedaço na sequência.
Nic Hartley
Eu não compro o conceito de "excedente variável". Se o jogador não estiver se movendo, o "fundo" será negativamente compensado pela mesma quantia que o jogador seria compensado se estivesse em movimento. Ambos os casos teriam exatamente o mesmo problema (em direções opostas), e ambos exigiriam tratamento especial nos limites do estouro.
spender
2
a 1000 pixels por segundo, levaria mais de 586 milhões de anos para ultrapassar um número inteiro de 64 bits. Isso é realmente realmente um problema se você estiver usando tipos muito pequenos, como números inteiros de 16 bits.
Lyndon Branco
38

Com base na resposta do XenoRo , em vez do método de re-root que eles descrevem, pode-se fazer o seguinte:

Crie um buffer circular de partes do seu mapa infinito gerado, pelas quais seu personagem se move com a posição atualizada com o módulo aritmético (para que você apenas rode o buffer circular). Comece substituindo partes do seu buffer assim que seu personagem sair do pedaço. A equação de atualização dos jogadores seria algo como:

player.position = (player.position + player.velocity) % worldBuffer.width;

Aqui está um exemplo pictórico do que estou falando:

insira a descrição da imagem aqui

Aqui está um exemplo do que acontece no acondicionamento no final.

insira a descrição da imagem aqui

Com esse método, você nunca se depara com erros de precisão, mas pode ser necessário criar um buffer muito grande se pretender fazer isso em 3d com uma distância de visão muito longa (pois você ainda precisará ver com antecedência) ) Se o tamanho do seu pedaço de pássaro flappy provavelmente for apenas o tamanho necessário para realizar uma única cena para uma única tela, e seu buffer poderá ser muito pequeno.

Observe que você começará a obter resultados repetidos com QUALQUER prng, e o número máximo de seqüências não repetitivas de uma geração PRNG é normalmente menor que o comprimento do pow (2, número de bits usado internamente), com merzenne twister, isso não é muito de um problema, pois usa 2,5k de estado interno, mas se você usar a variante minúscula, terá 2 ^ 127-1 iterações máximas antes de repetir (ou pior), ainda é um número astronomicamente grande . Você pode corrigir problemas de repetição de período, mesmo que o seu PRNG tenha um curto período por meio de boas funções de mistura de avalanches para a semente (à medida que você adiciona mais estado implicitamente) repetidamente.

opa
fonte
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
Josh
15

Como já solicitado e aceito, realmente depende do escopo e do estilo do seu jogo, mas como não foi mencionado: o FlappyBird move o obstáculo pela tela, e não pelo jogador em todo o mundo.

Um criador está instanciando objetos fora da tela com uma velocidade fixa na Vector2.leftdireção.

Stephan
fonte
3
Isso funciona para o FlappyBird porque é um jogo muito simples. Como mencionado na minha resposta, a mesma técnica, embora ainda possível , seria muito problemática em algo mais complexo (com mais objetos / detalhes do mundo), como o Subway Surfer.
XenoRo
15
Eu concordo, e sua resposta é muito completa. Eu simplesmente não vi ninguém mencionar qual método o pássaro para pássaros realmente usa, então achei que o adicionaria.
Stephan