Алгоритм Разбиения Карты На Листы


Карте

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

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

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

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

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

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

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

в Алгоритм

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

это пример сценария того, что движок может иметь для рендеринга, причем текущая плитка является той, которая отмечена Х.

создается массив 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]
     ...
}

что дает такой результат:

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

Идеальный Вариант

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

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

Решение?

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

5 150

5 ответов:

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

первым шагом было бы найти все ребра. В приведенном ниже примере края плитки отмечены 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.

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

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

heatplate

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

Если вы примените это к ваша проблема, она очень похожа, просто средние цвета вместо температур. Вы, вероятно, потребуется около 5 итераций. Я предлагаю использовать сетку 400x400. Это 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

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

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

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

[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" - это диагональ.

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

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

Я играл с чем-то подобным этому, он не был закончен по ряду причин; но в основном это займет матрицу 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

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

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

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

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

ребра[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, а остальное надо было бы проработать. Но поиск для конкретной плитки будет будьте тривиальны.