Dalam XNA, bagaimana cara memuat bagian peta dunia 2D besar secara dinamis?

17

Saya ingin mengembangkan peta dunia yang besar; setidaknya berukuran 8000 × 6000 piksel. Saya telah memecahnya menjadi 10 × 10 kotak gambar 800 × 600-pixel PNG. Untuk menghindari memuat semuanya ke dalam memori, gambar harus dimuat dan diturunkan tergantung pada posisi pemain di grid.

Sebagai contoh, berikut adalah pemain di posisi (3,3):

Pemain di (3,3)

Saat ia bergerak ke kanan ke (4,3), tiga gambar di paling kiri dideallocated sedangkan tiga gambar di kanan dialokasikan:

Pemain pindah ke (4,3)

Mungkin harus ada ambang batas di dalam setiap sel grid yang memicu pemuatan dan pembongkaran. Pemuatan mungkin terjadi di utas terpisah.

Bagaimana cara merancang sistem seperti itu?

pek
sumber
1
Saran cepat: Anda punya dua pertanyaan di sini, "bagaimana cara saya memuat ubin secara dinamis dalam game" dan "bagaimana saya melakukan threading pada XNA untuk XBox 360". Hapus segmen spesifik OS dari pos ini - kode tileloading akan identik pada XB360, Linux, dan Nintendo Wii - dan buat posting baru untuk threading di XBox360.
ZorbaTHut
Adapun pertanyaan tentang masalah Anda yang sebenarnya, apakah ini peta yang digulirkan dengan mulus, atau serangkaian layar non-gulir individual?
ZorbaTHut
Jika saya mengerti "peta gulir halus", ya, benar. Sedangkan untuk memisahkan pertanyaan, saya memikirkannya tetapi agak ragu untuk mengajukan pertanyaan secara konsekuen. Tapi, saya akan melakukannya sekarang.
pek

Jawaban:

10

Ada beberapa masalah yang harus dipecahkan di sini. Yang pertama adalah cara memuat dan membongkar ubin. ContentManager secara default tidak akan membiarkan Anda menurunkan konten tertentu. Namun, implementasi kustom ini akan:

public class ExclusiveContentManager : ContentManager
{
    public ExclusiveContentManager(IServiceProvider serviceProvider,
        string RootDirectory) : base(serviceProvider, RootDirectory)
    {
    }

    public T LoadContentExclusive<T>(string AssetName)
    {
        return ReadAsset<T>(AssetName, null);
    }

    public void Unload(IDisposable ContentItem)
    {
        ContentItem.Dispose();
    }
}

Masalah kedua adalah bagaimana memutuskan ubin mana yang akan dimuat dan dibongkar. Berikut ini akan menyelesaikan masalah ini:

public class Tile
{
    public int X;
    public int Y;
    public Texture2D Texture;
}

int playerTileX;
int playerTileY;
int width;
int height;

ExclusiveContentManager contentEx;
Tile[,] tiles;

void updateLoadedTiles()
{
    List<Tile> loaded = new List<Tile>();

    // Determine which tiles need to be loaded
    for (int x = playerTileX - 1; x <= playerTileX + 1; x++)
        for (int y = playerTileY - 1; y <= playerTileY; y++)
        {
            if (x < 0 || x > width - 1) continue;
            if (y < 0 || y > height - 1) continue;

            loaded.Add(tiles[x, y]);
        }

    // Load and unload as necessary
    for (int x = 0; x < width; x++)
        for (int y = 0; y < height; y++)
        {
            if (loaded.Contains(tiles[x, y]))
            {
                if (tiles[x, y].Texture == null)
                    loadTile(x, y);
            }
            else
            {
                if (tiles[x, y].Texture != null)
                    contentEx.Unload(tiles[x, y].Texture);
            }
        }
}

void loadTile(int x, int y)
{
    Texture2D tex = contentEx.LoadEx<Texture2D>("tile_" + x + "_" + y);
    tiles[x, y].Texture = tex;
}

Masalah terakhir adalah bagaimana memutuskan kapan memuat dan membongkar. Ini mungkin bagian yang paling mudah. Ini hanya dapat dilakukan dalam metode Pembaruan () setelah posisi layar pemain ditentukan:

int playerScreenX;
int playerScreenY;
int tileWidth;
int tileHeight;

protected override void Update(GameTime gameTime)
{
    // Code to update player position, etc...

    // Load/unload
    int newPlayerTileY = (int)((float)playerScreenX / (float)tileWidth);
    int newPlayerTileX = (int)((float)playerScreenY / (float)tileHeight);

    if (newPlayerTileX != playerTileX || newPlayerTileY != playerTileY)
        updateLoadedTiles();

    base.Update(gameTime);
}

Tentu saja Anda juga perlu menggambar ubin, tetapi mengingat tileWidth, tileHeight, dan array Tiles [], ini harus sepele.

Sean James
sumber
Terima kasih. Sangat baik dikatakan. Saya ingin memberikan saran, jika Anda tahu: dapatkah Anda menjelaskan bagaimana pemuatan ubin dilakukan dalam utas? Saya membayangkan bahwa memuat 3 800x600 ubin sekaligus akan sedikit menghentikan permainan.
pek
Kartu grafis perlu dilibatkan saat membuat objek Texture2D, jadi sejauh yang saya tahu masih akan menyebabkan lompatan meskipun pemuatan dilakukan pada utas kedua.
Sean James
0

Apa yang ingin Anda lakukan cukup umum. Untuk tutorial yang bagus tentang ini dan teknik umum lainnya, lihat seri mesin ubin ini .

Jika Anda belum pernah melakukan hal seperti ini sebelumnya, saya sarankan menonton serial ini. Namun, jika Anda mau, Anda bisa mendapatkan kode tutorial terakhir. Jika Anda melakukannya nanti, periksa metode menggambar.

Singkatnya, Anda harus menemukan poin min dan maks X / Y di sekitar pemain. Setelah Anda memilikinya, Anda hanya perlu mengulang masing-masing dan menggambar ubin itu.

public override void Draw(GameTime gameTime)
{
    Point min = currentMap.WorldPointToTileIndex(camera.Position);
    Point max = currentMap.WorldPointToTileIndex( camera.Position +
        new Vector2(
            ScreenManager.Viewport.Width + currentMap.TileWidth,
            ScreenManager.Viewport.Height + currentMap.TileHeight));


    min.X = (int)Math.Max(min.X, 0);
    min.Y = (int)Math.Max(min.Y, 0);
    max.X = (int)Math.Min(max.X, currentMap.Width);
    max.Y = (int)Math.Min(max.Y, currentMap.Height + 100);

    ScreenManager.SpriteBatch.Begin(SpriteBlendMode.AlphaBlend
                                    , SpriteSortMode.Immediate
                                    , SaveStateMode.None
                                    , camera.TransformMatrix);
    for (int x = min.X; x < max.X; x++)
    {
        for (int y = min.Y; y < max.Y; y++)
        {

            Tile tile = Tiles[x, y];
            if (tile == null)
                continue;

            Rectangle currentPos = new Rectangle(x * currentMap.TileWidth, y * currentMap.TileHeight, currentMap.TileWidth, currentMap.TileHeight);
            ScreenManager.SpriteBatch.Draw(tile.Texture, currentPos, tile.Source, Color.White);
        }
    }

    ScreenManager.SpriteBatch.End();
    base.Draw(gameTime);
}

public Point WorldPointToTileIndex(Vector2 worldPoint)
{
    worldPoint.X = MathHelper.Clamp(worldPoint.X, 0, Width * TileWidth);
    worldPoint.Y = MathHelper.Clamp(worldPoint.Y, 0, Height * TileHeight);


    Point p = new Point();

    // simple conversion to tile indices
    p.X = (int)Math.Floor(worldPoint.X / TileWidth);
    p.Y = (int)Math.Floor(worldPoint.Y / TileWidth);

    return p;
}

Seperti yang Anda lihat ada banyak hal yang terjadi. Anda memerlukan kamera yang akan membuat matriks Anda, Anda perlu mengubah lokasi piksel Anda saat ini ke indeks ubin, Anda menemukan titik min / maks Anda (saya menambahkan sedikit ke MAX saya sehingga sedikit menarik di luar layar terlihat sedikit ), dan kemudian Anda bisa menggambarnya.

Saya sangat menyarankan menonton seri tutorial. Dia membahas masalah Anda saat ini, cara membuat editor ubin, menjaga pemain dalam batas, animasi sprite, interaksi AI, dll ...

Di samping catatan TiledLib memiliki ini dibangun. Anda dapat mempelajari kode mereka juga.


sumber
Meskipun ini sangat membantu, itu hanya mencakup rendering peta petak. Bagaimana dengan memuat dan membongkar ubin yang relevan? Saya tidak dapat memuat 100 800x600 ubin dalam memori. Saya akan melihat tutorial video; Terima kasih.
pek
ahhh .. Saya salah mengerti pertanyaan Anda. Jadi, apakah Anda menggunakan tilemap tradisional, atau apakah Anda memiliki gambar besar untuk level Anda sehingga Anda terpecah-pecah?
Hanya sebuah pemikiran .... Anda bisa menyimpan nama ubin Anda dalam sebuah array, dan menggunakan teknik yang sama untuk mengetahui apa yang memuat dan menggambar. Ingatlah untuk mencoba menggunakan kembali objek apa pun atau Anda dapat membuat banyak sampah
Yang kedua: peta adalah 8000x6000 dipecah menjadi 10x10 ubin 800x600 px. Mengenai penggunaan kembali, saya sudah memiliki pengelola konten yang bertanggung jawab untuk itu.
pek
0

Saya telah mengerjakan sesuatu yang sangat mirip untuk proyek saya saat ini. Ini adalah langkah cepat bagaimana saya melakukannya, dengan beberapa catatan tambahan tentang cara membuat hal-hal sedikit lebih mudah pada diri Anda sendiri.

Bagi saya, masalah pertama adalah memecah dunia menjadi bongkahan-bongkahan yang lebih kecil yang sesuai untuk dimuat dan dibongkar dengan cepat. Karena Anda menggunakan peta berbasis ubin, langkah itu menjadi jauh lebih mudah. Daripada mempertimbangkan posisi masing-masing objek 3D di level, Anda sudah membagi level Anda menjadi ubin. Ini memungkinkan Anda memecah dunia menjadi potongan-potongan ubin berukuran X demi Y, dan memuatnya.

Anda ingin melakukannya secara otomatis, bukan dengan tangan. Karena Anda menggunakan XNA, Anda memiliki opsi untuk menggunakan Pipeline Konten dengan eksportir kustom untuk konten level Anda. Kecuali Anda tahu cara menjalankan proses ekspor tanpa kompilasi ulang, saya sejujurnya akan merekomendasikan untuk tidak melakukannya. Meskipun C # tidak terlalu lambat untuk dikompilasi seperti C ++ cenderung, Anda masih tidak ingin harus memuat Visual Studio dan mengkompilasi ulang game Anda setiap kali Anda membuat perubahan kecil pada peta.

Hal penting lain di sini adalah memastikan Anda menggunakan konvensi penamaan yang baik untuk file yang berisi potongan level Anda. Anda ingin dapat mengetahui bahwa Anda ingin memuat atau membongkar chunk C, dan kemudian menghasilkan nama file yang Anda perlu memuat untuk melakukan itu pada saat dijalankan. Akhirnya, pikirkan hal-hal kecil yang mungkin bisa membantu Anda. Sangat menyenangkan bisa mengubah seberapa besar bongkahan, mengekspor kembali, dan kemudian melihat efeknya terhadap kinerja dengan segera.

Pada saat run time, itu masih cukup mudah. Anda perlu beberapa cara untuk memuat dan membongkar bongkar secara asinkron, tetapi ini sangat tergantung pada cara kerja gim atau mesin Anda. Gambar kedua Anda benar-benar tepat - Anda perlu menentukan potongan apa yang harus dimuat atau diturunkan, dan membuat permintaan yang sesuai untuk membuat kasing jika belum. Tergantung pada berapa banyak potongan yang Anda muat pada satu waktu, Anda bisa melakukan ini setiap kali pemain melewati batas dari satu chunk ke chunk berikutnya. Bagaimanapun, Anda ingin memastikan bahwa cukup dimuat bahwa bahkan dalam waktu muat terburuk (wajar), potongan masih dimuat sebelum pemain dapat melihatnya. Anda mungkin ingin bermain dengan nomor ini sampai Anda mendapatkan keseimbangan yang baik antara kinerja dan konsumsi memori.

Sejauh arsitektur aktual berjalan, Anda akan ingin abstrak proses sebenarnya memuat dan menurunkan data dari memori dari proses menentukan apa yang harus dimuat / dibongkar. Untuk iterasi pertama Anda, saya bahkan tidak akan khawatir tentang kinerja bongkar / muat dan hanya mendapatkan hal paling sederhana yang mungkin dapat bekerja, dan memastikan bahwa Anda menghasilkan permintaan yang sesuai pada waktu yang tepat. Setelah itu, Anda dapat melihat mengoptimalkan cara memuat untuk meminimalkan sampah.

Saya mengalami banyak kompleksitas tambahan karena mesin yang saya gunakan, tapi itu implementasi yang cukup spesifik. Jika Anda memiliki pertanyaan tentang apa yang saya lakukan, beri komentar dan saya akan melakukan apa yang saya bisa untuk membantu.

Logan Kincaid
sumber
1
Anda bisa menggunakan msbuild untuk menjalankan pipa konten di luar Visual Studio. Sampel WinForms kedua telah Anda liput: creators.xna.com/en-US/sample/winforms_series2
Andrew Russell
@Andrew !!!!! Terima kasih banyak!!! Saya akan sepenuhnya meninggalkan pipa konten untuk alasan yang tepat!
pek