Geração processual, atualizações de jogos e efeito borboleta

10

NOTA: Perguntei isso no Stack Overflow há alguns dias, mas tinha muito poucas visualizações e nenhuma resposta. Achei que eu deveria perguntar em gamdev.stackexchange.

Essa é uma pergunta / solicitação geral de aconselhamento sobre manutenção de um sistema de geração de procedimentos por meio de várias atualizações pós-lançamento, sem interromper o conteúdo gerado anteriormente.

Estou tentando encontrar informações e técnicas para evitar problemas de "Efeito Borboleta" ao criar conteúdo processual para jogos. Ao usar um gerador de números aleatórios semeados, uma sequência repetida de números aleatórios pode ser usada para criar um mundo reproduzível. Enquanto alguns jogos simplesmente salvam o mundo gerado no disco, uma vez gerado, um dos poderosos recursos da geração procedural é o fato de que você pode confiar na reprodutibilidade da sequência numérica para recriar uma região várias vezes da mesma maneira, eliminando a necessidade de persistência. Devido às restrições da minha situação específica, devo minimizar a persistência e preciso confiar na concentração puramente propagada, tanto quanto possível.

O principal perigo dessa abordagem é que mesmo a menor mudança no sistema processual de geração pode causar um efeito borboleta que muda o mundo inteiro. Isso torna muito complicado atualizar o jogo sem destruir os mundos que os jogadores estão explorando.

A principal técnica que tenho usado para evitar esse problema é projetar a geração procedural em várias fases, cada uma das quais com seu próprio gerador de números aleatórios semeados. Isso significa que cada subsistema é independente e, se algo quebrar, não afetará tudo no mundo. No entanto, parece que ele ainda tem muito potencial de "quebra", mesmo que em uma parte isolada do jogo.

Outra maneira possível de lidar com esse problema pode ser manter versões completas de seus geradores dentro do código e continuar usando o gerador correto para uma determinada instância mundial. Isso me parece um pesadelo de manutenção e estou curioso para saber se alguém realmente faz isso.

Portanto, minha pergunta é realmente uma solicitação de conselhos gerais, técnicas e padrões de design para lidar com esse problema do efeito borboleta, especialmente no contexto de atualizações de jogos pós-lançamento. (Espero que essa não seja uma pergunta muito ampla.)

Atualmente, estou trabalhando no Unity3D / C #, embora essa seja uma pergunta independente de idioma.

ATUALIZAR:

Obrigado pelas respostas.

Parece cada vez mais que os dados estáticos são a melhor e mais segura abordagem, e também que ao armazenar muitos dados estáticos não é uma opção, ter uma campanha longa em um mundo gerado exigiria uma versão rigorosa dos geradores usados. O motivo da limitação no meu caso é a necessidade de salvar / sincronizar na nuvem baseado em dispositivos móveis. Minha solução pode ser encontrar maneiras de armazenar pequenas quantidades de dados compactos sobre coisas essenciais.

Acho que o conceito de "Gaiolas" de Ventobravo é uma maneira particularmente útil de pensar sobre as coisas. Uma gaiola é basicamente um ponto de repetição, impedindo os efeitos de pequenas mudanças, isto é, enjaulando a borboleta.

nulo
fonte
Estou votando para encerrar esta questão como fora de tópico, porque é muito ampla.
Almo15
Eu percebo que é muito amplo. Você recomendaria que eu tentasse um fórum gamedev ou algo assim? Não há realmente nenhuma maneira de tornar a questão mais específica. Eu esperava ouvir alguém de muita experiência nessa área, com alguns truques astutos que não me ocorreram.
Nulo
2
Almo está enganado. Não é nada amplo. Essa é uma pergunta excelente e suficientemente estreita para que sejam dadas boas respostas. É algo que eu acho que muitos de nós, processuais, frequentemente ponderamos.
coordenador

Respostas:

8

Eu acho que você cobriu as bases aqui:

  • Utilizando múltiplos geradores ou propagando novamente a intervalos (por exemplo, usando hashes espaciais) para limitar o transbordamento de mudanças. Provavelmente isso funciona para conteúdo cosmético, mas, como você aponta, ainda pode causar quebras contidas em uma seção.

  • Acompanhar a versão do gerador usada no arquivo salvo e responder adequadamente. O que "apropriado" significa poderia ser ...

    • Manter um histórico de todas as versões anteriores dos geradores em seu jogo executável e usar o que corresponder ao salvamento. Isso torna mais difícil corrigir erros se os jogadores continuarem usando salvamentos antigos.
    • Avisando o player que esse arquivo salvo é de uma versão antiga e fornecendo um link para acessar essa versão como um executável separado. Bom para jogos com campanhas que duram de algumas horas a dias, ruim para uma campanha que você espera jogar por semanas ou mais.
    • Mantenha apenas as últimas nversões do gerador em seu executável. Se o arquivo salvo usar uma dessas versões recentes, (ofereça-se para) atualize o arquivo salvo para a versão mais recente. Isso usa o gerador apropriado para descompactar qualquer estado desatualizado em literais (ou em deltas da saída do novo gerador na mesma semente, se forem muito semelhantes). Qualquer novo estado daqui em diante vem dos mais novos geradores. Jogadores que não jogam por muito tempo podem ficar para trás. E, no pior dos casos, você acaba armazenando todo o estado do jogo na forma literal; nesse caso, você também pode ...
  • Se você espera alterar sua lógica de geração com frequência e não deseja interromper a compatibilidade com versões anteriores, não confie no determinismo do gerador: salve todo o estado em seu arquivo salvo. (ou seja, "Nuke it orbit. É a única maneira de ter certeza")

DMGregory
fonte
Se você criou as regras de geração, existe uma maneira de reverter a geração? IE, dado o estado do jogo, você pode reverter isso para uma semente? Se isso for possível com seus dados, em vez de vincular o jogador a uma versão diferente do jogo, você poderá fornecer um utilitário de atualização que gere um mundo a partir de uma semente com o sistema mais antigo e use o estado gerado para produzir uma semente para o novo gerador. Talvez você precise avisar seus jogadores de uma espera pela conversão.
Joe
1
Isso geralmente não é possível. Você nem está garantido de que existe uma semente para o novo gerador que fornece a mesma saída que o antigo. Geralmente essas sementes contêm cerca de 64 bits, mas o número de mundos possíveis que seu jogo poderia suportar provavelmente será maior que 2 ^ 64, portanto, cada gerador apenas produz um subconjunto deles. Mudar o gerador provavelmente resultará em um novo subconjunto de níveis, que pode ter pouca ou nenhuma interseção com o conjunto do gerador anterior.
DMGregory
Foi difícil escolher a resposta "certa". Eu escolhi este porque era conciso e resumiu as principais questões de uma maneira clara. Obrigado.
Nulo
4

A fonte primária desse efeito borboleta não é a geração de números - o que deve ser fácil o suficiente para manter a determinação de um único gerador de números -, mas o uso desses números pelo código do cliente. Mudanças de código são o verdadeiro desafio para manter as coisas estáveis.

Código: testes de unidade A melhor maneira de garantir que algumas pequenas mudanças em algum lugar não se manifestem em nenhum lugar acidentalmente é incluir testes de unidade completos para todos os aspectos generativos em sua compilação. Isso é verdade para qualquer parte do código compacto em que a alteração de uma coisa possa afetar muitos outros - você precisa de testes para todos, para poder ver em uma única compilação o que foi impactado.

Números: Seqüências / Slots Periódicos Digamos que você tenha um gerador de números que serve para tudo. Não atribui significado, apenas cospe números em sequência - como qualquer PRNG. Dada a mesma semente em duas execuções, obtemos as mesmas seqüências, sim? Agora você pensa um pouco sobre as coisas e decide que haverá talvez 30 aspectos do seu jogo que precisarão ser regularmente fornecidos com um valor aleatório. Aqui, atribuímos uma sequência de ciclo de 30 slots, por exemplo, todo primeiro número na sequência é um layout de terreno acidentado, todo segundo número é perturbação do terreno ... etc. ... todo décimo número adiciona algum erro ao estado AI para realismo. Então seu período é 30.

Após 10, você tem 20 slots livres que podem ser usados ​​para outros aspectos à medida que o design do jogo avança. É claro que o custo aqui é que você deve gerar números para os slots 11 a 30, mesmo que eles não estejam em uso no momento , ou seja, complete o período, para voltar à próxima sequência de 1 a 10. Isso tem um custo de CPU, embora deva ser menor (dependendo do número de slots livres). A outra desvantagem é que você precisa ter certeza de que seu design final pode ser acomodado no número de slots que você disponibilizou no início de seu processo de desenvolvimento ... e quanto mais você atribuir no início, mais slots "vazios" você potencialmente precisa passar por cada uma delas, para fazer as coisas funcionarem.

Os efeitos disso são:

  • Você tem um gerador produzindo números para tudo
  • Alterar o número de aspectos para os quais você precisa gerar números não afetará o determinismo (desde que seu período seja grande o suficiente para acomodar todos os aspectos)

É claro que haverá um longo período durante o qual seu jogo não estará disponível ao público - em alfa, por assim dizer - para que você possa reduzir de, digamos, 30 para 20 aspectos sem impactar nenhum jogador, somente você, se perceber que você tinha atribuído maneira muitas ranhuras no início. Obviamente, isso salvaria alguns ciclos da CPU. Mas lembre-se de que uma boa função de hash (que você mesmo pode escrever) deve ser extremamente rápida, de qualquer maneira. Portanto, ter que executar slots extras não deve ser caro.

Engenheiro
fonte
Oi. Isso soa semelhante a algumas coisas que estou fazendo. Normalmente, eu gero um monte de sub-sementes com base na semente inicial do mundo. Recentemente, comecei a pré-gerar uma matriz longa de ruído e, em seguida, cada "slot" é simplesmente um índice nessa matriz. Dessa forma, cada subsistema pode simplesmente pegar a semente certa e trabalhar isoladamente. Outra ótima técnica é usar as coordenadas x, y para gerar uma semente para cada local. Eu estou usando o código de resposta de eufórico desta página pilha: programmers.stackexchange.com/questions/161336/...
nula
3

Se você deseja persistência com o PCG, sugiro que trate o próprio código do PCG como dados . Assim como você persistiria nos dados através de revisões com conteúdo regular, com conteúdo gerado, se desejar persistir entre revisões, você precisará persistir no gerador.

Obviamente, a abordagem mais popular é converter dados gerados em dados estáticos, como você mencionou.

Não conheço exemplos de jogos que mantêm muitas versões de geradores, porque a persistência é incomum nos jogos PCG - é por isso que o permadeath geralmente anda de mãos dadas com o PCG. No entanto, existem muitos exemplos de vários PCGs, mesmo do mesmo tipo, dentro do mesmo jogo. Por exemplo, o Unangband possui muitos geradores separados para salas de masmorra e, à medida que novos são adicionados, os antigos ainda funcionam da mesma maneira. A manutenção é de sua responsabilidade. Uma maneira de mantê-lo sustentável é usar scripts para implementar seus geradores, mantendo-os isolados com o restante do código do jogo.

congusbongus
fonte
É uma ideia inteligente, simplesmente usar geradores diferentes para áreas diferentes.
Nulo
2

Eu mantenho uma área de cerca de 30000 quilômetros quadrados, mantendo cerca de 1 milhão de edifícios e outros objetos, além de posicionamentos aleatórios de coisas diversas. Uma simulação ao ar livre ofc. Os dados armazenados são de aproximadamente 4 GB. Tenho sorte de ter espaço de armazenamento, mas não é ilimitado.

Aleatório é aleatório, não controlado. Mas pode-se prender um pouco:

  • Controle o início e o fim do fim (como mencionado em outras postagens, o número inicial e quantos números gerados).
  • Limite seu espaço numérico, por exemplo. gerar números inteiros entre 0 e 100 somente.
  • Desloque seu espaço numérico, adicionando um valor (por exemplo, 100 + [números gerados entre 0 e 100] produz números aleatórios entre 100 e 200)
  • Escale-o (por exemplo, multiplique por 0,1)
  • E aplique várias gaiolas à sua volta. Isso está reduzindo, destruindo parte da geração. Por exemplo. se gerar em espaço bidimensional, pode-se colocar um retângulo em cima de pares de números e desfazer o que está do lado de fora. Ou um círculo ou um polígono. Se no espaço 3D, pode-se aceitar, por exemplo, apenas trigêmeos que residem dentro de uma esfera ou alguma outra forma (agora pensando visualmente, mas isso não necessariamente tem nada a ver com a visualização ou o posicionamento real).

É sobre isso. Gaiolas também consomem dados, infelizmente.

Há um ditado em finlandês: Hajota ja hallitse. Traduz para dividir e conquistar .

Abandonei rapidamente a ideia de definição precisa dos mínimos detalhes. Random quer liberdade, então conseguiu a liberdade. Deixe a borboleta voar - dentro da gaiola. Em vez disso, concentrei-me em ter uma maneira rica de definir (e manter !!) as gaiolas. Não importa que carros sejam, desde que sejam azuis ou azuis escuros (um empregador chato disse uma vez :-)). "Azul ou azul escuro" é a gaiola (muito pequena) aqui, ao longo da dimensão da cor.

O que é gerenciável para controlar e gerenciar espaços numéricos?

  • Uma grade booleana é (os bits são pequenos!)
  • Os pontos de canto são
  • Como é uma estrutura de árvore (= a seguir em "gaiolas segurando gaiolas")

Em termos de manutenção e em termos de intercompatibilidade, temos
: se version = n then
: elseif version = m then ...
Sim, a base de código aumenta :-).

Coisas familiares. Sua maneira correta de avançar seria definir um método rico para dividir e conquistar e sacrificar alguns dados sobre isso. Então, sempre que possível, dê liberdade à randomização (local), onde não é crucial controlá-la.

Não é totalmente incompatível com o engraçado "nuke it fom orbit" proposto por DMGregory, mas talvez use armas pequenas e precisas? :-)

Ventobravo
fonte
Obrigado pela sua resposta. Parece uma área processual impressionantemente grande para manter. Eu posso ver como em uma área tão grande, mesmo quando você tem acesso a muito armazenamento, ainda é impossível simplesmente armazenar tudo. Parece que os geradores com versão terão que ser o caminho a seguir. Seja o que for :)
null
De todas as respostas, eu me pego pensando mais sobre essa. Gostei de suas descrições ligeiramente filosóficas das coisas. Acho o termo "gaiola" muito útil ao explicar as idéias, então obrigado por isso. Deixe a borboleta voar ... em sua gaiola :)
null
PS: Estou realmente curioso para saber em qual jogo você trabalha. Você é capaz de compartilhar essas informações?
Nulo
Poderia adicionar mais uma coisa sobre o espaço numérico: vale a pena ficar sempre perto de zero. Que você provavelmente já sabia. Perto de zero fornece a melhor precisão numérica e a maior quantidade de golpes por menos bits. Você sempre pode compensar um monte de números próximos de zero mais tarde, mas você só precisa de um único número para isso. Da mesma forma, você pode (eu quase diria que DEVE) mover o cálculo distante para mais perto de zero, com um deslocamento. - Stormwind
Stormwind
Avaliando o anterior, considere um pequeno movimento do veículo, um incremento de 1 quadro de 0,01 [metros, unidades]: Você não pode calcular com precisão 10000,1 + 0,01 na precisão numérica de 32 bits (única), mas é possível calcular 0,1 + 0,01. Portanto, se a "ação" ocorrer longe (atrás das montanhas :-)), não vá para lá; mova as montanhas para você (mova-se com 10000, então você está com 0,1 agora). Válido também para espaço de armazenamento. Pode-se ser ganancioso com o armazenamento de valores numéricos próximos um do outro. Armazene a parte comum uma vez e as variações individualmente - podem economizar bits! Você encontrou o link? ;-)
Stormwind