Алгоритм плитки карти


153

Карта

Я створюю RPG на основі плитки з Javascript, використовуючи перманентні карти висоти шуму, а потім призначаю тип плитки на основі висоти шуму.

Карти виглядають приблизно таким чином (у вигляді міні-карти).

введіть тут опис зображення

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

Потім двигун визначає плитки зі значень у масиві та малює їх на полотні. Отже, я закінчую цікавими світами, які мають риси реалістичного вигляду: гори, моря тощо.

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

Це приклад того, що бачив би користувач на карті, але це не таке місце розташування, як показано вікно перегляду вище!

введіть тут опис зображення

Саме з цього погляду я хочу, щоб відбувся перехід.

Алгоритм

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

Приклад профілю плитки

Це приклад сценарію того, що може відображати двигун, при цьому поточна плитка є тією, що позначена символом 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]
     ...
}

Що дає такий результат:

Результат

Який працює як перехід від [0][1]до [1][1], але не від [1][1]до [2][1], де жорстким краєм залишається. Тож я подумав, що в такому випадку доведеться використовувати кутову плитку. Я створив два спрайтових аркуші 3х3, які, на мою думку, утримуватимуть усі можливі комбінації плиток, які можуть знадобитися. Потім я повторив це для всіх плиток, які є в грі (Білі області прозорі). Таким чином, це 16 плиток для кожного типу плитки (центральні плитки на кожному spritesheet не використовуються.)

ПісокПісок2

Ідеальний результат

Отже, з цими новими плитками та правильним алгоритмом розділ прикладу виглядатиме так:

Правильно

Кожна зроблена мною спроба не вдалася, але в алгоритмі завжди є деякі вади, і шаблони закінчуються дивними. Я не можу здатись, що всі справи є правильними, і загалом це здається поганим способом.

Рішення?

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


7
Подивіться також цю статтю та пов’язані статті, особливо цю . Сам блог містить безліч ідей, які можуть послужити відправною точкою. Ось огляд.
Даркара

ви повинні спростити свій алгоритм. перевірте це:
Двомірні

Відповіді:


117

Основна ідея цього алгоритму - використовувати крок попередньої обробки, щоб знайти всі краї, а потім вибрати правильну плитку згладжування відповідно до форми краю.

Першим кроком було б знайти всі краї. У прикладі під крайовими плитками, позначеними символом X, - всі зелені плитки із засмаглою плиткою, як одна чи більше з восьми сусідніх плиток. За різних типів рельєфу ця умова може перетворюватися на черепицю, що є крайовою плиткою, якщо вона має сусідів з нижньою місцевістю.

Крайова плитка.

Після виявлення всіх крайових плиток наступне, що потрібно зробити, - це вибрати правильну плитку для вирівнювання для кожної крайової плитки. Ось моє представлення вашої гладкої плитки.

Згладжування плитки.

Зауважте, що насправді не так багато різних типів плиток. Нам потрібні вісім зовнішніх плиток з одного з квадратів 3х3, але лише чотири кутових квадрата з іншого, оскільки прямолінійні плитки вже знайдені в першому квадраті. Це означає, що загалом існує 12 різних випадків, які ми повинні розрізняти.

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

Шість справ.

Ці випадки використовуються для визначення відповідної згладжувальної плитки, і ми можемо відповідно пронумерувати плитки, що розгладжуються.

Згладжена плитка з цифрами.

Існує ще вибір а або b для кожного випадку. Це залежить від того, на якій стороні трава. Одним із способів визначити це може бути відстеження орієнтації кордону, але, мабуть, найпростіший спосіб зробити це - вибрати одну плитку поруч з краєм і подивитися, який колір у неї є. На зображенні нижче показані два випадки 5a) та 5b), які можна розрізнити, наприклад, перевіривши колір верхньої правої плитки.

Вибір 5a або 5b.

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

Підсумкове перерахування.

І після вибору відповідної крайової плитки межа буде виглядати приблизно так.

Кінцевий результат.

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


1
Це мені дуже допомагає. Я маю справу з випадком, коли одні плитки не можуть переходити безпосередньо в інші. Наприклад, плитка "бруду" може переходити в "легку траву", а "легка трава" може переходити в "середню траву". Черепиця (mapeditor.org) робить велику роботу з цим завданням, застосовуючи певний тип дерева для пошуку місцевості; Я все ще не зможу відтворити це.
Глина

12

Наступний квадрат являє собою металеву пластину. У верхньому правому куті є "вентиляція тепла". Ми можемо бачити, як температура цієї точки залишається постійною, металева плита перетворюється на постійну температуру в кожній точці, будучи природно гарячішою біля верху:

теплова плита

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

Якщо ви застосовуєте це до своєї проблеми, це дуже схоже, просто середні кольори замість температури. Можливо, вам знадобиться близько 5 ітерацій. Я пропоную використовувати сітку 400х400. Це 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

Ви могли б розширити це трохи більше? мені цікаво, і я не можу зрозуміти ваше пояснення. Як можна використовувати середнє значення кольору після того, як ви зробили ітерації?
Chii

1
Кожна сітка точок сітки [i] [j] може бути намальована на полотні у вигляді невеликого прямокутника (або окремого пікселя) відповідного кольору.
Роберт Кінг

5

Гаразд, першими думками є те, що автоматизація ідеального рішення проблеми вимагає певної математичної інтерполяції. Виходячи з того, що ви згадуєте попередньо відредаговані зображення плитки, я припускаю, що повне рішення про інтерполяцію тут не виправдане.

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

Ось простий алгоритм, який не дає ідеального результату, але це дуже корисно, грунтуючись на малих зусиллях.

Замість того, щоб намагатися змішати КОЖНУ крайну плитку (це означає, що вам потрібно або знати результат змішування сусідніх плиток спочатку - інтерполяцію, або вам потрібно кілька разів доопрацювати всю карту і не можете покладатися на попередньо створені плитки) чому б не змішати плитки за змінним шаблоном шашок?

[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' буде діагональною.

У кутах плитки будуть невеликі розриви, але вони будуть незначними порівняно з прикладом, який ви навели.

Якщо я можу, я згодом оновлю цю публікацію із зображеннями.


Це добре звучить, мені було б цікаво побачити це з деякими зображеннями, щоб краще зрозуміти, що ви маєте на увазі.
Дан принц

Я не можу скласти жодних зображень, тому що у мене немає програмного забезпечення, про яке я думав, що маю ... Але я думав, і це не таке хороше рішення, як це могло бути. Ви точно можете робити діагональні переходи, але цей алгоритм вирівнювання насправді не допомагає іншим переходам. Ви навіть не можете гарантувати, що ваша карта буде містити НЕ 90-градусних переходів. Вибачте, я гадаю, що це трохи підведено.
перфекціоніст

3

Я грав навколо себе чимось подібним, це не було закінчено з кількох причин; але в основному знадобиться матриця 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. Я також взяв на себе можливість експортувати зображення як файл, який використовується як ключ:

Настінна плитка

Це неповний і, мабуть, хитрий спосіб досягти цього, але я подумав, що це може принести певну користь.

Редагувати: знімок екрана результату цього коду.

Отриманий результат


1

Я б запропонував кілька речей:

  • не має значення, що таке "центральна" плитка, правда? це може бути 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, а решта доведеться опрацювати. Але пошук конкретної плитки був би тривіальним.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.