Bunu nasıl yaptılar: Terraria'da milyonlarca fayans


45

Terraria'ya benzer bir oyun motoru üzerinde çalıştım, çoğunlukla bir meydan okuma olarak ve çoğu şeyi çözdüğümde , kafamı milyonlarca etkileşimli / toplanabilir döşemeyle nasıl başa çıktıklarına dolamıyorum oyun bir anda var. Motorumda Terraria’da mümkün olanın 1 / 20’si civarında olan yaklaşık 500.000 kiremit oluşturmak, hala kiremitleri görüntülemişken bile, kare hızının 60’ın 20’ye düşmesine neden oluyor. Dikkat et, fayanslarla bir şey yapmıyorum, sadece hafızada tutuyorum.

Güncelleme : Nasıl yaptığımı göstermek için kod eklendi.

Bu, fayansları işleyen ve çizen bir sınıfın parçası. Suçlu her şeyi yineleyen, boş dizinler bile "foreach" kısmı sanırım.

...
    public void Draw(SpriteBatch spriteBatch, GameTime gameTime)
    {
        foreach (Tile tile in this.Tiles)
        {
            if (tile != null)
            {
                if (tile.Position.X < -this.Offset.X + 32)
                    continue;
                if (tile.Position.X > -this.Offset.X + 1024 - 48)
                    continue;
                if (tile.Position.Y < -this.Offset.Y + 32)
                    continue;
                if (tile.Position.Y > -this.Offset.Y + 768 - 48)
                    continue;
                tile.Draw(spriteBatch, gameTime);
            }
        }
    }
...

Ayrıca burada, her bir Döşeme SpriteBatch.Draw yöntemine dört çağrı kullandığı için güncelleme ile de yapabilecek Tile.Draw yöntemidir. Bu benim ototileme sistemimin bir parçası, komşu fayanslara bağlı olarak her köşeyi çizmek anlamına geliyor. texture_ * dikdörtgenler, her güncelleme için değil, seviye oluşturmada bir kez ayarlanır.

...
    public virtual void Draw(SpriteBatch spriteBatch, GameTime gameTime)
    {
        if (this.type == TileType.TileSet)
        {
            spriteBatch.Draw(this.texture, this.realm.Offset + this.Position, texture_tl, this.BlendColor);
            spriteBatch.Draw(this.texture, this.realm.Offset + this.Position + new Vector2(8, 0), texture_tr, this.BlendColor);
            spriteBatch.Draw(this.texture, this.realm.Offset + this.Position + new Vector2(0, 8), texture_bl, this.BlendColor);
            spriteBatch.Draw(this.texture, this.realm.Offset + this.Position + new Vector2(8, 8), texture_br, this.BlendColor);
        }
    }
...

Kodumla ilgili herhangi bir eleştiri veya öneri kabul edilir.

Güncelleme : Çözüm eklendi.

İşte son Level.Draw yöntemi. Level.TileAt yöntemi, OutOfRange istisnalarını önlemek için girilen değerleri kontrol eder.

...
    public void Draw(SpriteBatch spriteBatch, GameTime gameTime)
    {
        Int32 startx = (Int32)Math.Floor((-this.Offset.X - 32) / 16);
        Int32 endx = (Int32)Math.Ceiling((-this.Offset.X + 1024 + 32) / 16);
        Int32 starty = (Int32)Math.Floor((-this.Offset.Y - 32) / 16);
        Int32 endy = (Int32)Math.Ceiling((-this.Offset.Y + 768 + 32) / 16);

        for (Int32 x = startx; x < endx; x += 1)
        {
            for (Int32 y = starty; y < endy; y += 1)
            {
                Tile tile = this.TileAt(x, y);
                if (tile != null)
                    tile.Draw(spriteBatch, gameTime);

            }
        }
    }
...

2
Yalnızca kameranın görünümünde olanı oluşturduğuna kesinlikle pozitif misin, yani neyi düzelteceğini belirleme kodunun anlamı nedir?
Cooper

5
Framerrate sadece ayrılmış hafıza nedeniyle 60 ila 20 fps düşer? Bu pek mümkün değil, yanlış bir şeyler olmalı. Hangi platform? Sistem sanal belleği diske mi kaydırıyor?
Maik Semder

6
@Drackir Bu durumda, bir kiremit sınıfı olsa bile, uygun uzunluktaki bir tamsayı dizisi yapmalı ve yarım milyon nesne olduğunda, OO ek yükünün bir şaka olmadığını söylemek yanlış olur. Sanırım nesnelerle yapmanın mümkün olduğunu, ancak mesele ne olurdu? Bir tamsayı dizisi işlemek için basit öldü.
aaaaaaaaaaaa

3
Ah! Tüm döşemeleri yineleme ve arama Görünümdeki her birine dört kez çizin. Burada kesinlikle bazı iyileştirmeler var ....
thedaian

11
Sadece görüş alanını oluşturmak için aşağıda bahsettiğimiz tüm süslü bölümlere ihtiyacınız yok. Bu bir tilemap. Zaten düzenli bir ızgaraya bölünmüş durumda. Ekranı sol üst ve sağ alt döşemeyi hesapla ve her şeyi o dikdörtgene çiz.
Blecki

Yanıtlar:


56

Render yaparken 500.000'den fazla taşa mı dönüyorsunuz? Eğer öyleyse, bu muhtemelen sorunlarınızın bir parçası olacak. Oluştururken yarım milyon çini ve 'güncelleme' yaparken yarım milyon çini dolaştırırsanız, üzerlerindeki keneleri tıklatıyorsanız, her karede bir milyon çini olmasına rağmen loop yapıyorsunuzdur.

Açıkçası, bunun etrafında yollar var. Güncelleme onay işaretlerinizi aynı zamanda oluşturma sırasında da gerçekleştirebilirsiniz, böylece tüm bu döşemeler arasında geçen harcanan zamanın yarısından tasarruf edersiniz. Ancak bu, oluşturma kodunuzu ve güncelleme kodunuzu tek bir işlevde birbirine bağlar ve genellikle bir BAD FİKİRİ'dir .

Ekrandaki döşemeleri takip edebilir ve sadece bunlardan geçerek (ve oluşturabilirsiniz). Karolarınızın boyutu ve ekran boyutu gibi şeylere bağlı olarak, bu işlem, geçirmeniz gereken karo miktarını kolayca azaltabilir ve bu işlemin biraz zaman kazanmasını sağlar.

Son olarak ve belki de en iyi seçenek (çoğu dünya oyunu bunu yapar), arazinizi bölgelere ayırmaktır. Dünyayı 512x512 fayans topaklarına bölün ve oyuncunun bir bölgeye yaklaşırken veya bir bölgeden uzaklaştıkça bölgeleri yükleyin / boşaltın. Bu aynı zamanda, herhangi bir 'güncelleme' tıklaması gerçekleştirmek için uzaktaki taşlarla döngü oluşturmanıza gerek kalmamasını sağlar.

(Açıkçası, motorunuz kiremit üzerinde herhangi bir güncelleme tiklaması yapmazsa, bu cevapların bunlardan bahseden kısmını yok sayabilirsiniz.)


5
Bölge kısmı ilginç görünüyor, doğru hatırlıyorsam, minecraft böyle yapar, ancak çimenlikte yayılma, kaktüs yetiştirme ve benzeri gibi yakınlarda olsanız bile, Terraria'daki arazinin nasıl geliştiği ile çalışmaz. Düzenleme : Tabii ki, bölgenin aktif olduğu son zamandan beri geçen süreyi uygularlar ve daha sonra o dönemde gerçekleşen tüm değişiklikleri yaparlar. Bu gerçekten işe yarayabilir.
William Mariager

2
Minecraft kesinlikle bölgeleri kullanıyor (ve daha sonra Ultima oyunları yaptı ve Bethesda oyunlarının çoğunun kullandığından eminim). Terraria'nın araziyi nasıl idare ettiğinden daha az eminim, ancak geçen süreyi "yeni" bir bölgeye uygularlar veya tüm haritayı bellekte saklarlar. İkincisi yüksek RAM kullanımı gerektirecektir, ancak çoğu modern bilgisayar bunu kaldırabilir. Bahsedilmemiş başka seçenekler de olabilir.
thedaian

2
@MindWorX: Yeniden yüklediğinde tüm güncellemeleri yapmak her zaman işe yaramaz - bir sürü çorak alanınız olduğunu ve çimlerin olduğunu varsayalım. Sen yıllarca uzaklara gidip sonra çimlere doğru yürü. Çimenlerin bulunduğu blok yüklendiğinde, yakalanır ve bu blokta yayılır, ancak daha önce yüklenmiş olan daha yakın bloklara yayılmaz. Bu tür şeyler oyundan çok daha yavaş ilerler , ancak herhangi bir güncelleme döngüsünde kiremitlerin sadece küçük bir alt kümesini kontrol edin ve sistemi yüklemeden uzaktaki araziyi canlı hale getirebilirsiniz.
Loren Pechtel

1
Bir bölge oyuncuya yaklaştığında "yakalamak" için alternatif (veya ek) olarak, bunun yerine her uzak bölge üzerinde yinelenen ve daha yavaş bir hızda güncelleyen ayrı bir simülasyona sahip olabilirsiniz. Her kare için zaman ayırabilir veya ayrı bir iş parçacığında çalıştırabilirsiniz. Örneğin, oyuncunun bulunduğu bölgeyi artı her karenin bitişiğindeki 6 bölgeyi güncelleyebilir, ayrıca 1 veya 2 ilave bölgeyi de güncelleyebilirsiniz.
Lewis Wakeford,

1
Merak eden herkes için, Terraria tüm karo verilerini 2d dizisinde depolar (maksimum boyut 8400x2400). Ve sonra fayansların hangi bölümünün oluşturulacağına karar vermek için basit bir matematik kullanır.
FrenchyNZ

4

Burada cevapların hiçbiri tarafından ele alınmamış büyük bir hata görüyorum. Elbette asla çizmeyin ve daha fazla karo üzerinde yinelememelisiniz, o zaman da ihtiyacınız var. Daha az açık olan şey, aslında döşemeleri nasıl tanımladığınız. Bir kiremit dersi yaptığını görebildiğim gibi, bunu da hep yapardım ama bu çok büyük bir hata. Muhtemelen bu sınıfta her türlü işleve sahipsiniz ve bu da çok fazla gereksiz işleme yaratıyor.

İşlemek için gerçekten gerekli olan şeyleri yinelemelisiniz. Döşeme için gerçekte neye ihtiyacınız olduğunu düşünün. Çizmek için sadece bir dokuya ihtiyacınız var, ancak gerçek bir görüntü üzerinde yineleme yapmak istemezsiniz, çünkü bunlar işlenmesi çok büyük. Sadece bir int [,] veya hatta imzasız bir bayt [,] yapabilirsiniz (255'den fazla karo dokusu beklemiyorsanız). Yapmanız gereken tek şey, bu küçük diziler üzerinde yineleme yapmak ve doku çizmek için bir anahtar veya if ifadesi kullanmaktır.

Peki neyi güncellemelisiniz? Tip, sağlık ve hasar yeterli görünüyor. Bunların hepsi bayt olarak saklanabilir. Öyleyse neden güncelleme döngüsü için böyle bir yapı yapmıyorsunuz:

struct TileUpdate
{
public byte health;
public byte type;
public byte damage;
}

Döşemeyi çizmek için türü kullanabilirsiniz. Böylece bir tane (kendisinin bir dizisini yap) yapıdan çıkarabilir ve böylece gereksiz sağlık ve hasar alanlarını yinelemeyin, beraberlik döngüsünde. Güncelleme amacıyla oyun alanını daha canlı hissetmek için daha geniş bir alanı, ardından sadece ekranınızı düşünmelisiniz (varlıklar ekrandan pozisyon değiştirir), ancak sadece görünen karoya ihtiyacınız olan şeylerin çizilmesi için.

Yukarıdaki yapıyı tutarsanız, kutu başına sadece 3 bayt alır. Yani tasarruf ve hafıza amaçlı bu idealdir. İşlem hızı için int veya byte kullanmanız veya 64 bit sisteminiz varsa bile uzun int kullanmanız farketmez.


1
Evet, daha sonra motorumun tekrarları bunu kullanmak için gelişti. Çoğunlukla hafıza tüketimini azaltmak ve hızı arttırmak için. ByRef türleri, ByVal yapılarıyla dolu bir diziye kıyasla yavaş. Yine de bunu cevaba eklemeniz iyi olur.
William Mariager

1
Evet bazı rastgele algoritmalar ararken üzerine tökezledi. Kodunuzda kendi döşeme sınıfınızı nerede kullandığınızı hemen anlayın. Yeniliğin çok yaygın bir hata. 2 yıl önce gönderdiğinizden beri hala aktif olduğunuzu görmek güzel.
Madmenyo

3
Ya healthda ihtiyacın yok damage. En son seçilen kiremit konumlarının küçük bir tamponunu ve her birindeki hasarı tutabilirsiniz. Yeni bir döşeme seçilirse ve arabellek doluysa, yenisini eklemeden önce en eski konumu oradan çıkarın. Bu, bir seferde kaç fayansı mayınlayabileceğinizi sınırlar ancak yine de (kabaca #players * pick_tile_size) buna özgü bir sınır vardır . Bunu kolaylaştırırsa, oyuncu başına bu listeyi tutabilirsiniz. Boyut yapar hız için olsun; daha küçük boyut, her CPU önbelleğinde daha fazla döşeme anlamına gelir.
Sean Middleditch

@SeanMiddleditch Haklısın ama mesele o değildi. Önemli olan durmak ve fayansları ekranda çizmek ve hesaplamaları yapmak için gerçekte neye ihtiyacınız olduğunu düşünmekti. OP bütün bir sınıfı kullandığından, basit bir örnek verdim, basit bir öğrenme sürecinde çok yol kat ediyor. Şimdi piksel yoğunluğu için konumun kilidini aç lütfen, açıkça bir programcının bir CG sanatçının gözüyle bu soruyu incelemeye çalıştığını düşünüyorsun. Zira bu site afaik oyunlar için de CG için.
Madmenyo

2

Kullanabileceğiniz farklı kodlama teknikleri var.

RLE: Yani bir koordinatla (x, y) başlıyorsunuz ve ardından aynı karonun kaç tanesinin eksenlerden birinde yan yana (uzunluk) olduğunu sayıyorsunuz. Örnek: (1,1,10,5), 1,1 koordinatından başlayarak 5 numaralı karo tipinin yan yana 10 karo olduğu anlamına gelir.

Büyük dizi (bitmap): dizinin her elemanı, o alanda bulunan karo tipini tutar.

EDIT: Ben sadece bu mükemmel soru burada rastladım: Harita üretimi için rastgele tohum işlevi?

Perlin gürültü üreteci iyi bir cevap gibi gözüküyor.


Sorulan soruyu cevapladığınızdan gerçekten emin değilim, çünkü kodlamadan (ve perlin gürültüsünden) bahsediyorsunuz ve soru performans sorunlarıyla ilgileniyor gibi görünüyor.
thedaian

1
Uygun kodlamayı (perlin gürültüsü gibi) kullanarak, tüm bu döşemeleri bir kerede bellekte tutma konusunda endişelenemezsiniz. Bunun yerine, kodlayıcıdan (gürültü oluşturucu) belirli bir koordinatta neyin görünmesi gerektiğini size söylemesini istemeniz yeterlidir. CPU döngüleri ve bellek kullanımı arasında burada bir denge var ve bir ses üreteci bu dengeleme hareketine yardımcı olabilir.
Sam Axe

2
Buradaki sorun, kullanıcıların araziyi bir şekilde değiştirmesine izin veren bir oyun yapıyorsanız, bu bilginin işe yaramayacağı "depolamak" için bir gürültü üreteci kullanmaktır. Ayrıca, gürültü üretimi, çoğu kodlama tekniğindeki gibi oldukça pahalıdır. Fiili oyun eşyaları (fizik, çarpışmalar, aydınlatma vb.) İçin CPU çevrimlerini kaydetmekten daha iyisin. Perlin gürültüsü arazi üretimi için mükemmeldir ve kodlama diske kaydetme için iyidir, ancak bu durumda belleği ele almanın daha iyi yolları vardır.
thedaian

Bununla birlikte, çok iyi bir fikir, thedaian gibi, arazi kullanıcı ile etkileşime giriyor, bu da matematiksel olarak bilgi alamamam anlamına geliyor. Temel araziyi oluşturmak için zaten perlin gürültüsüne bakacağım.
William Mariager

1

Büyük olasılıkla tilemap’i daha önce önerildiği gibi bölmelisin. Örneğin , gereksiz (görsel olmayan) fayansların potansiyel işlemlerinden (örneğin yalnızca kolayca geçerek) kurtulmak için Quadtree yapısı ile. Bu şekilde, sadece veri setinin (fayans haritası) işlemden geçirilmesi ve boyutunun arttırılması gerekebilecek olanı işlemeniz pratik bir performans cezasına neden olmaz. Tabii ki, ağacın dengeli olduğunu varsayarsak.

"Eski" ifadesini tekrarlayarak sıkıcı veya başka bir şey duymak istemiyorum, ancak optimizasyon yaparken, her zaman alet zinciri / derleyiciniz tarafından desteklenen optimizasyonları kullanmayı unutmayın, onlarla biraz denemelisiniz. Ve evet, erken optimizasyon tüm kötülüklerin kökenidir. Derleyicinize güvenin, çoğu durumda sizden daha iyi bilir, ancak her zaman, her zamaniki kere ölçün ve asla tahminlere güvenmeyin. Asıl tıkanıklığın nerede olduğunu bilmediğiniz sürece, en hızlı algoritmanın hızlı bir şekilde uygulanmasından ibaret değildir. Bu nedenle, kodun en yavaş (en sıcak) yollarını bulmak için bir profil oluşturucu kullanmalı ve bunları kaldırmaya (veya en iyi duruma getirmeye) odaklanmalısınız. Hedef mimarinin düşük seviye bilgisi genellikle donanımın sunduğu her şeyi sıkmak için çok önemlidir, bu yüzden bu CPU önbelleklerini inceleyin ve bir dal tahmininin ne olduğunu öğrenin. Profilleyicinizin önbellek / dal vuruşları / özlüyor hakkında neler söylediğini görün. Ve bir ağaç veri yapısının bazı formlarını kullanmanın gösterdiği gibi, akıllı veri yapılarına ve dilsiz algoritmalara sahip olmak, diğer taraftan daha iyidir. Veriler performansa gelince önce gelir. :)


"Erken optimizasyon tüm kötülüklerin köküdür" için +1, bundan suçluyum :(
thedaian

16
-1 çeyrek var mı? Overkill biraz? Bütün fayanslar böyle bir 2D oyunda aynı boyda olduğunda (ki bunlar terraria için), ekranda hangi fayans aralığının olduğunu bulmak çok kolaydır - ekranda (ekranın en üstünde) en sağdaki fayans.
BlueRaja - Danny Pflughoeft

1

Her şey çok fazla beraberlik çağrısı ile ilgili değil mi? Tüm haritalarınızı karo dokularını tek bir görüntüye, karo atlasına koyarsanız, işleme sırasında doku geçişi olmaz. Ve tüm döşemelerinizi tek bir Örgüye birleştirirseniz, bir beraberlik çağrısında çizilmelidir.

Dinamik aditing hakkında ... Belki dörtlü ağaç o kadar da kötü bir fikir değil. Fayansların yaprak içine konduğunu ve yapraksız olmayan düğümlerin çocuklarından sadece toplu ağlar olduğu varsayılarak, kök tüm fayansları tek ağda toplamalıdır. Bir döşemeyi kaldırmak, düğüm köklerinin güncellemelerini (örgü yeniden oluşturma) gerektirir. Ancak her ağaç seviyesinde, ağ yapısının sadece 1 / 4'ü vardır ki, bu kadar olmamalıdır, 4 * tree_height mesh birleşimi?

Oh ve bu ağacı kırpma algoritmasında kullanırsanız her zaman kök düğümü değil, bazı alt öğelerini de oluşturacaksınız, bu nedenle tüm düğümleri köke kadar güncellemek / yeniden bağlamak zorunda değilsiniz, (siz yapraksız) düğüme kadar şu anda oluşturma.

Sadece düşüncelerim, kod yok, belki saçmalık.


0

@ arrival haklı. Sorun beraberlik kodudur. Kare başına 4 * 3000 + çizim dörtlü komutları (24000+ çizim çokgen komutları) dizisi oluşturuyorsunuz . Sonra bu komutlar işlenir ve GPU'ya iletilir. Bu oldukça kötü.

Bazı çözümler var.

  • Karoların büyük bloklarını (örneğin ekran boyutu) statik bir dokuya dönüştürün ve tek bir çağrıda çizin.
  • Her kareyi geometriyi GPU'ya göndermeden döşemeleri çizmek için gölgelendiriciler kullanın (ör. Döşeme haritasını GPU'da saklayın).

0

Yapmanız gereken, dünyayı bölgelere ayırmak. Perlin gürültülü arazi üretimi, ortak bir tohum kullanabilir, böylece dünya önceden oluşturulmuş olmasa bile, tohum, yeni araziyi mevcut parçalara güzel bir şekilde kesen gürültü algosunun bir parçasını oluşturacaktır. Bu şekilde, bir defada oyuncu görünümünün önünde küçük bir arabellekten daha fazlasını hesaplamanız gerekmez (geçerli olanın etrafında birkaç ekran).

Oyuncular mevcut ekrandan uzak bölgelerde büyüyen bitkiler gibi şeyleri ele alma konusunda, örneğin zamanlayıcılara sahip olabilirsiniz. Bu zamanlayıcılar, bitkiler, pozisyonları vb. İle ilgili bilgileri depolayan dosyalar diyerek yinelenir. Zamanlayıcılardaki dosyaları okumanız / güncellemeniz / kaydetmeniz yeterlidir. Müzikçalar dünyanın bu bölgelerine tekrar ulaştığında, motor dosyalarda normal olarak okuyacak ve ekranda yeni tesis verilerini sunacaktı.

Bu tekniği geçen yıl yaptığım benzer bir oyunda, hasat ve çiftçilik için kullandım. Oyuncu tarlalardan çok uzaklara yürüyebiliyordu ve geri döndüğünde, eşyalar güncellendi.


-2

Bu kadar zilyonlarca bloğu nasıl kaldıracağımı düşünüyordum ve kafama gelen tek şey Flyweight Design Pattern. Bilmiyorsanız, derinden okumayı öneriyorum: bellek tasarrufu ve işleme konusunda çok yardımcı olabilir: http://en.wikipedia.org/wiki/Flyweight_pattern


3
-1 Bu cevap faydalı olamayacak kadar genel ve verdiğiniz bağlantı bu sorunu doğrudan ele almıyor. Flyweight modeli, büyük veri kümelerini verimli bir şekilde yönetmenin birçok yolundan biridir; Terraria benzeri bir oyunda fayansların depolanması bağlamında bu kalıbı nasıl uygulayacağınızı açıklayabilir misiniz?
postgoodism
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.