Génération de donjon sans couloirs et dépendances de pièce


15

Je fais un jeu avec un monde généré de manière procédurale créé au début du jeu, composé de plusieurs zones représentées par des grilles (disons, 8x8, 9x6, les tailles seraient idéalement arbitraires). Ces zones sont censées être connectées les unes aux autres via une liste de dépendances.

Une connexion existe lorsqu'au moins 3 espaces de cette grille sont exposés entre ces deux zones. Dans la cellule centrale de cette zone de connexion à 3 espaces se trouve la porte entre les zones:

J'ai essayé de trouver un moyen de les connecter, mais cela devient de plus en plus complexe au fur et à mesure que vous devez prendre en compte plusieurs domaines.

J'ai essayé du prototypage papier et bien que ce soit un processus très simple pour le faire visuellement, je n'ai pas trouvé un bon ensemble d'expressions mathématiques qui me permettent de placer des pièces avec la même efficacité par code.

Voici un exemple "simple" avec lequel je me bats en ce moment:

  • La zone «a» doit être connectée à «b» et «c»
  • La zone «b» doit être connectée à «a» et «d»
  • La zone «c» doit être connectée à «a» et «d»
  • La zone «d» doit être connectée à «b» et «c»

Considérez, pour simplifier, nous plaçons les chambres par leur ordre d'apparition sur la liste (j'en ai essayé d'autres). J'approche donc cela comme votre algorithme de génération de donjons procédural standard.

Nous plaçons 'a' n'importe où sur le tableau, car c'est le premier domaine. Ensuite, nous choisissons un mur au hasard et, comme rien n'est connecté à ce mur, nous pouvons y placer «b»:

Maintenant, nous devons placer «c», mais «a» est déjà sur le plateau et a un mur occupé, nous décidons donc de le placer sur un autre mur. Mais tous les emplacements ne feront pas l'affaire, car «d» arrive et il doit également être connecté à «b» et «c»:

J'ai essayé une limitation possible que 2 chambres qui ont le même ensemble de dépendances ne peuvent pas être sur des murs opposés, mais même cela ne garantit pas le succès:

Et dans d'autres cas, lorsque les zones ont des tailles différentes, le fait d'être sur des murs opposés peut fonctionner:

De plus, ne pas considérer un mur utilisé est une hypothèse erronée car elle exclut les solutions valides:

J'ai essayé de chercher des recherches sur d'autres algorithmes de génération procédurale ou similaires, tels que les algorithmes d'optimisation de rectangle et de mise en page graphique, mais généralement ces algorithmes ne prennent pas en compte toutes les contraintes de ce problème et sont difficiles à mélanger.

J'ai pensé à un tas d'approches, y compris le placement d'une zone et la marche arrière jusqu'à ce qu'un placement approprié soit trouvé, mais elles semblent très dépendantes des essais et des erreurs et coûteuses en termes de calcul. Mais, compte tenu des recherches approfondies sur les deux derniers problèmes que j'ai mentionnés, ce pourrait être la seule / meilleure solution?

Je voulais juste voir si quelqu'un avait eu des problèmes similaires dans le passé ou s'il était prêt à m'aider à comprendre cela et à me donner quelques conseils sur la façon de commencer avec l'algorithme. Ou, à défaut, je devrai envisager de desserrer les contraintes que j'ai définies.


Les pièces doivent être complètement carrées?
wolfdawn

Si vous voulez dire s'ils doivent avoir 4 murs et pas plus alors oui, mais je l'ai fait pour simplifier l'espace du monde. J'ai besoin de calculer facilement l'espace occupé par chaque zone, donc je sais si je pourrai y mettre tout ce que je veux.
Joana Almeida

Réponses:


6

C'est un problème sympa. Je crois que cela peut être résolu en utilisant la planification d'actions dans l'espace de placement des chambres.

Définissez l' état du monde comme suit:

//State:
//    A list of room states.
//    Room state:
//      - Does room exist?
//      - Where is room's location?
//      - What is the room's size?

Définissez une contrainte comme:

 // Constraint(<1>, <2>):
 //  - If room <1> and <2> exist, Room <1> is adjacent to Room <2>

Où "adjacent" est comme vous l'avez décrit (partageant au moins 3 voisins)

Une contrainte serait invalidée chaque fois que les deux pièces ne sont pas adjacentes et que les deux pièces existent.

Définissez un État pour être valide lorsque:

// foreach Constraint:
//        The Constraint is "not invalidated".
// foreach Room:
//       The room does not intersect another room.

Définissez une action comme l'emplacement d'une pièce, compte tenu d'un état actuel. L' action est valide chaque fois que l'état résultant de l'action est valide. Par conséquent, nous pouvons générer une liste d'actions pour chaque état:

// Returns a list of valid actions from the current state
function List<Action> GetValidActions(State current, List<Constraint> constraints):
    List<Action> actions = new List<Action>();
    // For each non-existent room..
    foreach Room room in current.Rooms:
        if(!room.Exists)
            // Try to put it at every possible location
            foreach Position position in Dungeon:
                 State next = current.PlaceRoom(room, position)
                 // If the next state is valid, we add that action to the list.
                 if(next.IsValid(constraints))
                     actions.Add(new Action(room, position));

Maintenant, ce qui vous reste est un graphique , où les États sont des nœuds et les Actions sont des liens. Le but est de trouver un État qui soit à la fois valide, et toutes les chambres ont été placées. Nous pouvons trouver un emplacement valide en recherchant dans le graphique de manière arbitraire, peut-être en utilisant une recherche en profondeur d'abord. La recherche ressemblera à ceci:

// Given a start state (with all rooms set to *not exist*), and a set of
// constraints, finds a valid end state where all the constraints are met,
// using a depth-first search.
// Notice that this gives you the power to pre-define the starting conditions
// of the search, to for instance define some key areas of your dungeon by hand.
function State GetFinalState(State start, List<Constraint> constraints)
    Stack<State> stateStack = new Stack<State>();
    State current = start;
    stateStack.push(start);
    while not stateStack.IsEmpty():
        current = stateStack.pop();
        // Consider a new state to expand from.
        if not current.checked:
            current.checked = true;
            // If the state meets all the constraints, we found a solution!
            if(current.IsValid(constraints) and current.AllRoomsExist()):
                return current;

            // Otherwise, get all the valid actions
            List<Action> actions = GetValidActions(current, constraints);

            // Add the resulting state to the stack.
            foreach Action action in actions:
                State next = current.PlaceRoom(action.room, action.position);
                stateStack.push(next);

    // If the stack is completely empty, there is no solution!
    return NO_SOLUTION;

Maintenant, la qualité du donjon généré dépendra de l'ordre dans lequel les salles et les actions sont prises en compte. Vous pouvez obtenir des résultats intéressants et différents probablement en permutant simplement au hasard les actions que vous prenez à chaque étape, faisant ainsi une marche aléatoire dans le graphique état-action. L'efficacité de la recherche dépendra grandement de la rapidité avec laquelle vous pouvez rejeter les états non valides. Il peut être utile de générer des états valides à partir des contraintes chaque fois que vous souhaitez rechercher des actions valides.


Drôle, vous devriez mentionner cette solution. J'ai parlé à un ami plus tôt et il a mentionné que je devrais probablement étudier les algorithmes de recherche basée sur les arbres, mais je ne savais pas comment les utiliser dans ce contexte. Votre message m'a ouvert les yeux! Cela semble certainement une solution réalisable si vous gérez la génération de succursales et effectuez quelques optimisations pour couper les mauvaises succursales dès que possible.
Joana Almeida

7

Vos priorités de génération sont en conflit. Lors de la génération de niveaux, votre premier objectif doit être un réseau de points connectés planaires (sans chevauchement) , quelle que soit l'échelle. Ensuite, créez des pièces à partir des points de ce site Web. La création de formes de pièce en premier est généralement une erreur. Créez d'abord la connectivité, puis voyez quelles formes de salle peuvent être adaptées à cela.

Algorithme général

  1. Créez une grille de plancher quantifiée de taille suffisante pour prendre en charge votre niveau, à l'aide d'un tableau ou d'une image 2D.

  2. Dispersez des points au hasard sur cet espace vide. Vous pouvez utiliser une simple vérification aléatoire sur chaque tuile pour voir si elle obtient un point, ou utiliser la distribution standard / gaussienne pour disperser les points. Attribuez une couleur / valeur numérique unique à chaque point. Ce sont des identifiants. (PS Si après cette étape, vous sentez que vous avez besoin d'agrandir votre espace, faites-le.)

  3. Pour chacun de ces points générés, en séquence, augmentez progressivement un cercle de limites ou un rectangle de limites en une seule étape (généralement un taux de 0,5 à 1,0 cellules / pixels par étape) dans xet y. Vous pouvez soit faire pousser toutes les bornes en parallèle, toutes en commençant à partir de la taille zéro au même stade, soit vous pouvez commencer à les faire pousser à des moments et à des rythmes différents, ce qui donne un biais à la taille de celles qui commencent plus tôt (imaginez que les semis poussent, où certains commencer tard). Par «grandir», je veux dire remplir les limites nouvellement incrémentées avec la couleur / ID unique au point de départ pour ces limites. Une métaphore pour cela serait de tenir des marqueurs contre le dos d'un morceau de papier et de regarder des taches d'encre de différentes couleurs pousser jusqu'à ce qu'elles se rencontrent.

  4. À un moment donné, les limites d'un point et d'un autre vont entrer en collision, pendant l'étape de croissance. C'est le point auquel vous devez arrêter de repousser les limites de ces deux points - au moins dans le sens uniforme décrit à l'étape 3.

  5. Une fois que vous avez repoussé les limites de tous les points autant que possible et arrêté tous les processus de croissance, vous aurez une carte qui devrait être largement, mais pas entièrement remplie. Vous voudrez peut-être maintenant emballer ces espaces vides, que je suppose être blancs, comme si vous coloriez une feuille de papier.

Remplissage d'espace post-traitement

Diverses techniques peuvent être utilisées pour remplir les espaces vides / blancs qui restent, selon l'étape 5:

  • Demandez à une seule zone voisine déjà colorée de revendiquer l'espace, en l'inondant en remplissant cette couleur pour que tout se rejoigne.
  • Inondez avec de nouvelles couleurs / numéros / ID encore inutilisés, de sorte qu'ils forment des zones entièrement nouvelles.
  • L'approche du tournoi à la ronde telle que chaque zone voisine déjà remplie "grandit" un peu dans l'espace vide. Pensez aux animaux qui boivent autour d'un abreuvoir: ils reçoivent tous un peu d'eau.
  • Ne remplissez pas totalement l'espace vide, traversez-le simplement pour relier les zones existantes en utilisant des passages droits.

Perturbation

Comme dernière étape pour rendre les choses plus organiques, vous pouvez faire une perturbation des bords à divers degrés, sur les cellules des bords des zones. Assurez-vous simplement de ne pas bloquer les itinéraires de déplacement cruciaux.

Théorie, pour l'intérêt

Ceci est similaire à l'approche adoptée dans Diagrammes de Voronoï / Triangulation de Delaunay , sauf que dans ce qui précède, vous ne créez pas explicitement d'arêtes - au lieu de cela, lorsque les zones de délimitation entrent en collision, la croissance cesse. Vous remarquerez que les diagrammes de Voronoi remplissent l'espace; c'est parce qu'ils ne cessent pas la croissance simplement au toucher, mais plutôt sur un certain degré nominal de chevauchement. Vous pouvez essayer similaire.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.