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.
Respostas:
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 ...
n
versõ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")
fonte
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:
É 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.
fonte
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.
fonte
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:
É 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?
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? :-)
fonte