web-dev-qa-db-fra.com

Algorithme de tuilage de carte

La carte

Je fais un RPG basé sur des tuiles avec Javascript, en utilisant des cartes de hauteur de bruit perlin, puis en attribuant un type de tuile basé sur la hauteur du bruit.

Les cartes finissent par ressembler à ceci (dans la vue de la minicarte).

enter image description here

J'ai un algorithme assez simple qui extrait la valeur de couleur de chaque pixel de l'image et la convertit en un entier (0-5) en fonction de sa position entre (0-255) qui correspond à une tuile dans le dictionnaire de tuiles. Ce tableau 200x200 est ensuite transmis au client.

Le moteur détermine ensuite les tuiles à partir des valeurs du tableau et les dessine dans le canevas. Donc, je me retrouve avec des mondes intéressants qui ont des caractéristiques réalistes: montagnes, mers, etc.

Maintenant, la prochaine chose que je voulais faire était d'appliquer une sorte d'algorithme de fusion qui ferait en sorte que les tuiles se fondraient parfaitement dans leurs voisins, si le voisin n'est pas du même type. L'exemple de carte ci-dessus montre ce que le joueur voit dans sa minicarte. À l'écran, ils voient une version rendue de la section marquée par le rectangle blanc; où les tuiles sont rendues avec leurs images plutôt que sous forme de pixels d'une seule couleur.

Ceci est un exemple de ce que l'utilisateur verrait sur la carte mais ce n'est pas le même emplacement que la fenêtre ci-dessus montre!

enter image description here

C'est dans cette optique que je souhaite que la transition se fasse.

L'algorithme

J'ai trouvé un algorithme simple qui traverserait la carte dans la fenêtre et rendrait une autre image par-dessus chaque tuile, à condition qu'elle soit à côté d'une tuile de type différent. (Ne pas changer la carte! Juste rendre des images supplémentaires.) L'idée de l'algorithme était de profiler les voisins de la tuile actuelle:

An example of a tile profile

Ceci est un exemple de scénario de ce que le moteur pourrait avoir à rendre, la tuile actuelle étant celle marquée par le X.

Un tableau 3x3 est créé et les valeurs qui l'entourent sont lues. Ainsi, pour cet exemple, le tableau devrait ressembler.

[
    [1,2,2]
    [1,2,2]
    [1,1,2]
];

Mon idée était alors de travailler sur une série de cas pour les configurations de tuiles possibles. À un niveau très simple:

if(profile[0][1] != profile[1][1]){
     //draw a tile which is half sand and half transparent
     //Over the current tile -> profile[1][1]
     ...
}

Ce qui donne ce résultat:

Result

Qui fonctionne comme une transition de [0][1] à [1][1], mais pas de [1][1] à [2][1], où reste un Edge dur. J'ai donc pensé que dans ce cas, une tuile d'angle devrait être utilisée. J'ai créé deux feuilles Sprite 3x3 qui, selon moi, contiendraient toutes les combinaisons possibles de tuiles qui pourraient être nécessaires. Ensuite, j'ai reproduit cela pour toutes les tuiles du jeu (les zones blanches sont transparentes). Cela finit par être 16 tuiles pour chaque type de tuile (les tuiles centrales sur chaque feuille de sprites ne sont pas utilisées.)

SandSand2

Le résultat idéal

Ainsi, avec ces nouvelles tuiles et l'algorithme correct, la section d'exemple ressemblerait à ceci:

Correct

Chaque tentative que j'ai faite a échoué, il y a toujours une faille dans l'algorithme et les motifs finissent étrangement. Je n'arrive pas à comprendre tous les cas et dans l'ensemble, cela semble être une mauvaise façon de le faire.

Une solution?

Donc, si quelqu'un pouvait fournir une solution alternative quant à la façon dont je pourrais créer cet effet, ou dans quelle direction aller pour écrire l'algorithme de profilage, alors je serais très reconnaissant!

153
Dan Prince

L'idée de base de cet algorithme est d'utiliser une étape de prétraitement pour trouver tous les bords, puis sélectionner la tuile de lissage correcte en fonction de la forme du bord.

La première étape serait de trouver toutes les arêtes. Dans l'exemple ci-dessous, les tuiles Edge marquées d'un X sont toutes des tuiles vertes avec une tuile beige comme une ou plusieurs de leurs huit tuiles voisines. Avec différents types de terrain, cette condition pourrait se traduire par une tuile étant une tuile Edge si elle a des voisins de terrain-nombre inférieur.

Edge tiles.

Une fois que toutes les tuiles Edge sont détectées, la prochaine chose à faire est de sélectionner la bonne tuile de lissage pour chaque tuile Edge. Voici ma représentation de vos carreaux de lissage.

Smoothing tiles.

Notez qu'il n'y a en fait pas beaucoup de types de carreaux différents. Nous avons besoin des huit tuiles extérieures de l'un des carrés 3x3 mais uniquement des quatre carrés de coin de l'autre puisque les tuiles droites se trouvent déjà dans le premier carré. Cela signifie qu'il y a au total 12 cas différents que nous devons distinguer.

Maintenant, en regardant une tuile Edge, nous pouvons déterminer le sens de rotation de la frontière en regardant ses quatre tuiles voisines les plus proches. Marquer une tuile Edge avec X comme ci-dessus, nous avons les six cas différents suivants.

Six cases.

Ces cas sont utilisés pour déterminer la tuile de lissage correspondante et nous pouvons numéroter les tuiles de lissage en conséquence.

Smoothed tiles with numbers.

Il y a toujours un choix de a ou b pour chaque cas. Cela dépend de quel côté l'herbe est. Une façon de déterminer cela pourrait être de garder une trace de l'orientation de la frontière, mais probablement la façon la plus simple de le faire est de choisir une tuile à côté du bord et de voir de quelle couleur elle a. L'image ci-dessous montre les deux cas 5a) et 5b) qui peuvent être distingués en vérifiant par exemple la couleur de la tuile supérieure droite.

Choosing 5a or 5b.

L'énumération finale pour l'exemple d'origine ressemblerait alors à ceci.

Final enumeration.

Et après avoir sélectionné la tuile Edge correspondante, la bordure ressemblerait à ceci.

Final result.

Pour terminer, je pourrais dire que cela fonctionnerait tant que la frontière est quelque peu régulière. Plus précisément, les tuiles Edge qui n'ont pas exactement deux tuiles Edge car leurs voisins devront être traités séparément. Cela se produira pour les tuiles Edge sur le bord de la carte qui auront un seul voisin Edge et pour les parcelles de terrain très étroites où le nombre de tuiles Edge voisines pourrait être de trois, voire quatre.

117
user1884905

Le carré suivant représente une plaque métallique. Il y a un "évent de chaleur" dans le coin supérieur droit. Nous pouvons voir comment, comme la température de ce point reste constante, la plaque métallique converge vers une température constante à chaque point, étant naturellement plus chaude près du sommet:

heatplate

Le problème de trouver la température à chaque point peut être résolu comme un "problème de valeur limite". Cependant, la façon la plus simple de calculer la chaleur à chaque point est de modéliser la plaque sous forme de grille. Nous connaissons les points sur la grille à température constante. Nous avons réglé la température de tous les points inconnus sur la température ambiante (comme si l'évent venait juste d'être ouvert). Nous laissons ensuite la chaleur se propager à travers la plaque jusqu'à atteindre la convergence. Cela se fait par itération: nous parcourons chaque point (i, j). Nous fixons point (i, j) = (point (i + 1, j) + point (i-1, j) + point (i, j + 1) + point (i, j-1))/4 [sauf le point (i, j) a un évent de chaleur à température constante]

Si vous appliquez cela à votre problème, c'est très similaire, juste des couleurs moyennes au lieu des températures. Vous auriez probablement besoin d'environ 5 itérations. Je suggère d'utiliser une grille 400x400. C'est 400x400x5 = moins d'un million d'itérations qui seront rapides. Si vous n'utilisez que 5 itérations, vous n'aurez probablement pas à vous soucier de conserver des points de couleur constante, car ils ne se décaleront pas trop de leur original (en fait, seuls les points situés à une distance de 5 de la couleur peuvent être affectés par la couleur). Pseudo code:

iterations = 5
for iteration in range(iterations):
    for i in range(400):
        for j in range(400):
            try:
                grid[i][j] = average(grid[i+1][j], grid[i-1][j],
                                     grid[i][j+1], grid[i][j+1])
            except IndexError:
                pass
12
robert king

Ok, donc la première chose à penser est que l'automatisation d'une solution parfaite au problème nécessite des calculs d'interpolation plutôt charnus. Sur la base du fait que vous mentionnez des images de tuiles pré-rendues, je suppose que la solution d'interpolation complète n'est pas garantie ici.

D'un autre côté, comme vous l'avez dit, terminer la carte à la main conduira à un bon résultat ... mais je suppose également que tout processus manuel pour corriger les problèmes n'est pas non plus une option.

Voici un algorithme simple qui ne donne pas un résultat parfait, mais qui est très gratifiant en raison du faible effort qu'il faut.

Au lieu d'essayer de mélanger TOUTES les tuiles Edge, (ce qui signifie que vous devez soit connaître le résultat du mélange des tuiles adjacentes en premier - interpolation, soit vous devez affiner la carte entière plusieurs fois et ne pas compter sur des tuiles pré-générées) pourquoi ne pas mélanger les carreaux en alternance de damiers?

[1] [*] [2]
[*] [1] [*]
[1] [*] [2]

C'est à dire. mélanger uniquement les tuiles marquées dans la matrice ci-dessus?

En supposant que les seules étapes de valeur autorisées sont une à la fois, vous n'avez que quelques tuiles à concevoir ...

A    [1]      B    [2]      C    [1]      D    [2]      E    [1]           
 [1] [*] [1]   [1] [*] [1]   [1] [*] [2]   [1] [*] [2]   [1] [*] [1]   etc.
     [1]           [1]           [1]           [1]           [2]           

Il y aura 16 modèles au total. Si vous profitez de la symétrie de rotation et de réflexion, il y en aura encore moins.

"A" serait une tuile de style simple [1]. "D" serait une diagonale.

Il y aura de petites discontinuités aux coins des carreaux, mais celles-ci seront mineures par rapport à l'exemple que vous avez donné.

Si je peux, je mettrai à jour ce post avec des images plus tard.

5
perfectionist

Je jouais avec quelque chose de similaire à ça, ce n'était pas fini pour un certain nombre de raisons; mais en gros, il faudrait une matrice de 0 et 1, 0 étant le sol et 1 étant un mur pour une application de générateur de labyrinthe dans Flash. Comme AS3 est similaire à JavaScript, il ne serait pas difficile de réécrire dans JS.

var tileDimension:int = 20;
var levelNum:Array = new Array();

levelNum[0] = [1, 1, 1, 1, 1, 1, 1, 1, 1];
levelNum[1] = [1, 0, 0, 0, 0, 0, 0, 0, 1];
levelNum[2] = [1, 0, 1, 1, 1, 0, 1, 0, 1];
levelNum[3] = [1, 0, 1, 0, 1, 0, 1, 0, 1];
levelNum[4] = [1, 0, 1, 0, 0, 0, 1, 0, 1];
levelNum[5] = [1, 0, 0, 0, 0, 0, 0, 0, 1];
levelNum[6] = [1, 0, 1, 1, 1, 1, 0, 0, 1];
levelNum[7] = [1, 0, 0, 0, 0, 0, 0, 0, 1];
levelNum[8] = [1, 1, 1, 1, 1, 1, 1, 1, 1];

for (var rows:int = 0; rows < levelNum.length; rows++)
{
    for (var cols:int = 0; cols < levelNum[rows].length; cols++)
    {
        // set up neighbours
        var toprow:int = rows - 1;
        var bottomrow:int = rows + 1;

        var westN:int = cols - 1;
        var eastN:int = cols + 1;

        var rightMax =  levelNum[rows].length;
        var bottomMax = levelNum.length;

        var northwestTile =     (toprow != -1 && westN != -1) ? levelNum[toprow][westN] : 1;
        var northTile =         (toprow != -1) ? levelNum[toprow][cols] : 1;
        var northeastTile =     (toprow != -1 && eastN < rightMax) ? levelNum[toprow][eastN] : 1;

        var westTile =          (cols != 0) ? levelNum[rows][westN] : 1;
        var thistile =          levelNum[rows][cols];
        var eastTile =          (eastN == rightMax) ? 1 : levelNum[rows][eastN];

        var southwestTile =     (bottomrow != bottomMax && westN != -1) ? levelNum[bottomrow][westN] : 1;
        var southTile =         (bottomrow != bottomMax) ? levelNum[bottomrow][cols] : 1;
        var southeastTile =     (bottomrow != bottomMax && eastN < rightMax) ? levelNum[bottomrow][eastN] : 1;

        if (thistile == 1)
        {
            var w7:Wall7 = new Wall7();
            addChild(w7);
            pushTile(w7, cols, rows, 0);

            // wall 2 corners

            if      (northTile === 0 && northeastTile === 0 && eastTile === 1 && southeastTile === 1 && southTile === 1 && southwestTile === 0 && westTile === 0 && northwestTile === 0)
            {
                var w21:Wall2 = new Wall2();
                addChild(w21);
                pushTile(w21, cols, rows, 270);
            }

            else if (northTile === 0 && northeastTile === 0 && eastTile === 0 && southeastTile === 0 && southTile === 1 && southwestTile === 1 && westTile === 1 && northwestTile === 0)
            {
                var w22:Wall2 = new Wall2();
                addChild(w22);
                pushTile(w22, cols, rows, 0);
            }

            else if (northTile === 1 && northeastTile === 0 && eastTile === 0 && southeastTile === 0 && southTile === 0 && southwestTile === 0 && westTile === 1 && northwestTile === 1)
            {
                var w23:Wall2 = new Wall2();
                addChild(w23);
                pushTile(w23, cols, rows, 90);
            }

            else if (northTile === 1 && northeastTile === 1 && eastTile === 1 && southeastTile === 0 && southTile === 0 && southwestTile === 0 && westTile === 0 && northwestTile === 0)
            {
                var w24:Wall2 = new Wall2();
                addChild(w24);
                pushTile(w24, cols, rows, 180);
            }           

            //  wall 6 corners

            else if (northTile === 1 && northeastTile === 1 && eastTile === 1 && southeastTile === 0 && southTile === 1 && southwestTile === 1 && westTile === 1 && northwestTile === 1)
            {
                var w61:Wall6 = new Wall6();
                addChild(w61);
                pushTile(w61, cols, rows, 0); 
            }

            else if (northTile === 1 && northeastTile === 1 && eastTile === 1 && southeastTile === 1 && southTile === 1 && southwestTile === 0 && westTile === 1 && northwestTile === 1)
            {
                var w62:Wall6 = new Wall6();
                addChild(w62);
                pushTile(w62, cols, rows, 90); 
            }

            else if (northTile === 1 && northeastTile === 1 && eastTile === 1 && southeastTile === 1 && southTile === 1 && southwestTile === 1 && westTile === 1 && northwestTile === 0)
            {
                var w63:Wall6 = new Wall6();
                addChild(w63);
                pushTile(w63, cols, rows, 180);
            }

            else if (northTile === 1 && northeastTile === 0 && eastTile === 1 && southeastTile === 1 && southTile === 1 && southwestTile === 1 && westTile === 1 && northwestTile === 1)
            {
                var w64:Wall6 = new Wall6();
                addChild(w64);
                pushTile(w64, cols, rows, 270);
            }

            //  single wall tile

            else if (northTile === 0 && northeastTile === 0 && eastTile === 0 && southeastTile === 0 && southTile === 0 && southwestTile === 0 && westTile === 0 && northwestTile === 0)
            {
                var w5:Wall5 = new Wall5();
                addChild(w5);
                pushTile(w5, cols, rows, 0);
            }

            //  wall 3 walls

            else if (northTile === 0 && eastTile === 1 && southTile === 0 && westTile === 1)
            {
                var w3:Wall3 = new Wall3();
                addChild(w3);
                pushTile(w3, cols, rows, 0);
            }

            else if (northTile === 1 && eastTile === 0 && southTile === 1 && westTile === 0)
            {
                var w31:Wall3 = new Wall3();
                addChild(w31);
                pushTile(w31, cols, rows, 90);
            }

            //  wall 4 walls

            else if (northTile === 0 && eastTile === 0 && southTile === 1 && westTile === 0)
            {
                var w41:Wall4 = new Wall4();
                addChild(w41);
                pushTile(w41, cols, rows, 0);
            }

            else if (northTile === 1 && eastTile === 0 && southTile === 0 && westTile === 0)
            {
                var w42:Wall4 = new Wall4();
                addChild(w42);
                pushTile(w42, cols, rows, 180);
            }

            else if (northTile === 0 && northeastTile === 0 && eastTile === 1 && southeastTile === 0 && southTile === 0 && southwestTile === 0 && westTile === 0 && northwestTile === 0)
            {
                var w43:Wall4 = new Wall4();
                addChild(w43);
                pushTile(w43, cols, rows, 270);
            }

            else if (northTile === 0 && northeastTile === 0 && eastTile === 0 && southeastTile === 0 && southTile === 0 && southwestTile === 0 && westTile === 1 && northwestTile === 0)
            {
                var w44:Wall4 = new Wall4();
                addChild(w44);
                pushTile(w44, cols, rows, 90);
            }

            //  regular wall blocks

            else if (northTile === 1 && eastTile === 0 && southTile === 1 && westTile === 1)
            {
                var w11:Wall1 = new Wall1();
                addChild(w11);
                pushTile(w11, cols, rows, 90);
            }

            else if (northTile === 1 && eastTile === 1 && southTile === 1 && westTile === 0)
            {
                var w12:Wall1 = new Wall1();
                addChild(w12);
                pushTile(w12, cols, rows, 270);
            }

            else if (northTile === 0 && eastTile === 1 && southTile === 1 && westTile === 1)
            {
                var w13:Wall1 = new Wall1();
                addChild(w13);
                pushTile(w13, cols, rows, 0);
            }

            else if (northTile === 1 && eastTile === 1 && southTile === 0 && westTile === 1)
            {
                var w14:Wall1 = new Wall1();
                addChild(w14);
                pushTile(w14, cols, rows, 180);
            }

        }
        // debug === // trace('Top Left: ' + northwestTile + ' Top Middle: ' + northTile + ' Top Right: ' + northeastTile + ' Middle Left: ' + westTile + ' This: ' + levelNum[rows][cols] + ' Middle Right: ' + eastTile + ' Bottom Left: ' + southwestTile + ' Bottom Middle: ' + southTile + ' Bottom Right: ' + southeastTile);
    }
}

function pushTile(til:Object, tx:uint, ty:uint, degrees:uint):void
{
    til.x = tx * tileDimension;
    til.y = ty * tileDimension;
    if (degrees != 0) tileRotate(til, degrees);
}

function tileRotate(tile:Object, degrees:uint):void
{
    // http://www.flash-db.com/Board/index.php?topic=18625.0
    var midPoint:int = tileDimension/2;
    var point:Point=new Point(tile.x+midPoint, tile.y+midPoint);
    var m:Matrix=tile.transform.matrix;
    m.tx -= point.x;
    m.ty -= point.y;
    m.rotate (degrees*(Math.PI/180));
    m.tx += point.x;
    m.ty += point.y;
    tile.transform.matrix=m;
}

Fondamentalement, cela vérifie chaque tuile autour de lui de gauche à droite, de haut en bas et suppose que les tuiles Edge sont toujours 1. J'ai également pris la liberté d'exporter les images sous forme de fichier à utiliser comme clé:

Wall tiles

Ceci est incomplet et probablement un moyen hacky pour y parvenir, mais j'ai pensé que cela pourrait être utile.

Edit: Capture d'écran du résultat de ce code.

Generated Result

2
Ben

Je suggérerais quelques choses:

  • peu importe ce que la tuile "centre" est, non? ce pourrait être 2, mais si tous les autres sont 1, il afficherait 1?

  • il importe seulement quels sont les coins, quand il y a une différence dans les voisins immédiats vers le haut ou le côté. Si tous les voisins immédiats sont 1, et un coin est 2, il afficherait 1.

  • Je précalculerais probablement toutes les combinaisons possibles de voisins, créant un tableau à 8 index avec les quatre premiers indiquant les valeurs des voisins haut/bas et le second indiquant les diagonales:

bords [N] [E] [S] [W] [NE] [SE] [SW] [NW] = quel que soit le décalage dans Sprite

dans votre cas, [2] [2] [1] [1] [2] [2] [1] [1] = 4 (le 5e Sprite).

dans ce cas, [1] [1] [1] [1] serait égal à 1, [2] [2] [2] [2] serait égal à 2, et le reste devrait être réglé. Mais la recherche d'une tuile particulière serait triviale.

1
elijah