Pola prosedural yang dapat digunakan dengan kartu ubin

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:

  1. Kami membedakan apa itu ubin dan apa yang tidak menggunakan nilai biner. 1 adalah ubin, 0 adalah tidak adanya.
  2. Kami akan menyimpan semua kartu dalam array integer dua dimensi yang dikembalikan ke pengguna di akhir setiap fungsi (kecuali kartu yang menjalankan rendering).
  3. Saya akan menggunakan fungsi array GetUpperBound () untuk mendapatkan tinggi dan lebar setiap peta, sehingga fungsi menerima lebih sedikit variabel dan kode lebih bersih.
  4. Saya sering menggunakan Mathf.FloorToInt () , karena sistem koordinat Tilemap dimulai di kiri bawah, dan Mathf.FloorToInt () memungkinkan Anda untuk membulatkan angka menjadi bilangan bulat.
  5. 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) { //Clear the map (ensures we dont overlap) tilemap.ClearAllTiles(); //Loop through the width of the map for (int x = 0; x < map.GetUpperBound(0) ; x++) { //Loop through the height of the map for (int y = 0; y < map.GetUpperBound(1); y++) { // 1 = tile, 0 = no tile if (map[x, y] == 1) { tilemap.SetTile(new Vector3Int(x, y, 0), 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) //Takes in our map and tilemap, setting null tiles where needed { for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { //We are only going to update the map, rather than rendering again //This is because it uses less resources to update tiles to null //As opposed to re-drawing every single tile (and collision data) if (map[x, y] == 0) { tilemap.SetTile(new Vector3Int(x, y, 0), null); } } } } 

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; //Used to reduced the position of the Perlin point float reduction = 0.5f; //Create the Perlin for (int x = 0; x < map.GetUpperBound(0); x++) { newPoint = Mathf.FloorToInt((Mathf.PerlinNoise(x, seed) - reduction) * map.GetUpperBound(1)); //Make sure the noise starts near the halfway point of the height newPoint += (map.GetUpperBound(1) / 2); for (int y = newPoint; y >= 0; y--) { map[x, y] = 1; } } return map; } 

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) { //Smooth the noise and store it in the int array if (interval > 1) { int newPoint, points; //Used to reduced the position of the Perlin point float reduction = 0.5f; //Used in the smoothing process Vector2Int currentPos, lastPos; //The corresponding points of the smoothing. One list for x and one for y List<int> noiseX = new List<int>(); List<int> noiseY = new List<int>(); //Generate the noise for (int x = 0; x < map.GetUpperBound(0); x += interval) { newPoint = Mathf.FloorToInt((Mathf.PerlinNoise(x, (seed * reduction))) * map.GetUpperBound(1)); noiseY.Add(newPoint); noiseX.Add(x); } points = noiseY.Count; 

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.

 //Start at 1 so we have a previous position already for (int i = 1; i < points; i++) { //Get the current position currentPos = new Vector2Int(noiseX[i], noiseY[i]); //Also get the last position lastPos = new Vector2Int(noiseX[i - 1], noiseY[i - 1]); //Find the difference between the two Vector2 diff = currentPos - lastPos; //Set up what the height change value will be float heightChange = diff.y / interval; //Determine the current height float currHeight = lastPos.y; //Work our way through from the last x to the current x for (int x = lastPos.x; x < currentPos.x; x++) { for (int y = Mathf.FloorToInt(currHeight); y > 0; y--) { map[x, y] = 1; } currHeight += heightChange; } } } 

Smoothing dilakukan sebagai berikut:

  1. Kami mendapatkan posisi saat ini dan terakhir
  2. Kami mendapatkan perbedaan antara dua titik, informasi paling penting yang kami butuhkan adalah perbedaan sepanjang sumbu y
  3. Kemudian kita menentukan berapa banyak perubahan yang perlu dilakukan untuk sampai ke titik, ini dilakukan dengan membagi selisih y dengan variabel interval.
  4. Selanjutnya, kita mulai mengatur posisi, berjalan sampai nol
  5. Ketika kita mencapai 0 pada sumbu y, tambahkan perubahan ketinggian ke ketinggian saat ini dan ulangi proses untuk posisi x berikutnya
  6. 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 { //Defaults to a normal Perlin gen map = PerlinNoise(map, seed); } return map; 

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) { //Seed our random System.Random rand = new System.Random(seed.GetHashCode()); //Set our starting height int lastHeight = Random.Range(0, map.GetUpperBound(1)); //Cycle through our width for (int x = 0; x < map.GetUpperBound(0); x++) { //Flip a coin int nextMove = rand.Next(2); //If heads, and we aren't near the bottom, minus some height if (nextMove == 0 && lastHeight > 2) { lastHeight--; } //If tails, and we aren't near the top, add some height else if (nextMove == 1 && lastHeight < map.GetUpperBound(1) - 2) { lastHeight++; } //Circle through from the lastheight to the bottom for (int y = lastHeight; y >= 0; y--) { map[x, y] = 1; } } //Return the map return map; } 


Random Walk Top dengan anti-aliasing

Generasi 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) { //Seed our random System.Random rand = new System.Random(seed.GetHashCode()); //Determine the start position int lastHeight = Random.Range(0, map.GetUpperBound(1)); //Used to determine which direction to go int nextMove = 0; //Used to keep track of the current sections width int sectionWidth = 0; //Work through the array width for (int x = 0; x <= map.GetUpperBound(0); x++) { //Determine the next move nextMove = rand.Next(2); //Only change the height if we have used the current height more than the minimum required section width if (nextMove == 0 && lastHeight > 0 && sectionWidth > minSectionWidth) { lastHeight--; sectionWidth = 0; } else if (nextMove == 1 && lastHeight < map.GetUpperBound(1) && sectionWidth > minSectionWidth) { lastHeight++; sectionWidth = 0; } //Increment the section width sectionWidth++; //Work our way from the height down to 0 for (int y = lastHeight; y >= 0; y--) { map[x, y] = 1; } } //Return the modified map return map; } 

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; //Keep the edges as walls } else { //Generate a new point using Perlin noise, then round it to a value of either 0 or 1 newPoint = Mathf.RoundToInt(Mathf.PerlinNoise(x * modifier, y * modifier)); map[x, y] = newPoint; } } } return map; } 

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) { //Seed our random System.Random rand = new System.Random(seed.GetHashCode()); //Define our start x position int floorX = rand.Next(1, map.GetUpperBound(0) - 1); //Define our start y position int floorY = rand.Next(1, map.GetUpperBound(1) - 1); //Determine our required floorAmount int reqFloorAmount = ((map.GetUpperBound(1) * map.GetUpperBound(0)) * requiredFloorPercent) / 100; //Used for our while loop, when this reaches our reqFloorAmount we will stop tunneling int floorCount = 0; //Set our start position to not be a tile (0 = no tile, 1 = tile) map[floorX, floorY] = 0; //Increase our floor count floorCount++; 

Fungsi dimulai dengan yang berikut:

  1. Temukan posisi awal
  2. Hitung jumlah ubin lantai yang akan dihapus.
  3. Hapus ubin di posisi awal
  4. Tambahkan satu ke jumlah ubin.

Kemudian kita beralih ke while . Dia akan membuat gua:

 while (floorCount < reqFloorAmount) { //Determine our next direction int randDir = rand.Next(4); switch (randDir) { //Up case 0: //Ensure that the edges are still tiles if ((floorY + 1) < map.GetUpperBound(1) - 1) { //Move the y up one floorY++; //Check if that piece is currently still a tile if (map[floorX, floorY] == 1) { //Change it to not a tile map[floorX, floorY] = 0; //Increase floor count floorCount++; } } break; //Down case 1: //Ensure that the edges are still tiles if ((floorY - 1) > 1) { //Move the y down one floorY--; //Check if that piece is currently still a tile if (map[floorX, floorY] == 1) { //Change it to not a tile map[floorX, floorY] = 0; //Increase the floor count floorCount++; } } break; //Right case 2: //Ensure that the edges are still tiles if ((floorX + 1) < map.GetUpperBound(0) - 1) { //Move the x to the right floorX++; //Check if that piece is currently still a tile if (map[floorX, floorY] == 1) { //Change it to not a tile map[floorX, floorY] = 0; //Increase the floor count floorCount++; } } break; //Left case 3: //Ensure that the edges are still tiles if ((floorX - 1) > 1) { //Move the x to the left floorX--; //Check if that piece is currently still a tile if (map[floorX, floorY] == 1) { //Change it to not a tile map[floorX, floorY] = 0; //Increase the floor count floorCount++; } } break; } } //Return the updated map return map; } 

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) { //This value goes from its minus counterpart to its positive value, in this case with a width value of 1, the width of the tunnel is 3 int tunnelWidth = 1; //Set the start X position to the center of the tunnel int x = map.GetUpperBound(0) / 2; //Set up our random with the seed System.Random rand = new System.Random(Time.time.GetHashCode()); //Create the first part of the tunnel for (int i = -tunnelWidth; i <= tunnelWidth; i++) { map[x + i, 0] = 0; } 

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.

  //Cycle through the array for (int y = 1; y < map.GetUpperBound(1); y++) { //Check if we can change the roughness if (rand.Next(0, 100) > roughness) { //Get the amount we will change for the width int widthChange = Random.Range(-maxPathWidth, maxPathWidth); //Add it to our tunnel width value tunnelWidth += widthChange; //Check to see we arent making the path too small if (tunnelWidth < minPathWidth) { tunnelWidth = minPathWidth; } //Check that the path width isnt over our maximum if (tunnelWidth > maxPathWidth) { tunnelWidth = maxPathWidth; } } //Check if we can change the curve if (rand.Next(0, 100) > curvyness) { //Get the amount we will change for the x position int xChange = Random.Range(-maxPathChange, maxPathChange); //Add it to our x value x += xChange; //Check we arent too close to the left side of the map if (x < maxPathWidth) { x = maxPathWidth; } //Check we arent too close to the right side of the map if (x > (map.GetUpperBound(0) - maxPathWidth)) { x = map.GetUpperBound(0) - maxPathWidth; } } //Work through the width of the tunnel for (int i = -tunnelWidth; i <= tunnelWidth; i++) { map[x + i, y] = 0; } } return map; } 

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:

  1. 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.
  2. 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) { //Seed our random number generator System.Random rand = new System.Random(seed.GetHashCode()); //Initialise the map 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 we have the edges set to be walls, ensure the cell is set to on (1) if (edgesAreWalls && (x == 0 || x == map.GetUpperBound(0) - 1 || y == 0 || y == map.GetUpperBound(1) - 1)) { map[x, y] = 1; } else { //Randomly generate the grid map[x, y] = (rand.Next(0, 100) < fillPercent) ? 1 : 0; } } } return map; } 

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) { /* Moore Neighbourhood looks like this ('T' is our tile, 'N' is our neighbours) * * NNN * NTN * NNN * */ 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)) { //We don't want to count the tile we are checking the surroundings of if(neighbourX != x || neighbourY != y) { tileCount += map[neighbourX, neighbourY]; } } } } return tileCount; } 

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))) { //Set the edge to be a wall if we have edgesAreWalls to be true map[x, y] = 1; } //The default moore rule requires more than 4 neighbours else if (surroundingTiles > 4) { map[x, y] = 1; } else if (surroundingTiles < 4) { map[x, y] = 0; } } } } //Return the modified map return map; } 

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) { /* von Neumann Neighbourhood looks like this ('T' is our Tile, 'N' is our Neighbour) * * N * NTN * N * */ int tileCount = 0; //Keep the edges as walls if(edgesAreWalls && (x - 1 == 0 || x + 1 == map.GetUpperBound(0) || y - 1 == 0 || y + 1 == map.GetUpperBound(1))) { tileCount++; } //Ensure we aren't touching the left side of the map if(x - 1 > 0) { tileCount += map[x - 1, y]; } //Ensure we aren't touching the bottom of the map if(y - 1 > 0) { tileCount += map[x, y - 1]; } //Ensure we aren't touching the right side of the map if(x + 1 < map.GetUpperBound(0)) { tileCount += map[x + 1, y]; } //Ensure we aren't touching the top of the map if(y + 1 < map.GetUpperBound(1)) { tileCount += map[x, y + 1]; } return tileCount; } 

Setelah menerima jumlah tetangga, kita dapat melanjutkan untuk memperlancar susunan. Seperti sebelumnya, kita perlu loop foruntuk 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++) { //Get the surrounding tiles int surroundingTiles = GetVNSurroundingTiles(map, x, y, edgesAreWalls); if (edgesAreWalls && (x == 0 || x == map.GetUpperBound(0) - 1 || y == 0 || y == map.GetUpperBound(1))) { //Keep our edges as walls map[x, y] = 1; } //von Neuemann Neighbourhood requires only 3 or more surrounding tiles to be changed to a tile else if (surroundingTiles > 2) { map[x, y] = 1; } else if (surroundingTiles < 2) { map[x, y] = 0; } } } } //Return the modified map return map; } 

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 .

Source: https://habr.com/ru/post/id471084/


All Articles