Membuat roguelike di Unity dari awal

gambar

Tidak banyak tutorial tentang cara membuat roguelike di Unity, jadi saya memutuskan untuk menulisnya. Bukan untuk menyombongkan diri, tetapi untuk berbagi pengetahuan dengan mereka yang berada pada tahap di mana saya sudah cukup lama.

Catatan: Saya tidak mengatakan bahwa ini adalah satu - satunya cara untuk membuat roguelike di Unity. Dia hanya satu . Mungkin bukan yang terbaik dan paling efektif, saya belajar melalui coba-coba. Dan saya akan belajar beberapa hal dengan benar dalam proses membuat tutorial.

Mari kita asumsikan bahwa Anda tahu setidaknya dasar-dasar Persatuan, misalnya, cara membuat cetakan atau skrip, dan sejenisnya. Jangan berharap saya mengajari Anda cara membuat sprite sheet, ada banyak tutorial bagus tentang ini. Saya akan fokus bukan pada mempelajari engine, tetapi pada bagaimana mengimplementasikan game yang akan kita buat bersama. Jika Anda mengalami kesulitan, pergilah ke salah satu komunitas hebat Discord dan minta bantuan:

Komunitas Pengembang Unity

Roguelikes

Jadi, mari kita mulai!

Tahap 0 - perencanaan


Ya itu betul. Hal pertama yang harus dibuat adalah rencana. Ini akan baik bagi Anda untuk merencanakan permainan, dan bagi saya - untuk merencanakan tutorial sehingga setelah beberapa saat kami tidak akan terganggu dari topik. Sangat mudah untuk bingung dalam fungsi-fungsi gim, seperti di ruang bawah tanah roguelike.

Kami akan menulis roguelike. Kami terutama akan mengikuti saran bijaksana pengembang Cogmind Josh Ge di sini . Ikuti tautannya, baca posnya atau tonton videonya, lalu kembali.

Apa tujuan dari tutorial ini? Dapatkan dasar roguelike sederhana yang solid, yang dengannya Anda dapat bereksperimen. Seharusnya memiliki generasi penjara bawah tanah, pemain yang bergerak di peta, kabut visibilitas, musuh dan objek. Hanya yang paling penting. Jadi, pemain harus bisa turun tangga beberapa lantai. Katakanlah, pada usia lima tahun, tingkatkan level Anda, tingkatkan, dan pada akhirnya bertarunglah dengan bos dan kalahkan dia. Atau mati. Faktanya, itu saja.

Mengikuti saran Josh Ge, kami akan membangun fungsi-fungsi permainan sehingga mereka membawa kami ke tujuan. Jadi kami mendapatkan kerangka kerja seperti roguelike, yang dapat dikembangkan lebih lanjut, tambahkan chip Anda sendiri, menciptakan keunikan. Atau lempar semua yang ada di keranjang, manfaatkan pengalaman yang didapat dan mulailah dari awal. Lagipula itu akan luar biasa.

Saya tidak akan memberi Anda sumber daya grafis apa pun. Gambar sendiri atau gunakan tileset gratis, yang dapat diunduh di sini , di sini atau dengan mencari di Google. Hanya saja, jangan lupa menyebutkan penulis grafis dalam game.

Sekarang mari kita daftar semua fungsi yang akan ada di roguelike kami sesuai dengan urutan implementasinya:

  1. Pembuatan Peta Bawah Tanah
  2. Karakter pemain dan gerakannya
  3. Area visibilitas
  4. Musuh
  5. Cari cara
  6. Berjuang, Kesehatan, dan Kematian
  7. Tingkat Pemain
  8. Item (senjata dan ramuan)
  9. Cheat konsol (untuk pengujian)
  10. Lantai penjara
  11. Menyimpan dan memuat
  12. Bos terakhir

Setelah menerapkan semua ini, kami akan memiliki roguelike yang kuat, dan Anda akan sangat meningkatkan keterampilan pengembangan game Anda. Sebenarnya, itu adalah cara saya untuk meningkatkan keterampilan saya : membuat kode dan mengimplementasikan fungsi. Karena itu, saya yakin Anda dapat menangani ini juga.

Tahap 1 - Kelas MapManager


Ini adalah skrip pertama yang akan kita buat dan itu akan menjadi tulang punggung game kita. Ini sederhana, tetapi berisi sebagian besar informasi penting untuk permainan.

Jadi, buat skrip .cs bernama MapManager dan buka.

Hapus ": MonoBehaviour" karena tidak akan mewarisinya dan tidak akan dilampirkan ke GameObject apa pun.

Hapus fungsi Mulai () dan Perbarui ().

Di akhir kelas MapManager, buat kelas publik baru yang disebut Tile.


Kelas Tile akan berisi semua informasi dari satu ubin. Sejauh ini, kita tidak perlu banyak, hanya posisi x dan y, serta objek permainan yang terletak di posisi peta ini.


Jadi, kami memiliki informasi ubin dasar. Mari kita buat peta dari ubin ini. Sederhana saja, kita hanya perlu array dua dimensi dari objek Tile. Kedengarannya rumit, tetapi tidak ada yang istimewa tentang itu. Cukup tambahkan variabel Tile [,] ke kelas MapManager:


Voila! Kami punya peta!

Ya itu kosong. Tapi ini peta. Setiap kali sesuatu bergerak atau berubah status pada peta, informasi pada peta ini akan diperbarui. Artinya, jika, misalnya, seorang pemain mencoba untuk beralih ke ubin baru, kelas akan memeriksa alamat ubin tujuan di peta, keberadaan musuh dan patennya. Berkat ini, kami tidak perlu memeriksa ribuan tabrakan di setiap belokan, dan kami tidak membutuhkan collider untuk setiap objek game, yang akan memudahkan dan menyederhanakan pekerjaan dengan game.

Kode yang dihasilkan terlihat seperti ini:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MapManager 
{
    public static Tile[,] map; // the 2-dimensional map with the information for all the tiles
}

public class Tile { //Holds all the information for each tile on the map
    public int xPosition; // the position on the x axis
    public int yPosition; // the position on the y axis
    public GameObject baseObject; // the map game object attached to that position: a floor, a wall, etc.
}

Tahap pertama selesai, mari kita lanjutkan untuk mengisi kartu. Sekarang kita akan mulai membuat generator penjara bawah tanah.

Tahap 2 - beberapa kata tentang struktur data


Tetapi sebelum Anda mulai, izinkan saya berbagi kiat-kiat yang muncul berkat umpan balik yang diterima setelah penerbitan bagian pertama. Saat membuat struktur data, Anda harus berpikir dari awal bagaimana Anda akan mempertahankan kondisi permainan. Kalau tidak, nanti akan jauh lebih kacau. Pengguna Discord st33d, pengembang Star Shaped Bagel (Anda dapat memainkan game ini secara gratis di sini ), mengatakan bahwa pada awalnya ia membuat game, berpikir bahwa itu tidak akan menyelamatkan negara sama sekali. Perlahan-lahan, permainan mulai bertambah besar, dan penggemarnya meminta dukungan untuk peta yang disimpan. Tetapi karena metode yang dipilih untuk membuat struktur data, sangat sulit untuk menyimpan data, jadi dia tidak dapat melakukan ini.


Kami benar-benar belajar dari kesalahan kami. Terlepas dari kenyataan bahwa saya meletakkan bagian save / load di akhir tutorial, saya memikirkannya sejak awal, dan belum menjelaskannya. Pada bagian ini saya akan berbicara sedikit tentang mereka, tetapi agar tidak membebani pengembang yang tidak berpengalaman.

Kami akan menyimpan hal-hal seperti array variabel dari kelas Tile di mana peta disimpan. Kami akan menyimpan semua data ini, kecuali variabel kelas GameObject, yang ada di dalam kelas Tile. Mengapa? Hanya karena GameObjects tidak dapat diserialisasi dengan Unity ke data yang disimpan.

Karena itu, pada kenyataannya, kita tidak perlu menyimpan data yang disimpan di dalam GameObjects. Semua data akan disimpan di kelas seperti Tile, dan nanti juga Player, Musuh, dll. Kemudian kita akan memiliki GameObjects untuk menyederhanakan perhitungan hal-hal seperti visibilitas dan gerakan, serta menggambar sprite di layar. Oleh karena itu, di dalam kelas akan ada variabel GameObject, tetapi nilai variabel-variabel ini tidak akan disimpan dan dimuat. Saat memuat, kami akan dipaksa untuk menghasilkan GameObject lagi dari data yang disimpan (posisi, sprite, dll.).

Lalu apa yang perlu kita lakukan sekarang? Nah, tambahkan saja dua baris ke kelas Tile yang ada dan satu ke bagian atas skrip. Pertama kita tambahkan "using System;" ke judul skrip, dan kemudian [Serializable] di depan seluruh kelas dan [NonSerialized] tepat di depan variabel GameObject. Seperti ini:



Saya akan memberi tahu Anda lebih banyak tentang ini ketika kita sampai pada bagian tutorial tentang menabung / memuat. Untuk saat ini, mari kita tinggalkan semuanya dan lanjutkan.

Tahap 3 - lebih banyak tentang struktur data


Saya mendapat ulasan lain tentang struktur data yang ingin saya bagikan di sini.

Faktanya, ada banyak cara untuk mengimplementasikan data dalam game. Yang pertama saya gunakan dan yang akan diterapkan dalam tutorial ini: semua data ubin berada di kelas Tile, dan semuanya disimpan dalam array. Pendekatan ini memiliki banyak keuntungan: lebih mudah dibaca, semua yang Anda butuhkan ada di satu tempat, data lebih mudah untuk dimanipulasi dan diekspor ke file penyimpanan. Tetapi dari sudut pandang memori, itu tidak begitu efektif. Anda harus mengalokasikan banyak memori untuk variabel yang tidak akan pernah digunakan dalam game. Sebagai contoh, nanti kita akan meletakkan variabel GameObject Musuh di kelas Tile sehingga kita bisa mengarahkan langsung dari peta ke GameObject musuh yang berdiri di ubin ini untuk menyederhanakan semua perhitungan yang terkait dengan pertempuran. Tetapi ini berarti setiap ubin dalam gim akan mengalokasikan ruang dalam memori untuk variabel GameObject,bahkan jika tidak ada musuh di ubin ini. Jika ada 10 musuh di peta 2500 ubin, maka akan ada 2490 variabel GameObject kosong, tetapi dialokasikan - Anda dapat melihat berapa banyak memori yang terbuang.

Metode alternatif adalah menggunakan struktur untuk menyimpan data dasar ubin (misalnya, posisi dan jenis), dan semua data lainnya akan disimpan dalam hashmap-s, yang akan dihasilkan hanya jika perlu. Ini akan menghemat banyak memori, tetapi pengembaliannya akan menjadi implementasi yang sedikit lebih rumit. Sebenarnya itu akan menjadi sedikit lebih maju daripada yang saya inginkan dalam tutorial ini, tetapi jika Anda mau, maka di masa depan saya dapat menulis posting yang lebih rinci tentang hal itu.

Selain itu, jika Anda ingin membaca diskusi tentang topik ini, maka ini dapat dilakukan di Reddit .

Tahap 4 - Algoritma Generasi Bawah Tanah


Ya, ini adalah bagian lain di mana saya akan berbicara dan kami tidak akan mulai memprogram apa pun. Tapi ini penting, perencanaan algoritma yang cermat akan menghemat banyak waktu kerja kita di masa depan.

Ada beberapa cara untuk membuat generator penjara bawah tanah. Salah satu yang akan kami terapkan bersama bukanlah yang terbaik dan bukan yang paling efektif ... itu hanya cara awal yang mudah. Ini sangat sederhana, tetapi hasilnya akan cukup bagus. Masalah utama akan banyak koridor jalan buntu. Nanti, jika Anda mau, saya bisa menerbitkan tutorial lain tentang algoritma yang lebih baik.

Secara umum, algoritma yang kami gunakan berfungsi sebagai berikut: katakanlah kami memiliki seluruh peta yang diisi dengan nilai nol - tingkat yang terdiri dari batu. Awalnya kami memotong sebuah ruangan di tengah. Dari ruangan ini kita menerobos koridor dalam satu arah, dan kemudian menambahkan koridor dan kamar lain, selalu mulai secara acak dari ruang atau koridor yang ada, sampai kita mencapai jumlah maksimum koridor / kamar yang diberikan di awal. Atau hingga algoritme dapat menemukan tempat baru untuk menambahkan ruang / koridor baru, mana yang lebih dulu. Dan jadi kami mendapatkan ruang bawah tanah.

Jadi, mari kita jelaskan ini dengan cara yang lebih mirip algoritma, langkah demi langkah. Untuk kenyamanan, saya akan menyebut setiap detail peta (koridor atau ruangan) sebagai elemen sehingga saya tidak perlu mengatakan "kamar / koridor" setiap waktu.

  1. Potong ruangan di tengah peta
  2. Pilih salah satu dinding secara acak
  3. Kami menerobos koridor di dinding ini
  4. Pilih secara acak salah satu elemen yang ada.
  5. Pilih salah satu dinding elemen ini secara acak
  6. Jika item yang dipilih terakhir adalah ruangan, maka kami menghasilkan koridor. Jika koridor, maka pilih secara acak apakah elemen berikutnya akan menjadi ruangan atau koridor lain
  7. Periksa apakah ada cukup ruang di arah yang dipilih untuk membuat item yang diinginkan
  8. Jika ada tempat, buat elemen, jika tidak, kembali ke langkah 4
  9. Ulangi dari langkah 4

Itu saja. Kami akan mendapatkan peta sederhana dari ruang bawah tanah, di mana hanya ada kamar dan koridor, tanpa pintu dan elemen khusus, tetapi ini akan menjadi awal kami. Nanti kita akan mengisinya dengan peti, musuh dan jebakan. Dan Anda bahkan dapat menyesuaikannya: kita akan belajar cara menambahkan elemen menarik yang Anda butuhkan.

Tahap 5 - memotong ruangan


Akhirnya lanjutkan ke pengkodean! Mari kita potong kamar pertama kita.

Pertama, buat skrip baru dan beri nama DungeonGenerator. Ini akan mewarisi dari Monobehaviour, jadi Anda harus melampirkannya ke GameObject nanti. Kemudian kita perlu mendeklarasikan beberapa variabel publik di kelas sehingga kita dapat mengatur parameter ruang bawah tanah dari inspektur. Variabel-variabel ini akan menjadi lebar dan tinggi peta, tinggi minimum dan maksimum dan lebar ruangan, panjang maksimum koridor dan jumlah elemen yang harus ada di peta.


Selanjutnya kita perlu menginisialisasi generator penjara bawah tanah. Kami melakukan ini untuk menginisialisasi variabel yang akan diisi oleh generasi. Untuk saat ini, ini hanyalah peta. Dan, dan juga menghapus fungsi Mulai () dan Pembaruan () yang dihasilkan Unity untuk skrip baru, kami tidak akan membutuhkannya.



Di sini kita menginisialisasi variabel peta kelas MapManager (yang kita buat pada langkah sebelumnya), melewati lebar dan tinggi peta, didefinisikan oleh variabel di atas sebagai parameter dari dua dimensi array. Berkat ini, kami akan memiliki peta ukuran x horizontal (lebar) dan ukuran vertikal y (tinggi), dan kami dapat mengakses sel apa pun di peta dengan memasukkan MapManager.map [x, y]. Ini akan sangat berguna ketika memanipulasi posisi objek.

Sekarang kita akan membuat fungsi untuk membuat ruang pertama. Kami akan menyebutnya FirstRoom (). Kami menjadikan InitializeDungeon () fungsi publik, karena akan diluncurkan oleh skrip lain (Game Manager, yang akan segera kami buat; itu akan memusatkan pengelolaan seluruh proses peluncuran game). Kami tidak memerlukan skrip eksternal untuk memiliki akses ke FirstRoom (), jadi kami tidak membuatnya publik.

Sekarang, untuk melanjutkan, kami akan membuat tiga kelas baru dalam skrip MapManager sehingga Anda dapat membuat ruang. Ini adalah kelas Fitur, Dinding, dan Posisi. Kelas Position akan berisi posisi x dan y sehingga kita dapat melacak di mana semuanya berada. Dinding akan memiliki daftar posisi, arah di mana ia "terlihat" relatif terhadap pusat ruangan (utara, selatan, timur atau barat), panjangnya, dan keberadaan elemen baru yang diciptakan darinya. Elemen akan memiliki daftar semua posisi yang terdiri dari, jenis elemen (ruangan atau koridor), array variabel Wall, dan lebar dan tinggi.



Sekarang mari kita ke fungsi FirstRoom (). Mari kita kembali ke skrip DungeonGenerator dan membuat fungsi tepat di bawah InitializeDungeon. Dia tidak perlu menerima parameter apa pun, jadi kami akan membiarkannya sederhana (). Selanjutnya, di dalam fungsi, pertama-tama kita perlu membuat dan menginisialisasi variabel Room dan daftar variabel Posisi. Kami melakukannya seperti ini:


Sekarang mari kita mengatur ukuran ruangan. Ini akan menerima nilai acak antara tinggi dan lebar minimum dan maksimum yang dinyatakan pada awal skrip. Sementara mereka kosong, karena kami tidak menetapkan nilai untuk mereka di inspektur, tetapi jangan khawatir, kami akan segera melakukannya. Kami menetapkan nilai acak seperti ini:


Selanjutnya, kita perlu menyatakan di mana titik awal ruangan akan terletak, yaitu, di mana titik kamar 0,0 akan terletak di kisi peta. Kami ingin membuatnya mulai di tengah peta (setengah lebar dan setengah tinggi), tapi mungkin tidak tepat di tengah peta. Mungkin perlu menambahkan pengacak kecil sehingga bergerak sedikit ke kiri dan ke bawah. Oleh karena itu, kita menetapkan xStartingPoint sebagai setengah lebar peta, dan yStartingPoint sebagai setengah tinggi peta, dan kemudian mengambil roomWidth dan roomHeight yang baru saja diberikan, dapatkan nilai acak dari 0 hingga lebar / tinggi ini, dan kurangi dari awal x dan y. Seperti ini:



Selanjutnya, dalam fungsi yang sama kita akan menambahkan dinding. Kita perlu menginisialisasi array dinding yang ada di variabel kamar yang baru dibuat, dan kemudian menginisialisasi setiap variabel dinding di dalam array ini. Dan kemudian inisialisasi setiap daftar posisi, atur panjang dinding ke 0 dan masukkan arah di mana setiap dinding akan "terlihat".

Setelah array diinisialisasi, kita berputar di sekitar setiap elemen array di for () loop, menginisialisasi variabel dari setiap dinding, dan kemudian menggunakan switch, yang menamai arah setiap dinding. Ini dipilih secara sewenang-wenang, kita hanya perlu mengingat apa artinya.


Sekarang kita akan mengeksekusi dua bersarang untuk loop segera setelah menempatkan dinding. Di loop luar, kita berkeliling semua nilai y di ruangan, dan di loop bersarang, semua nilai x. Dengan cara ini kita akan memeriksa setiap sel x dalam baris y sehingga kita dapat mengimplementasikannya.


Maka hal pertama yang harus dilakukan adalah menemukan nilai sebenarnya dari posisi sel pada skala peta dari posisi ruangan. Ini cukup sederhana: kami memiliki titik awal x dan y. Mereka akan berada di posisi 0,0 di kisi-kisi ruangan. Kemudian jika kita perlu mendapatkan nilai nyata x, y dari sembarang lokal x, y, maka kita tambahkan x lokal dan y dengan posisi awal x dan y. Kemudian kami menyimpan nilai x, y asli ini ke variabel Posisi (dari kelas yang dibuat sebelumnya), dan kemudian menambahkannya ke Daftar <> dari posisi ruangan.


Langkah selanjutnya adalah menambahkan informasi ini ke peta. Sebelum mengubah nilai, ingatlah untuk menginisialisasi variabel Tile.


Sekarang kita akan membuat perubahan ke kelas Tile. Mari kita pergi ke skrip MapManager dan menambahkan satu baris ke definisi kelas Tile: "tipe string publik;". Ini akan memungkinkan kita untuk menambahkan kelas ubin dengan menyatakan bahwa ubin di x, y adalah dinding, lantai, atau yang lainnya. Selanjutnya, mari kita kembali ke siklus di mana kita melakukan pekerjaan dan menambahkan konstruksi if-else besar, yang akan memungkinkan kita tidak hanya menentukan setiap dinding, panjangnya dan semua posisi di dinding ini, tetapi juga untuk menentukan pada peta global apa ubin tertentu - dinding atau jenis kelamin.


Dan kita sudah melakukan sesuatu. Jika variabel y (kontrol variabel di loop luar) adalah 0, maka ubin milik baris sel terendah di ruangan, yaitu, itu adalah dinding selatan. Jika x (kontrol variabel loop dalam) adalah 0, maka ubin milik kolom sel paling kiri, yaitu, itu adalah dinding barat. Dan jika itu berada di garis paling atas, maka itu milik tembok utara, dan di kanan - tembok timur. Kita kurangi 1 dari variabel roomWidth dan roomHeight, karena nilai-nilai ini dihitung mulai dari 1, dan variabel x dan y dari siklus dimulai dari 0, jadi kita perlu memperhitungkan perbedaan ini. Dan semua sel yang tidak memenuhi kondisi bukanlah dinding, yaitu, mereka adalah lantai.


Hebat, kita hampir selesai dengan kamar pertama. Hampir siap, kita hanya perlu meletakkan nilai terakhir dalam variabel Fitur yang kita buat. Kami keluar dari loop dan mengakhiri fungsi seperti ini:


Baik! Kami punya kamar!

Tetapi bagaimana kita memahami bahwa semuanya bekerja? Perlu diuji. Tapi bagaimana cara mengujinya? Kita dapat menghabiskan waktu dan menambahkan aset untuk ini, tetapi itu akan membuang-buang waktu dan terlalu mengalihkan kita dari menyelesaikan algoritme. Hmm, tapi ini bisa dilakukan menggunakan ASCII! Ya, ide bagus! ASCII adalah cara sederhana dan murah untuk menggambar peta sehingga dapat diuji. Juga, jika Anda mau, Anda dapat melewati bagian tersebut dengan sprite dan efek visual, yang akan kami pelajari nanti, dan membuat seluruh permainan Anda di ASCII. Jadi mari kita lihat bagaimana ini dilakukan.

Tahap 6 - menggambar ruang pertama


Hal pertama yang harus dipikirkan ketika mengimplementasikan kartu ASCII adalah font mana yang harus dipilih. Faktor utama yang perlu dipertimbangkan ketika memilih font untuk ASCII adalah apakah itu proporsional (lebar variabel) atau monospasi (lebar tetap). Kami membutuhkan font monospace agar kartu terlihat sesuai kebutuhan (lihat contoh di bawah). Secara default, setiap proyek Unity baru menggunakan font Arial, dan itu bukan monospace, jadi kita perlu mencari yang lain. Windows 10 biasanya memiliki font monospaced Courier New, Consolas, dan Lucida Console. Pilih salah satu dari tiga ini atau unduh yang lain di tempat yang Anda butuhkan dan letakkan di folder Font di dalam folder Aset proyek.


Mari kita siapkan adegan untuk output ASCII. Sebagai permulaan, buat warna latar belakang kamera utama dari adegan itu menjadi hitam. Kemudian kita menambahkan objek Canvas ke adegan, dan menambahkan objek Text ke dalamnya. Atur transformasi persegi panjang Teks ke tengah tengah dan ke posisi 0,0,0. Atur objek Teks sehingga menggunakan font yang Anda pilih dan warna putih, overflow horisontal dan vertikal (overflow horisontal / vertikal), pilih Overflow, dan pusatkan pelurusan vertikal dan horizontal. Kemudian ganti nama objek Teks menjadi "ASCIITest" atau yang serupa.

Sekarang kembali ke kode. Di skrip DungeonGenerator, buat fungsi baru yang disebut DrawMap. Kami ingin dia mendapatkan parameter yang memberitahukan kartu mana yang akan dihasilkan - ASCII atau sprite, jadi buat parameter boolean dan sebut itu adalah ASCII.


Kemudian kami akan memeriksa apakah peta yang diberikan adalah ASCII. Jika ya (untuk saat ini, kami hanya akan mempertimbangkan kasus ini), maka kami akan mencari objek teks dalam adegan, meneruskan nama yang diberikan kepadanya sebagai parameter, dan mendapatkan komponen Teksnya. Tetapi pertama-tama, kita perlu memberi tahu Unity bahwa kita ingin bekerja dengan UI. Tambahkan baris menggunakan UnityEngine.UI ke header skrip:


Baik. Sekarang kita bisa mendapatkan komponen teks dari objek. Peta akan menjadi garis besar, yang tercermin di layar sebagai teks. Itulah mengapa sangat mudah diatur. Jadi mari kita buat string dan inisialisasi dengan nilai "".


Baik. Jadi, setiap kali DrawMap dipanggil, kita perlu memberi tahu apakah kartu tersebut adalah ASCII. Jika demikian (dan kami akan selalu melakukannya dengan cara ini, kami akan bekerja dengan yang lain nanti), maka fungsinya akan mencari hirarki adegan dalam mencari objek game yang disebut "ASCIITest". Jika ya, maka ia akan menerima komponen Teks dan menyimpannya ke variabel layar, yang kemudian dapat dengan mudah kita tulis peta. Kemudian ia menciptakan string yang nilainya awalnya kosong. Kami akan mengisi baris ini dengan peta kami yang ditandai dengan simbol.

Biasanya kita berkeliling peta dalam satu lingkaran, mulai dari 0 dan pergi ke ujung panjangnya. Tetapi untuk mengisi baris, kita mulai dengan baris teks pertama, yaitu baris paling atas. Oleh karena itu, pada sumbu y, kita perlu bergerak dalam satu lingkaran ke arah yang berlawanan, pergi dari ujung ke awal array. Tetapi sumbu x dari array bergerak dari kiri ke kanan, sama seperti teks, jadi ini cocok untuk kita.


Dalam siklus ini, kami memeriksa setiap sel peta untuk mencari tahu apa yang ada di dalamnya. Sejauh ini, kami hanya menginisialisasi sel sebagai Tile baru (), yang kami potong untuk ruangan, jadi semua orang akan mengembalikan kesalahan ketika mencoba mengakses. Jadi pertama-tama kita perlu memeriksa apakah ada sesuatu di sel ini, dan kita melakukan ini dengan memeriksa sel untuk nol. Jika bukan nol, maka kami terus bekerja, tetapi jika itu nol, maka tidak ada apa-apa di dalamnya, sehingga kami dapat menambahkan ruang kosong ke peta.


Jadi, untuk setiap sel yang tidak kosong, kami memeriksa tipenya, dan kemudian menambahkan simbol yang sesuai. Kami ingin dinding ditandai dengan simbol "#" dan lantai ditandai oleh ".". Dan sementara kita hanya memiliki dua tipe ini. Nanti, ketika kita menambahkan pemain, monster, dan jebakan, semuanya akan menjadi sedikit lebih rumit.


Selain itu, kita perlu melakukan jeda baris ketika mencapai akhir baris array, sehingga sel-sel dengan posisi x yang sama berada langsung di bawah satu sama lain. Kami akan memeriksa pada setiap iterasi dari loop apakah sel adalah yang terakhir di baris, dan kemudian menambahkan satu baris dengan karakter khusus "\ n".


Itu saja. Kemudian kita keluar dari loop sehingga kita dapat menambahkan baris ini setelah selesai ke objek teks di tempat kejadian.



Selamat! Anda telah menyelesaikan skrip yang menciptakan ruangan dan menampilkannya di layar. Sekarang kita hanya perlu menerapkannya. Kami tidak menggunakan Start () dalam skrip DungeonGenerator, karena kami ingin memiliki skrip terpisah untuk mengontrol semua yang dilakukan pada awal permainan, termasuk menghasilkan peta, tetapi juga menyiapkan pemain, musuh, dll. Karenanya, skrip lain ini akan berisi fungsi Mulai (), dan, jika perlu, akan memanggil fungsi skrip kami. Skrip DungeonGenerator memiliki fungsi Inisialisasi, yang bersifat publik, dan FirstRoom serta DrawMap tidak bersifat publik. Inisialisasi hanya menginisialisasi variabel untuk menyesuaikan proses pembuatan dungeon, jadi kita membutuhkan fungsi lain yang memanggil proses pembuatan, yang harus bersifat publik sehingga dapat dipanggil dari skrip lain.Untuk saat ini, ia hanya akan memanggil fungsi FirstRoom (), dan kemudian fungsi DrawMap (), meneruskannya dengan nilai yang benar sehingga ia menggambar peta ASCII. Oh, atau tidak, ini bahkan lebih baik - mari kita buat variabel publik isASCII, yang dapat dimasukkan dalam inspektur, dan kirimkan variabel ini sebagai parameter ke fungsi. Baik.


Jadi, sekarang mari kita buat skrip GameManager. Ini akan menjadi skrip yang mengontrol semua elemen level tinggi dari gim, misalnya, membuat peta dan arah gerakan. Mari kita hapus fungsi Update () di dalamnya, tambahkan variabel tipe DungeonGenerator yang disebut dungeonGenerator, dan buat instance variabel ini di fungsi Start ().


Setelah itu, kita cukup memanggil fungsi InitializeDungeon () dan GenerateDungeon () dari dungeonGenerator, dalam urutan itu . Ini penting - pertama Anda perlu menginisialisasi variabel, dan hanya setelah itu mulai membangun berdasarkan mereka.


Pada bagian ini dengan kode selesai. Kita perlu membuat objek game kosong di panel hierarki, mengganti namanya menjadi GameManager dan melampirkan skrip GameManager dan DungeonGenerator padanya. Dan kemudian mengatur nilai generator penjara bawah tanah di inspektur. Anda dapat mencoba berbagai skema untuk generator, dan saya memutuskan ini:


Sekarang klik saja bermain dan saksikan keajaiban! Anda akan melihat sesuatu yang serupa di layar game:


Selamat, sekarang kita punya kamar!

Saya ingin kami meletakkan karakter pemain di sana dan membuatnya bergerak, tetapi posnya sudah cukup panjang. Oleh karena itu, di bagian selanjutnya, kita dapat melanjutkan langsung ke implementasi sisa dari algoritma penjara bawah tanah, atau kita dapat menempatkan pemain di dalamnya dan mengajarkannya bagaimana cara bergerak. Pilih yang paling Anda sukai di komentar ke artikel asli.

MapManager.cs:

using System.Collections;
using System; // So the script can use the serialization commands
using System.Collections.Generic;
using UnityEngine;

public class MapManager {
    public static Tile[,] map; // the 2-dimensional map with the information for all the tiles
}

[Serializable] // Makes the class serializable so it can be saved out to a file
public class Tile { // Holds all the information for each tile on the map
    public int xPosition; // the position on the x axis
    public int yPosition; // the position on the y axis
    [NonSerialized]
    public GameObject baseObject; // the map game object attached to that position: a floor, a wall, etc.
    public string type; // The type of the tile, if it is wall, floor, etc
}

[Serializable]
public class Position { //A class that saves the position of any cell
    public int x;
    public int y;
}

[Serializable]
public class Wall { // A class for saving the wall information, for the dungeon generation algorithm
    public List<Position> positions;
    public string direction;
    public int length;
    public bool hasFeature = false;
}

[Serializable]
public class Feature { // A class for saving the feature (corridor or room) information, for the dungeon generation algorithm
    public List<Position> positions;
    public Wall[] walls;
    public string type;
    public int width;
    public int height;
}

DungeonGenerator.cs:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class DungeonGenerator : MonoBehaviour
{
    public int mapWidth;
    public int mapHeight;

    public int widthMinRoom;
    public int widthMaxRoom;
    public int heightMinRoom;
    public int heightMaxRoom;

    public int maxCorridorLength;
    public int maxFeatures;

    public bool isASCII;

    public void InitializeDungeon() {
        MapManager.map = new Tile[mapWidth, mapHeight];
    }

    public void GenerateDungeon() {
        FirstRoom();
        DrawMap(isASCII);
    }

    void FirstRoom() {
        Feature room = new Feature();
        room.positions = new List<Position>();

        int roomWidth = Random.Range(widthMinRoom, widthMaxRoom);
        int roomHeight = Random.Range(heightMinRoom, heightMaxRoom);

        int xStartingPoint = mapWidth / 2;
        int yStartingPoint = mapHeight / 2;

        xStartingPoint -= Random.Range(0, roomWidth);
        yStartingPoint -= Random.Range(0, roomHeight);

        room.walls = new Wall[4];

        for (int i = 0; i < room.walls.Length; i++) {
            room.walls[i] = new Wall();
            room.walls[i].positions = new List<Position>();
            room.walls[i].length = 0;

            switch (i) {
                case 0:
                    room.walls[i].direction = "South";
                    break;
                case 1:
                    room.walls[i].direction = "North";
                    break;
                case 2:
                    room.walls[i].direction = "West";
                    break;
                case 3:
                    room.walls[i].direction = "East";
                    break;
            }
        }

        for (int y = 0; y < roomHeight; y++) {
            for (int x = 0; x < roomWidth; x++) {
                Position position = new Position();
                position.x = xStartingPoint + x;
                position.y = yStartingPoint + y;

                room.positions.Add(position);

                MapManager.map[position.x, position.y] = new Tile();
                MapManager.map[position.x, position.y].xPosition = position.x;
                MapManager.map[position.x, position.y].yPosition = position.y;

                if (y == 0) {
                    room.walls[0].positions.Add(position);
                    room.walls[0].length++;
                    MapManager.map[position.x, position.y].type = "Wall";
                }
                else if (y == (roomHeight - 1)) {
                    room.walls[1].positions.Add(position);
                    room.walls[1].length++;
                    MapManager.map[position.x, position.y].type = "Wall";
                }
                else if (x == 0) {
                    room.walls[2].positions.Add(position);
                    room.walls[2].length++;
                    MapManager.map[position.x, position.y].type = "Wall";
                }
                else if (x == (roomWidth - 1)) {
                    room.walls[3].positions.Add(position);
                    room.walls[3].length++;
                    MapManager.map[position.x, position.y].type = "Wall";
                }
                else {
                    MapManager.map[position.x, position.y].type = "Floor";
                }
            }
        }

        room.width = roomWidth;
        room.height = roomHeight;
        room.type = "Room";
    }

    void DrawMap(bool isASCII) {
        if (isASCII) {
            Text screen = GameObject.Find("ASCIITest").GetComponent<Text>();

            string asciiMap = "";

            for (int y = (mapHeight - 1); y >= 0; y--) {
                for (int x = 0; x < mapWidth; x++) {
                    if (MapManager.map[x,y] != null) {
                        switch (MapManager.map[x, y].type) {
                            case "Wall":
                                asciiMap += "#";
                                break;
                            case "Floor":
                                asciiMap += ".";
                                break;
                        }
                    } else {
                        asciiMap += " ";
                    }

                    if (x == (mapWidth - 1)) {
                        asciiMap += "\n";
                    }
                }
            }

            screen.text = asciiMap;
        }
    }
}

GameManager.cs:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    DungeonGenerator dungeonGenerator;
    
    void Start() {
        dungeonGenerator = GetComponent<DungeonGenerator>();

        dungeonGenerator.InitializeDungeon();
        dungeonGenerator.GenerateDungeon();
    }
}

All Articles