Algoritmo de mosaico do mapa

153

O mapa

Estou criando um RPG baseado em bloco com Javascript, usando mapas de altura de ruído perlin e atribuindo um tipo de bloco com base na altura do ruído.

Os mapas acabam parecendo algo assim (na exibição de minimapa).

insira a descrição da imagem aqui

Eu tenho um algoritmo bastante simples que extrai o valor da cor de cada pixel na imagem e o converte em um número inteiro (0-5), dependendo da sua posição entre (0-255), que corresponde a um bloco no dicionário de blocos. Essa matriz de 200 x 200 é então passada para o cliente.

O mecanismo determina os blocos dos valores na matriz e os desenha na tela. Então, acabo com mundos interessantes que têm características de aparência realista: montanhas, mares etc.

Agora, a próxima coisa que eu queria fazer era aplicar algum tipo de algoritmo de mesclagem que faria com que os blocos se misturassem perfeitamente aos vizinhos, se o vizinho não fosse do mesmo tipo. O mapa de exemplo acima é o que o jogador vê em seu minimapa. Na tela, eles vêem uma versão renderizada da seção marcada pelo retângulo branco; onde os blocos são renderizados com suas imagens, e não como pixels de cor única.

Este é um exemplo do que o usuário veria no mapa, mas não é o mesmo local que a janela de exibição acima mostra!

insira a descrição da imagem aqui

É nessa visão que desejo que a transição ocorra.

O Algoritmo

Eu vim com um algoritmo simples que atravessaria o mapa dentro da janela de exibição e renderizaria outra imagem por cima de cada bloco, desde que estivesse ao lado de um bloco de tipo diferente. (Não alterando o mapa! Apenas renderizando algumas imagens extras.) A idéia do algoritmo era criar um perfil dos vizinhos do bloco atual:

Um exemplo de perfil de bloco

Este é um cenário de exemplo do que o mecanismo pode precisar renderizar, com o bloco atual sendo o marcado com o X.

Uma matriz 3x3 é criada e os valores à sua volta são lidos. Portanto, neste exemplo, a matriz seria semelhante.

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

Minha ideia era então elaborar uma série de casos para as possíveis configurações de bloco. Em um nível muito simples:

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

O que dá esse resultado:

Resultado

O que funciona como uma transição de [0][1]para [1][1], mas não de [1][1]para [2][1], onde permanece uma aresta dura. Então imaginei que, nesse caso, seria necessário usar um bloco de canto. Eu criei duas folhas de sprite 3x3 que achei que conteriam todas as combinações possíveis de peças necessárias. Então eu repliquei isso para todos os blocos que existem no jogo (as áreas brancas são transparentes). Isso acaba sendo 16 peças para cada tipo de peça (as peças centrais de cada planilha não são usadas.)

AreiaSand2

O resultado ideal

Portanto, com esses novos blocos e o algoritmo correto, a seção de exemplo ficaria assim:

Corrigir

Porém, todas as tentativas que eu fiz falharam, sempre há alguma falha no algoritmo e os padrões acabam estranhos. Não consigo acertar todos os casos e, no geral, parece uma maneira ruim de fazê-lo.

Uma solução?

Portanto, se alguém pudesse fornecer uma solução alternativa sobre como eu poderia criar esse efeito ou qual a direção a seguir para escrever o algoritmo de criação de perfil, ficaria muito grato!

Dan Prince
fonte
7
Dê uma olhada neste artigo e nos artigos vinculados, especialmente este . O blog em si contém muitas idéias que podem servir como ponto de partida. Aqui está uma visão geral.
Darcara 17/01/12
você deve simplificar seu algoritmo. verifique isso: Autômatos celulares
bidimensionais

Respostas:

117

A idéia básica desse algoritmo é usar uma etapa de pré-processamento para encontrar todas as arestas e, em seguida, selecionar o ladrilho de suavização correto de acordo com a forma da aresta.

O primeiro passo seria encontrar todas as arestas. No exemplo abaixo, os ladrilhos da borda marcados com um X são todos os ladrilhos verdes com um ladrilho marrom como um ou mais dos oito ladrilhos vizinhos. Com diferentes tipos de terreno, essa condição pode se transformar em um ladrilho como um ladrilho de borda, se houver vizinhos com menor número de terreno.

Ladrilhos de borda.

Quando todos os ladrilhos de aresta forem detectados, a próxima coisa a fazer é selecionar o ladrilho de suavização correto para cada ladrilho de aresta. Aqui está minha representação dos seus ladrilhos de suavização.

Suavização de azulejos.

Observe que, na verdade, não existem muitos tipos diferentes de blocos. Precisamos das oito peças externas de um dos quadrados 3x3, mas apenas dos quatro quadrados de canto do outro, uma vez que as peças retas já são encontradas no primeiro quadrado. Isso significa que existem 12 casos diferentes entre os quais devemos distinguir.

Agora, olhando para um bloco de borda, podemos determinar para que lado o limite gira, observando os quatro blocos vizinhos mais próximos. Marcando um ladrilho de borda com X, assim como acima, temos os seguintes seis casos diferentes.

Seis casos.

Esses casos são usados ​​para determinar o bloco de nivelamento correspondente e podemos numerar os blocos de nivelamento de acordo.

Azulejos suavizados com números.

Ainda há uma opção de a ou b para cada caso. Isso depende de qual lado a grama está. Uma maneira de determinar isso pode ser acompanhar a orientação do limite, mas provavelmente a maneira mais simples de fazer isso é escolher um bloco ao lado da borda e ver qual a cor que ele possui. A imagem abaixo mostra os dois casos 5a) e 5b) que podem ser distinguidos, por exemplo, verificando a cor do ladrilho superior direito.

Escolhendo 5a ou 5b.

A enumeração final para o exemplo original ficaria assim.

Enumeração final.

E depois de selecionar o bloco de arestas correspondente, a borda se pareceria com isso.

Resultado final.

Como nota final, posso dizer que isso funcionaria desde que a fronteira seja um pouco regular. Mais precisamente, os ladrilhos de borda que não possuem exatamente dois ladrilhos de borda, pois seus vizinhos terão que ser tratados separadamente. Isso ocorrerá para os ladrilhos de borda na borda do mapa, que terão um único vizinho de borda e para pedaços de terreno muito estreitos onde o número de ladrilhos de borda vizinhos pode ser de três ou até quatro.

user1884905
fonte
1
Isso é ótimo e muito útil para mim. Estou lidando com um caso em que alguns blocos não podem passar diretamente para outros. Por exemplo, os ladrilhos de "sujeira" podem fazer a transição para "grama clara" e "grama clara" pode fazer a transição para "grama média". O Tiled (mapeditor.org) faz um ótimo trabalho ao lidar com isso implementando algum tipo de pesquisa de árvore para a escova do terreno; Ainda não consegui reproduzi-lo.
argila
12

O quadrado a seguir representa uma placa de metal. Há uma "ventilação" no canto superior direito. Podemos ver como a temperatura desse ponto permanece constante, a placa de metal converge para uma temperatura constante em cada ponto, sendo naturalmente mais quente perto do topo:

placa de calor

O problema de encontrar a temperatura em cada ponto pode ser resolvido como um "problema de valor limite". No entanto, a maneira mais simples de calcular o calor em cada ponto é modelar a placa como uma grade. Conhecemos os pontos na grade em temperatura constante. Definimos a temperatura de todos os pontos desconhecidos para a temperatura ambiente (como se a ventilação tivesse acabado de ser ligada). Deixamos então o calor se espalhar pela placa até alcançarmos a convergência. Isso é feito por iteração: iteramos através de cada ponto (i, j). Definimos o ponto (i, j) = (ponto (i + 1, j) + ponto (i-1, j) + ponto (i, j + 1) + ponto (i, j-1)) / 4 [a menos que o ponto (i, j) possui um respiradouro de temperatura constante]

Se você aplicar isso ao seu problema, é muito semelhante, apenas cores médias em vez de temperaturas. Você provavelmente precisaria de cerca de 5 iterações. Eu sugiro usar uma grade de 400x400. Isso é 400x400x5 = menos de 1 milhão de iterações, que serão rápidas. Se você usar apenas 5 iterações, provavelmente não precisará se preocupar em manter os pontos em cores constantes, pois eles não mudarão muito do original (na verdade, apenas os pontos a uma distância de 5 da cor podem ser afetados pela cor). Pseudo-código:

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
Robert King
fonte
você poderia expandir isso um pouco mais? Estou curioso e não consigo entender sua explicação. Como alguém usa o valor médio da cor depois de fazer as iterações?
quer
1
Cada grade de pontos de grade [i] [j] pode ser desenhada para a tela como um pequeno retângulo (ou pixel individual) da cor apropriada.
Robert King
5

Ok, então, o primeiro pensamento é que automatizar uma solução perfeita para o problema requer um pouco de matemática de interpolação. Com base no fato de você mencionar imagens de bloco pré-renderizadas, presumo que a solução completa de interpolação não é garantida aqui.

Por outro lado, como você disse, terminar o mapa manualmente levará a um bom resultado ... mas também presumo que qualquer processo manual para corrigir falhas também não seja uma opção.

Aqui está um algoritmo simples que não fornece um resultado perfeito, mas que é muito gratificante com base no baixo esforço necessário.

Em vez de tentar misturar TODOS os blocos de arestas, (o que significa que você precisa saber o resultado da mistura dos blocos adjacentes primeiro - interpolação ou refinar o mapa inteiro várias vezes e não pode contar com blocos pré-gerados) por que não misturar peças em um padrão alternado de tabuleiro de damas?

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

Ou seja, apenas misturando as peças estreladas na matriz acima?

Supondo que as únicas etapas permitidas no valor sejam uma de cada vez, você tem apenas alguns blocos para projetar ...

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]           

Haverá 16 padrões no total. Se você tirar proveito da simetria rotacional e reflexiva, haverá ainda menos.

'A' seria um ladrilho de estilo simples [1]. 'D' seria uma diagonal.

Haverá pequenas descontinuidades nos cantos dos ladrilhos, mas essas serão menores em comparação com o exemplo que você deu.

Se eu puder, atualizarei este post com imagens posteriormente.

perfeccionista
fonte
Parece bom, eu estaria interessado em vê-lo com algumas imagens para ter uma idéia melhor do que você quer dizer.
Dan Prince
Não consigo montar nenhuma imagem porque não tenho o software que pensei ter ... Mas tenho pensado e não é a melhor solução possível. Você pode fazer transições diagonais, com certeza, mas outras transições não são realmente ajudadas por esse algoritmo de suavização. Você não pode garantir que seu mapa não contenha transições de 90 graus. Desculpe, acho que este é um pouco decepcionante.
perfeccionista
3

Eu estava brincando com algo parecido com isso, não foi finalizado por vários motivos; mas basicamente seria necessária uma matriz de 0 e 1, sendo 0 o chão e 1 uma parede para uma aplicação de gerador de labirinto no Flash. Como o AS3 é semelhante ao JavaScript, não seria difícil reescrever em 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;
}

Basicamente, isso verifica todos os blocos ao redor, da esquerda para a direita, de cima para baixo e assume que os blocos de borda são sempre 1. Também tomei a liberdade de exportar as imagens como um arquivo para usar como chave:

Wall tiles

Isso é incompleto e provavelmente uma maneira hacky de conseguir isso, mas achei que poderia ser de algum benefício.

Edit: Captura de tela do resultado desse código.

Resultado Gerado

Ben
fonte
1

Eu sugeriria algumas coisas:

  • não importa qual é o bloco "central", certo? poderia ser 2, mas se todos os outros forem 1, mostraria 1?

  • importa apenas quais são os cantos, quando há uma diferença nos vizinhos imediatos do topo ou do lado. Se todos os vizinhos imediatos forem 1 e um canto for 2, mostrará 1.

  • Provavelmente eu pré-calcularia todas as combinações possíveis de vizinhos, criando uma matriz de 8 índices com os quatro primeiros indicando os valores dos vizinhos superior / inferior e o segundo indicando as diagonais:

arestas [N] [E] [S] [W] [NE] [SE] [SW] [NW] = qualquer deslocamento no sprite

no seu caso, [2] [2] [1] [1] [2] [2] [1] [1] = 4 (o quinto sprite).

nesse caso, [1] [1] [1] [1] seria 1, [2] [2] [2] [2] seria 2 e o restante teria que ser resolvido. Mas a busca por um bloco específico seria trivial.

Elias
fonte