Harita Döşeme Algoritması


153

Harita

Perlin gürültü yükseklik haritalarını kullanarak, ardından gürültünün yüksekliğine göre bir döşeme türü atayarak Javascript ile bir çini tabanlı RPG yapıyorum.

Haritalar bunun gibi bir şeye benziyor (mini harita görünümünde).

resim açıklamasını buraya girin

Görüntüdeki her pikselden renk değerini ayıklayan ve kiremit sözlüğünde bir döşemeye karşılık gelen (0-255) arasındaki konumuna bağlı olarak bir tamsayıya (0-5) dönüştüren oldukça basit bir algoritmaya sahibim. Bu 200x200 dizisi daha sonra istemciye iletilir.

Motor daha sonra karoları dizideki değerlerden belirler ve bunları tuvale çizer. Bu yüzden, gerçekçi görünümlü özelliklere sahip ilginç dünyalarla sonuçlanıyorum: dağlar, denizler vb.

Şimdi yapmak istediğim bir sonraki şey , komşu aynı tipte değilse , fayansların komşularına sorunsuzca karışmasına neden olacak bir tür karıştırma algoritması uygulamaktı . Yukarıdaki örnek harita, oyuncunun mini haritalarında gördükleridir. Ekranda, beyaz dikdörtgenle işaretlenmiş bölümün oluşturulmuş bir sürümünü görürler; Burada döşemeler tek renkli pikseller yerine görüntüleri ile oluşturulur.

Bu, kullanıcının haritada göreceklerine bir örnektir, ancak yukarıdaki görünümle gösterilen konumla aynı değildir!

resim açıklamasını buraya girin

Bu görüşte geçişin gerçekleşmesini istiyorum.

Algoritma

Haritayı farklı bir çini yanında olması şartıyla haritayı görünüm alanı içinde gezdirecek ve her bir döşemenin üzerinde başka bir görüntü oluşturacak basit bir algoritma buldum. (Haritayı değiştirmiyorum! Sadece fazladan görüntü oluşturma.) Algoritmanın fikri mevcut döşemenin komşularını profillemekti:

Döşeme profiline bir örnek

Bu, geçerli karo X ile işaretlenmiş olan motorun oluşturması gerekebileceğine dair örnek bir senaryodur.

Bir 3x3 dizisi oluşturulur ve çevresindeki değerler okunur. Bu örnek için dizi şöyle görünür.

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

Benim fikrim o zaman mümkün karo konfigürasyonları için bir dizi vaka çalışmak oldu. Çok basit bir seviyede:

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

Bu da bu sonucu verir:

Sonuç

Hangi bir geçiş olarak çalışır [0][1]için [1][1]değil gelen [1][1]için [2][1]sert bir kenar kalıntıları nerede. Bu nedenle, bu durumda bir köşe döşemesinin kullanılması gerektiğini düşündüm. İhtiyaç duyulabilecek tüm karo kombinasyonlarını tutacağını düşündüğüm iki 3x3 sprite sayfası oluşturdum. Sonra bunu oyundaki tüm karolar için kopyaladım (Beyaz alanlar şeffaf). Bu, her bir karo türü için 16 karo olur (Her sprite sayfasındaki orta karolar kullanılmaz.)

KumSand2

İdeal Sonuç

Yani, bu yeni döşemeler ve doğru algoritma ile örnek bölüm şöyle görünecektir:

Doğru

Yaptığım her girişim başarısız oldu, algoritmada her zaman bir kusur var ve desenler garip oluyor. Tüm davaları doğru anlayamıyorum ve genel olarak bunu yapmanın kötü bir yolu gibi görünüyor.

Bir çözüm?

Eğer birisi bu efekti nasıl yaratabileceğim konusunda alternatif bir çözüm sunabilseydi ya da profil oluşturma algoritmasını yazmak için hangi yöne gideceğim konusunda çok minnettar olurum!


7
Göz at Bu yazıda ve sıra bağlantılı makale, özellikle bu bir . Blogun kendisi, bir başlangıç ​​noktası olarak kullanılabilecek birçok fikir içeriyor. İşte bir genel bakış.
Darcara

algoritmanızı basitleştirmelisiniz.
şuna bakın

Yanıtlar:


117

Bu algoritmanın temel fikri tüm kenarları bulmak için bir ön işleme adımı kullanmak ve daha sonra kenarın şekline göre doğru yumuşatma döşemesini seçmek.

İlk adım tüm kenarları bulmak olacaktır. Aşağıdaki örnekte, X ile işaretlenmiş kenar döşemelerinin tümü, sekiz komşu döşemesinden biri veya daha fazlası olarak bronz bir döşemeye sahip yeşil döşemelerdir. Farklı arazi türlerinde bu koşul, daha düşük arazi numarasına sahip komşuları varsa, bir kiremitin kenar döşemesi haline gelebilir.

Kenar döşemeleri.

Tüm kenar döşemeleri algılandıktan sonra yapılacak sonraki şey, her kenar döşemesi için doğru düzgünleştirme döşemesini seçmektir. İşte benim yumuşatma fayansları benim temsil.

Fayans yumuşatma.

Aslında o kadar farklı fayans türü olmadığını unutmayın. 3x3 karelerden birinden sekiz dış döşemeye ihtiyacımız var, ancak düz kenar döşemeleri zaten ilk karede bulunduğu için diğerinden sadece dört köşe karesine. Bu, toplamı ayırmamız gereken 12 farklı durum olduğu anlamına gelir.

Şimdi, bir kenar döşemesine bakarak, en yakın dört komşu döşemeye bakarak sınırın hangi yöne döndüğünü belirleyebiliriz. Bir kenar döşemesini X ile yukarıdaki gibi işaretlemek, aşağıdaki altı farklı duruma sahibiz.

Altı vaka.

Bu kasalar, karşılık gelen düzleme karolarını belirlemek için kullanılır ve düzleştirme karolarını buna göre numaralandırabiliriz.

Düzeltilmiş fayans numaraları ile.

Her vaka için hala a veya b seçeneği vardır. Bu, çimlerin hangi tarafa bağlı olduğuna bağlıdır. Bunu belirlemenin bir yolu, sınırın yönelimini takip etmek olabilir, ancak muhtemelen bunu yapmanın en basit yolu, kenarın yanında bir karo seçmek ve hangi renge sahip olduğunu görmektir. Aşağıdaki görüntü, örneğin sağ üst döşemenin rengini kontrol ederek birbirinden ayırt edilebilen iki durumu (5a) ve 5b) göstermektedir.

5a veya 5b seçimi.

Orijinal örneğin son numaralandırması böyle görünecektir.

Son numaralandırma.

Ve karşılık gelen kenar döşemesini seçtikten sonra kenarlık böyle bir şeye benzeyecektir.

Son sonuç.

Son bir not olarak, sınır biraz düzenli olduğu sürece bunun işe yarayacağını söyleyebilirim. Daha kesin olarak, komşuları ayrı ayrı ele alınacağı için tam olarak iki kenar döşemesine sahip olmayan kenar döşemeleri. Bu, haritanın kenarındaki tek bir kenar komşusuna sahip olacak kenar döşemeleri ve komşu kenar döşemelerinin sayısının üç hatta dört olabileceği çok dar arazi parçaları için gerçekleşecektir.


1
Bu harika ve benim için çok yararlı. Bazı karoların doğrudan diğerlerine geçemediği bir durumla ilgileniyorum. Örneğin, "kir" fayans "açık çim" ve "hafif çim" geçiş "orta çim" olabilir. Tiled (mapeditor.org), arazi fırçası için bir tür ağaç araması yaparak bunu ele almak için harika bir iş çıkarır; Yine de onu yeniden üretemedim.
Clay

12

Aşağıdaki kare bir metal plakayı temsil eder. Sağ üst köşede bir "ısı havalandırması" vardır. Bu noktanın sıcaklığı sabit kaldıkça, metal plakanın her noktada sabit bir sıcaklığa nasıl yaklaştığını ve doğal olarak tepenin yakınında daha sıcak olduğunu görebiliriz:

heatplate

Her bir noktada sıcaklığı bulma sorunu bir "Sınır değer problemi" olarak çözülebilir. Bununla birlikte, her bir noktada ısıyı çalıştırmanın en basit yolu, plakayı bir ızgara olarak modellemektir. Izgaradaki noktaları sabit sıcaklıkta biliyoruz. Tüm bilinmeyen noktaların sıcaklığını oda sıcaklığı olarak ayarladık (havalandırma deliği henüz açılmış gibi). Daha sonra yakınsamaya ulaşana kadar ısının plakadan yayılmasına izin verdik. Bu yineleme ile yapılır: her (i, j) noktadan yineliyoruz. Nokta (i, j) = (nokta (i + 1, j) + nokta (i-1, j) + nokta (i, j + 1) + nokta (i, j-1)) / 4 ayarladık noktası (i, j) sabit sıcaklıkta bir ısı havalandırmasına sahiptir]

Bunu probleminize uygularsanız, çok benzer, sıcaklıklar yerine sadece ortalama renkler. Muhtemelen yaklaşık 5 tekrarlamaya ihtiyacınız olacaktır. 400x400 ızgara kullanmanızı öneririm. Bu 400x400x5 = hızlı olacak 1 milyondan az yineleme. Yalnızca 5 yineleme kullanırsanız, herhangi bir noktayı sabit renkte tutmak konusunda endişelenmenize gerek yoktur, çünkü orijinallerinden çok fazla kaymayacaklardır (aslında sadece renkten 5 mesafe içindeki noktalar renkten etkilenebilir). Sahte kod:

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

bunu biraz daha genişletebilir misiniz? meraklıyım ve açıklamanı anlayamıyorum. Yinelemeleri yaptıktan sonra ortalama renk değeri nasıl kullanılır?
Chii

1
Her ızgara noktası ızgarası [i] [j], tuvale uygun renkte küçük bir dikdörtgen (veya tek tek piksel) olarak çizilebilir.
Robert King

5

Tamam, ilk düşünceler, soruna mükemmel bir çözümün otomatikleştirilmesinin, oldukça etli enterpolasyon matematiği gerektirmesidir. Önceden oluşturulmuş karo görüntülerinden bahsettiğiniz gerçeğine dayanarak, tam enterpolasyon çözümünün burada garanti edilmediğini varsayıyorum.

Öte yandan, dediğin gibi, haritayı elle bitirmek iyi bir sonuca yol açacaktır ... ama aynı zamanda aksaklıkları düzeltmek için herhangi bir manuel işlemin de bir seçenek olmadığını varsayıyorum.

İşte mükemmel bir sonuç vermeyen basit bir algoritma, ancak aldığı düşük çabaya göre çok ödüllendirici.

HER kenar döşemesini karıştırmaya çalışmak yerine (yani önce bitişik döşemeleri karıştırmanın sonucunu bilmeniz gerekir - enterpolasyon veya tüm haritayı birkaç kez hassaslaştırmanız gerekir ve önceden oluşturulmuş döşemelere güvenemezsiniz) neden farklı bir dama tahtası deseni fayans karıştırmayın?

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

Yani sadece yukarıdaki matristeki yıldız işaretli karoları mı karıştırıyorsunuz?

Değerde izin verilen tek adımın birer birer olduğu varsayıldığında, tasarım yapmak için sadece birkaç kutunuz var ...

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]           

Toplam 16 desen olacak. Dönme ve yansıma simetrisinden faydalanırsanız daha da az olacaktır.

'A' sade [1] tarzda bir karo olacaktır. 'D' köşegen olur.

Fayansların köşelerinde küçük süreksizlikler olacaktır, ancak verdiğiniz örneğe kıyasla bunlar küçük olacaktır.

Yapabilirsem bu gönderiyi daha sonra resimlerle güncelleyeceğim.


Kulağa hoş geliyor, ne demek istediğiniz hakkında daha iyi bir fikir edinmek için bazı görüntülerle görmek isterim.
Dan Prince

Görüntüleri bir araya getiremem çünkü sahip olduğumu düşündüğüm yazılıma sahip değilim ... Ama düşünüyordum ve olabildiğince iyi bir çözüm değil. Kesinlikle çapraz geçişler yapabilirsiniz, ancak bu düzeltme algoritması tarafından diğer geçişlere gerçekten yardımcı olmuyor. Haritanızın 90 derece geçiş içermeyeceğini bile garanti edemezsiniz. Üzgünüm, sanırım bu biraz hayal kırıklığına uğrattı.
mükemmeliyetçi

3

Buna benzer bir şeyle oynuyordum, birkaç nedenden dolayı bitmedi; ancak temel olarak Flash'ta bir labirent oluşturucu uygulaması için 0 ve 1, 0'ın zemin ve 1'in bir duvar olması gerekir. AS3 JavaScript'e benzer olduğu için JS'de yeniden yazmak zor olmaz.

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;
}

Temelde bu, etrafındaki her döşemeyi soldan sağa, yukarıdan aşağıya doğru kontrol eder ve kenar döşemelerinin her zaman 1 olduğunu varsayar. Ayrıca, görüntüleri anahtar olarak kullanmak için bir dosya olarak dışa aktarma özgürlüğünü de aldım:

Duvar karoları

Bu eksik ve muhtemelen bunu başarmak için acayip bir yol, ama bunun bir yararı olabileceğini düşündüm.

Düzenle: Bu kodun sonucunun ekran görüntüsü.

Oluşturulan Sonuç


1

Birkaç şey öneririm:

  • "orta" kutucuğun ne olduğu önemli değil, değil mi? 2 olabilir, ama eğer diğerleri 1 ise, 1 gösterecektir?

  • sadece köşelerin ne olduğu önemlidir, hemen komşularda üste veya yana bir fark olduğunda. Tüm yakın komşular 1 ve bir köşe 2 ise, 1 gösterecektir.

  • Muhtemelen komşuların tüm kombinasyonlarını önceden hesaplar, ilk dörtte üst / alt komşuların değerlerini ve ikincisi köşegenleri gösteren 8 dizin dizisi oluşturur:

kenarlar [N] [E] [S] [W] [NE] [SE] [SW] [NW] = hareketli grafiğe hangi ofset olursa olsun

yani sizin durumunuzda, [2] [2] [1] [1] [2] [2] [1] [1] = 4 (5. sprite).

bu durumda, [1] [1] [1] [1] 1, [2] [2] [2] [2] 2 ve geri kalanı üzerinde çalışmak gerekir. Ancak belirli bir döşemeyi aramak önemsiz olacaktır.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.