Génération procédurale, mises à jour du jeu et effet papillon

10

REMARQUE: j'ai posé cette question sur Stack Overflow il y a quelques jours, mais j'ai eu très peu de vues et aucune réponse. J'ai pensé que je devrais plutôt demander sur gamdev.stackexchange.

Il s'agit d'une question / demande générale de conseils sur la maintenance d'un système de génération de procédures par le biais de plusieurs mises à jour post-publication, sans interrompre le contenu généré précédemment.

J'essaie de trouver des informations et des techniques pour éviter les problèmes d '"effet papillon" lors de la création de contenu procédural pour les jeux. Lorsque vous utilisez un générateur de nombres aléatoires prédéfini, une séquence répétée de nombres aléatoires peut être utilisée pour créer un monde reproductible. Alors que certains jeux enregistrent simplement le monde généré sur le disque une fois généré, l'une des fonctionnalités puissantes de la génération procédurale est le fait que vous pouvez compter sur la reproductibilité de la séquence de nombres pour recréer une région plusieurs fois de la même manière, supprimant ainsi le besoin de persistance. En raison des contraintes de ma situation particulière, je dois minimiser la persistance et je dois me fier autant que possible à un contenu purement semé.

Le principal danger de cette approche est que même le moindre changement dans le système de génération procédurale peut provoquer un effet papillon qui change le monde entier. Il est donc très difficile de mettre à jour le jeu sans détruire les mondes que les joueurs explorent.

La principale technique que j'ai utilisée pour éviter ce problème est de concevoir la génération procédurale en plusieurs phases, chacune ayant son propre générateur de nombres aléatoires prédéfini. Cela signifie que chaque sous-système est autonome et que si quelque chose se brise, cela n'affectera pas tout dans le monde. Cependant, cela semble avoir encore beaucoup de potentiel de "casse", même si dans une partie isolée du jeu.

Une autre façon possible de résoudre ce problème pourrait être de maintenir des versions complètes de vos générateurs dans le code et de continuer à utiliser le bon générateur pour une instance mondiale donnée. Cela me semble cependant être un cauchemar de maintenance, et je suis curieux de savoir si quelqu'un le fait réellement.

Donc, ma question est vraiment une demande de conseils généraux, de techniques et de modèles de conception pour traiter ce problème de l'effet papillon, en particulier dans le contexte des mises à jour du jeu après la sortie. (J'espère que ce n'est pas une question trop large.)

Je travaille actuellement dans Unity3D / C #, bien que ce soit une question indépendante du langage.

METTRE À JOUR:

Merci pour les réponses.

Il semble de plus en plus que les données statiques soient l'approche la meilleure et la plus sûre, et aussi que lorsque le stockage d'un grand nombre de données statiques n'est pas une option, avoir une longue campagne dans un monde généré nécessiterait une version stricte des générateurs utilisés. La raison de la limitation dans mon cas est la nécessité d'une sauvegarde / synchronisation cloud mobile. Ma solution peut être de trouver des moyens de stocker de petites quantités de données compactes sur des choses essentielles.

Je trouve que le concept de Stormwind de "Cages" est une manière particulièrement utile de penser aux choses. Une cage est fondamentalement un point de réensemencement, empêchant les effets d'entraînement de petits changements, c'est-à-dire la mise en cage du papillon.

nul
la source
Je vote pour clore cette question comme hors sujet car elle est trop large.
Almo
Je me rends compte que c'est très large. Recommanderiez-vous plutôt d'essayer un forum gamedev ou quelque chose comme ça? Il n'y a vraiment aucun moyen de rendre la question plus précise. J'espérais que je pourrais entendre parler de quelqu'un avec beaucoup d'expérience dans ce domaine, avec quelques astuces astucieuses qui ne m'ont pas traversé l'esprit.
null
2
Almo se trompe. Ce n'est pas du tout large. C'est une excellente question et assez étroite pour donner de bonnes réponses. C'est quelque chose que je pense que beaucoup d'entre nous ont souvent réfléchi.
Ingénieur

Réponses:

8

Je pense que vous avez couvert les bases ici:

  • Utiliser plusieurs générateurs ou réensemencer à intervalles (par exemple en utilisant des hachages spatiaux) pour limiter les retombées des changements. Cela fonctionne probablement pour le contenu cosmétique, mais comme vous le signalez, il peut toujours provoquer une rupture contenue dans une section.

  • Garder une trace de la version du générateur utilisée dans le fichier de sauvegarde et répondre de manière appropriée. Que pourrait signifier «approprié» ...

    • Conserver un historique de toutes les versions précédentes des générateurs dans votre exécutable de jeu et utiliser celle qui correspond à la sauvegarde. Cela rend plus difficile de corriger les bugs si les joueurs continuent à utiliser les anciennes sauvegardes.
    • Avertir le lecteur que ce fichier de sauvegarde provient d'une ancienne version et fournir un lien pour accéder à cette version en tant qu'exécutable distinct. Bon pour les jeux avec des campagnes de quelques heures à quelques jours, mauvais pour une campagne que vous prévoyez de jouer pendant des semaines ou plus.
    • Conservez uniquement les dernières nversions du générateur dans votre exécutable. Si le fichier de sauvegarde utilise l'une de ces versions récentes, (proposez de) mettre à jour le fichier de sauvegarde vers la dernière version. Cela utilise le générateur approprié pour décompresser tout état obsolète en littéraux (ou en deltas de la sortie du nouveau générateur sur la même graine, s'ils sont très similaires). Tout nouvel état à partir de maintenant provient des nouveaux générateurs. Les joueurs qui ne jouent pas longtemps pourraient être laissés pour compte. Et dans le pire des cas, vous finissez par stocker tout l'état du jeu sous forme littérale, auquel cas vous pourriez aussi bien ...
  • Si vous prévoyez de modifier fréquemment votre logique de génération et que vous ne voulez pas rompre la compatibilité avec les versions précédentes, ne vous fiez pas au déterminisme du générateur: enregistrez tout votre état dans votre fichier de sauvegarde. (c.-à-d. "Nuke it of orbite. C'est le seul moyen d'en être sûr")

DMGregory
la source
Si vous avez construit les règles de génération, y a-t-il un moyen pour vous d'inverser la génération? IE, étant donné un état de jeu, pouvez-vous revenir à une graine? Si cela est possible avec vos données, au lieu de lier le joueur à une version de jeu différente, vous pouvez fournir un utilitaire de mise à jour qui génère un monde à partir d'une graine avec l'ancien système, puis utilise l'état généré pour produire une graine pour le nouveau générateur. Vous devrez peut-être avertir vos joueurs d'une attente de conversion.
Joe
1
Ce n'est généralement pas possible. Vous n'êtes même pas assuré qu'il existe une graine pour le nouveau générateur qui donne la même sortie que l'ancien. Habituellement, ces graines contiennent environ 64 bits, mais le nombre de mondes possibles que votre jeu pourrait prendre en charge est probablement supérieur à 2 ^ 64, de sorte que chaque générateur n'en produit jamais qu'un sous-ensemble. Changer le générateur entraînera très probablement un nouveau sous-ensemble de niveaux, qui peut avoir peu ou pas d'intersection avec le groupe électrogène précédent.
DMGregory
Il était difficile de choisir la «bonne» réponse. J'ai choisi celui-ci car il était concis et résumait clairement les principaux problèmes. Merci.
null
4

La principale source d'un tel effet papillon n'est sans doute pas la génération de nombres - ce qui devrait être assez facile pour rester déterministe à partir d'un seul générateur de nombres - mais plutôt l' utilisation de ces nombres par le code client. Les changements de code sont le vrai défi pour garder les choses stables.

Code: tests unitaires La meilleure façon de vous assurer qu'un changement mineur quelque part ne se manifeste pas involontairement ailleurs est d'inclure des tests unitaires approfondis pour chaque aspect génératif, dans votre build. Cela est vrai de n'importe quel morceau de code compact où le changement d'une chose peut avoir un impact sur beaucoup d'autres - vous avez besoin de tests pour tous afin que vous puissiez voir sur une seule construction ce qui a été impacté.

Numéros: séquences / emplacements périodiques Supposons que vous ayez un générateur de nombres qui sert à tout. Il n'attribue pas de sens, il crache simplement des nombres dans l'ordre - comme tout PRNG. Étant donné la même graine sur deux pistes, nous obtenons les mêmes séquences, oui? Maintenant, vous réfléchissez et décidez qu'il y aura peut-être 30 aspects de votre jeu qui devront régulièrement être fournis avec une valeur aléatoire. Ici, nous attribuons une séquence cyclique de 30 emplacements, par exemple, chaque premier numéro de la séquence est une disposition de terrain accidenté, chaque deuxième numéro est des perturbations du terrain ... etc ... Alors tes règles est donc de 30 ans.

Après 10, vous avez 20 emplacements libres que vous pouvez utiliser pour d'autres aspects à mesure que la conception du jeu progresse. Le coût ici est bien sûr que vous devez générer des numéros pour les emplacements 11-30 même s'ils ne sont pas actuellement utilisés , c'est-à-dire terminer la période, pour revenir à la séquence suivante de 1-10. Cela a un coût CPU, bien qu'il devrait être mineur (selon le nombre d'emplacements libres). L'autre inconvénient est que vous devez être sûr que votre conception finale peut être adaptée au nombre d'emplacements que vous avez mis à disposition au tout début de votre processus de développement ... et plus vous en attribuez au début, plus il y a d'emplacements "vides" vous devez potentiellement passer par chacun, pour faire fonctionner les choses.

Les effets de ceci sont:

  • Vous avez un générateur produisant des nombres pour tout
  • Changer le nombre d'aspects dont vous avez besoin pour générer des nombres, n'aura pas d'impact sur le déterminisme (à condition que votre période soit suffisamment longue pour accueillir tous les aspects)

Bien sûr, il y aura une longue période pendant laquelle votre jeu ne sera pas accessible au public - en alpha, pour ainsi dire - afin que vous puissiez réduire de 30 à 20 aspects sans affecter les joueurs, uniquement vous-même, si vous vous rendez compte que vous aviez attribué beaucoup trop d'emplacements au début. Cela permettrait bien sûr d'économiser certains cycles CPU. Mais gardez à l'esprit qu'une bonne fonction de hachage (que vous pouvez écrire vous-même) devrait de toute façon être rapide comme l'éclair. Donc, avoir à exécuter des emplacements supplémentaires ne devrait pas être coûteux.

Ingénieur
la source
Salut. Cela ressemble à certaines choses que je fais. Je génère généralement un tas de sous-graines à l'avance en fonction de la graine mondiale initiale. Récemment, j'ai commencé à pré-générer un tableau de bruit assez long, puis chaque "slot" est simplement un index dans ce tableau. De cette façon, chaque sous-système peut simplement saisir la bonne graine et travailler de manière isolée. Une autre excellente technique consiste à utiliser les coordonnées x, y pour générer une graine pour chaque emplacement. J'utilise le code de la réponse d'Euphoric sur cette page de pile: programmers.stackexchange.com/questions/161336/…
null
3

Si vous voulez persister avec PCG, je vous suggère de traiter le code PCG lui-même comme des données . Tout comme vous persisteriez des données entre les révisions avec un contenu régulier, avec le contenu généré, si vous souhaitez les conserver entre les révisions, vous devrez conserver le générateur.

Bien sûr, l'approche la plus populaire consiste à convertir les données générées en données statiques, comme vous l'avez mentionné.

Je ne connais pas d'exemples de jeux qui conservent beaucoup de versions de générateur, car la persistance est inhabituelle dans les jeux PCG - c'est pourquoi permadeath va souvent de pair avec PCG. Cependant, il existe de nombreux exemples de plusieurs PCG, même du même type, dans le même jeu. Par exemple, Unangband a de nombreux générateurs distincts pour les salles de donjons, et à mesure que de nouveaux sont ajoutés, les anciens fonctionnent toujours de la même manière. Que ce soit maintenable dépend de votre implémentation. Une façon de le maintenir maintenable est d'utiliser des scripts pour implémenter vos générateurs, en les gardant isolés avec le reste du code du jeu.

congusbongus
la source
C'est une idée intelligente, d'utiliser simplement différents générateurs pour différents domaines.
null
2

Je maintiens une superficie d'environ 30000 kilomètres carrés, contenant environ 1 million de bâtiments et autres objets, en plus de placements aléatoires de choses diverses. Une simulation en plein air ofc. Les données stockées sont d'environ 4 Go. J'ai de la chance d'avoir de l'espace de stockage, mais ce n'est pas illimité.

Aléatoire est aléatoire, incontrôlé. Mais on peut le mettre en cage un peu:

  • Contrôlez son début fin fin (comme mentionné dans d'autres articles, le numéro de départ et le nombre de numéros générés).
  • Limitez son espace numérique, par exemple. générer uniquement des entiers compris entre 0 et 100.
  • Décaler son espace numérique, en ajoutant une valeur (par exemple, 100 + [les nombres générés entre 0 et 100] produisent des nombres aléatoires entre 100 et 200)
  • Mettez-le à l'échelle (par exemple, multipliez par 0,1)
  • Et appliquez diverses cages autour de lui. Cela réduit, élimine une partie de la génération. Par exemple. si vous générez dans un espace bidimensionnel, vous pouvez placer un rectangle au-dessus des paires de nombres et supprimer ce qui se trouve à l'extérieur. Ou un cercle ou un polygone. Si dans l'espace 3D, on ne peut accepter par exemple que des triplets qui résident à l'intérieur d'une sphère, ou une autre forme (maintenant en pensant visuellement, mais cela n'a pas nécessairement quelque chose à voir avec la visualisation ou le positionnement réel).

C'est à peu près ça. Les cages consomment également des données, malheureusement.

Il y a un dicton en finnois, Hajota ja hallitse. Se traduit par Diviser et conquérir .

J'ai rapidement abandonné l'idée d'une définition précise des moindres détails. Random veut la liberté, alors il a obtenu la liberté. Laissez le papillon voler - à l'intérieur de sa cage. Au lieu de cela, je me suis concentré sur une manière riche de définir (et de maintenir !!) les cages. Peu importe quelles voitures elles sont, tant qu'elles sont bleues ou bleu foncé (un employeur ennuyeux a dit une fois :-)). "Bleu ou bleu foncé" étant la (très petite) cage ici, le long de la dimension de couleur.

Qu'est-ce qui est gérable, pour contrôler et gérer les espaces numériques?

  • Une grille booléenne est (les bits sont petits!)
  • Les points de coin sont
  • Comme une structure arborescente (= à suivre dans "cages contenant des cages")

En termes de maintenance et d'intercompatibilité de versions ... nous avons
: si version = n alors
: elseif version = m alors ...
Oui, la base de code s'agrandit :-).

Choses familières. Votre bonne façon d'aller de l'avant serait de définir une méthode riche pour diviser et conquérir , et sacrifier certaines données à ce sujet. Ensuite, si possible, donnez à la randomisation (locale) la liberté, où il n'est pas crucial de la contrôler.

Pas totalement incompatible avec le drôle "nuke it fom orbit" proposé par DMGregory, mais peut-être utiliser des armes nucléaires petites et précises? :-)

Hurlevent
la source
Merci pour votre réponse. Cela ressemble à une zone de procédure impressionnante à maintenir. Je peux voir comment pour une si grande surface, même lorsque vous avez accès à beaucoup de stockage, il est toujours impossible de tout stocker simplement. Il semble que les générateurs versionnés devront être la voie à suivre. Qu'il en soit ainsi
null
De toutes les réponses, je pense le plus à celle-ci. J'ai apprécié vos descriptions légèrement philosophiques des choses. Je trouve le terme "Cage" très utile pour expliquer les idées, alors merci pour cela. Laissez le papillon voler ... dans sa cage :)
null
PS Je suis vraiment curieux de savoir sur quel jeu vous travaillez. Pouvez-vous partager cette information?
null
Pourrait ajouter une chose à propos de l'espace numérique: il vaut toujours la peine de rester proche de zéro. Que vous saviez probablement déjà. Près de zéro donne la meilleure précision numérique et le plus de coup pour le moins de bits. Vous pouvez toujours compenser un tas de nombres proches de zéro plus tard, mais vous n'avez besoin que d'un seul nombre pour cela. De même, vous pouvez (je dirais presque que vous DEVEZ) déplacer le calcul distant plus près de zéro, avec un décalage. - Stormwind
Stormwind
En évaluant sur le précédent, considérez un mouvement de petit véhicule, un incrément d'une image de 0,01 [mètres, unités]: vous ne pouvez pas calculer avec précision 10000,1 + 0,01 en précision numérique 32 bits (unique) mais vous POUVEZ calculer 0,1 + 0,01. Par conséquent, si "l'action" se déroule loin (derrière les montagnes :-)), n'y allez pas, déplacez plutôt les montagnes vers vous (déplacez-vous avec 10000, alors vous êtes à 0,1 maintenant). Valable également pour l'espace de stockage. On peut être gourmand avec le stockage de valeurs numériques proches les unes des autres. Stockez la partie commune d'entre eux une fois, et les variations individuellement - peut économiser des bits! Avez-vous trouvé le lien? ;-)
Stormwind