Algoritmo di piastrellatura della mappa


153

La mappa

Sto realizzando un gioco di ruolo basato su piastrelle con Javascript, usando le mappe di altezza del rumore perlin, quindi assegnando un tipo di tessera in base all'altezza del rumore.

Le mappe finiscono per assomigliare a questa (nella vista della minimappa).

inserisci qui la descrizione dell'immagine

Ho un algoritmo abbastanza semplice che estrae il valore del colore da ciascun pixel sull'immagine e lo converte in un numero intero (0-5) a seconda della sua posizione tra (0-255) che corrisponde a una piastrella nel dizionario delle tessere. Questo array 200x200 viene quindi passato al client.

Il motore quindi determina le tessere dai valori nella matrice e le disegna sulla tela. Quindi, finisco con mondi interessanti che hanno caratteristiche dall'aspetto realistico: montagne, mari ecc.

Ora la prossima cosa che volevo fare era applicare un qualche tipo di algoritmo di fusione che avrebbe fatto sì che le tessere si fondessero perfettamente con i loro vicini, se il vicino non fosse dello stesso tipo. La mappa di esempio sopra è ciò che il giocatore vede nella sua minimappa. Sullo schermo vedono una versione renderizzata della sezione contrassegnata dal rettangolo bianco; dove le piastrelle sono renderizzate con le loro immagini piuttosto che come pixel a singolo colore.

Questo è un esempio di ciò che l'utente vedrebbe sulla mappa, ma non è la stessa posizione mostrata dalla finestra sopra!

inserisci qui la descrizione dell'immagine

È in questa visione che voglio che avvenga la transizione.

L'algoritmo

Ho escogitato un semplice algoritmo che avrebbe attraversato la mappa all'interno del viewport e avrebbe reso un'altra immagine sopra la parte superiore di ogni riquadro, a condizione che fosse accanto a un riquadro di diverso tipo. (Non cambiare la mappa! Solo il rendering di alcune immagini extra.) L'idea dell'algoritmo era di profilare i vicini della tessera corrente:

Un esempio di un profilo di riquadro

Questo è uno scenario di esempio di ciò che potrebbe essere necessario il rendering del motore, con il riquadro corrente è quello contrassegnato con la X.

Viene creato un array 3x3 e vengono letti i valori circostanti. Quindi, per questo esempio, l'array dovrebbe apparire.

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

La mia idea era quindi di elaborare una serie di casi per le possibili configurazioni delle tessere. A un livello molto semplice:

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

Che dà questo risultato:

Risultato

Che funziona come una transizione da [0][1]a [1][1], ma non da [1][1]a [2][1], in cui un disco resti bordo. Quindi ho pensato che in quel caso avrebbe dovuto essere usata una piastrella d'angolo. Ho creato due fogli sprite 3x3 che pensavo avrebbero potuto contenere tutte le possibili combinazioni di tessere che potrebbero essere necessarie. Quindi l'ho replicato per tutte le tessere presenti nel gioco (Le aree bianche sono trasparenti). Questo finisce per essere 16 tessere per ogni tipo di tessera (le tessere centrali su ogni foglio di calcolo non vengono utilizzate).

SabbiaSand2

Il risultato ideale

Quindi, con queste nuove tessere e l'algoritmo corretto, la sezione di esempio sarebbe simile a questa:

Corretta

Ogni tentativo che ho fatto è fallito, c'è sempre qualche difetto nell'algoritmo e gli schemi finiscono per essere strani. Non riesco a risolvere tutti i casi e nel complesso sembra un modo scadente di farlo.

Una soluzione?

Quindi, se qualcuno potesse fornire una soluzione alternativa su come potrei creare questo effetto, o quale direzione seguire per scrivere l'algoritmo di profilazione, allora sarei molto grato!


7
Dai un'occhiata a questo articolo e anche agli articoli collegati, in particolare a questo . Il blog stesso contiene molte idee che possono servire come punto di partenza. Ecco una panoramica.
Darcara,

dovresti semplificare il tuo algoritmo. controlla questo: Automi cellulari
bidimensionali

Risposte:


117

L'idea di base di questo algoritmo è quella di utilizzare una fase di pre-elaborazione per trovare tutti i bordi e quindi selezionare la piastrella di levigatura corretta in base alla forma del bordo.

Il primo passo sarebbe quello di trovare tutti i bordi. Nell'esempio seguente, le tessere di bordo contrassegnate con una X sono tutte tessere verdi con una tessera marrone chiaro come una o più delle otto tessere vicine. Con diversi tipi di terreno questa condizione potrebbe tradursi in una tessera che è una tessera bordo se ha vicini con un numero di terreno inferiore.

Piastrelle per bordi.

Una volta rilevate tutte le tessere bordo, la prossima cosa da fare è selezionare la tessera levigante giusta per ogni piastrella bordo. Ecco la mia rappresentazione delle tue piastrelle leviganti.

Piastrelle leviganti.

Nota che in realtà non ci sono molti tipi diversi di tessere. Abbiamo bisogno delle otto tessere esterne da uno dei quadrati 3x3 ma solo i quattro quadrati angolari dall'altro poiché le tessere a bordo diritto sono già presenti nel primo quadrato. Ciò significa che ci sono in totale 12 casi diversi che dobbiamo distinguere.

Ora, guardando una tessera bordo possiamo determinare in che direzione gira il confine guardando le sue quattro tessere vicine più vicine. Contrassegnando una tessera bordo con X come sopra abbiamo i seguenti sei casi diversi.

Sei casi.

Questi casi vengono utilizzati per determinare la piastrella di levigatura corrispondente e possiamo numerare le piastrelle di levigatura di conseguenza.

Piastrelle levigate con numeri.

C'è ancora una scelta di a o b per ogni caso. Questo dipende da quale lato si trova l'erba. Un modo per determinare ciò potrebbe essere quello di tenere traccia dell'orientamento del confine, ma probabilmente il modo più semplice per farlo è quello di scegliere una tessera vicino al bordo e vedere di che colore ha. L'immagine seguente mostra i due casi 5a) e 5b) che possono essere distinti controllando ad esempio il colore della tessera in alto a destra.

Scelta di 5a o 5b.

L'enumerazione finale per l'esempio originale sarebbe quindi simile a questa.

Enumerazione finale.

E dopo aver selezionato la tessera bordo corrispondente, il bordo sarebbe simile a questo.

Risultato finale.

Come nota finale, potrei dire che funzionerebbe fintanto che il confine è piuttosto regolare. Più precisamente, le tessere bordo che non hanno esattamente due tessere bordo poiché i loro vicini dovranno essere trattate separatamente. Ciò si verificherà per le tessere bordo sul bordo della mappa che avrà un bordo singolo vicino e per i pezzi di terreno molto stretti in cui il numero di tessere bordo vicino potrebbe essere tre o addirittura quattro.


1
Questo è fantastico e molto utile per me. Ho a che fare con un caso in cui alcune tessere non possono passare direttamente ad altre. Ad esempio, le tessere "sporco" possono passare a "erba leggera" e "erba leggera" può passare a "erba media". Tiled (mapeditor.org) fa un ottimo lavoro nel gestirlo implementando un tipo di ricerca degli alberi per il pennello del terreno; Devo ancora essere in grado di riprodurlo, però.
Clay

12

Il quadrato seguente rappresenta una piastra metallica. C'è una "ventola di calore" nell'angolo in alto a destra. Possiamo vedere come man mano che la temperatura di questo punto rimane costante, la piastra metallica converge a una temperatura costante in ciascun punto, essendo naturalmente più calda nella parte superiore:

heatplate

Il problema di trovare la temperatura in ciascun punto può essere risolto come "Problema del valore limite". Tuttavia, il modo più semplice per calcolare il calore in ogni punto è modellare la piastra come una griglia. Conosciamo i punti sulla griglia a temperatura costante. Impostiamo la temperatura di tutti i punti sconosciuti sulla temperatura ambiente (come se lo sfiato fosse appena stato acceso). Lasciamo quindi che il calore si diffonda attraverso la piastra fino a raggiungere la convergenza. Questo viene fatto per iterazione: ripetiamo ogni punto (i, j). Impostiamo punto (i, j) = (punto (i + 1, j) + punto (i-1, j) + punto (i, j + 1) + punto (i, j-1)) / 4 [a meno il punto (i, j) ha una ventola di calore a temperatura costante]

Se lo applichi al tuo problema, è molto simile, solo colori medi invece di temperature. Probabilmente avrai bisogno di circa 5 iterazioni. Suggerisco di utilizzare una griglia 400x400. Quello è 400x400x5 = meno di 1 milione di iterazioni che saranno veloci. Se usi solo 5 iterazioni, probabilmente non dovrai preoccuparti di mantenere i punti a colori costanti, poiché non si sposteranno troppo dal loro originale (in effetti solo i punti a distanza 5 dal colore possono essere influenzati dal colore). Pseudo codice:

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

potresti espanderlo un po 'di più? Sono curioso e non riesco a capire la tua spiegazione. Come si usa il valore del colore medio dopo aver eseguito le iterazioni?
Chii,

1
Ogni griglia del punto della griglia [i] [j] può essere disegnata sulla tela come un piccolo rettangolo (o singolo pixel) del colore appropriato.
robert king,

5

Ok, quindi i primi pensieri sono che l'automazione di una soluzione perfetta al problema richiede alcune matematiche di interpolazione piuttosto carnose. Sulla base del fatto che menzioni immagini di riquadri pre-renderizzate, presumo che la soluzione di interpolazione completa non sia garantita qui.

D'altra parte, come hai detto, finire la mappa a mano porterà a un buon risultato ... ma presumo anche che qualsiasi processo manuale per correggere i glitch non sia un'opzione.

Ecco un semplice algoritmo che non dà un risultato perfetto, ma che è molto gratificante in base al basso sforzo richiesto.

Invece di provare a fondere OGNI piastrella di bordo, (ciò significa che devi conoscere il risultato della fusione delle tessere adiacenti prima - interpolazione, oppure devi affinare l'intera mappa più volte e non puoi fare affidamento su tessere pre-generate) perché non mescolare le tessere in uno schema a scacchiera alternato?

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

Cioè mescolare solo le tessere recitate nella matrice sopra?

Supponendo che gli unici passaggi di valore consentiti siano uno alla volta, hai solo poche tessere da progettare ...

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]           

Ci saranno 16 modelli in totale. Se approfitti della simmetria rotazionale e riflessiva, ce ne saranno anche meno.

'A' sarebbe una semplice piastrella in stile [1]. 'D' sarebbe una diagonale.

Ci saranno piccole discontinuità agli angoli delle tessere, ma queste saranno minori rispetto all'esempio che hai dato.

Se posso aggiornerò questo post con le immagini in seguito.


Questo suona bene, sarei interessato a vederlo con alcune immagini per avere un'idea migliore di cosa intendi.
Dan Prince,

Non riesco a mettere insieme nessuna immagine perché non ho il software che pensavo di avere ... Ma ci ho pensato e non è una soluzione valida come potrebbe essere. Puoi fare transizioni diagonali, sicuramente, ma altre transizioni non sono davvero aiutate da questo algoritmo di smoothing. Non puoi nemmeno garantire che la tua mappa non contenga transizioni di 90 gradi. Mi dispiace, immagino che questo sia un po 'deludente.
perfezionista il

3

Stavo giocando con qualcosa di simile a questo, non è stato completato per una serie di motivi; ma in sostanza ci vorrebbe una matrice di 0 e 1, 0 è il terreno e 1 è un muro per un'applicazione del generatore di labirinto in Flash. Poiché AS3 è simile a JavaScript, non sarebbe difficile riscrivere in 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;
}

Fondamentalmente questo controlla ogni riquadro intorno ad esso andando da sinistra a destra, dall'alto verso il basso e presume che i bordi siano sempre 1. Ho anche preso la libertà di esportare le immagini come file da usare come chiave:

piastrelle

Questo è incompleto e probabilmente un modo bizzarro per raggiungere questo obiettivo, ma ho pensato che potesse essere di qualche beneficio.

Modifica: schermata del risultato di quel codice.

Risultato generato


1

Vorrei suggerire alcune cose:

  • non importa quale sia la tessera "centrale", giusto? potrebbe essere 2, ma se tutti gli altri sono 1, mostrerebbe 1?

  • importa solo quali sono gli angoli, quando c'è una differenza nei vicini immediati nella parte superiore o laterale. Se tutti i vicini immediati sono 1 e un angolo è 2, verrà visualizzato 1.

  • Probabilmente avrei precalcolato tutte le possibili combinazioni di vicini, creando un array di 8 indici con i primi quattro che indicano i valori dei vicini superiore / inferiore e il secondo che indica le diagonali:

spigoli [N] [E] [S] [W] [NE] [SE] [SW] [NW] = qualunque offset in sprite

quindi nel tuo caso, [2] [2] [1] [1] [2] [2] [1] [1] = 4 (il 5 ° sprite).

in questo caso, [1] [1] [1] [1] sarebbe 1, [2] [2] [2] [2] sarebbe 2 e il resto dovrebbe essere elaborato. Ma la ricerca di una particolare tessera sarebbe banale.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.