Generasi prosedural digunakan untuk meningkatkan variabilitas game. Proyek terkenal termasuk
Minecraft ,
Enter the Gungeon, dan
Descenders . Dalam posting ini, saya akan menjelaskan beberapa algoritma yang dapat digunakan ketika bekerja dengan sistem
Tilemap , yang muncul sebagai fungsi 2D di Unity 2017.2, dan dengan
RuleTile .
Dengan pembuatan peta secara prosedural, setiap permainan yang lewat akan menjadi unik. Anda dapat menggunakan berbagai data input, seperti waktu atau level pemain saat ini, untuk secara dinamis mengubah konten bahkan setelah perakitan game.
Tentang apa posting ini?
Kami akan melihat beberapa cara paling umum untuk menciptakan dunia prosedural, serta beberapa variasi yang saya buat. Berikut adalah contoh dari apa yang dapat Anda buat setelah membaca artikel. Tiga algoritma bekerja bersama untuk membuat peta menggunakan
Tilemap dan
RuleTile :
Dalam proses menghasilkan peta menggunakan algoritma apa pun, kami mendapatkan array
int
berisi semua data baru. Anda dapat terus memodifikasi data ini atau membuatnya menjadi peta ubin.
Sebelum membaca lebih lanjut, alangkah baiknya mengetahui hal-hal berikut:
- Kami membedakan apa itu ubin dan apa yang tidak menggunakan nilai biner. 1 adalah ubin, 0 adalah tidak adanya.
- Kami akan menyimpan semua kartu dalam array integer dua dimensi yang dikembalikan ke pengguna di akhir setiap fungsi (kecuali kartu yang menjalankan rendering).
- Saya akan menggunakan fungsi array GetUpperBound () untuk mendapatkan tinggi dan lebar setiap peta, sehingga fungsi menerima lebih sedikit variabel dan kode lebih bersih.
- Saya sering menggunakan Mathf.FloorToInt () , karena sistem koordinat Tilemap dimulai di kiri bawah, dan Mathf.FloorToInt () memungkinkan Anda untuk membulatkan angka menjadi bilangan bulat.
- Semua kode dalam posting ini ditulis dalam C #.
Generasi array
GenerateArray membuat array
int
baru dari ukuran yang diberikan. Kami juga dapat menunjukkan apakah array harus diisi atau kosong (1 atau 0). Ini kodenya:
public static int[,] GenerateArray(int width, int height, bool empty) { int[,] map = new int[width, height]; for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { if (empty) { map[x, y] = 0; } else { map[x, y] = 1; } } } return map; }
Perenderan peta
Fungsi ini digunakan untuk membuat peta pada peta ubin. Kami memutar di sekitar lebar dan tinggi peta, menempatkan ubin hanya ketika array pada titik yang diuji memiliki nilai 1.
public static void RenderMap(int[,] map, Tilemap tilemap, TileBase tile) {
Pembaruan peta
Fungsi ini hanya digunakan untuk memperbarui peta, dan bukan untuk merender ulang. Berkat ini, kami dapat menggunakan lebih sedikit sumber daya tanpa menggambar ulang setiap ubin dan data ubinnya.
public static void UpdateMap(int[,] map, Tilemap tilemap)
Perlin kebisingan
Perlin noise dapat digunakan untuk berbagai keperluan. Pertama, kita dapat menggunakannya untuk membuat lapisan teratas peta kita. Untuk melakukan ini, dapatkan poin baru menggunakan posisi saat ini x dan seed.
Solusi sederhana
Metode generasi ini menggunakan bentuk realisasi paling sederhana dari kebisingan Perlin dalam generasi tingkat. Kami dapat mengambil fungsi Unity untuk kebisingan Perlin sehingga kami tidak dapat menulis kode sendiri. Kami juga akan menggunakan hanya bilangan bulat untuk peta ubin, menggunakan fungsi
Mathf.FloorToInt () .
public static int[,] PerlinNoise(int[,] map, float seed) { int newPoint;
Berikut ini tampilannya setelah merender ke peta petak:
Menghaluskan
Anda juga dapat mengambil fungsi ini dan melicinkannya. Tetapkan interval untuk memperbaiki ketinggian Perlin, dan kemudian lakukan penghalusan di antara titik-titik ini. Fungsi ini akan menjadi sedikit lebih rumit, karena untuk interval Anda perlu mempertimbangkan daftar nilai integer.
public static int[,] PerlinNoiseSmooth(int[,] map, float seed, int interval) {
Pada bagian pertama dari fungsi ini, pertama-tama kita memeriksa apakah intervalnya lebih besar dari satu. Jika demikian, maka hasilkan noise. Pembangkitan dilakukan secara berkala sehingga penghalusan dapat diterapkan. Bagian selanjutnya dari fungsi ini adalah untuk menghaluskan poin.
Smoothing dilakukan sebagai berikut:
- Kami mendapatkan posisi saat ini dan terakhir
- Kami mendapatkan perbedaan antara dua titik, informasi paling penting yang kami butuhkan adalah perbedaan sepanjang sumbu y
- Kemudian kita menentukan berapa banyak perubahan yang perlu dilakukan untuk sampai ke titik, ini dilakukan dengan membagi selisih y dengan variabel interval.
- Selanjutnya, kita mulai mengatur posisi, berjalan sampai nol
- Ketika kita mencapai 0 pada sumbu y, tambahkan perubahan ketinggian ke ketinggian saat ini dan ulangi proses untuk posisi x berikutnya
- Selesai dengan masing-masing posisi antara posisi terakhir dan saat ini, kami pindah ke titik berikutnya
Jika intervalnya kurang dari satu, maka kita cukup menggunakan fungsi sebelumnya yang akan melakukan semua pekerjaan untuk kita.
else {
Mari kita lihat rendernya:
Berjalan acak
Acak berjalan di atas
Algoritma ini melakukan flip koin. Kami bisa mendapatkan salah satu dari dua hasil. Jika hasilnya "elang", maka kita bergerak satu blok ke atas, jika hasilnya "ekor", maka kita memindahkan blok ke bawah. Ini menciptakan ketinggian dengan terus bergerak ke atas atau ke bawah. Satu-satunya kelemahan dari algoritma semacam itu adalah sifatnya yang sangat mencolok. Mari kita lihat bagaimana cara kerjanya.
public static int[,] RandomWalkTop(int[,] map, float seed) {
Random Walk Top dengan anti-aliasingGenerasi seperti itu memberi kita ketinggian yang lebih halus dibandingkan dengan generasi kebisingan Perlin.
Variasi dari Random Walk ini memberikan hasil yang jauh lebih mulus dibandingkan dengan versi sebelumnya. Kita bisa mengimplementasikannya dengan menambahkan dua variabel lagi ke fungsi:
- Variabel pertama digunakan untuk menentukan berapa lama untuk mempertahankan ketinggian saat ini. Itu bilangan bulat dan diatur ulang ketika ketinggian berubah
- Variabel kedua adalah input ke fungsi dan digunakan sebagai lebar bagian minimum untuk tinggi. Ini akan menjadi lebih jelas ketika kita melihat fungsinya.
Sekarang kita tahu apa yang harus ditambahkan. Mari kita lihat fungsinya:
public static int[,] RandomWalkTopSmoothed(int[,] map, float seed, int minSectionWidth) {
Seperti yang dapat Anda lihat di gif yang ditunjukkan di bawah ini, smoothing the random walk algoritma memungkinkan Anda untuk mendapatkan segmen flat yang indah di level tersebut.
Kesimpulan
Saya harap artikel ini menginspirasi Anda untuk menggunakan generasi prosedural dalam proyek Anda. Jika Anda ingin mempelajari lebih lanjut tentang peta yang dibuat secara prosedural, maka jelajahi sumber daya yang sangat baik dari
Wiki Generasi Prosedural atau
Roguebasin.com .
Di bagian kedua artikel, kita akan menggunakan generasi prosedural untuk membuat sistem gua.
Bagian 2
Segala sesuatu yang akan kita bahas di bagian ini dapat ditemukan di
proyek ini . Anda dapat mengunduh aset dan mencoba algoritma prosedural Anda sendiri.
Perlin kebisingan
Di bagian sebelumnya, kami mencari cara untuk menerapkan
Perlin noise untuk membuat lapisan atas. Untungnya, suara Perlin juga bisa digunakan untuk membuat gua. Ini diwujudkan dengan menghitung nilai kebisingan Perlin baru, yang menerima parameter posisi saat ini dikalikan dengan pengubah. Pengubah adalah nilai dari 0 hingga 1. Semakin tinggi nilai pengubah, semakin banyak generasi Perlin yang kacau. Lalu kami membulatkan nilai ini ke integer (0 atau 1), yang kami simpan di larik peta. Lihat bagaimana ini diterapkan:
public static int[,] PerlinNoiseCave(int[,] map, float modifier, bool edgesAreWalls) { int newPoint; for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { if (edgesAreWalls && (x == 0 || y == 0 || x == map.GetUpperBound(0) - 1 || y == map.GetUpperBound(1) - 1)) { map[x, y] = 1;
Kami menggunakan pengubah alih-alih seed karena hasil generasi Perlin terlihat lebih baik ketika dikalikan dengan angka dari 0 menjadi 0,5. Semakin rendah nilainya, semakin gumpal hasilnya. Lihatlah hasil sampel. Gif dimulai dengan nilai pengubah 0,01 dan secara bertahap mencapai nilai 0,25.
Dari gif ini dapat dilihat bahwa generasi Perlin dengan setiap kenaikan hanya meningkatkan polanya.
Berjalan acak
Di bagian sebelumnya, kami melihat bahwa Anda dapat menggunakan lemparan koin untuk menentukan di mana platform akan naik atau turun. Pada bagian ini, kita akan menggunakan ide yang sama, tetapi
dengan dua opsi tambahan untuk shift kiri dan kanan. Variasi dari algoritma Random Walk ini memungkinkan kami membuat gua. Untuk melakukan ini, kami memilih arah acak, kemudian memindahkan posisi kami dan menghapus ubin. Kami melanjutkan proses ini hingga kami mencapai jumlah ubin yang diperlukan yang perlu dihancurkan. Sejauh ini, kami hanya menggunakan 4 arah: atas, bawah, kiri, kanan.
public static int[,] RandomWalkCave(int[,] map, float seed, int requiredFloorPercent) {
Fungsi dimulai dengan yang berikut:
- Temukan posisi awal
- Hitung jumlah ubin lantai yang akan dihapus.
- Hapus ubin di posisi awal
- Tambahkan satu ke jumlah ubin.
Kemudian kita beralih ke
while
. Dia akan membuat gua:
while (floorCount < reqFloorAmount) {
Apa yang kita lakukan disini
Pertama, dengan bantuan angka acak, kami memilih arah mana yang akan dipindahkan. Kemudian kami memeriksa arah baru dengan pernyataan
switch case
. Dalam pernyataan ini, kami memeriksa apakah posisinya adalah dinding. Jika tidak, maka hapus elemen dengan ubin dari array. Kami terus melakukan ini sampai kami mencapai area lantai yang diinginkan. Hasilnya ditunjukkan di bawah ini:
Saya juga membuat versi saya sendiri dari fungsi ini, yang juga mencakup arah diagonal. Kode fungsinya cukup panjang, jadi jika Anda ingin melihatnya, unduh proyek dari tautan di awal bagian artikel ini.
Terowongan terarah
Sebuah terowongan arah dimulai pada satu sisi peta dan mencapai sisi yang berlawanan. Kita dapat mengontrol kelengkungan dan kekasaran terowongan dengan mengirimkannya ke fungsi input. Kita juga dapat mengatur panjang minimum dan maksimum dari bagian-bagian terowongan. Mari kita lihat implementasinya:
public static int[,] DirectionalTunnel(int[,] map, int minPathWidth, int maxPathWidth, int maxPathChange, int roughness, int curvyness) {
Apa yang sedang terjadi
Pertama kita atur nilai lebarnya. Nilai lebar akan berubah dari nilai minus ke positif. Berkat ini, kita akan mendapatkan ukuran yang kita butuhkan. Dalam hal ini, kami menggunakan nilai 1, yang pada gilirannya akan memberi kami total lebar 3, karena kami menggunakan nilai -1, 0, 1.
Selanjutnya, kita atur posisi awal dalam x, untuk ini kita ambil bagian tengah dari lebar peta. Setelah itu, kita bisa meletakkan terowongan di bagian pertama peta.
Sekarang mari kita masuk ke sisa peta.
Kami menghasilkan angka acak untuk perbandingan dengan nilai kekasaran, dan jika lebih tinggi dari nilai ini, maka lebar jalur dapat diubah. Kami juga memeriksa nilainya agar tidak membuat lebar terlalu kecil. Pada bagian selanjutnya dari kode, kita membuat jalan melalui peta. Pada setiap tahap, berikut ini terjadi:
- Kami menghasilkan angka acak baru dibandingkan dengan nilai kelengkungan. Seperti pada pengujian sebelumnya, jika lebih besar dari nilai, maka kita mengubah titik pusat jalan. Kami juga melakukan pemeriksaan agar tidak melampaui peta.
- Akhirnya, kami meletakkan terowongan di bagian yang baru dibuat.
Hasil implementasi ini terlihat seperti ini:
Automata seluler
Automata seluler menggunakan sel tetangga untuk menentukan apakah sel saat ini dihidupkan (1) atau dimatikan (0). Dasar untuk menentukan sel tetangga dibuat berdasarkan grid sel yang dihasilkan secara acak. Kami akan menghasilkan kotak sumber ini menggunakan fungsi C #
Random.Next .
Karena kami memiliki beberapa implementasi automata seluler yang berbeda, saya menulis fungsi terpisah untuk menghasilkan grid dasar ini. Fungsi terlihat seperti ini:
public static int[,] GenerateCellularAutomata(int width, int height, float seed, int fillPercent, bool edgesAreWalls) {
Dalam fungsi ini, Anda juga dapat mengatur apakah jaringan kami membutuhkan dinding. Dalam semua hal lain, ini cukup sederhana. Kami memeriksa nomor acak dengan persen isi untuk menentukan apakah sel saat ini diaktifkan. Lihatlah hasilnya:
Lingkungan Moore
Lingkungan Moore digunakan untuk memperlancar generasi awal automata seluler. Lingkungan Moore terlihat seperti ini:
Aturan berikut ini berlaku untuk lingkungan:
- Kami memeriksa tetangga di setiap arah.
- Jika tetangga adalah ubin aktif, kemudian tambahkan satu ke jumlah ubin sekitarnya.
- Jika tetangga adalah ubin tidak aktif, maka kita tidak melakukan apa pun.
- Jika sel memiliki lebih dari 4 ubin di sekitarnya, maka aktifkan sel tersebut.
- Jika sel memiliki persis 4 ubin di sekitarnya, maka kita tidak melakukan apa-apa dengannya.
- Ulangi sampai kami memeriksa setiap ubin peta.
Fungsi pengecekan lingkungan Moore adalah sebagai berikut:
static int GetMooreSurroundingTiles(int[,] map, int x, int y, bool edgesAreWalls) { int tileCount = 0; for(int neighbourX = x - 1; neighbourX <= x + 1; neighbourX++) { for(int neighbourY = y - 1; neighbourY <= y + 1; neighbourY++) { if (neighbourX >= 0 && neighbourX < map.GetUpperBound(0) && neighbourY >= 0 && neighbourY < map.GetUpperBound(1)) {
Setelah memeriksa ubin, kami menggunakan informasi ini dalam fungsi perataan. Di sini, seperti pada generasi automata seluler, orang dapat menunjukkan apakah tepi peta harus dinding.
public static int[,] SmoothMooreCellularAutomata(int[,] map, bool edgesAreWalls, int smoothCount) { for (int i = 0; i < smoothCount; i++) { for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { int surroundingTiles = GetMooreSurroundingTiles(map, x, y, edgesAreWalls); if (edgesAreWalls && (x == 0 || x == (map.GetUpperBound(0) - 1) || y == 0 || y == (map.GetUpperBound(1) - 1))) {
Penting untuk dicatat di sini bahwa fungsi memiliki
for
loop yang melakukan pemulusan beberapa kali. Berkat ini, kartu yang lebih indah diperoleh.
Kami selalu dapat memodifikasi algoritme ini dengan menghubungkan kamar jika, misalnya, hanya ada dua blok di antaranya.
Lingkungan Von Neumann
Lingkungan von Neumann adalah cara populer lainnya untuk mengimplementasikan automata seluler. Untuk generasi seperti itu, kami menggunakan lingkungan yang lebih sederhana daripada di generasi Moore. Lingkungannya terlihat seperti ini:
Aturan berikut ini berlaku untuk lingkungan:
- Kami memeriksa tetangga langsung ubin, tidak mempertimbangkan yang diagonal.
- Jika sel aktif, tambahkan satu ke kuantitasnya.
- Jika sel tidak aktif, maka jangan lakukan apa-apa.
- Jika sel memiliki lebih dari 2 tetangga, maka kami membuat sel saat ini aktif.
- Jika sel memiliki kurang dari 2 tetangga, maka kami membuat sel saat ini tidak aktif.
- Jika ada tepat 2 tetangga, maka jangan ubah sel saat ini.
Hasil kedua menggunakan prinsip yang sama dengan yang pertama, tetapi memperluas area lingkungan.
Kami memeriksa tetangga dengan fungsi berikut: static int GetVNSurroundingTiles(int[,] map, int x, int y, bool edgesAreWalls) { int tileCount = 0;
Setelah menerima jumlah tetangga, kita dapat melanjutkan untuk memperlancar susunan. Seperti sebelumnya, kita perlu loop for
untuk menyelesaikan jumlah iterasi perataan yang diteruskan ke input. public static int[,] SmoothVNCellularAutomata(int[,] map, bool edgesAreWalls, int smoothCount) { for (int i = 0; i < smoothCount; i++) { for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) {
Seperti yang Anda lihat di bawah, hasil akhirnya jauh lebih kuning daripada lingkungan Moore:Di sini, seperti di sekitar Moore, Anda dapat menjalankan skrip tambahan untuk mengoptimalkan koneksi antara bagian-bagian peta.Kesimpulan
Saya harap artikel ini menginspirasi Anda untuk menggunakan semacam generasi prosedural dalam proyek Anda. Jika Anda belum mengunduh proyek, maka Anda bisa mendapatkannya di sini .