Алгоритм разбиения карты

Карта

Я создаю RPG на основе плитки с Javascript, используя карты высоты шума perlin, а затем назначаю тип плитки в зависимости от высоты шума.

Карты выглядят примерно так (в мини-карте).

enter image description here

У меня есть довольно простой алгоритм, который извлекает значение цвета из каждого пикселя на изображении и преобразует его в целое число (0-5) в зависимости от его положения между (0-255), которое соответствует плитке в словаре плитки. Этот массив 200x200 затем передается клиенту.

Затем двигатель определяет плитки из значений в массиве и рисует их на холсте. Итак, я заканчиваю интересными мирами, которые имеют реалистичные функции: горы, моря и т.д.

Теперь следующее, что я хотел сделать, это применить какой-то алгоритм смешивания, который заставил бы плитки плавно вписаться в своих соседей, если сосед не относится к одному типу. Пример карты выше - это то, что игрок видит в своей мини-карте. На экране отображаются рендеринг версии раздела, отмеченного белым прямоугольником; где плитки отображаются с их изображениями, а не как одиночные цветные пиксели.

Это пример того, что пользователь увидит на карте, но это не то же место, что показано выше в окне просмотра!

enter image description here

В этом представлении я хочу, чтобы переход произошел.

Алгоритм

Я придумал простой алгоритм, который будет пересекать карту в окне просмотра и отображать другое изображение поверх каждой плитки, обеспечивая ее рядом с плиткой другого типа. (Не меняя карту! Просто отрисовка дополнительных изображений.) Идея алгоритма состояла в том, чтобы профилировать существующих соседей плитки:

An example of a tile profile

Это примерный сценарий того, что движок может отображать, при этом текущий фрагмент будет тем, который помечен X.

Создается массив 3x3, и значения вокруг него считываются. Таким образом, для этого примера будет выглядеть массив.

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

Моя идея заключалась в том, чтобы выработать ряд случаев для возможных конфигураций плитки. На очень простом уровне:

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

Что дает этот результат:

Result

Работает как переход от [0][1] до [1][1], но не от [1][1] до [2][1], где сохраняется жесткий край. Поэтому я решил, что в этом случае нужно будет использовать угловую плитку. Я создал два листа спрайтов 3x3, которые, как я думал, будут содержать все возможные комбинации плиток, которые могут понадобиться. Затем я воспроизвел это для всех плиток, которые есть в игре (белые области прозрачны). Это заканчивается 16 плитами для каждого типа плитки (центральные плитки на каждом спрайте не используются.)

SandSand2

Идеальный результат

Итак, с этими новыми фрагментами и правильным алгоритмом примерный раздел будет выглядеть так:

Correct

Каждая попытка, которую я сделал, потерпела неудачу, хотя в алгоритме всегда есть некоторые недостатки, и шаблоны в конечном итоге странны. Кажется, я не могу все правильно рассмотреть, и в целом это кажется плохим способом сделать это.

Решение?

Итак, если кто-то может предоставить альтернативное решение о том, как я могу создать этот эффект или какое направление для написания алгоритма профилирования, тогда я был бы очень благодарен!

Ответ 1

Основная идея этого алгоритма заключается в использовании этапа предварительной обработки для поиска всех ребер, а затем выбора правильной сглаживающей плитки в соответствии с формой края.

Первым шагом будет поиск всех ребер. В приведенном ниже примере краевые плитки, обозначенные символом X, представляют собой зеленые плитки с черепицей в виде одного или нескольких из восьми соседних плит. С различными типами рельефа это условие может привести к тому, что плитка станет краевой плитой, если она имеет соседние с меньшим рельефом.

Edge tiles.

Как только все граничные плитки обнаружены, следующая вещь, чтобы сделать, - это выбрать правильную сглаживающую плитку для каждой кромочной плитки. Вот мое представление ваших сглаживающих плит.

Smoothing tiles.

Обратите внимание, что на самом деле не так много разных типов плиток. Нам нужны восемь внешних плиток из одного из квадратов 3x3, но только четыре угловых квадрата от другого, поскольку прямолинейные плитки уже найдены в первом квадрате. Это означает, что в общей сложности 12 различных случаев мы должны различать.

Теперь, глядя на одну граничную плиту, мы можем определить, каким путем граница поворачивается, глядя на ее четыре ближайших соседних плит. Маркировка кромочной плитки с помощью X, как и выше, имеет следующие шесть различных случаев.

Six cases.

Эти случаи используются для определения соответствующей сглаживающей плитки, и мы можем соответствующим образом подсчитать сглаживающие плитки.

Smoothed tiles with numbers.

По-прежнему существует выбор a или b для каждого случая. Это зависит от того, на какой стороне трава. Одним из способов определить это может быть отслеживание ориентации границы, но, вероятно, самый простой способ сделать это - выбрать одну плиту рядом с краем и посмотреть, какой у нее цвет. На изображении ниже показаны два случая 5a) и 5b), которые можно различить, например, для проверки цвета верхней правой плитки.

Choosing 5a or 5b.

Окончательное перечисление для исходного примера будет выглядеть следующим образом.

Final enumeration.

И после выбора соответствующей граничной плитки граница будет выглядеть примерно так.

Final result.

В качестве заключительной заметки я могу сказать, что это будет работать до тех пор, пока граница будет несколько регулярной. Точнее, краевые плитки, которые не имеют ровно двух граничных плит, как их соседи, придется рассматривать отдельно. Это произойдет для краевых плиток на краю карты, которые будут иметь один соседний край и для очень узких участков местности, где количество соседних граничных плит может быть три или даже четыре.

Ответ 2

Следующий квадрат представляет собой металлическую пластину. В правом верхнем углу находится "теплоотвод". Мы видим, как температура этой точки остается постоянной, металлическая пластина сходится к постоянной температуре в каждой точке, будучи естественным образом более горячим около верха:

heatplate

Проблема нахождения температуры в каждой точке может быть решена как "краевая задача". Однако самый простой способ выработать тепло в каждой точке - это моделирование пластины как сетки. Мы знаем точки на сетке при постоянной температуре. Мы устанавливаем температуру всех неизвестных точек как комнатную температуру (как будто вентиляционное отверстие только что было включено). Затем мы даем тепло распространиться через пластину, пока не достигнем сходимости. Это делается путем итерации: мы перебираем каждую точку (i, j). Устанавливаем точку (i, j) = (точка (i + 1, j) + точка (i-1, j) + точка (i, j + 1) + точка (i, j-1))/4 [если точка (i, j) имеет теплоотвод постоянной температуры]

Если вы примените это к своей проблеме, это очень похоже, только средние цвета вместо температур. Вам, вероятно, потребуется около 5 итераций. Я предлагаю использовать сетку 400x400. Thats 400x400x5 = менее 1 миллиона итераций, которые будут быстрыми. Если вы используете только 5 итераций, вам, вероятно, не нужно будет беспокоиться о том, чтобы держать какие-либо точки в постоянном цвете, так как они не будут слишком сильно смещаться от их оригинала (на самом деле только цвета на расстоянии 5 от цвета могут быть выполнены цветом). Псевдокод:

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

Ответ 3

Итак, первые мысли состоят в том, что автоматизация идеального решения проблемы требует некоторой довольно мясистой математики интерполяции. Основываясь на том факте, что вы упоминаете предварительно обработанные изображения, я полагаю, что полное интерполяционное решение здесь не оправдано.

С другой стороны, как вы сказали, окончание карты вручную приведет к хорошему результату... но я также предполагаю, что любой ручной процесс исправления сбоев также не является вариантом.

Вот простой алгоритм, который не дает идеального результата, но это очень полезно, учитывая низкое усилие, которое оно требует.

Вместо того, чтобы пытаться смешивать КАЖДОЙ кромочной плитки (это означает, что вам нужно либо знать результат смешивания смежных фрагментов вначале - интерполяцию, либо вам нужно доработать всю карту несколько раз и не можете полагаться на предварительные настройки, генерируемые плитки), почему бы не смешать плитки в чередующемся шаблоне контрольной панели?

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

т.е. только смешение плиток, снятых в матрице выше?

Предполагая, что только разрешенные шаги в значении являются единичными, у вас есть только несколько плиток для проектирования...

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]           

Всего будет 16 шаблонов. Если вы воспользуетесь вращательной и отражательной симметрией, будет еще меньше.

'A' будет простой плиткой [1]. "D" будет диагональю.

В углах плиток будут небольшие разрывы, но они будут незначительными по сравнению с примером, который вы дали.

Если я смогу, я обновлю это сообщение позже.

Ответ 4

Я играл с чем-то похожим на это, он не был завершен по ряду причин; но в основном это займет матрица из 0 и 1, 0 - земля, а 1 - стена для приложения генератора лабиринта во Flash. Поскольку AS3 похож на JavaScript, нетрудно переписать в 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;
}

В основном это проверяет, что каждая плитка вокруг него идет слева направо, сверху вниз и предполагает, что крайние плитки всегда 1. Я также позволил экспортировать изображения в качестве файла для использования в качестве ключа:

Wall tiles

Это неполный и, вероятно, хакерский способ достичь этого, но я подумал, что это может принести пользу.

Изменить: Снимок экрана с результатом этого кода.

Generated Result

Ответ 5

Я бы предложил несколько вещей:

  • Не имеет значения, что такое "центральная" плитка, не так ли? это может быть 2, но если все остальные равны 1, оно будет показывать 1?

  • это только имеет значение, каковы углы, когда есть разница в непосредственных соседях с вершиной или стороной. Если все непосредственные соседи равны 1, а угол равен 2, он будет показывать 1.

  • Я бы, вероятно, предварительно вычислил все возможные комбинации соседей, создав 8 индексный массив с четырьмя четырьмя, указывающими значения верхних/нижних соседей, а второй - диагоналями:

edge [N] [E] [S] [W] [NE] [SE] [SW] [NW] = любое смещение в спрайт

поэтому в вашем случае [2] [2] [1] [1] [2] [2] [1] [1] = 4 (5-й спрайт).

в этом случае [1] [1] [1] [1] будет 1, [2] [2] [2] [2] будет 2, а остальное нужно будет разработать. Но поиск конкретной плитки был бы тривиальным.