Peta segi enam dalam Persatuan: siklus air, erosi, bioma, peta silindris

Bagian 1-3: jala, warna, dan tinggi sel

Bagian 4-7: gundukan, sungai, dan jalan

Bagian 8-11: air, bentang alam, dan benteng

Bagian 12-15: menyimpan dan memuat, tekstur, jarak

Bagian 16-19: menemukan jalan, regu pemain, animasi

Bagian 20-23: Kabut Perang, Penelitian Peta, Generasi Prosedural

Bagian 24-27: siklus air, erosi, bioma, peta silindris

Bagian 24: daerah dan erosi


  • Tambahkan batas air di sekitar peta.
  • Kami membagi peta menjadi beberapa wilayah.
  • Kami menggunakan erosi untuk memotong tebing.
  • Kami memindahkan tanah untuk memuluskan pertolongan.

Pada bagian sebelumnya, kami meletakkan dasar untuk pembuatan peta prosedural. Kali ini kami akan membatasi tempat-tempat yang mungkin terjadinya tanah dan menindakinya dengan erosi.

Tutorial ini dibuat di Unity 2017.1.0.


Pisahkan dan menghaluskan tanah.

Batas peta


Karena kami menaikkan luas lahan secara acak, mungkin saja tanah itu menyentuh tepi peta. Ini mungkin tidak diinginkan. Peta terbatas air berisi penghalang alami yang mencegah pemain mendekati tepi. Karena itu, alangkah baiknya jika kita melarang tanah naik di atas permukaan air di dekat tepi peta.

Ukuran perbatasan


Seberapa dekat jarak mendarat ke tepi peta? Tidak ada jawaban yang tepat untuk pertanyaan ini, jadi kami akan membuat parameter ini dapat disesuaikan. Kami akan menambahkan dua slider ke komponen HexMapGenerator , satu untuk perbatasan di sepanjang tepi sepanjang sumbu X, yang lain untuk perbatasan di sepanjang sumbu Z. Jadi kita dapat menggunakan perbatasan yang lebih luas di salah satu dimensi, atau bahkan membuat perbatasan hanya dalam satu dimensi. Mari kita gunakan interval dari 0 hingga 10 dengan nilai default 5.

  [Range(0, 10)] public int mapBorderX = 5; [Range(0, 10)] public int mapBorderZ = 5; 


Peta berbatasan dengan bilah geser.

Kami membatasi pusat area lahan


Tanpa batas, semua sel valid. Ketika ada batas, koordinat offset minimum yang diizinkan meningkat, dan koordinat maksimum yang diijinkan berkurang. Karena untuk menghasilkan plot kita perlu mengetahui interval yang diizinkan, mari kita lacak menggunakan empat bidang integer.

  int xMin, xMax, zMin, zMax; 

Kami menginisialisasi kendala di GenerateMap sebelum membuat sushi. Kami menggunakan nilai-nilai ini sebagai parameter untuk panggilan Random.Range , sehingga tertinggi sebenarnya luar biasa. Tanpa batas, mereka sama dengan jumlah sel pengukuran, oleh karena itu, tidak minus 1.

  public void GenerateMap (int x, int z) { … for (int i = 0; i < cellCount; i++) { grid.GetCell(i).WaterLevel = waterLevel; } xMin = mapBorderX; xMax = x - mapBorderX; zMin = mapBorderZ; zMax = z - mapBorderZ; CreateLand(); … } 

Kami tidak akan secara tegas melarang penampilan tanah di luar perbatasan, karena ini akan membuat pinggiran tajam. Sebagai gantinya, kami hanya akan membatasi sel yang digunakan untuk memulai pembuatan plot. Artinya, perkiraan pusat dari situs akan terbatas, tetapi bagian dari situs akan dapat melampaui area perbatasan. Ini dapat dilakukan dengan memodifikasi GetRandomCell sehingga memilih sel dalam kisaran offset yang diizinkan.

  HexCell GetRandomCell () { // return grid.GetCell(Random.Range(0, cellCount)); return grid.GetCell(Random.Range(xMin, xMax), Random.Range(zMin, zMax)); } 





Perbatasan peta adalah 0 × 0, 5 × 5, 10 × 10, dan 0 × 10.

Ketika semua parameter peta diatur ke nilai standarnya, batas ukuran 5 andal akan melindungi tepi peta dari menyentuh tanah. Namun, ini tidak dijamin. Tanah kadang-kadang bisa mendekati tepi, dan kadang-kadang menyentuhnya di beberapa tempat.

Kemungkinan tanah melintasi seluruh perbatasan tergantung pada ukuran perbatasan dan ukuran maksimum situs. Tanpa ragu-ragu, bagian itu tetap segi enam. Hexagon penuh dengan jari-jari r berisi 3 r 2 + 3 r + 1 sel. Jika ada segi enam dengan jari-jari sama dengan ukuran perbatasan, maka mereka dapat melintasinya. Hexagon penuh dengan jari-jari 5 berisi 91 sel. Karena secara default maksimum adalah 100 sel per bagian, ini berarti bahwa tanah akan dapat meletakkan jembatan di 5 sel, terutama jika ada getaran. Untuk mencegah hal ini terjadi, kurangi ukuran maksimum plot, atau tambah ukuran perbatasan.

Bagaimana rumus untuk jumlah sel di daerah heksagonal diturunkan?
Dengan jari-jari 0, kita berhadapan dengan satu sel. Itu berasal dari 1. Dengan jari-jari 1 di sekitar pusat, ada enam sel tambahan, yaitu 6 + 1 . Keenam sel ini dapat dianggap sebagai ujung dari enam segitiga yang menyentuh bagian tengah. Dengan jari-jari 2, baris kedua ditambahkan ke segitiga ini, yaitu, dua sel lagi diperoleh pada segitiga, dan secara total 6 ( 1 + 2 ) + 1 . Dengan jari-jari 3, baris ketiga ditambahkan, yaitu tiga sel lagi per segitiga, dan total 6 ( 1 + 2 + 3 ) + 1 . Dan sebagainya. Artinya, secara umum, rumusnya seperti 6(jumlah(i=1)ri)+1=6((r(r+1))/2)+1=3r(r+1)+1=3r2+3r+1 .

Untuk melihat ini lebih jelas, kita dapat mengatur ukuran perbatasan menjadi 200. Karena segi enam penuh dengan jari-jari 8 berisi 217 sel, tanah cenderung menyentuh tepi peta. Setidaknya jika Anda menggunakan nilai ukuran batas default (5). Jika Anda meningkatkan batas menjadi 10, kemungkinan akan sangat menurun.



Plot tanah memiliki ukuran konstan 200, batas peta adalah 5 dan 10.

Pangea


Perhatikan bahwa saat Anda meningkatkan perbatasan peta dan mempertahankan persentase lahan yang sama, kami memaksa lahan untuk membentuk area yang lebih kecil. Sebagai akibatnya, sebuah peta besar secara default kemungkinan besar akan menciptakan satu massa tanah besar - Pangaea superkontinen - mungkin dengan beberapa pulau kecil. Dengan peningkatan ukuran perbatasan, kemungkinan ini meningkat, dan pada nilai-nilai tertentu kami hampir dijamin untuk mendapatkan benua super. Namun, ketika persentase tanah terlalu besar, sebagian besar wilayah yang tersedia terisi dan sebagai hasilnya kami mendapatkan massa tanah yang hampir persegi. Untuk mencegah hal ini terjadi, Anda perlu mengurangi persentase lahan.


40% sushi dengan batas kartu 10.

Dari mana nama Pangea berasal?
Itulah nama superbenua terakhir yang diketahui ada di Bumi beberapa tahun yang lalu. Nama ini terdiri dari kata Yunani pan dan Gaia, yang berarti sesuatu seperti "semua alam" atau "semua tanah".


Kami melindungi dari kartu yang tidak mungkin


Kami menghasilkan jumlah lahan yang tepat hanya dengan terus menaikkan lahan sampai kami mencapai massa lahan yang diinginkan. Ini bekerja karena cepat atau lambat kita akan menaikkan setiap sel di permukaan air. Namun, ketika menggunakan perbatasan peta, kami tidak dapat mencapai setiap sel. Ketika persentase tanah terlalu tinggi diperlukan, ini akan menyebabkan “upaya dan kegagalan” generator yang tak berkesudahan untuk meningkatkan lebih banyak lahan, dan itu akan menjadi terjebak dalam siklus tanpa akhir. Dalam hal ini, aplikasi akan membeku, tetapi ini seharusnya tidak terjadi.

Kami tidak dapat menemukan konfigurasi yang mungkin dengan andal sebelumnya, tetapi kami dapat melindungi diri dari siklus tanpa akhir. Kami hanya akan melacak jumlah siklus yang dijalankan di CreateLand . Jika ada terlalu banyak iterasi, maka kita kemungkinan besar macet dan harus berhenti.

Untuk peta besar, seribu iterasi tampaknya dapat diterima, dan sepuluh ribu iterasi sudah tampak tidak masuk akal. Jadi mari kita gunakan nilai ini sebagai titik terminasi.

  void CreateLand () { int landBudget = Mathf.RoundToInt(cellCount * landPercentage * 0.01f); // while (landBudget > 0) { for (int guard = 0; landBudget > 0 && guard < 10000; guard++) { int chunkSize = Random.Range(chunkSizeMin, chunkSizeMax - 1); … } } 

Jika kita mendapatkan peta yang rusak, maka melakukan 10.000 iterasi tidak akan memakan banyak waktu, karena banyak sel akan dengan cepat mencapai ketinggian maksimum, yang akan mencegah area baru tumbuh.

Bahkan setelah memutus perulangan, kita masih mendapatkan peta yang benar. Itu hanya tidak memiliki jumlah tanah yang tepat dan itu tidak akan terlihat sangat menarik. Mari kita tampilkan pemberitahuan tentang hal ini di konsol, memberi tahu kami sisa lahan yang gagal kami habiskan.

  void CreateLand () { … if (landBudget > 0) { Debug.LogWarning("Failed to use up " + landBudget + " land budget."); } } 


95% tanah dengan batas kartu 10 tidak dapat menghabiskan seluruh jumlah.

Mengapa kartu yang gagal masih memiliki variasi?
Garis pantai memiliki variabilitas, karena ketika ketinggian di dalam area pembuatan menjadi terlalu tinggi, area baru tidak memungkinkan mereka untuk tumbuh ke luar. Prinsip yang sama tidak memungkinkan plot untuk tumbuh menjadi area kecil tanah, sampai mereka mencapai ketinggian maksimum dan ternyata hilang. Selain itu, variabilitas meningkat saat menurunkan plot.

paket unity

Mempartisi kartu


Sekarang kita memiliki perbatasan peta, pada dasarnya kita membagi peta menjadi dua wilayah yang terpisah: wilayah perbatasan dan wilayah di mana plot dibuat. Karena hanya wilayah penciptaan yang penting bagi kita, kita dapat mempertimbangkan kasus seperti itu sebagai situasi dengan satu wilayah. Wilayah ini tidak mencakup seluruh peta. Tetapi jika ini tidak mungkin, maka tidak ada yang menghalangi kita untuk membagi peta menjadi beberapa wilayah penciptaan tanah yang tidak terhubung. Ini akan memungkinkan massa tanah terbentuk secara independen satu sama lain, menunjuk benua yang berbeda.

Wilayah Peta


Mari kita mulai dengan menggambarkan satu wilayah peta sebagai struct. Ini akan mempermudah pekerjaan kami dengan beberapa daerah. Mari kita buat struktur MapRegion untuk ini, yang hanya berisi bidang perbatasan wilayah. Karena kita tidak akan menggunakan struktur ini di luar HexMapGenerator , kita dapat mendefinisikannya di dalam kelas ini sebagai struktur internal pribadi. Kemudian empat bidang bilangan bulat dapat diganti dengan satu bidang MapRegion .

 // int xMin, xMax, zMin, zMax; struct MapRegion { public int xMin, xMax, zMin, zMax; } MapRegion region; 

Agar semuanya berfungsi, kita perlu menambahkan awalan region. ke bidang minimum-maksimum di GenerateMap region. .

  region.xMin = mapBorderX; region.xMax = x - mapBorderX; region.zMin = mapBorderZ; region.zMax = z - mapBorderZ; 

Dan juga di GetRandomCell .

  HexCell GetRandomCell () { return grid.GetCell( Random.Range(region.xMin, region.xMax), Random.Range(region.zMin, region.zMax) ); } 

Beberapa daerah


Untuk mendukung beberapa wilayah, ganti satu bidang MapRegion daftar wilayah.

 // MapRegion region; List<MapRegion> regions; 

Pada titik ini, alangkah baiknya untuk menambahkan metode terpisah untuk membuat daerah. Ini harus membuat daftar yang diinginkan atau menghapusnya jika sudah ada. Setelah itu, dia akan menentukan satu wilayah, seperti yang kita lakukan sebelumnya, dan menambahkannya ke daftar.

  void CreateRegions () { if (regions == null) { regions = new List<MapRegion>(); } else { regions.Clear(); } MapRegion region; region.xMin = mapBorderX; region.xMax = grid.cellCountX - mapBorderX; region.zMin = mapBorderZ; region.zMax = grid.cellCountZ - mapBorderZ; regions.Add(region); } 

Kami akan memanggil metode ini di GenerateMap , dan kami tidak akan membuat wilayah secara langsung.

 // region.xMin = mapBorderX; // region.xMax = x - mapBorderX; // region.zMin = mapBorderZ; // region.zMax = z - mapBorderZ; CreateRegions(); CreateLand(); 

Agar GetRandomCell dapat bekerja dengan wilayah arbitrer, berikan parameter MapRegion .

  HexCell GetRandomCell (MapRegion region) { return grid.GetCell( Random.Range(region.xMin, region.xMax), Random.Range(region.zMin, region.zMax) ); } 

Sekarang metode RaiseTerraion dan SinkTerrain harus meneruskan wilayah terkait ke GetRandomCell . Untuk melakukan ini, masing-masing dari mereka juga memerlukan parameter wilayah.

  int RaiseTerrain (int chunkSize, int budget, MapRegion region) { searchFrontierPhase += 1; HexCell firstCell = GetRandomCell(region); … } int SinkTerrain (int chunkSize, int budget, MapRegion region) { searchFrontierPhase += 1; HexCell firstCell = GetRandomCell(region); … } 

Metode CreateLand harus menentukan untuk setiap wilayah untuk menaikkan atau menurunkan bagian. Untuk menyeimbangkan tanah antar daerah, kami hanya akan berulang kali berkeliling daftar wilayah dalam siklus.

  void CreateLand () { int landBudget = Mathf.RoundToInt(cellCount * landPercentage * 0.01f); for (int guard = 0; landBudget > 0 && guard < 10000; guard++) { for (int i = 0; i < regions.Count; i++) { MapRegion region = regions[i]; int chunkSize = Random.Range(chunkSizeMin, chunkSizeMax - 1); if (Random.value < sinkProbability) { landBudget = SinkTerrain(chunkSize, landBudget, region); } else { landBudget = RaiseTerrain(chunkSize, landBudget, region); } } } if (landBudget > 0) { Debug.LogWarning("Failed to use up " + landBudget + " land budget."); } } 

Namun, kita masih perlu membuat plot yang diturunkan merata. Ini dapat dilakukan sambil memutuskan untuk semua wilayah apakah akan menghilangkannya.

  for (int guard = 0; landBudget > 0 && guard < 10000; guard++) { bool sink = Random.value < sinkProbability; for (int i = 0; i < regions.Count; i++) { MapRegion region = regions[i]; int chunkSize = Random.Range(chunkSizeMin, chunkSizeMax - 1); // if (Random.value < sinkProbability) { if (sink) { landBudget = SinkTerrain(chunkSize, landBudget, region); } else { landBudget = RaiseTerrain(chunkSize, landBudget, region); } } } 

Akhirnya, untuk menggunakan persis seluruh jumlah lahan, kita harus menghentikan proses segera setelah jumlahnya mencapai nol. Ini dapat terjadi pada setiap tahap siklus wilayah. Oleh karena itu, kami memindahkan pemeriksaan zero-sum ke loop dalam. Bahkan, kami hanya dapat melakukan pemeriksaan ini setelah meningkatkan lahan, karena ketika menurunkan, jumlahnya tidak pernah dihabiskan. Jika sudah selesai, kita dapat segera keluar dari metode CreateLand .

 // for (int guard = 0; landBudget > 0 && guard < 10000; guard++) { for (int guard = 0; guard < 10000; guard++) { bool sink = Random.value < sinkProbability; for (int i = 0; i < regions.Count; i++) { MapRegion region = regions[i]; int chunkSize = Random.Range(chunkSizeMin, chunkSizeMax - 1); if (sink) { landBudget = SinkTerrain(chunkSize, landBudget, region); } else { landBudget = RaiseTerrain(chunkSize, landBudget, region); if (landBudget == 0) { return; } } } } 

Dua daerah


Meskipun kami sekarang mendapat dukungan dari beberapa daerah, kami masih meminta hanya satu. Mari kita ubah CreateRegions sehingga ia membagi peta menjadi dua secara vertikal. Untuk melakukan ini, kami membagi dua nilai xMax dari wilayah yang ditambahkan. Kemudian kami menggunakan nilai yang sama untuk xMin dan lagi menggunakan nilai asli untuk xMax , menggunakannya sebagai wilayah kedua.

  MapRegion region; region.xMin = mapBorderX; region.xMax = grid.cellCountX / 2; region.zMin = mapBorderZ; region.zMax = grid.cellCountZ - mapBorderZ; regions.Add(region); region.xMin = grid.cellCountX / 2; region.xMax = grid.cellCountX - mapBorderX; regions.Add(region); 

Membuat kartu pada tahap ini tidak akan membuat perbedaan. Meskipun kami telah mengidentifikasi dua wilayah, mereka menempati wilayah yang sama dengan satu wilayah lama. Untuk memisahkan mereka, Anda harus meninggalkan ruang kosong di antara mereka. Ini dapat dilakukan dengan menambahkan slider ke perbatasan wilayah, menggunakan interval dan nilai default yang sama dengan perbatasan peta.

  [Range(0, 10)] public int regionBorder = 5; 


Slider perbatasan wilayah.

Karena tanah dapat dibentuk di kedua sisi ruang antar wilayah, kemungkinan membuat jembatan tanah di tepi peta akan meningkat. Untuk mencegah hal ini, kami menggunakan perbatasan wilayah untuk menentukan zona bebas tanah antara garis pemisah dan wilayah di mana plot dapat dimulai. Ini berarti bahwa jarak antara wilayah tetangga adalah dua lebih besar dari ukuran perbatasan wilayah tersebut.

Untuk menerapkan batas wilayah ini, kurangi dari xMax wilayah pertama dan tambahkan wilayah kedua ke xMin .

  MapRegion region; region.xMin = mapBorderX; region.xMax = grid.cellCountX / 2 - regionBorder; region.zMin = mapBorderZ; region.zMax = grid.cellCountZ - mapBorderZ; regions.Add(region); region.xMin = grid.cellCountX / 2 + regionBorder; region.xMax = grid.cellCountX - mapBorderX; regions.Add(region); 


Peta ini dibagi secara vertikal menjadi dua wilayah.

Dengan pengaturan default, dua wilayah yang terpisah akan dibuat, namun, seperti dalam kasus dengan satu wilayah dan perbatasan peta besar, kami tidak dijamin untuk menerima tepat dua massa tanah. Paling sering itu akan menjadi dua benua besar, mungkin dengan beberapa pulau. Tetapi terkadang dua atau lebih pulau besar dapat dibuat di suatu wilayah. Dan terkadang dua benua dapat dihubungkan oleh tanah genting.

Tentu saja, kita juga dapat membagi peta secara horizontal, mengubah pendekatan untuk mengukur X dan Z. Mari kita secara acak memilih salah satu dari dua orientasi yang mungkin.

  MapRegion region; if (Random.value < 0.5f) { region.xMin = mapBorderX; region.xMax = grid.cellCountX / 2 - regionBorder; region.zMin = mapBorderZ; region.zMax = grid.cellCountZ - mapBorderZ; regions.Add(region); region.xMin = grid.cellCountX / 2 + regionBorder; region.xMax = grid.cellCountX - mapBorderX; regions.Add(region); } else { region.xMin = mapBorderX; region.xMax = grid.cellCountX - mapBorderX; region.zMin = mapBorderZ; region.zMax = grid.cellCountZ / 2 - regionBorder; regions.Add(region); region.zMin = grid.cellCountZ / 2 + regionBorder; region.zMax = grid.cellCountZ - mapBorderZ; regions.Add(region); } 


Peta secara horizontal dibagi menjadi dua wilayah.

Karena kami menggunakan peta yang luas, daerah yang lebih luas dan lebih tipis akan dibuat dengan pemisahan horizontal. Akibatnya, daerah-daerah ini lebih cenderung membentuk beberapa massa tanah yang terbagi.

Empat daerah


Mari kita buat jumlah wilayah yang dapat disesuaikan, buat dukungan dari 1 hingga 4 wilayah.

  [Range(1, 4)] public int regionCount = 1; 


Slider untuk jumlah wilayah.

Kita dapat menggunakan switch untuk memilih eksekusi kode wilayah yang sesuai. Kami mulai dengan mengulangi kode satu wilayah, yang akan digunakan secara default, dan meninggalkan kode dua wilayah untuk kasus 2.

  MapRegion region; switch (regionCount) { default: region.xMin = mapBorderX; region.xMax = grid.cellCountX - mapBorderX; region.zMin = mapBorderZ; region.zMax = grid.cellCountZ - mapBorderZ; regions.Add(region); break; case 2: if (Random.value < 0.5f) { region.xMin = mapBorderX; region.xMax = grid.cellCountX / 2 - regionBorder; region.zMin = mapBorderZ; region.zMax = grid.cellCountZ - mapBorderZ; regions.Add(region); region.xMin = grid.cellCountX / 2 + regionBorder; region.xMax = grid.cellCountX - mapBorderX; regions.Add(region); } else { region.xMin = mapBorderX; region.xMax = grid.cellCountX - mapBorderX; region.zMin = mapBorderZ; region.zMax = grid.cellCountZ / 2 - regionBorder; regions.Add(region); region.zMin = grid.cellCountZ / 2 + regionBorder; region.zMax = grid.cellCountZ - mapBorderZ; regions.Add(region); } break; } 

Apa pernyataan sakelar?
Ini adalah alternatif untuk menulis urutan pernyataan if-else-if-else. switch diterapkan ke variabel, dan label digunakan untuk menunjukkan kode mana yang perlu dieksekusi. Ada juga label default , yang digunakan sebagai blok yang terakhir. Setiap opsi harus diakhiri dengan pernyataan break atau return .

Agar blok switch dapat dibaca, biasanya yang terbaik adalah membuat semua case pendek, idealnya dengan satu pernyataan atau metode panggilan. Saya tidak akan melakukan ini sebagai contoh kode wilayah, tetapi jika Anda ingin membuat daerah yang lebih menarik, saya sarankan Anda menggunakan metode terpisah. Sebagai contoh:

  switch (regionCount) { default: CreateOneRegion(); break; case 2: CreateTwoRegions(); break; case 3: CreateThreeRegions(); break; case 4: CreateFourRegions(); break; } 

Tiga daerah mirip dengan dua, hanya pertiganya digunakan alih-alih setengah. Dalam hal ini, pembagian horizontal akan membuat daerah terlalu sempit, jadi kami membuat dukungan hanya untuk pembagian vertikal. Perhatikan bahwa sebagai hasilnya, kami telah menggandakan area perbatasan wilayah, sehingga ruang untuk membuat bagian baru kurang dari pada kasus dua wilayah.

  switch (regionCount) { default: … break; case 2: … break; case 3: region.xMin = mapBorderX; region.xMax = grid.cellCountX / 3 - regionBorder; region.zMin = mapBorderZ; region.zMax = grid.cellCountZ - mapBorderZ; regions.Add(region); region.xMin = grid.cellCountX / 3 + regionBorder; region.xMax = grid.cellCountX * 2 / 3 - regionBorder; regions.Add(region); region.xMin = grid.cellCountX * 2 / 3 + regionBorder; region.xMax = grid.cellCountX - mapBorderX; regions.Add(region); break; } 


Tiga wilayah.

Empat wilayah dapat dibuat dengan menggabungkan pemisahan horizontal dan vertikal dan menambahkan satu wilayah ke setiap sudut peta.

  switch (regionCount) { … case 4: region.xMin = mapBorderX; region.xMax = grid.cellCountX / 2 - regionBorder; region.zMin = mapBorderZ; region.zMax = grid.cellCountZ / 2 - regionBorder; regions.Add(region); region.xMin = grid.cellCountX / 2 + regionBorder; region.xMax = grid.cellCountX - mapBorderX; regions.Add(region); region.zMin = grid.cellCountZ / 2 + regionBorder; region.zMax = grid.cellCountZ - mapBorderZ; regions.Add(region); region.xMin = mapBorderX; region.xMax = grid.cellCountX / 2 - regionBorder; regions.Add(region); break; } } 


Empat daerah.

Pendekatan yang digunakan di sini adalah cara paling sederhana untuk membagi peta. Ini menghasilkan kira-kira daerah yang sama berdasarkan massa tanah, dan variabilitasnya dikontrol oleh parameter lain dari pembuatan peta. Namun, akan selalu cukup jelas bahwa kartu itu dibagi dalam garis lurus. Semakin banyak kontrol yang kita butuhkan, semakin sedikit organik hasilnya akan terlihat. Karena itu, ini normal jika Anda membutuhkan wilayah yang kira-kira sama untuk permainan. Tetapi jika Anda membutuhkan tanah yang paling bervariasi dan tidak terbatas, Anda harus membuatnya dengan bantuan satu wilayah.

Selain itu, ada cara lain untuk membagi peta. Kami tidak bisa dibatasi hanya untuk garis lurus. Kami bahkan tidak harus menggunakan wilayah dengan ukuran yang sama, serta mencakup seluruh peta dengan mereka. Kita bisa meninggalkan lubang. Anda juga dapat mengizinkan persimpangan wilayah atau mengubah distribusi tanah antar wilayah. Anda bahkan dapat mengatur parameter generator Anda sendiri untuk setiap wilayah (meskipun ini lebih rumit), misalnya, untuk memiliki benua besar dan kepulauan di peta.

paket unity

Erosi


Sejauh ini, semua kartu yang kami hasilkan terlihat agak kasar dan rusak.Kelegaan nyata mungkin terlihat seperti ini, tetapi seiring waktu menjadi lebih halus dan halus, bagian-bagian yang tajam menjadi kusam karena erosi. Untuk memperbaiki peta, kita bisa menerapkan proses erosi ini. Kami akan melakukan ini setelah membuat tanah kasar, dengan metode terpisah.

  public void GenerateMap (int x, int z) { … CreateRegions(); CreateLand(); ErodeLand(); SetTerrainType(); … } … void ErodeLand () {} 

Persentase erosi


Semakin banyak waktu berlalu, semakin banyak erosi yang muncul. Karena itu, kami ingin erosi tidak permanen, tetapi dapat disesuaikan. Minimal, erosi adalah nol, yang sesuai dengan peta yang dibuat sebelumnya. Pada erosi maksimum bersifat komprehensif, yaitu, penerapan kekuatan erosi lebih lanjut tidak akan lagi mengubah medan. Artinya, parameter erosi harus berupa persentase dari 0 hingga 100, dan secara default kami akan mengambil 50.

  [Range(0, 100)] public int erosionPercentage = 50; 


Slider erosi.

Cari sel yang merusak erosi


Erosi membuat kelegaan lebih halus. Dalam kasus kami, satu-satunya bagian yang tajam adalah tebing. Karena itu, mereka akan menjadi target dari proses erosi. Jika ada tebing, erosi harus menguranginya sampai akhirnya berubah menjadi lereng. Kami tidak akan menghaluskan lereng, karena ini akan menyebabkan medan yang membosankan. Untuk melakukan ini, kita perlu menentukan sel mana yang berada di puncak tebing, dan menurunkan ketinggiannya. Ini akan menjadi sel yang rentan erosi.

Mari kita membuat metode yang menentukan apakah sebuah sel bisa rentan terhadap erosi. Dia menentukan ini dengan memeriksa tetangga sel sampai dia menemukan perbedaan ketinggian yang cukup besar. Karena tebing memerlukan perbedaan setidaknya satu atau dua tingkat ketinggian, sel dapat mengalami erosi jika satu atau lebih tetangganya setidaknya dua langkah di bawahnya. Jika tidak ada tetangga seperti itu, maka sel tidak bisa mengalami erosi.

  bool IsErodible (HexCell cell) { int erodibleElevation = cell.Elevation - 2; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = cell.GetNeighbor(d); if (neighbor && neighbor.Elevation <= erodibleElevation) { return true; } } return false; } 

Kita dapat menggunakan metode ini ErodeLanduntuk mengulang semua sel dan menulis semua sel yang rentan erosi ke daftar sementara.

  void ErodeLand () { List<HexCell> erodibleCells = ListPool<HexCell>.Get(); for (int i = 0; i < cellCount; i++) { HexCell cell = grid.GetCell(i); if (IsErodible(cell)) { erodibleCells.Add(cell); } } ListPool<HexCell>.Add(erodibleCells); } 

Setelah kita mengetahui jumlah total sel rawan erosi, kita dapat menggunakan persentase erosi untuk menentukan jumlah sel rawan erosi yang tersisa. Misalnya, jika persentasenya 50, maka kita harus erosi sel sampai setengah dari jumlah aslinya tetap. Jika persentasenya 100, maka kita tidak akan berhenti sampai kita menghancurkan semua sel yang rentan erosi.

  void ErodeLand () { List<HexCell> erodibleCells = ListPool<HexCell>.Get(); for (int i = 0; i < cellCount; i++) { … } int targetErodibleCount = (int)(erodibleCells.Count * (100 - erosionPercentage) * 0.01f); ListPool<HexCell>.Add(erodibleCells); } 

Bukankah seharusnya kita hanya mempertimbangkan sel rawan erosi tanah?
. , , .

Pengurangan sel


Mari kita mulai dengan pendekatan naif dan anggaplah bahwa pengurangan sederhana pada ketinggian erosi yang dihancurkan sel akan membuatnya tidak lagi rentan terhadap erosi. Jika ini benar, maka kita bisa mengambil sel acak dari daftar, mengurangi ketinggiannya, dan kemudian menghapusnya dari daftar. Kami akan mengulangi operasi ini sampai kami mencapai jumlah sel yang diinginkan yang rentan terhadap erosi.

  int targetErodibleCount = (int)(erodibleCells.Count * (100 - erosionPercentage) * 0.01f); while (erodibleCells.Count > targetErodibleCount) { int index = Random.Range(0, erodibleCells.Count); HexCell cell = erodibleCells[index]; cell.Elevation -= 1; erodibleCells.Remove(cell); } ListPool<HexCell>.Add(erodibleCells); 

Untuk mencegah pencarian yang diperlukan erodibleCells.Remove, kami akan menimpa sel terakhir saat ini dalam daftar, dan kemudian menghapus elemen terakhir. Kami masih tidak peduli dengan pesanan mereka.

 // erodibleCells.Remove(cell); erodibleCells[index] = erodibleCells[erodibleCells.Count - 1]; erodibleCells.RemoveAt(erodibleCells.Count - 1); 



Penurunan naif dari 0% dan 100% sel rawan erosi, peta benih 1957632474.

Pelacakan erosi


Pendekatan naif kami memungkinkan kami menerapkan erosi, tetapi tidak pada tingkat yang tepat. Ini terjadi karena sel setelah satu penurunan ketinggian masih dapat tetap rentan terhadap erosi. Karena itu, kami akan menghapus sel dari daftar hanya ketika tidak lagi mengalami erosi.

  if (!IsErodible(cell)) { erodibleCells[index] = erodibleCells[erodibleCells.Count - 1]; erodibleCells.RemoveAt(erodibleCells.Count - 1); } 


Erosi 100% sambil mempertahankan sel-sel yang rentan erosi dalam daftar.

Jadi kita mendapatkan erosi yang jauh lebih kuat, tetapi saat menggunakan 100% kita masih belum bisa menyingkirkan semua tebing. Alasannya adalah bahwa setelah mengurangi ketinggian sel, salah satu tetangganya menjadi rentan terhadap erosi. Karena itu, sebagai hasilnya, kita mungkin memiliki lebih banyak sel rawan erosi daripada aslinya.

Setelah menurunkan sel, kita perlu memeriksa semua tetangganya. Jika sekarang mereka rentan terhadap erosi, tetapi mereka belum ada dalam daftar, maka Anda perlu menambahkannya di sana.

  if (!IsErodible(cell)) { erodibleCells[index] = erodibleCells[erodibleCells.Count - 1]; erodibleCells.RemoveAt(erodibleCells.Count - 1); } for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = cell.GetNeighbor(d); if ( neighbor && IsErodible(neighbor) && !erodibleCells.Contains(neighbor) ) { erodibleCells.Add(neighbor); } } 


Semua sel yang tererosi dihilangkan.

Kami menghemat banyak tanah


Sekarang proses erosi dapat berlanjut sampai semua tebing hilang. Ini sangat mempengaruhi tanah. Sebagian besar daratan hilang dan kami mendapat jauh lebih sedikit dari persentase tanah yang dibutuhkan. Itu terjadi karena kami memindahkan tanah dari peta.

Erosi sejati tidak menghancurkan materi. Dia mengambilnya dari satu tempat dan menempatkannya di tempat lain. Kita bisa melakukan hal yang sama. Dengan penurunan satu sel, kita harus meningkatkan salah satu tetangganya. Bahkan, satu tingkat ketinggian ditransfer ke sel yang lebih rendah. Ini menyimpan jumlah total ketinggian peta, sementara hanya menghaluskannya.

Untuk merealisasikan hal ini, kita perlu memutuskan ke mana harus mentransfer produk erosi. Ini akan menjadi target erosi kami. Mari kita membuat metode untuk menentukan titik target sel yang akan terkikis. Karena sel ini berisi istirahat, akan logis untuk memilih sel yang terletak di bawah istirahat ini sebagai target. Tetapi sel yang rentan erosi dapat mengalami beberapa kali istirahat, jadi kami akan memeriksa semua tetangga dan menempatkan semua kandidat pada daftar sementara, dan kemudian kami akan memilih salah satu dari mereka secara acak.

  HexCell GetErosionTarget (HexCell cell) { List<HexCell> candidates = ListPool<HexCell>.Get(); int erodibleElevation = cell.Elevation - 2; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = cell.GetNeighbor(d); if (neighbor && neighbor.Elevation <= erodibleElevation) { candidates.Add(neighbor); } } HexCell target = candidates[Random.Range(0, candidates.Count)]; ListPool<HexCell>.Add(candidates); return target; } 

Dalam ErodeLandkami menentukan sel target segera setelah memilih sel erosi. Lalu kami mengurangi dan meningkatkan ketinggian sel segera satu demi satu. Dalam hal ini, sel target itu sendiri mungkin menjadi rentan terhadap erosi, tetapi situasi ini diselesaikan ketika kita memeriksa tetangga sel yang baru tererosi.

  HexCell cell = erodibleCells[index]; HexCell targetCell = GetErosionTarget(cell); cell.Elevation -= 1; targetCell.Elevation += 1; if (!IsErodible(cell)) { erodibleCells[index] = erodibleCells[erodibleCells.Count - 1]; erodibleCells.RemoveAt(erodibleCells.Count - 1); } 

Karena kami menaikkan sel target, sebagian tetangga sel ini mungkin tidak lagi mengalami erosi. Penting untuk mengelilingi mereka dan memeriksa apakah mereka rentan terhadap erosi. Jika tidak, tetapi mereka ada dalam daftar, maka Anda harus menghapusnya dari itu.

  for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = cell.GetNeighbor(d); … } for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = targetCell.GetNeighbor(d); if ( neighbor && !IsErodible(neighbor) && erodibleCells.Contains(neighbor) ) { erodibleCells.Remove(neighbor); } } 


100% erosi sambil mempertahankan massa tanah.

Erosi sekarang dapat memuluskan medan jauh lebih baik, menurunkan beberapa area dan meningkatkan lainnya. Akibatnya, massa tanah dapat meningkat dan menyempit. Ini dapat mengubah persentase tanah beberapa persen dalam satu arah atau lainnya, tetapi penyimpangan serius jarang terjadi. Artinya, semakin banyak erosi yang kita terapkan, semakin sedikit kontrol yang kita miliki atas persentase lahan yang dihasilkan.

Erosi yang dipercepat


Meskipun kami tidak perlu benar-benar peduli tentang efektivitas algoritma erosi, kami dapat melakukan perbaikan sederhana untuk itu. Pertama, perhatikan bahwa kami secara eksplisit memeriksa apakah sel yang kami erosi dapat terkikis. Jika tidak, maka pada dasarnya kami menghapusnya dari daftar. Oleh karena itu, Anda dapat melewati pemeriksaan sel ini saat melintasi tetangga sel target.

  for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = targetCell.GetNeighbor(d); if ( neighbor && neighbor != cell && !IsErodible(neighbor) && erodibleCells.Contains(neighbor) ) { erodibleCells.Remove(neighbor); } } 

Kedua, kami perlu memeriksa tetangga sel target hanya ketika ada jeda di antara mereka, tapi sekarang ini tidak perlu. Ini hanya terjadi ketika tetangga sekarang selangkah lebih tinggi dari sel target. Jika demikian, maka tetangga dijamin ada dalam daftar, jadi kita tidak perlu memeriksa ini, yaitu, kita dapat melewati pencarian yang tidak perlu.

  HexCell neighbor = targetCell.GetNeighbor(d); if ( neighbor && neighbor != cell && neighbor.Elevation == targetCell.Elevation + 1 && !IsErodible(neighbor) // && erodibleCells.Contains(neighbor) ) { erodibleCells.Remove(neighbor); } 

Ketiga, kita bisa menggunakan trik serupa ketika memeriksa tetangga sel yang rentan erosi. Jika sekarang ada tebing di antara mereka, maka tetangga rentan terhadap erosi. Untuk mengetahuinya, kita tidak perlu menelepon IsErodible.

  HexCell neighbor = cell.GetNeighbor(d); if ( neighbor && neighbor.Elevation == cell.Elevation + 2 && // IsErodible(neighbor) && !erodibleCells.Contains(neighbor) ) { erodibleCells.Add(neighbor); } 

Namun, kita masih perlu memeriksa apakah sel target rentan terhadap erosi, tetapi siklus yang ditunjukkan di atas tidak lagi melakukan ini. Oleh karena itu, kami melakukan ini secara eksplisit untuk sel target.

  if (!IsErodible(cell)) { erodibleCells[index] = erodibleCells[erodibleCells.Count - 1]; erodibleCells.RemoveAt(erodibleCells.Count - 1); } for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … } if (IsErodible(targetCell) && !erodibleCells.Contains(targetCell)) { erodibleCells.Add(targetCell); } for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … } 

Sekarang kita dapat menerapkan erosi dengan cukup cepat dan dengan persentase yang diinginkan relatif terhadap jumlah awal tebing yang dihasilkan. Perhatikan bahwa karena kami sedikit mengubah tempat sel target ditambahkan ke daftar rawan erosi, hasilnya telah sedikit berubah dari hasil sebelum optimisasi.





25%, 50%, 75% dan 100% erosi.

Juga perhatikan bahwa meskipun bentuk pantai berubah, topologi belum berubah secara mendasar. Massa tanah biasanya tetap terhubung atau terpisah. Hanya pulau-pulau kecil yang bisa tenggelam sepenuhnya. Rincian bantuan dihaluskan, tetapi bentuk umumnya tetap sama. Sendi yang sempit dapat menghilang, atau tumbuh sedikit. Sebuah celah kecil dapat mengisi atau sedikit berkembang. Karena itu, erosi tidak akan menyatukan wilayah yang terbelah.


Empat daerah yang tererosi sepenuhnya masih terpisah.

paket unity

Bagian 25: Siklus Air


  • Menampilkan data peta mentah.
  • Kami membentuk iklim sel.
  • Buat simulasi parsial dari siklus air.

Pada bagian ini kami akan menambahkan kelembaban di darat.

Tutorial ini dibuat di Unity 2017.3.0.


Kami menggunakan siklus air untuk menentukan bioma.

Awan


Hingga saat ini, algoritma pembuatan peta hanya mengubah ketinggian sel. Perbedaan terbesar antara sel-sel adalah apakah mereka berada di atas atau di bawah air. Meskipun kami dapat menentukan berbagai jenis medan, ini hanyalah visualisasi ketinggian yang sederhana. Akan lebih baik untuk menentukan jenis bantuan, mengingat iklim setempat.

Iklim bumi adalah sistem yang sangat kompleks. Untungnya, kita tidak perlu membuat simulasi iklim yang realistis. Kami akan membutuhkan sesuatu yang terlihat cukup alami. Aspek terpenting dari iklim adalah siklus air, karena flora dan fauna membutuhkan air cair untuk bertahan hidup. Suhu juga sangat penting, tetapi untuk saat ini, kami fokus pada air, pada dasarnya membiarkan suhu global konstan dan hanya mengubah kelembaban.

Siklus air menggambarkan pergerakan air di lingkungan. Sederhananya, kolam menguap, yang mengarah ke penciptaan awan hujan, yang lagi-lagi mengalir ke kolam. Ada banyak aspek dalam sistem, tetapi mensimulasikan langkah-langkah ini mungkin sudah cukup untuk membuat distribusi air yang tampak alami pada peta.

Visualisasi data


Sebelum kita masuk ke simulasi ini, akan berguna untuk langsung melihat data yang relevan. Untuk melakukan ini, kami akan mengubah shader Terrain . Kami menambahkan properti yang dapat dialihkan ke sana, yang dapat dialihkan ke mode visualisasi data, yang menampilkan data peta mentah alih-alih tekstur relief yang biasa. Ini dapat diimplementasikan menggunakan properti float dengan atribut switchable yang mendefinisikan kata kunci. Karena ini, itu akan muncul di inspektur materi sebagai bendera yang mengontrol definisi kata kunci. Nama properti itu sendiri tidak penting, kami hanya tertarik pada kata kunci. Kami menggunakan SHOW_MAP_DATA .

  Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Terrain Texture Array", 2DArray) = "white" {} _GridTex ("Grid Texture", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Specular ("Specular", Color) = (0.2, 0.2, 0.2) _BackgroundColor ("Background Color", Color) = (0,0,0) [Toggle(SHOW_MAP_DATA)] _ShowMapData ("Show Map Data", Float) = 0 } 


Beralih untuk menampilkan data peta.

Tambahkan fungsi shader untuk mengaktifkan dukungan kata kunci.

  #pragma multi_compile _ GRID_ON #pragma multi_compile _ HEX_MAP_EDIT_MODE #pragma shader_feature SHOW_MAP_DATA 

Kami akan membuatnya menampilkan float tunggal, seperti halnya dengan sisa data bantuan. Untuk menerapkan ini, kami akan menambahkan Inputbidang ke struktur mapDatasaat kata kunci ditentukan.

  struct Input { float4 color : COLOR; float3 worldPos; float3 terrain; float4 visibility; #if defined(SHOW_MAP_DATA) float mapData; #endif }; 

Dalam program vertex, kami menggunakan saluran Z dari sel-sel ini untuk mengisi mapData, seperti yang selalu diinterpolasi antar sel.

  void vert (inout appdata_full v, out Input data) { … #if defined(SHOW_MAP_DATA) data.mapData = cell0.z * v.color.x + cell1.z * v.color.y + cell2.z * v.color.z; #endif } 

Saat Anda perlu menampilkan data sel, gunakan langsung sebagai fragmen albedo, bukan warna yang biasa. Lipat gandakan dengan grid sehingga grid tetap dihidupkan saat merender data.

  void surf (Input IN, inout SurfaceOutputStandardSpecular o) { … o.Albedo = c.rgb * grid * _Color * explored; #if defined(SHOW_MAP_DATA) o.Albedo = IN.mapData * grid; #endif … } 

Untuk benar-benar mentransfer data ke shader. kita perlu menambahkan HexCellShaderDatametode yang menulis sesuatu ke saluran data tekstur biru. Data adalah nilai float tunggal terbatas pada 0-1.

  public void SetMapData (HexCell cell, float data) { cellTextureData[cell.Index].b = data < 0f ? (byte)0 : (data < 1f ? (byte)(data * 255f) : (byte)255); enabled = true; } 

Namun, keputusan ini mempengaruhi sistem penelitian. Nilai data saluran biru 255 digunakan untuk menunjukkan bahwa visibilitas sel sedang dalam transisi. Agar sistem ini dapat terus bekerja, kita perlu menggunakan nilai byte 254 sebagai maksimum. Perhatikan bahwa gerakan detasemen akan menghapus semua data kartu, tetapi ini cocok untuk kita, karena mereka digunakan untuk pembuatan kartu debugging.

  cellTextureData[cell.Index].b = data < 0f ? (byte)0 : (data < 1f ? (byte)(data * 254f) : (byte)254); 

Tambahkan metode dengan nama yang sama dan dalam HexCell. Ini akan mentransfer permintaan ke data shader-nya.

  public void SetMapData (float data) { ShaderData.SetMapData(this, data); } 

Untuk memeriksa operasi kode, ubahlah HexMapGenerator.SetTerrainTypesehingga menetapkan data setiap sel peta. Mari kita memvisualisasikan ketinggian yang dikonversi dari bilangan bulat menjadi mengambang dalam interval 0-1. Ini dilakukan dengan mengurangi ketinggian minimum dari tinggi sel, diikuti dengan membagi dengan ketinggian maksimum dikurangi minimum. Mari kita buat floating point divisi.

  void SetTerrainType () { for (int i = 0; i < cellCount; i++) { … cell.SetMapData( (cell.Elevation - elevationMinimum) / (float)(elevationMaximum - elevationMinimum) ); } } 

Sekarang kita dapat beralih antara dataran normal dan visualisasi data menggunakan kotak centang Tampilkan Peta Data aset material Terrain .



Peta 1208905299, dataran normal dan visualisasi ketinggian.

Penciptaan iklim


Untuk mensimulasikan iklim, kita perlu melacak data iklim. Karena peta terdiri dari sel-sel yang terpisah, masing-masing memiliki iklim lokalnya sendiri. Buat struktur ClimateDatauntuk menyimpan semua data yang relevan. Tentu saja, Anda dapat menambahkan data ke sel itu sendiri, tetapi kami hanya akan menggunakannya saat membuat peta. Karenanya, kami akan menyimpannya secara terpisah. Ini berarti bahwa kita dapat mendefinisikan struktur ini secara internal HexMapGenerator, seperti MapRegion. Kami akan mulai dengan hanya melacak awan, yang dapat diimplementasikan menggunakan bidang float tunggal.

  struct ClimateData { public float clouds; } 

Tambahkan daftar untuk melacak data iklim untuk semua sel.

  List<ClimateData> climate = new List<ClimateData>(); 

Sekarang kita membutuhkan metode untuk membuat peta iklim. Itu harus dimulai dengan membersihkan daftar zona iklim, dan kemudian menambahkan satu elemen untuk setiap sel. Data iklim awal hanyalah nol, ini dapat dicapai dengan menggunakan konstruktor standar ClimateData.
  void CreateClimate () { climate.Clear(); ClimateData initialData = new ClimateData(); for (int i = 0; i < cellCount; i++) { climate.Add(initialData); } } 

Iklim harus dibuat setelah terpapar erosi tanah sebelum menetapkan jenis bantuan. Pada kenyataannya, erosi terutama disebabkan oleh pergerakan udara dan air, yang merupakan bagian dari iklim, tetapi kami tidak akan mensimulasikan ini.

  public void GenerateMap (int x, int z) { … CreateRegions(); CreateLand(); ErodeLand(); CreateClimate(); SetTerrainType(); … } 

Ubah SetTerrainTypesehingga kita bisa melihat data cloud, bukan tinggi sel. Awalnya, itu akan terlihat seperti kartu hitam.

  void SetTerrainType () { for (int i = 0; i < cellCount; i++) { … cell.SetMapData(climate[i].clouds); } } 

Mengubah iklim


Langkah pertama dalam simulasi iklim adalah penguapan. Berapa banyak air yang harus diuapkan? Mari kita kontrol nilai ini menggunakan slider. Nilai 0 berarti tidak ada evaporasi, 1 - evaporasi maksimum. Secara default, kami menggunakan 0,5.

  [Range(0f, 1f)] public float evaporation = 0.5f; 


Slider penguapan.

Mari kita buat metode lain khusus untuk membentuk iklim satu sel. Kami memberikannya indeks sel sebagai parameter dan menggunakannya untuk mendapatkan sel yang sesuai dan data iklimnya. Jika sel berada di bawah air, maka kita berhadapan dengan reservoir yang harus menguap. Kami segera mengubah uap menjadi awan (mengabaikan titik embun dan kondensasi), jadi kami akan langsung menambahkan penguapan ke nilai awan sel. Setelah selesai dengan ini, salin kembali data iklim ke daftar.

  void EvolveClimate (int cellIndex) { HexCell cell = grid.GetCell(cellIndex); ClimateData cellClimate = climate[cellIndex]; if (cell.IsUnderwater) { cellClimate.clouds += evaporation; } climate[cellIndex] = cellClimate; } 

Panggil metode ini untuk setiap sel di CreateClimate.

  void CreateClimate () { … for (int i = 0; i < cellCount; i++) { EvolveClimate(i); } } 

Tetapi ini tidak cukup. Untuk membuat simulasi yang kompleks, kita perlu membentuk iklim sel beberapa kali. Semakin sering kita melakukan ini, semakin baik hasilnya. Mari kita pilih nilai konstan. Saya menggunakan 40 siklus.

  for (int cycle = 0; cycle < 40; cycle++) { for (int i = 0; i < cellCount; i++) { EvolveClimate(i); } } 

Karena sementara kita hanya meningkatkan nilai awan di atas sel yang dibanjiri air, akibatnya kita mendapatkan tanah hitam dan reservoir putih.


Penguapan di atas air.

Hamburan awan


Awan tidak terus-menerus di satu tempat, terutama ketika semakin banyak air menguap. Perbedaan tekanan membuat udara bergerak, yang memanifestasikan dirinya dalam bentuk angin, yang juga membuat awan bergerak.

Jika tidak ada arah angin yang dominan, maka rata-rata awan sel secara merata akan menyebar ke segala arah, muncul di sel tetangga. Saat membuat awan baru di siklus berikutnya, mari kita distribusikan semua awan di sel ke tetangganya. Artinya, setiap tetangga menerima seperenam dari awan sel, setelah itu ada penurunan lokal ke nol.

  if (cell.IsUnderwater) { cellClimate.clouds += evaporation; } float cloudDispersal = cellClimate.clouds * (1f / 6f); cellClimate.clouds = 0f; climate[cellIndex] = cellClimate; 

Untuk benar-benar menambahkan cloud ke tetangga Anda, Anda harus mengelilingi mereka dalam satu lingkaran, mendapatkan data iklim mereka, meningkatkan nilai cloud dan menyalinnya kembali ke daftar.

  float cloudDispersal = cellClimate.clouds * (1f / 6f); for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = cell.GetNeighbor(d); if (!neighbor) { continue; } ClimateData neighborClimate = climate[neighbor.Index]; neighborClimate.clouds += cloudDispersal; climate[neighbor.Index] = neighborClimate; } cellClimate.clouds = 0f; 


Awan yang berhamburan.

Ini menciptakan peta yang hampir putih, karena pada setiap siklus, sel-sel bawah laut menambah lebih banyak awan ke iklim global. Setelah siklus pertama, sel-sel tanah di sebelah air juga akan memiliki awan yang perlu disebarkan. Proses ini berlanjut hingga sebagian besar peta tertutup awan. Dalam kasus peta 1208905299 dengan parameter default, hanya bagian dalam dari massa tanah yang besar di timur laut yang benar-benar terbuka.

Perhatikan bahwa kolam dapat menghasilkan awan dalam jumlah tak terbatas. Permukaan air bukan bagian dari simulasi iklim. Pada kenyataannya, waduk dipertahankan hanya karena air mengalir kembali ke mereka dengan laju penguapan. Artinya, kami hanya mensimulasikan siklus air parsial. Ini normal, tetapi kita harus memahami semakin lama simulasi berlangsung, semakin banyak air ditambahkan ke iklim. Sejauh ini, kehilangan air hanya terjadi di tepi peta, di mana awan yang tersebar hilang karena kurangnya tetangga.

Anda dapat melihat hilangnya air di bagian atas peta, terutama di sel-sel di kanan atas. Di sel terakhir tidak ada awan sama sekali, karena itu tetap yang terakhir di mana iklim terbentuk. Dia belum menerima awan dari tetangga.

Bukankah seharusnya iklim semua sel terbentuk secara paralel?
, . - , . 40 . - , .

Curah hujan


Air tidak selamanya dingin. Pada titik tertentu, dia harus jatuh ke tanah lagi. Ini biasanya terjadi dalam bentuk hujan, tetapi kadang-kadang bisa berupa salju, hujan es, atau salju basah. Semua ini umumnya disebut presipitasi. Besarnya dan tingkat hilangnya awan sangat bervariasi, tetapi kami hanya menggunakan tingkat curah hujan global khusus. Nilai 0 berarti tidak ada presipitasi, nilai 1 berarti bahwa semua awan menghilang secara instan. Nilai standarnya adalah 0,25. Ini berarti bahwa dalam setiap siklus, seperempat awan akan menghilang.

  [Range(0f, 1f)] public float precipitationFactor = 0.25f; 


Slider koefisien presipitasi.

Kami akan mensimulasikan curah hujan setelah penguapan dan sebelum hamburan awan. Ini berarti bahwa sebagian air menguap dari reservoir segera mengendap, sehingga jumlah awan yang menyebar berkurang. Di darat, curah hujan akan menyebabkan hilangnya awan.

  if (cell.IsUnderwater) { cellClimate.clouds += evaporation; } float precipitation = cellClimate.clouds * precipitationFactor; cellClimate.clouds -= precipitation; float cloudDispersal = cellClimate.clouds * (1f / 6f); 


Awan menghilang.

Sekarang, ketika kita menghancurkan 25% dari awan di setiap siklus, tanah itu lagi hampir hitam. Awan berhasil bergerak ke pedalaman hanya beberapa langkah, setelah itu mereka menjadi tidak terlihat.

paket unity

Kelembaban


Meskipun curah hujan menghancurkan awan, mereka seharusnya tidak menghilangkan air dari iklim. Setelah jatuh ke tanah, air terselamatkan, hanya dalam keadaan berbeda. Ini bisa ada dalam banyak bentuk, yang umumnya kita anggap kelembaban.

Pelacakan kelembaban


Kami akan meningkatkan model iklim dengan melacak dua kondisi air: awan dan kelembaban. Untuk mengimplementasikan ini, tambahkan ClimateDatabidang moisture.

  struct ClimateData { public float clouds, moisture; } 

Dalam bentuknya yang paling umum, penguapan adalah proses mengubah kelembaban menjadi awan, setidaknya dalam model iklim sederhana kami. Ini berarti bahwa penguapan seharusnya tidak menjadi nilai konstan, tetapi faktor lain. Oleh karena itu, kami melakukan penggantian nama refactoring evaporationke evaporationFactor.

  [Range(0f, 1f)] public float evaporationFactor = 0.5f; 

Ketika sel berada di bawah air, kami hanya mengumumkan bahwa tingkat kelembaban adalah 1. Ini berarti bahwa penguapan sama dengan koefisien penguapan. Tapi sekarang kita juga bisa mendapatkan penguapan dari sel sushi. Dalam hal ini, kita perlu menghitung penguapan, kurangi dari kelembaban dan tambahkan hasilnya ke awan. Setelah itu, presipitasi ditambahkan ke kelembaban.

  if (cell.IsUnderwater) { cellClimate.moisture = 1f; cellClimate.clouds += evaporationFactor; } else { float evaporation = cellClimate.moisture * evaporationFactor; cellClimate.moisture -= evaporation; cellClimate.clouds += evaporation; } float precipitation = cellClimate.clouds * precipitationFactor; cellClimate.clouds -= precipitation; cellClimate.moisture += precipitation; 

Karena awan sekarang didukung oleh penguapan dari atas tanah, kita dapat memindahkannya lebih jauh ke daratan. Sekarang sebagian besar tanah telah menjadi abu-abu.


Awan dengan penguapan kelembaban.

Mari kita ubah SetTerrainTypesehingga menampilkan kelembapan dan bukan awan, karena kita akan menggunakannya untuk menentukan jenis bantuan.

  cell.SetMapData(climate[i].moisture); 


Tampilan kelembaban.

Pada titik ini, kelembabannya terlihat sangat mirip dengan awan (kecuali bahwa semua sel bawah air berwarna putih), tetapi itu akan segera berubah.

Limpasan curah hujan


Penguapan bukan satu-satunya cara agar kelembaban dapat meninggalkan sel. Siklus air memberi tahu kita bahwa sebagian besar kelembaban yang ditambahkan ke tanah entah bagaimana berakhir di air. Proses yang paling nyata adalah aliran air di atas tanah di bawah pengaruh gravitasi. Kami tidak akan mensimulasikan sungai nyata, tetapi menggunakan koefisien limpasan curah hujan khusus. Ini akan menunjukkan persentase pengeringan air ke daerah yang lebih rendah. Mari kita default stok akan sama dengan 25%.

  [Range(0f, 1f)] public float runoffFactor = 0.25f; 


Lepaskan slider.

Kami tidak akan menghasilkan sungai?
.

Aliran air bertindak seperti hamburan awan, tetapi dengan tiga perbedaan. Pertama, tidak semua uap air dikeluarkan dari sel. Kedua, ia membawa kelembaban, bukan awan. Ketiga, turun, yaitu hanya untuk tetangga dengan ketinggian lebih rendah. Koefisien limpasan menggambarkan jumlah air yang akan mengalir keluar dari sel jika semua tetangga lebih rendah, tetapi seringkali mereka lebih sedikit. Ini berarti bahwa kita akan mengurangi kelembaban sel hanya ketika kita menemukan tetangga di bawah ini.

  float cloudDispersal = cellClimate.clouds * (1f / 6f); float runoff = cellClimate.moisture * runoffFactor * (1f / 6f); for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = cell.GetNeighbor(d); if (!neighbor) { continue; } ClimateData neighborClimate = climate[neighbor.Index]; neighborClimate.clouds += cloudDispersal; int elevationDelta = neighbor.Elevation - cell.Elevation; if (elevationDelta < 0) { cellClimate.moisture -= runoff; neighborClimate.moisture += runoff; } climate[neighbor.Index] = neighborClimate; } 


Pengurasan air ke ketinggian yang lebih rendah.

Sebagai hasilnya, kami memiliki distribusi kelembaban yang lebih beragam, karena sel-sel tinggi mentransmisikan kelembabannya ke yang lebih rendah. Kami juga melihat lebih sedikit kelembaban di sel-sel pesisir, karena mereka mengalirkan uap air ke sel-sel bawah air. Untuk melemahkan efek ini, kita juga perlu menggunakan level air ketika menentukan apakah sel lebih rendah, yaitu, mengambil ketinggian yang terlihat.

  int elevationDelta = neighbor.ViewElevation - cell.ViewElevation; 


Gunakan ketinggian yang terlihat.

Rembesan


Air tidak hanya mengalir ke bawah, itu menyebar, merembes melalui topografi tingkat, dan diserap oleh tanah yang berdekatan dengan badan air. Efek ini mungkin memiliki sedikit efek, tetapi berguna untuk memperlancar distribusi kelembaban, jadi mari kita tambahkan ke simulasi. Mari kita buat dia koefisien kustom sendiri, secara default sama dengan 0,125.

  [Range(0f, 1f)] public float seepageFactor = 0.125f; 


Slider kebocoran.

Rembesan mirip dengan selokan, kecuali digunakan ketika tetangga memiliki ketinggian yang sama dengan sel itu sendiri.

  float runoff = cellClimate.moisture * runoffFactor * (1f / 6f); float seepage = cellClimate.moisture * seepageFactor * (1f / 6f); for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … int elevationDelta = neighbor.ViewElevation - cell.ViewElevation; if (elevationDelta < 0) { cellClimate.moisture -= runoff; neighborClimate.moisture += runoff; } else if (elevationDelta == 0) { cellClimate.moisture -= seepage; neighborClimate.moisture += seepage; } climate[neighbor.Index] = neighborClimate; } 


Ditambahkan sedikit kebocoran.

paket unity

Bayangan hujan


Meskipun kami telah membuat simulasi yang layak dari siklus air, itu tidak terlihat sangat menarik, karena tidak memiliki bayangan hujan, yang paling jelas menunjukkan perbedaan iklim. Bayang-bayang hujan adalah daerah di mana ada curah hujan yang kurang signifikan dibandingkan dengan daerah tetangga. Daerah-daerah seperti itu ada karena gunung menghalangi awan untuk menjangkau mereka. Penciptaan mereka membutuhkan gunung yang tinggi dan arah angin yang dominan.

Angin


Mari kita mulai dengan menambahkan arah angin dominan ke simulasi. Meskipun arah angin dominan sangat bervariasi di permukaan Bumi, kami akan mengelola dengan arah angin global yang dapat disesuaikan. Mari kita gunakan barat laut secara default. Selain itu, mari kita buat kekuatan angin dapat disesuaikan dari 1 hingga 10 dengan nilai default 4.

  public HexDirection windDirection = HexDirection.NW; [Range(1f, 10f)] public float windStrength = 4f; 


Arah dan kekuatan angin.

Kekuatan angin dominan diekspresikan relatif terhadap dispersi total awan. Jika kekuatan angin adalah 1, maka hamburan adalah sama di semua arah. Ketika 2, hamburan dua lebih tinggi ke arah angin daripada di arah lain, dan seterusnya. Kita dapat melakukan ini dengan mengubah pembagi dalam rumus cloud scatter. Alih-alih enam, itu akan sama dengan lima ditambah tenaga angin.

  float cloudDispersal = cellClimate.clouds * (1f / (5f + windStrength)); 

Selain itu, arah angin menentukan arah angin bertiup. Oleh karena itu, kita perlu menggunakan arah yang berlawanan sebagai arah utama hamburan.

  HexDirection mainDispersalDirection = windDirection.Opposite(); float cloudDispersal = cellClimate.clouds * (1f / (5f + windStrength)); 

Sekarang kita dapat memeriksa apakah tetangga berada di arah utama hamburan. Jika demikian, maka kita harus melipatgandakan hamburan awan dengan kekuatan angin.

  ClimateData neighborClimate = climate[neighbor.Index]; if (d == mainDispersalDirection) { neighborClimate.clouds += cloudDispersal * windStrength; } else { neighborClimate.clouds += cloudDispersal; } 


Angin barat laut, kekuatan 4.

Angin dominan menambah arah ke distribusi kelembaban di darat. Semakin kuat angin, semakin kuat efeknya.

Tinggi absolut


Bahan kedua dalam mendapatkan bayangan hujan adalah pegunungan. Kami tidak memiliki klasifikasi yang ketat tentang apa itu gunung, seperti halnya alam tidak memilikinya. Hanya ketinggian absolut yang penting. Bahkan, ketika udara bergerak di atas gunung, ia dipaksa untuk naik, didinginkan dan mungkin mengandung lebih sedikit air, yang mengarah ke presipitasi sebelum udara melewati gunung. Akibatnya, di sisi lain kita mendapat udara kering, yaitu bayangan hujan.

Yang terpenting, semakin tinggi udara naik, semakin sedikit air yang bisa dikandungnya. Dalam simulasi kami, kami dapat membayangkan ini sebagai pembatasan paksa dari nilai cloud maksimum untuk setiap sel. Semakin tinggi tinggi sel yang terlihat, semakin rendah maksimum ini seharusnya. Cara termudah untuk melakukan ini adalah dengan mengatur maksimum ke 1 dikurangi ketinggian yang terlihat, dibagi dengan ketinggian maksimum. Tetapi pada kenyataannya, mari kita bagi dengan maksimum minus 1. Ini akan memungkinkan sebagian kecil dari awan untuk tetap melewati bahkan sel-sel tertinggi. Kami menetapkan maksimum ini setelah menghitung curah hujan dan sebelum hamburan.

  float precipitation = cellClimate.clouds * precipitationFactor; cellClimate.clouds -= precipitation; cellClimate.moisture += precipitation; float cloudMaximum = 1f - cell.ViewElevation / (elevationMaximum + 1f); HexDirection mainDispersalDirection = windDirection.Opposite(); 

Jika akibatnya kita mendapatkan lebih banyak awan daripada yang dapat diterima, maka kita cukup mengubah awan berlebih menjadi kelembaban. Sebenarnya, ini adalah bagaimana kami menambahkan curah hujan tambahan, seperti yang terjadi di pegunungan nyata.

  float cloudMaximum = 1f - cell.ViewElevation / (elevationMaximum + 1f); if (cellClimate.clouds > cloudMaximum) { cellClimate.moisture += cellClimate.clouds - cloudMaximum; cellClimate.clouds = cloudMaximum; } 


Bayangan hujan yang disebabkan oleh ketinggian.

paket unity

Kami menyelesaikan simulasi


Pada tahap ini, kami sudah memiliki simulasi parsial yang sangat tinggi dari siklus air. Mari kita susun sedikit, dan kemudian menerapkannya untuk menentukan jenis relief sel.

Komputasi paralel


Seperti disebutkan sebelumnya di bawah spoiler, urutan pembentukan sel mempengaruhi hasil simulasi. Idealnya, ini seharusnya tidak dan pada dasarnya kita membentuk semua sel secara paralel. Ini dapat dilakukan dengan menerapkan semua perubahan pada tahap pembentukan saat ini ke daftar iklim kedua nextClimate.

  List<ClimateData> climate = new List<ClimateData>(); List<ClimateData> nextClimate = new List<ClimateData>(); 

Kosongkan dan inisialisasi daftar ini, seperti yang lainnya. Kemudian kami akan bertukar daftar pada setiap siklus. Dalam hal ini, simulasi akan menggunakan dua daftar secara bergantian dan menerapkan data iklim saat ini dan selanjutnya.

  void CreateClimate () { climate.Clear(); nextClimate.Clear(); ClimateData initialData = new ClimateData(); for (int i = 0; i < cellCount; i++) { climate.Add(initialData); nextClimate.Add(initialData); } for (int cycle = 0; cycle < 40; cycle++) { for (int i = 0; i < cellCount; i++) { EvolveClimate(i); } List<ClimateData> swap = climate; climate = nextClimate; nextClimate = swap; } } 

Ketika sebuah sel memengaruhi iklim tetangganya, kita harus mengubah data iklim berikut, bukan yang saat ini.

  for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = cell.GetNeighbor(d); if (!neighbor) { continue; } ClimateData neighborClimate = nextClimate[neighbor.Index]; … nextClimate[neighbor.Index] = neighborClimate; } 

Dan alih-alih menyalin data iklim berikut kembali ke daftar iklim saat ini, kami mendapatkan data iklim berikut, menambahkan kelembapan saat ini ke mereka dan menyalinnya ke daftar berikutnya. Setelah itu, kami mereset data dalam daftar saat ini sehingga diperbarui untuk siklus berikutnya.

 // cellClimate.clouds = 0f; ClimateData nextCellClimate = nextClimate[cellIndex]; nextCellClimate.moisture += cellClimate.moisture; nextClimate[cellIndex] = nextCellClimate; climate[cellIndex] = new ClimateData(); 

Sementara kita melakukan ini, mari kita juga mengatur tingkat kelembaban ke maksimum 1 sehingga sel-sel tanah tidak bisa lebih basah daripada di bawah air.

  nextCellClimate.moisture += cellClimate.moisture; if (nextCellClimate.moisture > 1f) { nextCellClimate.moisture = 1f; } nextClimate[cellIndex] = nextCellClimate; 


Komputasi paralel.

Sumber kelembaban


Ada kemungkinan bahwa simulasi akan menghasilkan terlalu banyak lahan kering, terutama dengan persentase lahan yang tinggi. Untuk meningkatkan gambar, kita dapat menambahkan tingkat kelembaban awal khusus dengan nilai default 0,1.

  [Range(0f, 1f)] public float startingMoisture = 0.1f; 


Di atas adalah slider dari kelembaban aslinya.

Kami menggunakan nilai ini untuk kelembaban dari daftar iklim awal, tetapi tidak untuk yang berikut ini.

  ClimateData initialData = new ClimateData(); initialData.moisture = startingMoisture; ClimateData clearData = new ClimateData(); for (int i = 0; i < cellCount; i++) { climate.Add(initialData); nextClimate.Add(clearData); } 


Dengan kelembaban asli.

Mendefinisikan bioma


Kami menyimpulkan dengan menggunakan kelembaban alih-alih ketinggian untuk menentukan jenis bantuan sel. Mari kita gunakan salju untuk tanah yang benar-benar kering, untuk daerah kering kita menggunakan salju, lalu ada batu, rumput untuk cukup lembab, dan tanah untuk sel-sel jenuh air dan bawah air. Cara termudah adalah dengan menggunakan lima interval dengan kenaikan 0,2.

  void SetTerrainType () { for (int i = 0; i < cellCount; i++) { HexCell cell = grid.GetCell(i); float moisture = climate[i].moisture; if (!cell.IsUnderwater) { if (moisture < 0.2f) { cell.TerrainTypeIndex = 4; } else if (moisture < 0.4f) { cell.TerrainTypeIndex = 0; } else if (moisture < 0.6f) { cell.TerrainTypeIndex = 3; } else if (moisture < 0.8f) { cell.TerrainTypeIndex = 1; } else { cell.TerrainTypeIndex = 2; } } else { cell.TerrainTypeIndex = 2; } cell.SetMapData(moisture); } } 


Bioma.

Ketika menggunakan distribusi yang seragam, hasilnya tidak terlalu bagus, dan itu terlihat tidak wajar. Lebih baik menggunakan ambang lainnya, misalnya 0,05, 0,12, 0,28 dan 0,85.

  if (moisture < 0.05f) { cell.TerrainTypeIndex = 4; } else if (moisture < 0.12f) { cell.TerrainTypeIndex = 0; } else if (moisture < 0.28f) { cell.TerrainTypeIndex = 3; } else if (moisture < 0.85f) { cell.TerrainTypeIndex = 1; } 


Bioma yang dimodifikasi.

paket unity

Bagian 26: bioma dan sungai


  • Kami menciptakan sungai yang berasal dari sel tinggi dengan kelembaban.
  • Kami membuat model suhu sederhana.
  • Kami menggunakan matriks bioma untuk sel, dan kemudian mengubahnya.

Pada bagian ini, kami akan melengkapi siklus air dengan sungai dan suhu, serta memberikan bioma yang lebih menarik ke sel.

Tutorial dibuat menggunakan Unity 2017.3.0p3.


Panas dan air meramaikan peta.

Generasi sungai


Sungai adalah konsekuensi dari siklus air. Bahkan, mereka terbentuk oleh limpasan yang robek dengan bantuan erosi saluran. Ini menyiratkan bahwa Anda dapat menambahkan sungai berdasarkan nilai drainase sel. Namun, ini tidak menjamin bahwa kita akan mendapatkan sesuatu yang menyerupai sungai sungguhan. Ketika kita memulai sungai, sungai harus mengalir sejauh mungkin, berpotensi melalui banyak sel. Ini tidak konsisten dengan simulasi siklus air kami, yang memproses sel secara paralel. Selain itu, kontrol atas jumlah sungai pada peta biasanya diperlukan.

Karena sungai sangat berbeda, kami akan membuatnya secara terpisah. Kami menggunakan hasil simulasi siklus air untuk menentukan lokasi sungai, tetapi sungai, pada gilirannya, tidak akan mempengaruhi simulasi.

Mengapa aliran sungai terkadang salah?
TriangulateWaterShore , . , . , , . , . , , . («»).

  void TriangulateWaterShore ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { … if (cell.HasRiverThroughEdge(direction)) { TriangulateEstuary( e1, e2, cell.HasIncomingRiver && cell.IncomingRiver == direction, indices ); } … } 

Sel-sel kelembaban tinggi


Pada peta kami, sel mungkin atau mungkin tidak memiliki sungai. Selain itu, mereka dapat bercabang atau terhubung. Pada kenyataannya, sungai jauh lebih fleksibel, tetapi kita harus bertahan dengan perkiraan ini, yang hanya menciptakan sungai besar. Yang terpenting, kita perlu menentukan lokasi awal sebuah sungai besar, yang dipilih secara acak.

Karena sungai membutuhkan air, sumber sungai harus dalam sel dengan kelembaban tinggi. Tetapi ini tidak cukup. Sungai mengalir menuruni lereng, jadi idealnya sumbernya harus memiliki ketinggian yang besar. Semakin tinggi sel di atas permukaan air, semakin baik kandidatnya untuk peran sumber sungai. Kita dapat memvisualisasikan ini sebagai data peta dengan membagi tinggi sel dengan tinggi maksimum. Agar hasilnya diperoleh relatif terhadap ketinggian air, kami akan mengurangkannya dari kedua ketinggian sebelum membaginya.

  void SetTerrainType () { for (int i = 0; i < cellCount; i++) { … float data = (float)(cell.Elevation - waterLevel) / (elevationMaximum - waterLevel); cell.SetMapData(data); } } 



Kelembaban dan ketinggian. Nomor peta besar 1208905299 dengan pengaturan default.

Kandidat terbaik adalah sel-sel yang memiliki kelembaban tinggi dan tinggi. Kami dapat menggabungkan kriteria ini dengan mengalikannya. Hasilnya akan menjadi nilai kebugaran atau berat untuk sumber-sumber sungai.

  float data = moisture * (cell.Elevation - waterLevel) / (elevationMaximum - waterLevel); cell.SetMapData(data); 


Bobot untuk sumber sungai.

Idealnya, kita akan menggunakan bobot ini untuk menolak pemilihan acak sel sumber. Meskipun kita dapat membuat daftar dengan bobot yang benar dan memilihnya, ini adalah pendekatan non-sepele dan memperlambat proses pembuatan. Klasifikasi signifikansi yang lebih sederhana dibagi menjadi empat tingkatan sudah cukup bagi kita. Kandidat pertama adalah bobot dengan nilai di atas 0,75. Kandidat yang baik memiliki bobot mulai 0,5. Kandidat yang memenuhi syarat lebih besar dari 0,25. Semua sel lainnya dibuang. Mari kita tunjukkan tampilannya secara grafis.

  float data = moisture * (cell.Elevation - waterLevel) / (elevationMaximum - waterLevel); if (data > 0.75f) { cell.SetMapData(1f); } else if (data > 0.5f) { cell.SetMapData(0.5f); } else if (data > 0.25f) { cell.SetMapData(0.25f); } // cell.SetMapData(data); 


Kategori bobot sumber sungai.

Dengan skema klasifikasi ini, kita cenderung mendapatkan sungai dengan sumber di wilayah peta tertinggi dan terbasah. Namun demikian, kemungkinan menciptakan sungai di daerah yang relatif kering atau rendah tetap ada, yang meningkatkan variabilitas.

Tambahkan metode CreateRiversyang mengisi daftar sel berdasarkan kriteria ini. Sel yang memenuhi syarat ditambahkan ke daftar ini satu kali, sel yang baik dua kali, dan kandidat utama empat kali. Sel bawah air selalu dibuang, jadi Anda tidak bisa memeriksanya.

  void CreateRivers () { List<HexCell> riverOrigins = ListPool<HexCell>.Get(); for (int i = 0; i < cellCount; i++) { HexCell cell = grid.GetCell(i); if (cell.IsUnderwater) { continue; } ClimateData data = climate[i]; float weight = data.moisture * (cell.Elevation - waterLevel) / (elevationMaximum - waterLevel); if (weight > 0.75f) { riverOrigins.Add(cell); riverOrigins.Add(cell); } if (weight > 0.5f) { riverOrigins.Add(cell); } if (weight > 0.25f) { riverOrigins.Add(cell); } } ListPool<HexCell>.Add(riverOrigins); } 

Metode ini harus dipanggil setelah CreateClimatesehingga data kelembaban tersedia untuk kita.

  public void GenerateMap (int x, int z) { … CreateRegions(); CreateLand(); ErodeLand(); CreateClimate(); CreateRivers(); SetTerrainType(); … } 

Setelah menyelesaikan klasifikasi, Anda dapat menyingkirkan visualisasi datanya di peta.

  void SetTerrainType () { for (int i = 0; i < cellCount; i++) { … // float data = // moisture * (cell.Elevation - waterLevel) / // (elevationMaximum - waterLevel); // if (data > 0.6f) { // cell.SetMapData(1f); // } // else if (data > 0.4f) { // cell.SetMapData(0.5f); // } // else if (data > 0.2f) { // cell.SetMapData(0.25f); // } } } 

Poin Sungai


Berapa banyak sungai yang kita butuhkan? Parameter ini harus disesuaikan. Karena panjang sungai bervariasi, akan lebih logis untuk mengendalikannya dengan bantuan titik-titik sungai, yang menentukan jumlah sel tanah di mana sungai harus terkandung. Mari kita ungkapkan sebagai persentase dengan maksimum 20% dan nilai default 10%. Seperti persentase sushi, ini adalah nilai target, bukan yang dijamin. Akibatnya, kami mungkin memiliki terlalu sedikit kandidat atau sungai yang terlalu pendek untuk menutupi jumlah lahan yang diperlukan. Itu sebabnya persentase maksimum tidak boleh terlalu besar.

  [Range(0, 20)] public int riverPercentage = 10; 


Slider persen sungai.

Untuk menentukan titik sungai, dinyatakan sebagai jumlah sel, kita perlu mengingat berapa banyak sel tanah yang dihasilkan CreateLand.

  int cellCount, landCells; … void CreateLand () { int landBudget = Mathf.RoundToInt(cellCount * landPercentage * 0.01f); landCells = landBudget; for (int guard = 0; guard < 10000; guard++) { … } if (landBudget > 0) { Debug.LogWarning("Failed to use up " + landBudget + " land budget."); landCells -= landBudget; } } 

Di dalam, CreateRiversjumlah titik sungai sekarang dapat dihitung dengan cara yang sama seperti yang kita lakukan di CreateLand.

  void CreateRivers () { List<HexCell> riverOrigins = ListPool<HexCell>.Get(); for (int i = 0; i < cellCount; i++) { … } int riverBudget = Mathf.RoundToInt(landCells * riverPercentage * 0.01f); ListPool<HexCell>.Add(riverOrigins); } 

Selanjutnya, kami akan terus mengambil dan menghapus sel acak dari daftar asli, sementara kami masih memiliki poin dan sumber sel. Dalam hal penyelesaian jumlah poin, kami akan menampilkan peringatan di konsol.

  int riverBudget = Mathf.RoundToInt(landCells * riverPercentage * 0.01f); while (riverBudget > 0 && riverOrigins.Count > 0) { int index = Random.Range(0, riverOrigins.Count); int lastIndex = riverOrigins.Count - 1; HexCell origin = riverOrigins[index]; riverOrigins[index] = riverOrigins[lastIndex]; riverOrigins.RemoveAt(lastIndex); } if (riverBudget > 0) { Debug.LogWarning("Failed to use up river budget."); } 

Selain itu, kami menambahkan metode untuk membuat sungai secara langsung. Sebagai parameter, ia membutuhkan sel awal, dan setelah selesai ia harus mengembalikan panjang sungai. Kami mulai dengan menyimpan metode yang mengembalikan panjang nol.

  int CreateRiver (HexCell origin) { int length = 0; return length; } 

Kami akan memanggil metode ini di akhir siklus yang baru saja kami tambahkan CreateRivers, gunakan untuk mengurangi jumlah poin yang tersisa. Kami memastikan bahwa sungai baru dibuat hanya jika sel yang dipilih tidak memiliki sungai yang mengalir melewatinya.

  while (riverBudget > 0 && riverOrigins.Count > 0) { … if (!origin.HasRiver) { riverBudget -= CreateRiver(origin); } } 

Sungai saat ini


Adalah logis untuk membuat sungai yang mengalir ke laut atau badan air lainnya. Ketika kita mulai dari sumber, kita segera mendapatkan panjang 1. Setelah itu, kita memilih tetangga acak dan menambah panjangnya. Kami terus bergerak hingga mencapai sel bawah air.

  int CreateRiver (HexCell origin) { int length = 1; HexCell cell = origin; while (!cell.IsUnderwater) { HexDirection direction = (HexDirection)Random.Range(0, 6); cell.SetOutgoingRiver(direction); length += 1; cell = cell.GetNeighbor(direction); } return length; } 


Sungai acak.

Sebagai hasil dari pendekatan naif seperti itu, kami mendapatkan pecahan sungai yang tersebar secara acak, terutama karena penggantian sungai yang dihasilkan sebelumnya. Ini bahkan dapat menyebabkan kesalahan, karena kami tidak memeriksa apakah tetangga benar-benar ada. Kita perlu memeriksa semua arah dalam loop dan memastikan bahwa ada tetangga di sana. Jika ya, maka kita tambahkan arah ini ke daftar arah aliran potensial, tetapi hanya jika sungai belum mengalir melalui tetangga ini. Kemudian pilih nilai acak dari daftar ini.

  List<HexDirection> flowDirections = new List<HexDirection>(); … int CreateRiver (HexCell origin) { int length = 1; HexCell cell = origin; while (!cell.IsUnderwater) { flowDirections.Clear(); for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = cell.GetNeighbor(d); if (!neighbor || neighbor.HasRiver) { continue; } flowDirections.Add(d); } HexDirection direction = // (HexDirection)Random.Range(0, 6); flowDirections[Random.Range(0, flowDirections.Count)]; cell.SetOutgoingRiver(direction); length += 1; cell = cell.GetNeighbor(direction); } return length; } 

Dengan pendekatan baru ini, kami mungkin memiliki nol arah aliran yang tersedia. Ketika ini terjadi, sungai tidak lagi bisa mengalir lebih jauh dan harus berakhir. Jika saat ini panjangnya adalah 1, maka ini berarti kita tidak bisa bocor dari sel aslinya, yaitu tidak ada sungai sama sekali. Dalam hal ini, panjang sungai adalah nol.

  flowDirections.Clear(); for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … } if (flowDirections.Count == 0) { return length > 1 ? length : 0; } 


Sungai yang diawetkan.

Lari ke bawah


Sekarang kita menyelamatkan sungai yang sudah dibuat, tetapi kita masih bisa mendapatkan fragmen sungai yang terisolasi. Ini terjadi karena saat kita mengabaikan ketinggian. Setiap kali kami memaksa sungai mengalir ke ketinggian yang lebih besar, HexCell.SetOutgoingRiverupaya ini terputus, yang menyebabkan pecahnya sungai. Karena itu, kita juga perlu melewati arah yang menyebabkan sungai mengalir.

  if (!neighbor || neighbor.HasRiver) { continue; } int delta = neighbor.Elevation - cell.Elevation; if (delta > 0) { continue; } flowDirections.Add(d); 


Sungai mengalir ke bawah.

Jadi kami menyingkirkan banyak fragmen sungai, tetapi beberapa masih tetap ada. Mulai saat ini, menyingkirkan sungai yang paling buruk menjadi masalah penyempurnaan. Untuk mulai dengan, sungai lebih suka mengalir turun secepat mungkin. Mereka tidak harus memilih rute yang sesingkat mungkin, tetapi kemungkinannya besar. Untuk mensimulasikan ini, kami akan menambahkan arah tiga kali ke daftar.

  if (delta > 0) { continue; } if (delta < 0) { flowDirections.Add(d); flowDirections.Add(d); flowDirections.Add(d); } flowDirections.Add(d); 

Hindari belokan tajam


Selain mengalir ke bawah, air juga memiliki kelembaman. Sebuah sungai lebih cenderung mengalir lurus atau sedikit bengkok daripada berbelok tajam secara tiba-tiba. Kita dapat menambahkan distorsi ini dengan melacak arah terakhir sungai. Jika arah potensial saat ini tidak menyimpang terlalu banyak dari arah ini, maka tambahkan ke daftar lagi. Ini bukan masalah bagi sumbernya, jadi kami akan selalu menambahkannya lagi.

  int CreateRiver (HexCell origin) { int length = 1; HexCell cell = origin; HexDirection direction = HexDirection.NE; while (!cell.IsUnderwater) { flowDirections.Clear(); for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … if (delta < 0) { flowDirections.Add(d); flowDirections.Add(d); flowDirections.Add(d); } if ( length == 1 || (d != direction.Next2() && d != direction.Previous2()) ) { flowDirections.Add(d); } flowDirections.Add(d); } if (flowDirections.Count == 0) { return length > 1 ? length : 0; } // HexDirection direction = direction = flowDirections[Random.Range(0, flowDirections.Count)]; cell.SetOutgoingRiver(direction); length += 1; cell = cell.GetNeighbor(direction); } return length; } 

Ini sangat mengurangi kemungkinan zig-zag sungai yang terlihat jelek.


Lebih sedikit tikungan tajam.

Pertemuan sungai


Terkadang ternyata sungai itu mengalir tepat di sebelah sumber sungai yang sebelumnya dibuat. Jika sumber sungai ini tidak pada ketinggian yang lebih tinggi, maka kita dapat memutuskan bahwa sungai baru mengalir ke yang lama. Akibatnya, kami mendapatkan satu sungai yang panjang, dan bukan dua yang bersebelahan.

Untuk melakukan ini, kita akan membiarkan tetangga lewat hanya jika ada sungai yang masuk di dalamnya, atau jika itu adalah sumber sungai saat ini. Setelah menentukan bahwa arah ini tidak naik, kami memeriksa untuk melihat apakah ada sungai keluar. Jika ada, maka kita kembali menemukan sungai tua. Karena ini jarang terjadi, kami tidak akan terlibat dalam memeriksa sumber tetangga lainnya dan akan segera menggabungkan sungai.

  HexCell neighbor = cell.GetNeighbor(d); // if (!neighbor || neighbor.HasRiver) { // continue; // } if (!neighbor || neighbor == origin || neighbor.HasIncomingRiver) { continue; } int delta = neighbor.Elevation - cell.Elevation; if (delta > 0) { continue; } if (neighbor.HasOutgoingRiver) { cell.SetOutgoingRiver(d); return length; } 



Sungai sebelum dan sesudah pengumpulan.

Jaga jarak


Karena kandidat yang baik untuk peran sumber biasanya dikelompokkan bersama, kita akan mendapatkan kelompok sungai. Selain itu, kami mungkin memiliki sungai yang mengambil sumber tepat di sebelah waduk, menghasilkan sungai dengan panjang 1. Kami dapat mendistribusikan sumber, membuang yang bersebelahan dengan sungai atau waduk. Kami melakukan ini dengan melewati tetangga dari sumber yang dipilih dalam satu lingkaran di dalam CreateRivers. Jika kami menemukan tetangga yang melanggar aturan, maka sumbernya tidak cocok untuk kami dan kami harus melewatinya.

  while (riverBudget > 0 && riverOrigins.Count > 0) { int index = Random.Range(0, riverOrigins.Count); int lastIndex = riverOrigins.Count - 1; HexCell origin = riverOrigins[index]; riverOrigins[index] = riverOrigins[lastIndex]; riverOrigins.RemoveAt(lastIndex); if (!origin.HasRiver) { bool isValidOrigin = true; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = origin.GetNeighbor(d); if (neighbor && (neighbor.HasRiver || neighbor.IsUnderwater)) { isValidOrigin = false; break; } } if (isValidOrigin) { riverBudget -= CreateRiver(origin); } } 

Dan meskipun sungai masih akan mengalir berdampingan, mereka cenderung menutupi area yang lebih besar.



Tanpa jarak dan dengan itu.

Kami mengakhiri sungai dengan danau


Tidak semua sungai mencapai reservoir, beberapa terjebak di lembah atau tersumbat oleh sungai lain. Ini bukan masalah khusus, karena sering kali sungai sungguhan juga tampaknya menghilang. Ini bisa terjadi, misalnya, jika mereka mengalir di bawah tanah, tersebar di daerah berawa atau mengering. Sungai kami tidak dapat memvisualisasikan ini, jadi mereka berakhir begitu saja.

Namun, kami dapat mencoba meminimalkan jumlah kasus tersebut. Meskipun kita tidak dapat menyatukan sungai atau membuatnya mengalir, kita dapat membuatnya berakhir di danau, yang sering ditemukan dalam kenyataan dan terlihat bagus. Untuk iniCreateRiverharus menaikkan level air dalam sel jika macet. Kemungkinan ini tergantung pada ketinggian minimum tetangga sel ini. Oleh karena itu, untuk melacak ini ketika mempelajari tetangga, diperlukan sedikit perubahan kode.

  while (!cell.IsUnderwater) { int minNeighborElevation = int.MaxValue; flowDirections.Clear(); for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = cell.GetNeighbor(d); // if (!neighbor || neighbor == origin || neighbor.HasIncomingRiver) { // continue; // } if (!neighbor) { continue; } if (neighbor.Elevation < minNeighborElevation) { minNeighborElevation = neighbor.Elevation; } if (neighbor == origin || neighbor.HasIncomingRiver) { continue; } int delta = neighbor.Elevation - cell.Elevation; if (delta > 0) { continue; } … } … } 

Jika kita macet, maka pertama-tama kita perlu memeriksa apakah kita masih di sumbernya. Jika ya, maka batalkan saja sungai. Jika tidak, kami memeriksa untuk melihat apakah semua tetangga setidaknya setinggi sel saat ini. Jika demikian, maka kita dapat menaikkan air ke level ini. Ini akan membuat danau dari satu sel, kecuali tinggi sel tetap pada level yang sama. Jika demikian, maka cukup tetapkan ketinggian satu tingkat di bawah permukaan air.

  if (flowDirections.Count == 0) { // return length > 1 ? length : 0; if (length == 1) { return 0; } if (minNeighborElevation >= cell.Elevation) { cell.WaterLevel = minNeighborElevation; if (minNeighborElevation == cell.Elevation) { cell.Elevation = minNeighborElevation - 1; } } break; } 



Ujung-ujung sungai tanpa danau dan danau. Dalam hal ini, persentase sungai adalah 20.

Perhatikan bahwa sekarang kita mungkin memiliki sel bawah air di atas level air yang digunakan untuk menghasilkan peta. Mereka akan menunjukkan danau di atas permukaan laut.

Danau tambahan


Kami juga dapat membuat danau, bahkan jika kami tidak terjebak. Ini dapat menyebabkan sungai mengalir masuk dan keluar dari danau. Jika kita tidak terjebak, maka danau dapat dibuat dengan menaikkan level air lalu ketinggian sel saat ini, dan kemudian mengurangi tinggi sel. Ini hanya berlaku ketika ketinggian minimum tetangga setidaknya sama dengan tinggi sel saat ini. Kami melakukan ini di akhir siklus sungai dan sebelum pindah ke sel berikutnya.

  while (!cell.IsUnderwater) { … if (minNeighborElevation >= cell.Elevation) { cell.WaterLevel = cell.Elevation; cell.Elevation -= 1; } cell = cell.GetNeighbor(direction); } 



Tanpa danau tambahan dan bersama mereka.

Beberapa danau indah, tetapi tanpa batas kita dapat membuat terlalu banyak danau. Oleh karena itu, mari kita tambahkan probabilitas khusus untuk danau tambahan, dengan nilai default 0,25.

  [Range(0f, 1f)] public float extraLakeProbability = 0.25f; 

Dia akan mengendalikan kemungkinan menghasilkan danau tambahan, jika memungkinkan.

  if ( minNeighborElevation >= cell.Elevation && Random.value < extraLakeProbability ) { cell.WaterLevel = cell.Elevation; cell.Elevation -= 1; } 



Danau tambahan.

Bagaimana dengan membuat danau dengan lebih dari satu sel?
, , , . . : . , . , , , .

paket unity

Suhu


Air hanyalah salah satu faktor yang dapat menentukan bioma suatu sel. Faktor penting lainnya adalah suhu. Meskipun kita dapat mensimulasikan aliran dan difusi suhu seperti mensimulasikan air, untuk menciptakan iklim yang menarik, kita hanya perlu satu faktor kompleks. Karena itu, mari kita menjaga suhu tetap sederhana dan mengaturnya untuk setiap sel.

Suhu dan garis lintang


Pengaruh terbesar pada suhu adalah garis lintang. Panas di khatulistiwa, dingin di kutub, dan ada transisi yang mulus di antara mereka. Mari kita membuat metode DetermineTemperatureyang mengembalikan suhu sel yang diberikan. Untuk memulai, kita cukup menggunakan koordinat Z sel dibagi dengan dimensi Z sebagai garis lintang, dan kemudian menggunakan nilai ini sebagai suhu.

  float DetermineTemperature (HexCell cell) { float latitude = (float)cell.coordinates.Z / grid.cellCountZ; return latitude; } 

Kami mendefinisikan suhu SetTerrainTypedan menggunakannya sebagai data peta.

  void SetTerrainType () { for (int i = 0; i < cellCount; i++) { HexCell cell = grid.GetCell(i); float temperature = DetermineTemperature(cell); cell.SetMapData(temperature); float moisture = climate[i].moisture; … } } 


Lintang sebagai suhu, belahan bumi selatan.

Kami mendapatkan peningkatan suhu linear dari bawah ke atas. Anda dapat menggunakannya untuk mensimulasikan belahan bumi selatan, dengan kutub di bagian bawah dan khatulistiwa di bagian atas. Tetapi kita tidak perlu menggambarkan seluruh belahan bumi. Dengan perbedaan suhu yang lebih kecil atau tidak ada perbedaan sama sekali, kita dapat menggambarkan area yang lebih kecil. Untuk melakukan ini, kami akan membuat suhu rendah dan tinggi dapat disesuaikan. Kami akan mengatur suhu ini dalam kisaran 0-1, dan menggunakan nilai ekstrem sebagai nilai default.

  [Range(0f, 1f)] public float lowTemperature = 0f; [Range(0f, 1f)] public float highTemperature = 1f; 


Penggeser suhu.

Kami menerapkan rentang suhu menggunakan interpolasi linier, menggunakan garis lintang sebagai interpolator. Karena kita menyatakan garis lintang sebagai nilai dari 0 hingga 1, kita dapat menggunakannya Mathf.LerpUnclamped.

  float DetermineTemperature (HexCell cell) { float latitude = (float)cell.coordinates.Z / grid.cellCountZ; float temperature = Mathf.LerpUnclamped(lowTemperature, highTemperature, latitude); return temperature; } 

Perhatikan bahwa suhu rendah tidak selalu lebih rendah dari tinggi. Jika diinginkan, Anda dapat mengubahnya.

Belahan bumi


Sekarang kita bisa mensimulasikan belahan bumi selatan, dan mungkin belahan utara, jika kita mengukur suhu terlebih dahulu. Tetapi jauh lebih nyaman untuk menggunakan opsi konfigurasi terpisah untuk beralih antar belahan. Mari kita membuat enumerasi dan bidang untuk itu. Jadi, kami juga akan menambahkan opsi untuk membuat kedua belahan otak, yang berlaku secara default.

  public enum HemisphereMode { Both, North, South } public HemisphereMode hemisphere; 


Pilihan belahan bumi.

Jika kita membutuhkan belahan bumi utara, maka kita cukup membalik garis lintang, mengurangi dari 1. Untuk mensimulasikan kedua belahan bumi, kutubnya harus di bawah dan di atas peta, dan garis khatulistiwa harus di tengah. Anda dapat melakukan ini dengan menggandakan garis lintang, sementara belahan bumi bawah akan diproses dengan benar, dan belahan bumi bagian atas akan memiliki garis lintang 1 hingga 2. Untuk memperbaikinya, kami kurangi garis lintang dari 2 saat melebihi 1.

  float DetermineTemperature (HexCell cell) { float latitude = (float)cell.coordinates.Z / grid.cellCountZ; if (hemisphere == HemisphereMode.Both) { latitude *= 2f; if (latitude > 1f) { latitude = 2f - latitude; } } else if (hemisphere == HemisphereMode.North) { latitude = 1f - latitude; } float temperature = Mathf.LerpUnclamped(lowTemperature, highTemperature, latitude); return temperature; } 


Kedua belahan otak.

Perlu dicatat bahwa ini menciptakan kemungkinan menciptakan peta eksotis di mana khatulistiwa dingin dan kutubnya hangat.

Semakin tinggi dinginnya


Selain garis lintang, suhu juga sangat dipengaruhi oleh ketinggian. Rata-rata, semakin tinggi kita memanjat, semakin dingin. Kita bisa mengubah ini menjadi faktor, seperti yang kita lakukan pada kandidat sungai. Dalam hal ini, kami menggunakan tinggi sel. Selain itu, indikator ini berkurang dengan ketinggian, yaitu, sama dengan 1 minus tinggi dibagi dengan maksimum relatif terhadap ketinggian air. Agar indikator di level tertinggi tidak jatuh ke nol, kami menambah pembagi. Kemudian gunakan indikator ini untuk mengukur suhu.

  float temperature = Mathf.LerpUnclamped(lowTemperature, highTemperature, latitude); temperature *= 1f - (cell.ViewElevation - waterLevel) / (elevationMaximum - waterLevel + 1f); return temperature; 


Tinggi mempengaruhi suhu.

Fluktuasi suhu


Kita dapat membuat kesederhanaan gradien suhu kurang terlihat dengan menambahkan fluktuasi suhu acak. Peluang kecil untuk membuatnya lebih realistis, tetapi dengan terlalu banyak fluktuasi, mereka akan terlihat sewenang-wenang. Mari kita membuat kekuatan fluktuasi suhu dapat disesuaikan dan menyatakannya sebagai penyimpangan suhu maksimum dengan nilai default 0,1.

  [Range(0f, 1f)] public float temperatureJitter = 0.1f; 


Slider fluktuasi suhu.

Fluktuasi seperti itu harus lancar dengan sedikit perubahan lokal. Anda dapat menggunakan tekstur derau kami untuk ini. Kami akan memanggil HexMetrics.SampleNoisedan menggunakan sebagai argumen posisi sel, diskalakan dengan 0,1. Mari kita ambil saluran W, pusatkan dan skala dengan koefisien osilasi. Kemudian kami menambahkan nilai ini ke suhu yang dihitung sebelumnya.

  temperature *= 1f - (cell.ViewElevation - waterLevel) / (elevationMaximum - waterLevel + 1f); temperature += (HexMetrics.SampleNoise(cell.Position * 0.1f).w * 2f - 1f) * temperatureJitter; return temperature; 



Fluktuasi suhu dengan nilai 0,1 dan 1.

Kita dapat menambahkan sedikit variabilitas pada fluktuasi pada setiap peta, memilih secara acak dari empat saluran kebisingan. Atur saluran sekali dalam SetTerrainType, lalu indeks saluran warna di DetermineTemperature.

  int temperatureJitterChannel; … void SetTerrainType () { temperatureJitterChannel = Random.Range(0, 4); for (int i = 0; i < cellCount; i++) { … } } float DetermineTemperature (HexCell cell) { … float jitter = HexMetrics.SampleNoise(cell.Position * 0.1f)[temperatureJitterChannel]; temperature += (jitter * 2f - 1f) * temperatureJitter; return temperature; } 


Fluktuasi suhu berbeda dengan gaya maksimum.

paket unity

Bioma


Sekarang kita memiliki data tentang kelembaban dan suhu, kita dapat membuat matriks bioma. Dengan mengindeks matriks ini, kita dapat menetapkan bioma ke semua sel, menciptakan lanskap yang lebih kompleks daripada hanya menggunakan satu dimensi data.

Matriks bioma


Ada banyak model iklim, tetapi kami tidak akan menggunakannya. Kami akan membuatnya sangat sederhana, kami hanya tertarik pada logika. Kering berarti gurun (dingin atau panas), untuk itu kami menggunakan pasir. Dingin dan basah berarti salju. Panas dan lembab berarti banyak tumbuh-tumbuhan, yaitu rumput. Di antara mereka kita akan memiliki taiga atau tundra, yang akan kita sebut sebagai tekstur bumi yang keabu-abuan. Matriks 4 × 4 akan cukup untuk membuat transisi di antara bioma-bioma ini.

Sebelumnya, kami menetapkan tipe ketinggian berdasarkan pada lima interval kelembaban. Kami cukup menurunkan strip terkering ke 0,05, dan menyimpan sisanya. Untuk pita suhu kami menggunakan 0,1, 0,3, 0,6 dan lebih tinggi. Untuk kenyamanan, kami akan menetapkan nilai-nilai ini dalam array statis.

  static float[] temperatureBands = { 0.1f, 0.3f, 0.6f }; static float[] moistureBands = { 0.12f, 0.28f, 0.85f }; 

Meskipun kami hanya menentukan jenis bantuan berdasarkan bioma, kami dapat menggunakannya untuk menentukan parameter lainnya. Oleh karena itu, mari kita definisikan dalam HexMapGeneratorstruktur Biomeyang menggambarkan konfigurasi bioma individu. Sejauh ini, hanya berisi indeks bump plus metode konstruktor yang sesuai.

  struct Biome { public int terrain; public Biome (int terrain) { this.terrain = terrain; } } 

Kami menggunakan struktur ini untuk membuat array statis yang berisi data matriks. Kami menggunakan kelembaban sebagai koordinat X, dan suhu sebagai Y. Kami mengisi garis dengan suhu terendah dengan salju, baris kedua dengan tundra, dan dua lainnya dengan rumput. Lalu kami mengganti kolom paling kering dengan padang pasir, mendefinisikan kembali pilihan suhu.

  static Biome[] biomes = { new Biome(0), new Biome(4), new Biome(4), new Biome(4), new Biome(0), new Biome(2), new Biome(2), new Biome(2), new Biome(0), new Biome(1), new Biome(1), new Biome(1), new Biome(0), new Biome(1), new Biome(1), new Biome(1) }; 


Matriks bioma dengan indeks array satu dimensi.

Definisi bioma


Untuk menentukan SetTerrainTypesel - sel dalam bioma, kita berkeliling kisaran suhu dan kelembaban dalam siklus untuk menentukan indeks matriks yang kita butuhkan. Kami menggunakannya untuk mendapatkan bioma yang diinginkan dan menentukan jenis topografi sel.

  void SetTerrainType () { temperatureJitterChannel = Random.Range(0, 4); for (int i = 0; i < cellCount; i++) { HexCell cell = grid.GetCell(i); float temperature = DetermineTemperature(cell); // cell.SetMapData(temperature); float moisture = climate[i].moisture; if (!cell.IsUnderwater) { // if (moisture < 0.05f) { // cell.TerrainTypeIndex = 4; // } // … // else { // cell.TerrainTypeIndex = 2; // } int t = 0; for (; t < temperatureBands.Length; t++) { if (temperature < temperatureBands[t]) { break; } } int m = 0; for (; m < moistureBands.Length; m++) { if (moisture < moistureBands[m]) { break; } } Biome cellBiome = biomes[t * 4 + m]; cell.TerrainTypeIndex = cellBiome.terrain; } else { cell.TerrainTypeIndex = 2; } } } 


Relief berdasarkan matriks bioma.

Pengaturan bioma


Kita bisa melampaui bioma yang didefinisikan dalam matriks. Sebagai contoh, dalam matriks, semua bioma kering didefinisikan sebagai gurun pasir, tetapi tidak semua gurun kering diisi dengan pasir. Ada banyak gurun berbatu yang terlihat sangat berbeda. Karena itu, mari kita ganti beberapa sel gurun dengan batu. Kami akan melakukan ini hanya berdasarkan ketinggian: pasir berada di ketinggian rendah, dan bebatuan telanjang biasanya ditemukan di atas.

Misalkan pasir berubah menjadi batu ketika ketinggian sel lebih dekat dengan ketinggian maksimum daripada ketinggian air. Ini adalah garis ketinggian gurun berbatu yang bisa kita hitung di awal SetTerrainType. Ketika kami bertemu sel dengan pasir, dan tingginya cukup besar, kami mengubah relief bioma menjadi batu.

  void SetTerrainType () { temperatureJitterChannel = Random.Range(0, 4); int rockDesertElevation = elevationMaximum - (elevationMaximum - waterLevel) / 2; for (int i = 0; i < cellCount; i++) { … if (!cell.IsUnderwater) { … Biome cellBiome = biomes[t * 4 + m]; if (cellBiome.terrain == 0) { if (cell.Elevation >= rockDesertElevation) { cellBiome.terrain = 3; } } cell.TerrainTypeIndex = cellBiome.terrain; } else { cell.TerrainTypeIndex = 2; } } } 


Gurun berpasir dan berbatu.

Perubahan lain berdasarkan ketinggian adalah untuk memaksa sel pada ketinggian maksimum untuk berubah menjadi puncak salju, terlepas dari suhu mereka, hanya jika mereka tidak terlalu kering. Ini akan meningkatkan kemungkinan puncak salju di dekat khatulistiwa yang panas dan lembab.

  if (cellBiome.terrain == 0) { if (cell.Elevation >= rockDesertElevation) { cellBiome.terrain = 3; } } else if (cell.Elevation == elevationMaximum) { cellBiome.terrain = 4; } 


Tutup salju pada ketinggian maksimum.

Tanaman


Sekarang mari kita membuat bioma menentukan tingkat sel tanaman. Untuk melakukan ini, tambahkan ke Biomebidang tanaman dan sertakan dalam konstruktor.

  struct Biome { public int terrain, plant; public Biome (int terrain, int plant) { this.terrain = terrain; this.plant = plant; } } 

Dalam bioma terdingin dan terkering tidak akan ada tanaman sama sekali. Dalam semua hal lain, semakin hangat dan basah iklim, semakin banyak tanaman. Kolom kedua dari kelembaban hanya menerima tanaman tingkat pertama untuk baris terpanas, oleh karena itu [0, 0, 0, 1]. Kolom ketiga meningkatkan level satu per satu, dengan pengecualian salju, yaitu, [0, 1, 1, 2]. Dan kolom terbasah menambahnya lagi, yaitu, ternyata [0, 2, 2, 3]. Ubah array biomesdengan menambahkan konfigurasi instalasi ke dalamnya.

  static Biome[] biomes = { new Biome(0, 0), new Biome(4, 0), new Biome(4, 0), new Biome(4, 0), new Biome(0, 0), new Biome(2, 0), new Biome(2, 1), new Biome(2, 2), new Biome(0, 0), new Biome(1, 0), new Biome(1, 1), new Biome(1, 2), new Biome(0, 0), new Biome(1, 1), new Biome(1, 2), new Biome(1, 3) }; 


Matriks bioma dengan tingkat tanaman.

Sekarang kita dapat mengatur tingkat tanaman untuk sel.

  cell.TerrainTypeIndex = cellBiome.terrain; cell.PlantLevel = cellBiome.plant; 


Bioma dengan tanaman.

Apakah tanaman sekarang terlihat berbeda?
, . (1, 2, 1) (0.75, 1, 0.75). (1.5, 3, 1.5) (2, 1.5, 2). — (2, 4.5, 2) (2.5, 3, 2.5).

, : (13, 114, 0).

Kami dapat mengubah tingkat tanaman untuk bioma. Pertama-tama kita perlu memastikan bahwa mereka tidak muncul di medan bersalju, yang sudah bisa kita atur. Kedua, mari tingkatkan tanaman di sepanjang sungai, jika belum maksimal.

  if (cellBiome.terrain == 4) { cellBiome.plant = 0; } else if (cellBiome.plant < 3 && cell.HasRiver) { cellBiome.plant += 1; } cell.TerrainTypeIndex = cellBiome.terrain; cell.PlantLevel = cellBiome.plant; 


Tanaman yang dimodifikasi.

Bioma bawah air


Sampai saat itu, kami sepenuhnya mengabaikan sel-sel bawah air. Mari kita tambahkan sedikit variasi pada mereka, dan kita tidak akan menggunakan tekstur bumi untuk semuanya. Solusi sederhana berdasarkan ketinggian sudah cukup untuk membuat gambar yang lebih menarik. Sebagai contoh, mari kita gunakan rumput untuk sel satu langkah di bawah permukaan air. Mari kita juga menggunakan rumput untuk sel di atas permukaan air, yaitu untuk danau yang dibuat oleh sungai. Sel dengan ketinggian negatif adalah wilayah laut dalam, jadi kami menggunakan batu untuknya. Semua sel lainnya tetap dihaluskan.

  void SetTerrainType () { … if (!cell.IsUnderwater) { … } else { int terrain; if (cell.Elevation == waterLevel - 1) { terrain = 1; } else if (cell.Elevation >= waterLevel) { terrain = 1; } else if (cell.Elevation < 0) { terrain = 3; } else { terrain = 2; } cell.TerrainTypeIndex = terrain; } } } 


Variabilitas bawah air.

Mari kita tambahkan beberapa detail lagi untuk sel-sel bawah laut di sepanjang pantai. Ini adalah sel dengan setidaknya satu tetangga di atas air. Jika sel seperti itu dangkal, maka kita akan membuat pantai. Dan jika di sebelah tebing, maka itu akan menjadi detail visual yang dominan, dan kami menggunakan batu.

Untuk menentukan ini, kami akan memeriksa tetangga sel yang terletak satu langkah di bawah permukaan air. Mari kita hitung jumlah koneksi dengan tebing dan lereng dengan sel-sel tanah tetangga.

  if (cell.Elevation == waterLevel - 1) { int cliffs = 0, slopes = 0; for ( HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++ ) { HexCell neighbor = cell.GetNeighbor(d); if (!neighbor) { continue; } int delta = neighbor.Elevation - cell.WaterLevel; if (delta == 0) { slopes += 1; } else if (delta > 0) { cliffs += 1; } } terrain = 1; } 

Sekarang kita dapat menggunakan informasi ini untuk mengklasifikasikan sel. Pertama, jika lebih dari setengah tetangga adalah tanah, maka kita berurusan dengan danau atau teluk. Untuk sel-sel ini kami menggunakan tekstur rumput. Kalau tidak, jika kita memiliki tebing, maka kita menggunakan batu. Kalau tidak, jika kita memiliki lereng, maka kita menggunakan pasir untuk membuat pantai. Satu-satunya pilihan yang tersisa adalah area dangkal di lepas pantai, di mana kami masih menggunakan rumput.

  if (cell.Elevation == waterLevel - 1) { int cliffs = 0, slopes = 0; for ( HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++ ) { … } if (cliffs + slopes > 3) { terrain = 1; } else if (cliffs > 0) { terrain = 3; } else if (slopes > 0) { terrain = 0; } else { terrain = 1; } } 



Variabilitas pantai.

Sebagai sentuhan terakhir, mari kita periksa bahwa kita tidak memiliki sel bawah air hijau dalam kisaran suhu terdingin. Untuk sel seperti itu kita menggunakan bumi.

  if (terrain == 1 && temperature < temperatureBands[0]) { terrain = 2; } cell.TerrainTypeIndex = terrain; 

Kami mendapat kesempatan untuk membuat kartu acak yang terlihat cukup menarik dan alami, dengan banyak opsi konfigurasi.

paket unity

Bagian 27: melipat kartu


  • Kami membagi kartu menjadi kolom yang dapat dipindahkan.
  • Pusatkan kartu di kamera.
  • Kami menghancurkan segalanya.

Di bagian terakhir ini, kami akan menambahkan dukungan untuk meminimalkan peta, menghubungkan tepi timur dan barat.

Tutorial dibuat menggunakan Unity 2017.3.0p3.


Lipat membuat dunia berputar.

Kartu lipat


Peta kami dapat digunakan untuk memodelkan area dengan ukuran berbeda, tetapi mereka selalu terbatas pada bentuk persegi panjang. Kita dapat membuat peta dari satu pulau atau seluruh benua, tetapi tidak seluruh planet. Planet-planet itu bulat, tidak memiliki batas kaku yang menghambat gerakan di permukaannya. Jika Anda terus bergerak dalam satu arah, maka cepat atau lambat Anda akan kembali ke titik awal.

Kita tidak bisa membungkus kisi segi enam di sekitar bola, tumpang tindih seperti itu tidak mungkin. Dalam perkiraan terbaik, topologi icosahedral digunakan, di mana kedua belas sel harus pentagon. Namun, tanpa distorsi atau pengecualian, mesh dapat melilitkan silinder. Untuk melakukan ini, cukup sambungkan tepi timur dan barat peta. Dengan pengecualian dari logika pembungkus, semua yang lain tetap sama.

Silinder adalah pendekatan bulatan yang buruk, karena kita tidak dapat memodelkan kutub. Tapi ini tidak menghentikan para pengembang banyak game menggunakan lipatan timur ke barat untuk memodelkan peta planet. Wilayah kutub sama sekali bukan bagian dari zona permainan.

Bagaimana kalau berbelok ke utara dan selatan?
, . , , . -, -. .

Ada dua cara untuk menerapkan lipat silindris. Yang pertama adalah untuk benar-benar membuat peta silindris dengan menekuk permukaannya dan semua yang ada di atasnya sehingga ujung timur dan barat bersentuhan. Sekarang Anda akan bermain bukan pada permukaan datar, tetapi pada silinder nyata. Pendekatan kedua adalah menyimpan peta datar dan menggunakan teleportasi atau duplikasi untuk runtuh. Sebagian besar game menggunakan pendekatan kedua, jadi kami akan menerimanya.

Lipat opsional


Kebutuhan untuk menutup peta tergantung pada skalanya - lokal atau planet. Kita dapat menggunakan dukungan keduanya dengan membuat lipat opsional. Untuk melakukan ini, tambahkan saklar baru ke menu Buat Peta Baru dengan keruntuhan diaktifkan secara default.


Menu peta baru dengan opsi untuk runtuh.

Tambahkan ke NewMapMenubidang untuk melacak seleksi, serta metode untuk mengubahnya. Mari kita membuat metode ini dipanggil ketika keadaan saklar berubah.

  bool wrapping = true; … public void ToggleWrapping (bool toggle) { wrapping = toggle; } 

Saat peta baru diminta, kami meneruskan nilai opsi perkecil.

  void CreateMap (int x, int z) { if (generateMaps) { mapGenerator.GenerateMap(x, z, wrapping); } else { hexGrid.CreateMap(x, z, wrapping); } HexMapCamera.ValidatePosition(); Close(); } 

Ubah HexMapGenerator.GenerateMapsehingga menerima argumen baru ini dan kemudian meneruskannya ke HexGrid.CreateMap.

  public void GenerateMap (int x, int z, bool wrapping) { … grid.CreateMap(x, z, wrapping); … } 

kode> HexGrid harus tahu jika kita runtuh, jadi tambahkan bidang dan CreateMapsetel. Kelas-kelas lain harus mengubah logika mereka tergantung pada apakah grid diminimalkan, jadi kami akan membuat bidang umum. Selain itu, ini memungkinkan Anda untuk menetapkan nilai default melalui inspektur.

  public int cellCountX = 20, cellCountZ = 15; public bool wrapping; … public bool CreateMap (int x, int z, bool wrapping) { … cellCountX = x; cellCountZ = z; this.wrapping = wrapping; … } 

HexGridpanggilan sendiri CreateMapdi dua tempat. Kita bisa menggunakan bidangnya sendiri untuk argumen runtuh.

  void Awake () { … CreateMap(cellCountX, cellCountZ, wrapping); } … public void Load (BinaryReader reader, int header) { … if (x != cellCountX || z != cellCountZ) { if (!CreateMap(x, z, wrapping)) { return; } } … } 


Sakelar lipat kisi dinyalakan secara default.

Menyimpan dan memuat


Karena lipat diatur untuk setiap kartu, maka harus disimpan dan dimuat. Ini berarti Anda perlu mengubah format penyimpanan file, jadi tambah konstanta versi SaveLoadMenu.

  const int mapFileVersion = 5; 

Saat menyimpan, biarkan HexGridmenuliskan nilai lipat boolean setelah ukuran peta.

  public void Save (BinaryWriter writer) { writer.Write(cellCountX); writer.Write(cellCountZ); writer.Write(wrapping); … } 

Saat memuat, kami hanya akan membacanya dengan versi file yang benar. Jika berbeda, maka ini adalah kartu lama dan tidak boleh diminimalisir. Simpan informasi ini dalam variabel lokal dan bandingkan dengan keadaan lipatan saat ini. Jika berbeda, maka kita tidak dapat menggunakan kembali topologi peta yang ada dengan cara yang sama seperti ketika memuat peta dengan ukuran lain.

  public void Load (BinaryReader reader, int header) { ClearPath(); ClearUnits(); int x = 20, z = 15; if (header >= 1) { x = reader.ReadInt32(); z = reader.ReadInt32(); } bool wrapping = header >= 5 ? reader.ReadBoolean() : false; if (x != cellCountX || z != cellCountZ || this.wrapping != wrapping) { if (!CreateMap(x, z, wrapping)) { return; } } … } 

Metrik Lipat


Meminimalkan peta akan membutuhkan perubahan besar dalam logika, misalnya saat menghitung jarak. Oleh karena itu, mereka dapat menyentuh kode yang tidak memiliki tautan langsung ke kisi. Alih-alih meneruskan informasi ini sebagai argumen, mari tambahkan saja HexMetrics. Tambahkan bilangan bulat statis yang berisi ukuran lipat yang cocok dengan lebar peta. Jika lebih besar dari nol, maka kita berurusan dengan kartu yang dapat dilipat. Untuk memverifikasi ini, tambahkan properti.

  public static int wrapSize; public static bool Wrapping { get { return wrapSize > 0; } } 

Kita perlu mengatur ukuran lipat untuk setiap panggilan HexGrid.CreateMap.

  public bool CreateMap (int x, int z, bool wrapping) { … this.wrapping = wrapping; HexMetrics.wrapSize = wrapping ? cellCountX : 0; … } 

Karena data ini tidak akan selamat dari kompilasi ulang dalam mode Play, kami akan memasangnya OnEnable.

  void OnEnable () { if (!HexMetrics.noiseSource) { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; HexMetrics.wrapSize = wrapping ? cellCountX : 0; ResetVisibility(); } } 

Lebar sel


Ketika bekerja dengan kartu yang dapat dilipat, kita sering harus berurusan dengan posisi di sepanjang sumbu X, diukur dalam lebar sel. Meskipun dapat digunakan untuk ini HexMetrics.innerRadius * 2f, akan lebih nyaman jika kita tidak menambahkan perkalian setiap saat. Jadi mari kita tambahkan konstanta HexMetrics.innerDiameter.

  public const float innerRadius = outerRadius * outerToInner; public const float innerDiameter = innerRadius * 2f; 

Kita sudah bisa menggunakan diameter di tiga tempat. Pertama, HexGrid.CreateCellsaat memposisikan sel baru.

  void CreateCell (int x, int z, int i) { Vector3 position; position.x = (x + z * 0.5f - z / 2) * HexMetrics.innerDiameter; … } 

Kedua, HexMapCameramembatasi posisi kamera.

  Vector3 ClampPosition (Vector3 position) { float xMax = (grid.cellCountX - 0.5f) * HexMetrics.innerDiameter; position.x = Mathf.Clamp(position.x, 0f, xMax); … } 

Dan juga dalam HexCoordinateskonversi dari posisi ke koordinat.

  public static HexCoordinates FromPosition (Vector3 position) { float x = position.x / HexMetrics.innerDiameter; … } 

paket unity

Pemusatan kartu


Ketika peta tidak runtuh, ia telah dengan jelas mendefinisikan tepi timur dan barat, dan karenanya merupakan pusat horizontal yang jelas. Tetapi dalam kasus kartu yang dapat dilipat, semuanya berbeda. Tidak memiliki tepi timur atau barat, atau pusat. Sebagai alternatif, kita dapat mengasumsikan bahwa pusat adalah tempat kamera berada. Ini akan berguna karena kami ingin peta selalu berpusat pada sudut pandang kami. Lalu, di mana pun kita berada, kita tidak akan melihat tepi peta bagian timur atau barat.

Kolom Fragmen Peta


Agar visualisasi peta terpusat relatif ke kamera, kita perlu mengubah penempatan elemen tergantung pada pergerakan kamera. Jika bergerak ke barat, maka kita perlu mengambil apa yang ada di tepi bagian timur dan memindahkannya ke tepi bagian barat. Hal yang sama berlaku untuk arah yang berlawanan.

Idealnya, begitu kamera bergerak ke kolom sel tetangga, kita harus segera memindahkan kolom sel terjauh ke sisi lain. Namun, kita tidak perlu begitu tepat. Sebagai gantinya, kami dapat mentransfer seluruh fragmen peta. Ini memungkinkan kita untuk memindahkan bagian-bagian peta tanpa harus memodifikasi jerat.

Karena kami memindahkan seluruh kolom fragmen pada saat yang bersamaan, mari kelompokkan mereka dengan membuat objek kolom induk untuk setiap grup. Tambahkan array untuk objek-objek ini HexGrid, dan kami akan menginisialisasi CreateChunks. Kami hanya akan menggunakannya sebagai wadah, jadi kami hanya perlu melacak tautan ke komponennya Transform. Seperti dalam kasus fragmen, posisi awal mereka terletak di titik asal koordinat kotak.

  Transform[] columns; … void CreateChunks () { columns = new Transform[chunkCountX]; for (int x = 0; x < chunkCountX; x++) { columns[x] = new GameObject("Column").transform; columns[x].SetParent(transform, false); } … } 

Sekarang fragmen harus menjadi anak dari kolom yang sesuai, bukan kotak.

  void CreateChunks () { … chunks = new HexGridChunk[chunkCountX * chunkCountZ]; for (int z = 0, i = 0; z < chunkCountZ; z++) { for (int x = 0; x < chunkCountX; x++) { HexGridChunk chunk = chunks[i++] = Instantiate(chunkPrefab); chunk.transform.SetParent(columns[x], false); } } } 


Fragmen dikelompokkan ke dalam kolom.

Karena semua fragmen sekarang menjadi anak-anak kolom, CreateMapitu sudah cukup bagi kita untuk menghancurkan semua kolom secara langsung, bukan fragmen. Jadi kita akan menyingkirkan fragmen putri.

  public bool CreateMap (int x, int z, bool wrapping) { … if (columns != null) { for (int i = 0; i < columns.Length; i++) { Destroy(columns[i].gameObject); } } … } 

Teleport Columns


Tambahkan ke HexGridmetode baru CenterMapdengan posisi X sebagai parameter. Konversi posisi ke indeks kolom, membaginya dengan lebar fragmen dalam unit Unity. Ini akan menjadi indeks kolom di mana kamera saat ini berada, yaitu, itu akan menjadi kolom tengah peta.

  public void CenterMap (float xPosition) { int centerColumnIndex = (int) (xPosition / (HexMetrics.innerDiameter * HexMetrics.chunkSizeX)); } 

Cukup bagi kita untuk mengubah visualisasi peta hanya ketika indeks kolom pusat berubah. Jadi mari kita lacak di lapangan. Kami menggunakan nilai default −1 saat membuat peta sehingga peta baru selalu terpusat.

  int currentCenterColumnIndex = -1; … public bool CreateMap (int x, int z, bool wrapping) { … this.wrapping = wrapping; currentCenterColumnIndex = -1; … } … public void CenterMap (float xPosition) { int centerColumnIndex = (int) (xPosition / (HexMetrics.innerDiameter * HexMetrics.chunkSizeX)); if (centerColumnIndex == currentCenterColumnIndex) { return; } currentCenterColumnIndex = centerColumnIndex; } 

Sekarang kita tahu indeks kolom pusat, kita dapat menentukan indeks minimum dan maksimum hanya dengan mengurangi dan menambahkan setengah jumlah kolom. Karena kami menggunakan nilai integer, dengan jumlah kolom ganjil, ini berfungsi dengan baik. Dalam kasus bilangan genap, tidak mungkin ada kolom yang berpusat sempurna, sehingga salah satu indeks akan selangkah lebih maju dari yang diperlukan. Ini menciptakan offset satu kolom ke arah tepi terjauh peta, tetapi bagi kami ini bukan masalah.

  currentCenterColumnIndex = centerColumnIndex; int minColumnIndex = centerColumnIndex - chunkCountX / 2; int maxColumnIndex = centerColumnIndex + chunkCountX / 2; 

Perhatikan bahwa indeks ini mungkin negatif atau lebih besar dari indeks kolom maksimum alami. Minimum adalah nol hanya ketika kamera berada di dekat pusat alami peta. Tugas kita adalah memindahkan kolom sehingga sesuai dengan indeks relatif ini. Ini dapat dilakukan dengan mengubah koordinat X lokal dari setiap kolom dalam loop.

  int minColumnIndex = centerColumnIndex - chunkCountX / 2; int maxColumnIndex = centerColumnIndex + chunkCountX / 2; Vector3 position; position.y = position.z = 0f; for (int i = 0; i < columns.Length; i++) { position.x = 0f; columns[i].localPosition = position; } 

Untuk setiap kolom, kami memeriksa apakah indeks indeks minimum kurang. Jika demikian, maka itu terlalu jauh di sebelah kiri pusat. Dia harus berteleportasi ke sisi lain peta. Ini dapat dilakukan dengan membuat koordinat X-nya sama dengan lebar peta. Demikian pula, jika indeks kolom lebih besar dari indeks maksimum, maka itu terlalu jauh ke kanan tengah, dan harus teleport ke sisi lain.

  for (int i = 0; i < columns.Length; i++) { if (i < minColumnIndex) { position.x = chunkCountX * (HexMetrics.innerDiameter * HexMetrics.chunkSizeX); } else if (i > maxColumnIndex) { position.x = chunkCountX * -(HexMetrics.innerDiameter * HexMetrics.chunkSizeX); } else { position.x = 0f; } columns[i].localPosition = position; } 

Kamera bergerak


Ubah HexMapCamera.AdjustPositionsehingga saat bekerja dengan kartu yang dapat dilipat, ia malah ClampPositionmenelepon WrapPosition. Pertama, buat saja metode baru itu WrapPositionduplikat ClampPosition, tetapi dengan satu-satunya perbedaan: pada akhirnya, ia akan memanggil CenterMap.

  void AdjustPosition (float xDelta, float zDelta) { … transform.localPosition = grid.wrapping ? WrapPosition(position) : ClampPosition(position); } … Vector3 WrapPosition (Vector3 position) { float xMax = (grid.cellCountX - 0.5f) * HexMetrics.innerDiameter; position.x = Mathf.Clamp(position.x, 0f, xMax); float zMax = (grid.cellCountZ - 1) * (1.5f * HexMetrics.outerRadius); position.z = Mathf.Clamp(position.z, 0f, zMax); grid.CenterMap(position.x); return position; } 

Agar kartu langsung terpusat, kami memanggil OnEnablemetode ValidatePosition.

  void OnEnable () { instance = this; ValidatePosition(); } 


Bergerak ke kiri dan ke kanan saat memusatkan pada kamera.

Meskipun kami masih membatasi pergerakan kamera, peta sekarang mencoba memusatkan relatif ke kamera, memindahkan kolom-kolom fragmen peta jika perlu. Dengan peta kecil dan kamera jarak jauh, ini terlihat jelas, tetapi pada peta besar, fragmen yang diteleportasi berada di luar rentang tampilan kamera. Jelas, hanya tepi awal timur dan barat peta yang terlihat, karena belum ada triangulasi di antara mereka.

Untuk meruntuhkan kamera, kami menghapus batasan koordinat X-nya WrapPosition. Sebagai gantinya, kami akan terus meningkatkan koordinat X dengan lebar peta saat itu di bawah nol, dan menguranginya saat itu lebih besar dari lebar peta.

  Vector3 WrapPosition (Vector3 position) { // float xMax = (grid.cellCountX - 0.5f) * HexMetrics.innerDiameter; // position.x = Mathf.Clamp(position.x, 0f, xMax); float width = grid.cellCountX * HexMetrics.innerDiameter; while (position.x < 0f) { position.x += width; } while (position.x > width) { position.x -= width; } float zMax = (grid.cellCountZ - 1) * (1.5f * HexMetrics.outerRadius); position.z = Mathf.Clamp(position.z, 0f, zMax); grid.CenterMap(position.x); return position; } 


Kamera roll-up bergerak di sepanjang peta.

Tekstur Shader yang Dapat Dilipat


Dengan pengecualian ruang triangulasi, meminimalkan kamera dalam mode permainan seharusnya tidak terlihat. Namun, ketika ini terjadi, perubahan visual terjadi di setengah dari topografi dan air. Ini terjadi karena kami menggunakan posisi di dunia untuk mencicipi tekstur ini. Teleportasi fragmen yang tajam mengubah lokasi tekstur.

Kita bisa menyelesaikan masalah ini dengan membuat tekstur muncul di ubin yang kelipatan dari ukuran fragmen. Ukuran fragmen dihitung dari konstanta di HexMetrics, jadi mari kita membuat shader HexMetrics.cginc termasuk file dan tempel definisi yang sesuai ke dalamnya. Skala ubin dasar dihitung dari ukuran fragmen dan jari-jari luar sel. Jika Anda menggunakan metrik lain, Anda harus memodifikasi file yang sesuai.

 #define OUTER_TO_INNER 0.866025404 #define OUTER_RADIUS 10 #define CHUNK_SIZE_X 5 #define TILING_SCALE (1 / (CHUNK_SIZE_X * 2 * OUTER_RADIUS / OUTER_TO_INNER)) 

Ini memberi kami skala ubin 0,00866025404. Jika kita menggunakan kelipatan integer dari nilai ini, maka tekstur tidak akan terpengaruh oleh teleportasi fragmen. Selain itu, tekstur di tepi timur dan barat peta akan bergabung dengan mulus setelah kami melakukan triangulasi koneksi dengan benar. Kami menggunakan 0,02

sebagai skala UV di Terrain shader . Sebagai gantinya, kita bisa menggunakan skala ubin berlipat ganda, yaitu 0,01732050808. Skala diperoleh sedikit kurang dari itu, dan skala tekstur telah meningkat sedikit, tetapi secara visual tidak terlihat.

  #include "../HexMetrics.cginc" #include "../HexCellData.cginc" … float4 GetTerrainColor (Input IN, int index) { float3 uvw = float3( IN.worldPos.xz * (2 * TILING_SCALE), IN.terrain[index] ); … } 

Dalam shader Roads untuk noise UV, kami menggunakan skala 0,025. Sebagai gantinya, Anda dapat menggunakan skala ubin tiga lapis. Ini memberi kita 0,02598076212, yang cukup dekat.

  #include "HexMetrics.cginc" #include "HexCellData.cginc" … void surf (Input IN, inout SurfaceOutputStandardSpecular o) { float4 noise = tex2D(_MainTex, IN.worldPos.xz * (3 * TILING_SCALE)); … } 

Akhirnya, di Water.cginc kami menggunakan 0,015 untuk busa dan 0,025 untuk gelombang. Di sini kita dapat kembali mengganti nilai-nilai ini dengan skala ubin berlipat ganda dan tiga kali lipat.

 #include "HexMetrics.cginc" float Foam (float shore, float2 worldXZ, sampler2D noiseTex) { shore = sqrt(shore) * 0.9; float2 noiseUV = worldXZ + _Time.y * 0.25; float4 noise = tex2D(noiseTex, noiseUV * (2 * TILING_SCALE)); … } … float Waves (float2 worldXZ, sampler2D noiseTex) { float2 uv1 = worldXZ; uv1.y += _Time.y; float4 noise1 = tex2D(noiseTex, uv1 * (3 * TILING_SCALE)); float2 uv2 = worldXZ; uv2.x += _Time.y; float4 noise2 = tex2D(noiseTex, uv2 * (3 * TILING_SCALE)); … } 

paket unity

Persatuan timur dan barat


Pada tahap ini, satu-satunya bukti visual untuk meminimalkan peta adalah celah kecil antara kolom paling timur dan paling barat. Kesenjangan ini terjadi karena kami belum melakukan triangulasi hubungan tepi dan sudut antara sel-sel di sisi yang berlawanan dari peta tanpa melipat.


Ruang di tepi.

Melipat tetangga


Untuk melakukan triangulasi pada koneksi timur-barat, kita perlu membuat sel-sel yang bersebelahan bersebelahan. Sejauh ini kami tidak melakukan ini, karena HexGrid.CreateCellkoneksi E - W dibuat dengan sel sebelumnya hanya jika indeksnya di X lebih besar dari nol. Untuk menutup koneksi ini, kita perlu menghubungkan sel terakhir dari baris dengan sel pertama di baris yang sama saat melipat peta.

  void CreateCell (int x, int z, int i) { … if (x > 0) { cell.SetNeighbor(HexDirection.W, cells[i - 1]); if (wrapping && x == cellCountX - 1) { cell.SetNeighbor(HexDirection.E, cells[i - x]); } } … } 

Setelah membangun koneksi tetangga E - W, kami memperoleh triangulasi sebagian dari celah tersebut. Sambungan tepi tidak ideal, karena distorsi disembunyikan secara tidak benar. Kami akan mengatasinya nanti.


Senyawa E - W.

Kami juga harus menutup tautan NE - SW. Ini dapat dilakukan dengan menghubungkan sel pertama dari setiap baris genap dengan sel terakhir dari baris sebelumnya. Itu hanya sel yang sebelumnya.

  if (z > 0) { if ((z & 1) == 0) { cell.SetNeighbor(HexDirection.SE, cells[i - cellCountX]); if (x > 0) { cell.SetNeighbor(HexDirection.SW, cells[i - cellCountX - 1]); } else if (wrapping) { cell.SetNeighbor(HexDirection.SW, cells[i - 1]); } } else { … } } 


Koneksi NE - SW.

Akhirnya, koneksi SE - NW dibuat pada akhir setiap garis ganjil di bawah yang pertama. Sel-sel ini harus terhubung ke sel pertama dari baris sebelumnya.

  if (z > 0) { if ((z & 1) == 0) { … } else { cell.SetNeighbor(HexDirection.SW, cells[i - cellCountX]); if (x < cellCountX - 1) { cell.SetNeighbor(HexDirection.SE, cells[i - cellCountX + 1]); } else if (wrapping) { cell.SetNeighbor( HexDirection.SE, cells[i - cellCountX * 2 + 1] ); } } } 


Senyawa SE - NW.

Lipat kebisingan


Untuk menyembunyikan celah dengan sempurna, kita perlu memastikan bahwa tepi timur dan barat peta cocok dengan kebisingan yang digunakan dengan sempurna untuk mendistorsi posisi simpul. Kita dapat menggunakan trik yang sama yang digunakan untuk shader, tetapi skala kebisingan 0,003 digunakan untuk distorsi. Untuk memastikan ubin, Anda perlu meningkatkan skala secara signifikan, yang akan menyebabkan distorsi yang lebih kacau dari simpul.

Solusi alternatif bukan untuk mengatasi kebisingan, tetapi untuk membuat pelemahan halus pada bagian tepi peta. Jika Anda melakukan pelemahan halus di sepanjang lebar satu sel, maka distorsi akan membuat transisi yang mulus tanpa celah. Kebisingan di daerah ini akan sedikit dihaluskan, dan dari jarak jauh perubahan akan tampak tajam, tetapi ini tidak begitu jelas ketika menggunakan sedikit distorsi dari simpul.

Bagaimana dengan fluktuasi suhu?
. , . , . , .

Jika kita tidak menutup kartu, maka kita dapat bertahan dengan HexMetrics.SampleNoisesatu sampel. Tetapi ketika melipat perlu menambahkan atenuasi. Oleh karena itu, sebelum mengembalikan sampel, simpan dalam variabel.

  public static Vector4 SampleNoise (Vector3 position) { Vector4 sample = noiseSource.GetPixelBilinear( position.x * noiseScale, position.z * noiseScale ); return sample; } 

Saat meminimalkan, kita perlu mencampur dengan sampel kedua. Kami akan melakukan transisi di bagian timur peta, sehingga sampel kedua harus dipindahkan ke barat.

  Vector4 sample = noiseSource.GetPixelBilinear( position.x * noiseScale, position.z * noiseScale ); if (Wrapping && position.x < innerDiameter) { Vector4 sample2 = noiseSource.GetPixelBilinear( (position.x + wrapSize * innerDiameter) * noiseScale, position.z * noiseScale ); } 

Redaman dilakukan dengan menggunakan interpolasi linier sederhana dari bagian barat ke bagian timur, melebihi lebar satu sel.

  if (Wrapping && position.x < innerDiameter) { Vector4 sample2 = noiseSource.GetPixelBilinear( (position.x + wrapSize * innerDiameter) * noiseScale, position.z * noiseScale ); sample = Vector4.Lerp( sample2, sample, position.x * (1f / innerDiameter) ); } 


Pencampuran kebisingan, solusi yang tidak sempurna.

Akibatnya, kami tidak mendapatkan kecocokan yang tepat, karena beberapa sel di sisi timur memiliki koordinat X negatif. Agar tidak mendekati area ini, mari kita pindahkan wilayah transisi ke barat setengah dari lebar sel.

  if (Wrapping && position.x < innerDiameter * 1.5f) { Vector4 sample2 = noiseSource.GetPixelBilinear( (position.x + wrapSize * innerDiameter) * noiseScale, position.z * noiseScale ); sample = Vector4.Lerp( sample2, sample, position.x * (1f / innerDiameter) - 0.5f ); } 


Atenuasi yang benar.

Pengeditan Sel


Sekarang triangulasi tampaknya benar, mari kita pastikan bahwa kita dapat mengedit semua yang ada di peta dan pada lipatan lipatan. Ternyata, dalam fragmen teleportasi, koordinatnya salah dan sikat besar dipotong oleh sebuah jahitan.


Kuas dipangkas.

Untuk mengatasinya, kita perlu melaporkan HexCoordinateslipatan. Kita dapat melakukan ini dengan mencocokkan koordinat X dalam metode konstruktor. Kita tahu bahwa koordinat aksial X diperoleh dari koordinat X offset dengan mengurangi setengah dari koordinat Z. Anda dapat menggunakan informasi ini untuk melakukan transformasi terbalik dan memeriksa apakah koordinat nol kurang dari nol. Jika demikian, maka kita memiliki koordinat di luar sisi timur peta yang terbuka. Karena di setiap arah yang kita teleport tidak lebih dari setengah peta, itu akan cukup bagi kita untuk menambahkan ukuran lipat ke X satu kali. Dan ketika koordinat offset lebih besar dari ukuran lipat, kita perlu melakukan pengurangan.

  public HexCoordinates (int x, int z) { if (HexMetrics.Wrapping) { int oX = x + z / 2; if (oX < 0) { x += HexMetrics.wrapSize; } else if (oX >= HexMetrics.wrapSize) { x -= HexMetrics.wrapSize; } } this.x = x; this.z = z; } 

Kadang-kadang ketika mengedit bagian bawah atau atas peta saya mendapat kesalahan.
Ini terjadi ketika, karena distorsi titik, kursor muncul di baris sel di luar peta. Ini adalah bug yang terjadi karena kami tidak mencocokkan koordinat HexGrid.GetCelldengan parameter vektor. Ini dapat diperbaiki dengan menerapkan metode GetCelldengan koordinat sebagai parameter yang akan melakukan pemeriksaan yang diperlukan.

  public HexCell GetCell (Vector3 position) { position = transform.InverseTransformPoint(position); HexCoordinates coordinates = HexCoordinates.FromPosition(position); // int index = // coordinates.X + coordinates.Z * cellCountX + coordinates.Z / 2; // return cells[index]; return GetCell(coordinates); } 

Pesisir lipat


Triangulasi bekerja dengan baik untuk medan, tetapi sepanjang lapisan timur-barat tidak ada tepi pantai air. Bahkan, mereka, mereka tidak runtuh. Mereka dibalik dan direntangkan ke sisi lain peta.


Tepi air tidak ada.

Ini terjadi, karena ketika melakukan triangulasi air pantai, kami menggunakan posisi tetangga. Untuk memperbaikinya, kita perlu menentukan apa yang kita hadapi, terletak di sisi lain kartu. Untuk menyederhanakan tugas, kami akan menambahkan HexCellkolom sel ke properti untuk indeks.

  public int ColumnIndex { get; set; } 

Tetapkan indeks ini ke HexGrid.CreateCell. Ini sama dengan koordinat offset X dibagi dengan ukuran fragmen.

  void CreateCell (int x, int z, int i) { … cell.Index = i; cell.ColumnIndex = x / HexMetrics.chunkSizeX; … } 

Sekarang kita dapat HexGridChunk.TriangulateWaterShoremenentukan apa yang diminimalkan dengan membandingkan indeks kolom sel saat ini dan tetangganya. Jika indeks kolom tetangga kurang dari satu langkah lebih sedikit, maka kita berada di sisi barat, dan tetangga di sisi timur. Karena itu, kita perlu mengubah tetangga kita ke barat. Sama dan berlawanan arah.

  Vector3 center2 = neighbor.Position; if (neighbor.ColumnIndex < cell.ColumnIndex - 1) { center2.x += HexMetrics.wrapSize * HexMetrics.innerDiameter; } else if (neighbor.ColumnIndex > cell.ColumnIndex + 1) { center2.x -= HexMetrics.wrapSize * HexMetrics.innerDiameter; } 


Iga pantai, tapi tidak ada sudut.

Jadi kami merawat tulang rusuk pantai, tetapi sejauh ini tidak berurusan dengan sudut. Kita perlu melakukan hal yang sama dengan tetangga berikutnya.

  if (nextNeighbor != null) { Vector3 center3 = nextNeighbor.Position; if (nextNeighbor.ColumnIndex < cell.ColumnIndex - 1) { center3.x += HexMetrics.wrapSize * HexMetrics.innerDiameter; } else if (nextNeighbor.ColumnIndex > cell.ColumnIndex + 1) { center3.x -= HexMetrics.wrapSize * HexMetrics.innerDiameter; } Vector3 v3 = center3 + (nextNeighbor.IsUnderwater ? HexMetrics.GetFirstWaterCorner(direction.Previous()) : HexMetrics.GetFirstSolidCorner(direction.Previous())); … } 


Pantai yang dibatasi dengan benar.

Pembuatan kartu


Pilihan untuk menghubungkan sisi timur dan barat mempengaruhi pembuatan peta. Saat meminimalkan peta, algoritme pembangkitan juga harus diminimalkan. Ini akan mengarah pada pembuatan peta lain, tetapi saat menggunakan Map Border X yang tidak nol , pelipatan tidak jelas.



Peta besar 1208905299 dengan pengaturan default. Dengan melipat dan tanpa itu.

Ketika diminimalkan tidak masuk akal untuk menggunakan Border Peta X . Tapi kita tidak bisa begitu saja menyingkirkannya, karena pada saat yang sama daerah akan bergabung. Saat meminimalisasi, kita bisa menggunakan RegionBorder .

Kami berubah HexMapGenerator.CreateRegions, mengganti dalam semua kasus mapBorderXdengan borderX. Variabel baru ini akan sama dengan atau regionBorder, atau mapBorderX, tergantung pada nilai opsi runtuh. Di bawah ini saya menunjukkan perubahan hanya untuk kasus pertama.

  int borderX = grid.wrapping ? regionBorder : mapBorderX; MapRegion region; switch (regionCount) { default: region.xMin = borderX; region.xMax = grid.cellCountX - borderX; region.zMin = mapBorderZ; region.zMax = grid.cellCountZ - mapBorderZ; regions.Add(region); break; … } 

Pada saat yang sama, daerah-daerah tetap terpisah, tetapi ini hanya diperlukan jika ada daerah yang berbeda di sisi timur dan barat peta. Ada dua kasus di mana ini tidak dihormati. Yang pertama adalah ketika kita hanya memiliki satu wilayah. Yang kedua adalah ketika ada dua wilayah yang membagi peta secara horizontal. Dalam kasus ini, kita dapat menetapkan borderXnilai nol, yang akan memungkinkan massa tanah untuk melintasi lapisan timur-barat.

  switch (regionCount) { default: if (grid.wrapping) { borderX = 0; } region.xMin = borderX; region.xMax = grid.cellCountX - borderX; region.zMin = mapBorderZ; region.zMax = grid.cellCountZ - mapBorderZ; regions.Add(region); break; case 2: if (Random.value < 0.5f) { … } else { if (grid.wrapping) { borderX = 0; } region.xMin = borderX; region.xMax = grid.cellCountX - borderX; region.zMin = mapBorderZ; region.zMax = grid.cellCountZ / 2 - regionBorder; regions.Add(region); region.zMin = grid.cellCountZ / 2 + regionBorder; region.zMax = grid.cellCountZ - mapBorderZ; regions.Add(region); } break; … } 


Satu wilayah runtuh.

Sekilas, tampaknya semuanya bekerja dengan benar, tetapi sebenarnya ada celah di sepanjang jahitan. Ini menjadi lebih terlihat jika Anda mengatur Persentase Erosi ke nol.



Ketika erosi dinonaktifkan, jahitan pada relief menjadi nyata.

Kesenjangan terjadi karena lapisan mencegah pertumbuhan fragmen relief. Untuk menentukan apa yang ditambahkan terlebih dahulu, jarak dari sel ke pusat fragmen digunakan, dan sel-sel di sisi lain peta bisa sangat jauh, sehingga mereka hampir tidak pernah menyala. Tentu saja ini salah. Kita perlu memastikan bahwa kita HexCoordinates.DistanceTotahu tentang peta yang diperkecil.

Kami menghitung jarak antara HexCoordinates, menjumlahkan jarak absolut di sepanjang masing-masing dari tiga sumbu dan membagi dua hasilnya. Jarak sepanjang Z selalu benar, tetapi melipat di sepanjang dapat mempengaruhi jarak X dan Y. Jadi mari kita mulai dengan perhitungan X + Y yang terpisah.

  public int DistanceTo (HexCoordinates other) { // return // ((x < other.x ? other.x - x : x - other.x) + // (Y < other.Y ? other.Y - Y : Y - other.Y) + // (z < other.z ? other.z - z : z - other.z)) / 2; int xy = (x < other.x ? other.x - x : x - other.x) + (Y < other.Y ? other.Y - Y : Y - other.Y); return (xy + (z < other.z ? other.z - z : z - other.z)) / 2; } 

Menentukan apakah melipat membuat jarak yang lebih pendek untuk sel arbitrer bukanlah tugas yang mudah, jadi mari kita menghitung X + Y untuk kasus di mana kita melipat koordinat lain ke sisi barat. Jika nilainya kurang dari X + Y asli, maka gunakan.

  int xy = (x < other.x ? other.x - x : x - other.x) + (Y < other.Y ? other.Y - Y : Y - other.Y); if (HexMetrics.Wrapping) { other.x += HexMetrics.wrapSize; int xyWrapped = (x < other.x ? other.x - x : x - other.x) + (Y < other.Y ? other.Y - Y : Y - other.Y); if (xyWrapped < xy) { xy = xyWrapped; } } 

Jika ini tidak mengarah ke jarak yang lebih pendek, maka dimungkinkan untuk berbelok lebih pendek ke arah lain, jadi kami akan memeriksanya.

  if (HexMetrics.Wrapping) { other.x += HexMetrics.wrapSize; int xyWrapped = (x < other.x ? other.x - x : x - other.x) + (Y < other.Y ? other.Y - Y : Y - other.Y); if (xyWrapped < xy) { xy = xyWrapped; } else { other.x -= 2 * HexMetrics.wrapSize; xyWrapped = (x < other.x ? other.x - x : x - other.x) + (Y < other.Y ? other.Y - Y : Y - other.Y); if (xyWrapped < xy) { xy = xyWrapped; } } } 

Sekarang kami selalu mendapatkan jarak terpendek pada peta yang dapat dilipat. Fragmen medan tidak lagi terhalang oleh jahitan, yang memungkinkan massa daratan meringkuk.



Melipat relief dengan benar tanpa erosi dan erosi.

paket unity

Berkeliling dunia


Setelah mempertimbangkan pembuatan peta dan triangulasi, sekarang mari kita beralih ke memeriksa regu, eksplorasi, dan visibilitas.

Uji jahitan


Kendala pertama yang kita temui ketika memindahkan skuad di seluruh dunia adalah tepi peta, yang tidak dapat dieksplorasi.


Jahitan kartu tidak dapat diperiksa.

Sel-sel di sepanjang tepi peta dibuat tidak dijelajahi untuk menyembunyikan penyelesaian peta yang tiba-tiba. Tetapi ketika peta diminimalkan, hanya sel utara dan selatan yang harus ditandai, tetapi tidak di timur dan barat. Ubah HexGrid.CreateCelluntuk mempertimbangkan ini.

  if (wrapping) { cell.Explorable = z > 0 && z < cellCountZ - 1; } else { cell.Explorable = x > 0 && z > 0 && x < cellCountX - 1 && z < cellCountZ - 1; } 

Visibilitas fitur bantuan


Sekarang mari kita periksa apakah visibilitas bekerja di sepanjang jahitan. Ini berfungsi untuk medan, tetapi tidak untuk objek medan. Sepertinya benda yang kolaps mendapatkan visibilitas sel terakhir yang tidak kolaps.


Visibilitas objek yang salah.

Ini terjadi karena mode HexCellShaderDatapenjepit diatur untuk mode lipat tekstur yang digunakan . Untuk mengatasi masalah, cukup ubah mode klemnya untuk mengulang. Tetapi kita perlu melakukan ini hanya untuk koordinat U, jadi Initializekita akan mengaturnya wrapModeUsecara wrapModeVterpisah.

  public void Initialize (int x, int z) { if (cellTexture) { cellTexture.Resize(x, z); } else { cellTexture = new Texture2D( x, z, TextureFormat.RGBA32, false, true ); cellTexture.filterMode = FilterMode.Point; // cellTexture.wrapMode = TextureWrapMode.Clamp; cellTexture.wrapModeU = TextureWrapMode.Repeat; cellTexture.wrapModeV = TextureWrapMode.Clamp; Shader.SetGlobalTexture("_HexCellData", cellTexture); } … } 

Pasukan dan Kolom


Masalah lain adalah bahwa unit belum runtuh. Setelah memindahkan kolom di mana mereka berada, unit tetap di tempat yang sama.


Unit tidak ditransfer dan berada di sisi yang salah.

Masalah ini dapat diatasi dengan membuat elemen anak-anak regu kolom, seperti yang kita lakukan dengan fragmen. Pertama, kita tidak akan lagi menjadikan mereka anak langsung dari jaringan HexGrid.AddUnit.

  public void AddUnit (HexUnit unit, HexCell location, float orientation) { units.Add(unit); unit.Grid = this; // unit.transform.SetParent(transform, false); unit.Location = location; unit.Orientation = orientation; } 

Karena unit bergerak, mereka dapat muncul di kolom lain, yaitu, akan perlu untuk mengubah induknya. Untuk memungkinkan ini, kami menambah HexGridmetode umum MakeChildOfColumn, dan sebagai parameter kami meneruskannya komponen Transformelemen anak dan indeks kolom.

  public void MakeChildOfColumn (Transform child, int columnIndex) { child.SetParent(columns[columnIndex], false); } 

Kami akan memanggil metode ini ketika properti diatur HexUnit.Location.

  public HexCell Location { … set { … Grid.MakeChildOfColumn(transform, value.ColumnIndex); } } 

Ini memecahkan masalah membuat unit. Tetapi kita juga perlu membuatnya bergerak ke kolom yang diinginkan saat bergerak. Untuk melakukan ini, Anda perlu melacak HexUnit.TravelPathkolom saat ini dalam indeks. Di awal metode ini, ini adalah indeks kolom sel di awal jalan, atau yang saat ini jika langkah itu terputus oleh kompilasi ulang.

  IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; yield return LookAt(pathToTravel[1].Position); // Grid.DecreaseVisibility( // currentTravelLocation ? currentTravelLocation : pathToTravel[0], // VisionRange // ); if (!currentTravelLocation) { currentTravelLocation = pathToTravel[0]; } Grid.DecreaseVisibility(currentTravelLocation, VisionRange); int currentColumn = currentTravelLocation.ColumnIndex; … } 

Selama setiap iterasi langkah, kami akan memeriksa apakah indeks kolom berikutnya berbeda, dan jika demikian, maka kami akan mengubah induk dari urutan.

  int currentColumn = currentTravelLocation.ColumnIndex; float t = Time.deltaTime * travelSpeed; for (int i = 1; i < pathToTravel.Count; i++) { … Grid.IncreaseVisibility(pathToTravel[i], VisionRange); int nextColumn = currentTravelLocation.ColumnIndex; if (currentColumn != nextColumn) { Grid.MakeChildOfColumn(transform, nextColumn); currentColumn = nextColumn; } … } 

Ini akan memungkinkan unit untuk bergerak mirip dengan fragmen. Namun, ketika bergerak melalui jahitan kartu, unit belum runtuh. Sebaliknya, mereka tiba-tiba mulai bergerak ke arah yang salah. Ini terjadi terlepas dari lokasi jahitan, tetapi paling mencolok ketika mereka melompati seluruh peta.


Balap kuda melintasi peta.

Di sini kita bisa menggunakan pendekatan yang sama yang digunakan untuk pantai, hanya kali ini kita akan memutar kurva di mana detasemen bergerak. Jika kolom berikutnya berbelok ke timur, maka kita akan memindahkan kurva juga ke timur, sama untuk arah lainnya. Anda perlu mengubah titik kontrol kurva adan b, yang juga akan mempengaruhi titik kontrol c.

  for (int i = 1; i < pathToTravel.Count; i++) { currentTravelLocation = pathToTravel[i]; a = c; b = pathToTravel[i - 1].Position; // c = (b + currentTravelLocation.Position) * 0.5f; // Grid.IncreaseVisibility(pathToTravel[i], VisionRange); int nextColumn = currentTravelLocation.ColumnIndex; if (currentColumn != nextColumn) { if (nextColumn < currentColumn - 1) { ax -= HexMetrics.innerDiameter * HexMetrics.wrapSize; bx -= HexMetrics.innerDiameter * HexMetrics.wrapSize; } else if (nextColumn > currentColumn + 1) { ax += HexMetrics.innerDiameter * HexMetrics.wrapSize; bx += HexMetrics.innerDiameter * HexMetrics.wrapSize; } Grid.MakeChildOfColumn(transform, nextColumn); currentColumn = nextColumn; } c = (b + currentTravelLocation.Position) * 0.5f; Grid.IncreaseVisibility(pathToTravel[i], VisionRange); … } 


Gerakan dengan melipat.

Hal terakhir yang harus dilakukan adalah mengubah giliran awal regu ketika melihat sel pertama yang akan dipindahkan. Jika sel ini berada di sisi lain dari lapisan timur-barat, unit akan melihat ke arah yang salah.

Saat meminimalkan peta, ada dua cara untuk melihat titik yang tidak persis di utara atau selatan. Anda dapat melihat timur atau barat. Akan logis untuk melihat ke arah yang sesuai dengan jarak terdekat ke titik, karena itu juga arah gerakan, jadi mari kita gunakan LookAt.

Ketika meminimalkan, kita akan memeriksa jarak relatif sepanjang sumbu X. Jika kurang dari setengah negatif dari lebar peta, maka kita harus melihat ke barat, yang dapat dilakukan dengan memutar titik ke barat. Jika tidak, jika jaraknya lebih dari setengah lebar peta, maka kita harus jatuh ke timur.

  IEnumerator LookAt (Vector3 point) { if (HexMetrics.Wrapping) { float xDistance = point.x - transform.localPosition.x; if (xDistance < -HexMetrics.innerRadius * HexMetrics.wrapSize) { point.x += HexMetrics.innerDiameter * HexMetrics.wrapSize; } else if (xDistance > HexMetrics.innerRadius * HexMetrics.wrapSize) { point.x -= HexMetrics.innerDiameter * HexMetrics.wrapSize; } } … } 

Jadi, kami memiliki peta minimal yang berfungsi penuh. Dan ini menyimpulkan serangkaian tutorial tentang peta segi enam. Seperti disebutkan di bagian sebelumnya, topik lain dapat dipertimbangkan, tetapi mereka tidak spesifik untuk peta segi enam. Mungkin saya akan mempertimbangkannya di seri tutorial mendatang.

Saya mengunduh paket terakhir dan mendapatkan kesalahan belokan dalam mode Putar
, Rotation . . . 5.

Saya mengunduh paket terakhir dan gambarnya tidak seindah di screenshot
. - .

Saya mengunduh paket terakhir dan secara konstan menghasilkan kartu yang sama
seed (1208905299), . , Use Fixed Seed .

paket unity

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


All Articles