Algoritma Tiling Peta

153

Peta

Saya membuat RPG berbasis ubin dengan Javascript, menggunakan perlin noise noise tinggi, kemudian menetapkan jenis ubin berdasarkan ketinggian suara.

Peta akhirnya terlihat seperti ini (dalam tampilan minimap).

masukkan deskripsi gambar di sini

Saya memiliki algoritma yang cukup sederhana yang mengekstraksi nilai warna dari setiap piksel pada gambar dan mengubahnya menjadi bilangan bulat (0-5) tergantung pada posisinya antara (0-255) yang sesuai dengan ubin di kamus ubin. Array 200x200 ini kemudian diteruskan ke klien.

Mesin kemudian menentukan ubin dari nilai-nilai dalam array dan menariknya ke kanvas. Jadi, saya berakhir dengan dunia menarik yang memiliki fitur tampak realistis: gunung, laut dll.

Sekarang hal berikutnya yang ingin saya lakukan adalah menerapkan semacam algoritma pencampuran yang akan menyebabkan ubin berbaur mulus dengan tetangga mereka, jika tetangga tidak dari jenis yang sama. Contoh peta di atas adalah apa yang dilihat pemain di minimapnya. Pada layar mereka melihat versi yang diberikan dari bagian yang ditandai oleh kotak putih; di mana ubin diberikan dengan gambar mereka daripada sebagai piksel warna tunggal.

Ini adalah contoh dari apa yang akan dilihat pengguna di peta tetapi ini bukan lokasi yang sama dengan yang ditampilkan di viewport!

masukkan deskripsi gambar di sini

Dalam pandangan inilah saya ingin transisi terjadi.

Algoritma

Saya datang dengan algoritma sederhana yang akan melintasi peta di dalam viewport dan membuat gambar lain di atas setiap ubin, asalkan itu di sebelah ubin dari jenis yang berbeda. (Tidak mengubah peta! Hanya merender beberapa gambar tambahan.) Gagasan algoritma ini adalah untuk profil tetangga ubin saat ini:

Contoh profil ubin

Ini adalah contoh skenario dari apa yang mungkin mesin render, dengan ubin saat ini yang ditandai dengan X.

Array 3x3 dibuat dan nilai-nilai di sekitarnya dibaca. Jadi untuk contoh ini array akan terlihat seperti.

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

Gagasan saya adalah untuk mengerjakan serangkaian kasus untuk kemungkinan konfigurasi ubin. Pada tingkat yang sangat sederhana:

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

Yang memberikan hasil ini:

Hasil

Yang berfungsi sebagai transisi dari [0][1]ke [1][1], tetapi tidak dari [1][1]ke [2][1], di mana tepi yang keras tetap. Jadi saya pikir dalam hal itu ubin sudut harus digunakan. Saya membuat dua lembar sprite 3x3 yang saya pikir akan menampung semua kombinasi ubin yang mungkin diperlukan. Lalu saya meniru ini untuk semua ubin yang ada di dalam game (Area putih transparan). Ini akhirnya menjadi 16 ubin untuk setiap jenis ubin (Ubin tengah pada setiap spritesheet tidak digunakan.)

PasirSand2

Hasil Yang Ideal

Jadi, dengan ubin baru ini dan algoritma yang benar, bagian contoh akan terlihat seperti ini:

Benar

Namun, setiap upaya yang saya lakukan gagal, selalu saja ada kekurangan dalam algoritme dan pola yang aneh. Saya sepertinya tidak bisa menyelesaikan semua kasus dengan benar dan secara keseluruhan sepertinya cara yang buruk untuk melakukannya.

Sebuah solusi?

Jadi, jika ada yang bisa memberikan solusi alternatif tentang bagaimana saya bisa membuat efek ini, atau arah apa yang harus dilakukan untuk menulis algoritma profil, maka saya akan sangat berterima kasih!

Dan Prince
sumber
7
Lihat artikel ini dan artikel yang terhubung juga, terutama yang ini . Blog itu sendiri mengandung banyak ide yang dapat berfungsi sebagai titik awal. Berikut ini adalah ikhtisar.
Darcara
Anda harus menyederhanakan algoritma Anda. periksa ini: Dua-Dimensi-Seluler-Automata
user1097489

Jawaban:

117

Ide dasar dari algoritma ini adalah menggunakan langkah pra-pemrosesan untuk menemukan semua tepi dan kemudian memilih ubin penghalusan yang benar sesuai dengan bentuk tepi.

Langkah pertama adalah menemukan semua sisi. Pada contoh di bawah ubin tepi ditandai dengan X semua ubin hijau dengan ubin tan sebagai satu atau lebih dari delapan ubin tetangga mereka. Dengan berbagai jenis medan, kondisi ini dapat diterjemahkan ke ubin yang menjadi ubin tepi jika memiliki tetangga dengan angka medan yang lebih rendah.

Ubin tepi.

Setelah semua ubin tepi terdeteksi, hal selanjutnya yang harus dilakukan adalah memilih ubin penghalusan yang tepat untuk setiap ubin tepi. Ini representasi saya tentang ubin smoothing Anda.

Ubin halus.

Perhatikan bahwa sebenarnya tidak ada banyak jenis ubin yang berbeda. Kita membutuhkan delapan ubin luar dari salah satu kotak 3x3 tetapi hanya empat kotak sudut dari yang lain karena ubin tepi lurus sudah ditemukan di kotak pertama. Ini berarti ada total 12 kasus berbeda yang harus kita bedakan.

Sekarang, melihat satu ubin tepi kita dapat menentukan ke arah mana batas berbelok dengan melihat empat ubin tetangga terdekat. Menandai ubin tepi dengan X seperti di atas kami memiliki enam kasus berikut.

Enam kasus.

Kasing ini digunakan untuk menentukan ubin smoothing yang sesuai dan kami dapat menghitung ubin smoothing yang sesuai.

Ubin yang dihaluskan dengan angka.

Masih ada pilihan a atau b untuk setiap kasus. Ini tergantung pada sisi mana rumput itu berada. Salah satu cara untuk menentukan ini bisa dengan melacak orientasi batas tetapi mungkin cara paling sederhana untuk melakukannya adalah dengan memilih satu ubin di sebelah tepi dan melihat warna apa yang dimilikinya. Gambar di bawah ini menunjukkan dua case 5a) dan 5b) yang dapat dibedakan antara dengan misalnya memeriksa warna ubin kanan atas.

Memilih 5a atau 5b.

Penghitungan akhir untuk contoh asli akan terlihat seperti ini.

Pencacahan akhir.

Dan setelah memilih ubin tepi yang sesuai perbatasan akan terlihat seperti ini.

Hasil akhir.

Sebagai catatan terakhir saya bisa mengatakan bahwa ini akan berfungsi selama batasnya agak teratur. Lebih tepatnya, ubin tepi yang tidak memiliki tepat dua ubin tepi karena tetangga mereka harus diperlakukan secara terpisah. Ini akan terjadi untuk ubin tepi di tepi peta yang akan memiliki tetangga tepi tunggal dan untuk potongan medan yang sangat sempit di mana jumlah ubin tepi tetangga bisa tiga atau bahkan empat.

pengguna1884905
sumber
1
Ini bagus dan sangat membantu saya. Saya berurusan dengan kasus di mana beberapa ubin tidak dapat bertransisi langsung menjadi yang lain. Misalnya, ubin "tanah" dapat bertransisi menjadi "rumput ringan" dan "rumput ringan" dapat beralih ke "rumput menengah". Tiled (mapeditor.org) melakukan pekerjaan yang baik untuk menangani hal ini dengan menerapkan beberapa jenis pencarian pohon untuk sikat medan; Saya belum bisa mereproduksinya.
Clay
12

Kotak berikut mewakili pelat logam. Ada "lubang panas" di sudut kanan atas. Kita dapat melihat bagaimana suhu titik ini tetap konstan, pelat logam menyatu ke suhu konstan di setiap titik, secara alami lebih panas di dekat bagian atas:

pelat pemanas

Masalah menemukan suhu di setiap titik dapat diselesaikan sebagai "masalah nilai batas". Namun cara paling sederhana untuk mengetahui panas pada setiap titik adalah dengan memodelkan pelat sebagai kisi. Kami tahu titik-titik di grid pada suhu konstan. Kami mengatur suhu semua titik yang tidak diketahui menjadi suhu kamar (seolah-olah ventilasi baru saja dihidupkan). Kami kemudian membiarkan panas menyebar melalui piring sampai kami mencapai konvergensi. Ini dilakukan dengan iterasi: kita beralih melalui setiap poin (i, j). Kami menetapkan titik (i, j) = (titik (i + 1, j) + titik (i-1, j) + titik (i, j + 1) + titik (i, j-1)) / 4 [kecuali titik (i, j) memiliki lubang panas dengan suhu konstan]

Jika Anda menerapkan ini untuk masalah Anda, ini sangat mirip, hanya warna rata-rata, bukan suhu. Anda mungkin membutuhkan sekitar 5 iterasi. Saya sarankan menggunakan kotak 400x400. Thats 400x400x5 = kurang dari 1 juta iterasi yang akan cepat. Jika Anda hanya menggunakan 5 iterasi, Anda mungkin tidak perlu khawatir tentang memegang titik warna konstan, karena mereka tidak akan terlalu banyak bergeser dari aslinya (pada kenyataannya hanya titik dalam jarak 5 dari warna yang dapat dipengaruhi oleh warna). Kode palsu:

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
raja robert
sumber
Bisakah Anda mengembangkan ini sedikit lebih? Saya ingin tahu, dan saya tidak dapat memahami penjelasan Anda. Bagaimana cara menggunakan nilai warna rata-rata setelah Anda melakukan iterasi?
Chii
1
Setiap kotak titik kisi [i] [j] dapat ditarik ke kanvas sebagai kotak kecil (atau piksel individu) dari warna yang sesuai.
robert king
5

Ok, jadi pemikiran pertama adalah bahwa mengotomatisasi solusi sempurna untuk masalah ini membutuhkan beberapa matematika interpolasi yang agak gemuk. Berdasarkan fakta bahwa Anda menyebutkan gambar ubin yang sudah dibuat sebelumnya, saya berasumsi bahwa solusi interpolasi lengkap tidak diperlukan di sini.

Di sisi lain seperti yang Anda katakan, menyelesaikan peta dengan tangan akan menghasilkan hasil yang baik ... tapi saya juga berasumsi bahwa setiap proses manual untuk memperbaiki gangguan juga bukan merupakan pilihan.

Berikut ini adalah algoritma sederhana yang tidak memberikan hasil yang sempurna, tetapi itu sangat bermanfaat berdasarkan upaya rendah yang diperlukan.

Alih-alih mencoba untuk mencampur SETIAP ubin tepi, (yang berarti bahwa Anda harus mengetahui hasil dari pencampuran ubin yang berdekatan terlebih dahulu - interpolasi, atau Anda perlu memperbaiki seluruh peta beberapa kali dan tidak dapat mengandalkan ubin yang dibuat sebelumnya) mengapa tidak mencampur ubin dalam pola checker-board bergantian?

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

Yaitu hanya memadukan ubin yang membintangi matriks di atas?

Dengan asumsi bahwa satu-satunya langkah yang diizinkan dalam nilai adalah satu per satu, Anda hanya memiliki beberapa ubin untuk dirancang ...

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]           

Akan ada total 16 pola. Jika Anda mengambil keuntungan dari simetri rotasi dan reflektif, maka akan semakin sedikit.

'A' akan menjadi ubin gaya [1] polos. 'D' akan menjadi diagonal.

Akan ada diskontinuitas kecil di sudut ubin, tetapi ini akan kecil dibandingkan dengan contoh yang Anda berikan.

Jika saya bisa, saya akan memperbarui posting ini dengan gambar nanti.

perfeksionis
sumber
Ini kedengarannya bagus, saya akan tertarik melihatnya dengan beberapa gambar untuk mendapatkan ide yang lebih baik tentang apa yang Anda maksudkan.
Dan Prince
Saya tidak dapat menyatukan gambar karena saya tidak memiliki perangkat lunak yang saya pikir saya punya ... Tapi saya sudah berpikir dan itu bukan solusi yang sebaik mungkin. Anda dapat melakukan transisi diagonal, pasti, tetapi transisi lain tidak benar-benar terbantu oleh algoritma perataan ini. Anda bahkan tidak dapat menjamin bahwa peta Anda akan berisi transisi NO 90 derajat. Maaf, saya kira yang satu ini agak mengecewakan.
perfeksionis
3

Saya bermain-main dengan sesuatu yang mirip dengan ini, itu tidak selesai karena sejumlah alasan; tetapi pada dasarnya itu akan mengambil matriks 0 dan 1, 0 sebagai tanah dan 1 menjadi dinding untuk aplikasi generator maze di Flash. Karena AS3 mirip dengan JavaScript, tidak akan sulit untuk menulis ulang di 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;
}

Pada dasarnya ini memeriksa setiap ubin di sekitarnya dari kiri ke kanan, atas ke bawah dan mengasumsikan bahwa ubin tepi selalu 1. Saya juga mengambil kebebasan mengekspor gambar sebagai file untuk digunakan sebagai kunci:

Ubin dinding

Ini tidak lengkap dan mungkin cara yang sulit untuk mencapai ini, tapi saya pikir itu mungkin bermanfaat.

Sunting: Screenshot dari hasil kode itu.

Hasil yang Dihasilkan

Ben
sumber
1

Saya akan menyarankan beberapa hal:

  • tidak masalah apa ubin "tengah" itu, kan? itu bisa 2, tetapi jika yang lainnya 1, itu akan menunjukkan 1?

  • itu hanya masalah apa sudutnya, ketika ada perbedaan tetangga terdekat ke atas atau samping. Jika semua tetangga terdekat adalah 1, dan sudutnya adalah 2, itu akan menunjukkan 1.

  • Saya mungkin akan menghitung ulang semua kemungkinan kombinasi tetangga, membuat array indeks 8 dengan empat yang pertama menunjukkan nilai-nilai tetangga atas / bawah, dan yang kedua menunjukkan diagonal:

tepi [N] [E] [S] [W] [NE] [SE] [SW] [NW] = apa pun yang diimbangi menjadi sprite

jadi dalam kasus Anda, [2] [2] [1] [1] [2] [2] [1] [1] = 4 (sprite ke-5).

dalam hal ini, [1] [1] [1] [1] akan menjadi 1, [2] [2] [2] [2] akan menjadi 2, dan sisanya harus diselesaikan. Tetapi pencarian untuk ubin tertentu akan sepele.

Elia
sumber