Bagian 1-3: jala, warna, dan tinggi selBagian 4-7: gundukan, sungai, dan jalanBagian 8-11: air, bentang alam, dan bentengBagian 12-15: menyimpan dan memuat, tekstur, jarakBagian 16-19: menemukan jalan, regu pemain, animasiBagian 20-23: Kabut Perang, Penelitian Peta, Generasi ProseduralBagian 24-27: siklus air, erosi, bioma, peta silindrisBagian 4: Kekasaran
Daftar isi
- Cicipi tekstur suara.
- Pindahkan simpul.
- Kami menjaga kerataan sel.
- Membagi lagi tepi sel.
Sementara kisi kami adalah pola ketat sarang lebah. Di bagian ini, kami akan menambahkan tonjolan untuk membuat peta terlihat lebih alami.
Tidak ada lagi segi enam.Kebisingan
Untuk menambahkan benjolan, kita perlu pengacakan, tetapi bukan pengacakan yang benar. Kami ingin semuanya konsisten saat mengubah peta. Kalau tidak, ketika Anda membuat perubahan, objek akan melompat. Yaitu, kita memerlukan beberapa bentuk pseudo-random noise yang dapat direproduksi.
Calon yang baik adalah kebisingan Perlin. Ini dapat direproduksi di mana saja. Ketika menggabungkan beberapa frekuensi, itu juga menciptakan kebisingan, yang dapat sangat bervariasi pada jarak yang jauh, tetapi tetap hampir sama pada jarak yang kecil. Berkat ini, distorsi yang relatif lancar dapat dibuat. Poin yang berdekatan satu sama lain biasanya tetap di dekatnya, dan tidak tersebar di arah yang berlawanan.
Kami dapat menghasilkan suara Perlin secara terprogram. Dalam tutorial
Kebisingan , saya menjelaskan cara melakukan ini. Tapi kami juga bisa mencicipi dari tekstur noise yang dihasilkan sebelumnya. Keuntungan menggunakan tekstur adalah lebih sederhana dan lebih cepat daripada menghitung suara multi-frekuensi Perlin. Kerugiannya adalah teksturnya memakan lebih banyak memori dan hanya mencakup sedikit kebisingan. Oleh karena itu, ia harus terhubung secara mulus dan cukup besar sehingga pengulangan tidak mencolok.
Tekstur kebisingan
Kami akan menggunakan tekstur, sehingga tutorial
Kebisingan adalah opsional. Jadi kita perlu tekstur. Ini dia:
Menghubungkan tekstur perlin noise dengan mulus.Tekstur yang ditunjukkan di atas mengandung suara multi-frekuensi Perlin yang mulus. Ini adalah gambar skala abu-abu. Nilai rata-rata adalah 0,5, dan nilai ekstrim cenderung 0 dan 1.
Tapi tunggu, hanya ada satu nilai untuk setiap poin. Jika kita membutuhkan distorsi 3D, maka kita memerlukan setidaknya tiga sampel pseudo-acak! Karena itu, kita perlu dua tekstur lagi dengan noise berbeda.
Kita dapat membuat mereka atau menyimpan nilai noise yang berbeda di masing-masing saluran warna. Ini akan memungkinkan kita untuk menyimpan hingga empat pola noise dalam satu tekstur. Ini teksturnya.
Empat dalam satu.Bagaimana cara membuat tekstur seperti itu?Saya menggunakan
NumberFlow . Ini adalah editor tekstur prosedural yang saya buat untuk Unity.
Unduh tekstur ini dan impor ke proyek Unity Anda. Karena kita akan mengambil sampel tekstur melalui kode, itu harus dapat dibaca.
Ubah Jenis Tekstur ke
Tingkat Lanjut dan aktifkan
Baca / Tulis Diaktifkan . Ini akan menyimpan data tekstur dalam memori dan dapat diakses dari kode C #. Atur
Format ke
Automatic Truecolor , jika tidak, tidak ada yang akan berfungsi. Kami tidak ingin kompresi tekstur merusak pola noise kami.
Anda dapat menonaktifkan
Generate Mip Maps , karena kami tidak memerlukannya. Aktifkan juga
Bypass sRGB Sampling . Kita tidak akan membutuhkan ini, tetapi memang akan begitu. Parameter ini menunjukkan bahwa tekstur tidak mengandung data warna dalam ruang gamma.
Tekstur kebisingan yang diimpor.
Kapan pengambilan sampel sRGB penting?Jika kita ingin menggunakan tekstur dalam shader, itu akan membuat perbedaan. Saat menggunakan mode render Linear, pengambilan sampel tekstur secara otomatis mengubah data warna dari gamut ke ruang warna linier. Dalam hal tekstur suara kami, ini akan mengarah pada hasil yang salah, jadi kami tidak memerlukan ini.
Mengapa pengaturan impor tekstur saya terlihat berbeda?Mereka berubah setelah tutorial ini ditulis. Anda perlu menggunakan pengaturan tekstur 2D default, sRGB (Tekstur Warna) harus dinonaktifkan, dan Kompresi harus diatur ke Tidak ada .
Pengambilan sampel derau
Mari menambahkan fungsionalitas pengambilan sampel derau ke
HexMetrics
sehingga Anda dapat menggunakannya di mana saja. Ini berarti bahwa
HexMetrics
harus mengandung referensi ke tekstur noise.
public static Texture2D noiseSource;
Karena ini bukan komponen, kami tidak dapat menetapkan tekstur melalui editor. Karena itu, sebagai perantara, kami menggunakan
HexGrid
. Karena
HexGrid
akan bertindak lebih dulu, itu akan baik-baik saja jika kita melewatkan tekstur di awal metode
Awake
-nya.
public Texture2D noiseSource; void Awake () { HexMetrics.noiseSource = noiseSource; β¦ }
Namun, pendekatan ini tidak akan selamat dari kompilasi dalam mode Play. Variabel statis tidak diserialisasi oleh mesin Unity. Untuk mengatasi masalah ini, tetapkan kembali tekstur dalam metode acara
OnEnable
. Metode ini akan dipanggil setelah kompilasi ulang.
void OnEnable () { HexMetrics.noiseSource = noiseSource; }
Tetapkan tekstur kebisingan.Sekarang
HexMetrics
memiliki akses ke tekstur, mari tambahkan metode pengambilan sampel noise yang nyaman. Metode ini mengambil posisi di dunia dan membuat vektor 4D yang berisi empat sampel derau.
public static Vector4 SampleNoise (Vector3 position) { }
Sampel dibuat dengan mengambil sampel tekstur menggunakan bilinear filtering, di mana koordinat dunia X dan Z digunakan sebagai koordinat UV.Karena sumber kebisingan kami adalah dua dimensi, kami mengabaikan koordinat ketiga dunia. Jika sumber kebisingan tiga dimensi, kami juga akan menggunakan koordinat Y.
Hasilnya, kami mendapatkan warna yang dapat dikonversi ke vektor 4D. Pengurangan seperti itu bisa tidak langsung, yaitu, kita dapat mengembalikan warna secara langsung, tidak termasuk secara eksplisit
(Vector4)
.
public static Vector4 SampleNoise (Vector3 position) { return noiseSource.GetPixelBilinear(position.x, position.z); }
paket unityGerakan verteks
Kami akan mendistorsi jaringan sarang lebah yang halus, masing-masing menggerakkan masing-masing simpul. Untuk melakukan ini, mari kita tambahkan metode
Perturb
ke
Perturb
. Dibutuhkan titik tidak bergerak dan mengembalikan yang dipindahkan. Untuk melakukan ini, ia menggunakan titik yang tidak bergeser ketika mengambil sampel noise.
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); }
Mari kita tambahkan sampel noise X, Y, dan Z langsung ke titik koordinat yang sesuai dan gunakan ini sebagai hasilnya.
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); position.x += sample.x; position.y += sample.y; position.z += sample.z; return position; }
Bagaimana kita dengan cepat mengubah
HexMesh
untuk memindahkan semua simpul?
AddTriangle
mengubah setiap simpul saat menambahkan simpul ke daftar di metode
AddTriangle
dan
AddQuad
. Ayo lakukan.
void AddTriangle (Vector3 v1, Vector3 v2, Vector3 v3) { int vertexIndex = vertices.Count; vertices.Add(Perturb(v1)); vertices.Add(Perturb(v2)); vertices.Add(Perturb(v3)); β¦ } void AddQuad (Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4) { int vertexIndex = vertices.Count; vertices.Add(Perturb(v1)); vertices.Add(Perturb(v2)); vertices.Add(Perturb(v3)); vertices.Add(Perturb(v4)); β¦ }
Apakah quadrangles akan tetap rata setelah memindahkan simpul mereka?Kemungkinan besar tidak. Mereka terdiri dari dua segitiga yang tidak lagi terletak di bidang yang sama. Namun, karena segitiga ini memiliki dua simpul yang sama, normalnya dari simpul-simpul ini akan dihaluskan. Ini berarti bahwa kita tidak akan memiliki transisi yang tajam antara dua segitiga. Jika distorsi tidak terlalu besar, maka kita akan tetap menganggap quadrangles sebagai datar.
Verteks dipindahkan atau tidak.Meskipun perubahannya tidak terlalu terlihat, hanya label sel yang hilang. Ini terjadi karena kami menambahkan sampel suara ke titik, dan mereka selalu positif. Karena itu, sebagai hasilnya, semua segitiga naik di atas tanda mereka, menutupnya. Kita harus memusatkan perubahan agar terjadi di kedua arah. Ubah interval sampel derau dari 0-1 menjadi -1-1.
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); position.x += sample.x * 2f - 1f; position.y += sample.y * 2f - 1f; position.z += sample.z * 2f - 1f; return position; }
Perpindahan terpusat.Besarnya (kekuatan) perpindahan
Sekarang sudah jelas bahwa kami mendistorsi grid, tetapi efeknya hampir tidak terlihat. Perubahan di setiap dimensi tidak lebih dari 1 unit. Artinya, perpindahan maksimum teoritis adalah β3 β 1,73 unit, yang akan terjadi sangat jarang, jika sama sekali. Karena jari-jari luar sel adalah 10 unit, perpindahannya relatif kecil.
Solusinya adalah menambahkan parameter
HexMetrics
ke
HexMetrics
sehingga Anda dapat mengatur skala gerakan. Mari kita coba menggunakan kekuatan 5. Dalam hal ini, perpindahan maksimum teoritis akan menjadi β75 β 8,66 unit, yang jauh lebih terlihat.
public const float cellPerturbStrength = 5f;
Kami menerapkan kekuatan dengan mengalikannya dengan sampel di
HexMesh.Perturb
.
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); position.x += (sample.x * 2f - 1f) * HexMetrics.cellPerturbStrength; position.y += (sample.y * 2f - 1f) * HexMetrics.cellPerturbStrength; position.z += (sample.z * 2f - 1f) * HexMetrics.cellPerturbStrength; return position; }
Peningkatan Kekuatan.Skala kebisingan
Meskipun grid terlihat bagus sebelum perubahan, semuanya mungkin salah setelah tepian muncul. Puncaknya dapat terdistorsi ke arah yang berbeda-beda, menciptakan kekacauan. Saat menggunakan kebisingan Perlin, ini seharusnya tidak terjadi.
Masalah muncul karena kita langsung menggunakan koordinat dunia untuk mencicipi kebisingan. Karena itu, teksturnya tersembunyi melalui setiap unit, dan sel-selnya jauh lebih besar dari nilai ini. Bahkan, teksturnya diambil sampelnya pada titik-titik yang berubah-ubah, sehingga merusak integritasnya.
Baris 10 dengan 10 sel tumpang tindih kotak.Kami harus mengukur noise sampling sehingga teksturnya mencakup area yang jauh lebih besar. Mari tambahkan skala ini ke
HexMetrics
dan berikan nilai 0,003, lalu skala koordinat sampel dengan faktor ini.
public const float noiseScale = 0.003f; public static Vector4 SampleNoise (Vector3 position) { return noiseSource.GetPixelBilinear( position.x * noiseScale, position.z * noiseScale ); }
Tiba-tiba ternyata tekstur kami mencakup 333 & frac13; unit persegi, dan integritas lokalnya menjadi jelas.
Suara bersisik.Selain itu, skala baru meningkatkan jarak antara sambungan kebisingan. Faktanya, karena sel memiliki diameter internal 10β3 unit, itu tidak akan pernah benar-benar ubin di dimensi X. Namun, karena integritas lokal kebisingan, pada skala yang lebih besar, kita masih akan dapat mengenali pola berulang, kira-kira setiap 20 sel, bahkan jika detailnya tidak cocok. Tetapi mereka hanya akan terlihat jelas di peta tanpa fitur karakteristik lainnya.
paket unityMenyelaraskan pusat sel
Memindahkan semua simpul memberikan peta tampilan yang lebih alami, tetapi ada beberapa masalah. Karena sel sekarang bergerigi, label mereka bersinggungan dengan jala. Dan di sendi tepian dengan tebing, retakan muncul. Kita akan meninggalkan celah untuk nanti, tetapi sekarang kita akan fokus pada permukaan sel.
Peta menjadi kurang ketat, tetapi lebih banyak masalah muncul.Cara termudah untuk menyelesaikan masalah persimpangan adalah membuat pusat sel rata. Mari kita tidak mengubah koordinat Y di
HexMesh.Perturb
.
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); position.x += (sample.x * 2f - 1f) * HexMetrics.cellPerturbStrength;
Sel selaras.Dengan perubahan ini, semua posisi vertikal akan tetap tidak berubah, baik di pusat sel maupun di langkah tepian. Perlu dicatat bahwa ini mengurangi perpindahan maksimum menjadi β50 β 7.07 hanya di bidang XZ.
Ini adalah perubahan yang baik, karena menyederhanakan identifikasi sel-sel individual dan tidak memungkinkan tepian menjadi terlalu kacau. Tetapi akan tetap menyenangkan untuk menambahkan sedikit gerakan vertikal.
Pindahkan ketinggian sel
Alih-alih menerapkan gerakan vertikal ke setiap titik, kita bisa menerapkannya ke sel. Dalam hal ini, setiap sel akan tetap rata, tetapi variabilitas akan tetap ada di antara sel-sel tersebut. Juga logis untuk menggunakan skala yang berbeda untuk memindahkan ketinggian, jadi tambahkan ke
HexMetrics
. Kekuatan 1,5 unit menciptakan sedikit variasi, kira-kira sama dengan ketinggian satu langkah langkan.
public const float elevationPerturbStrength = 1.5f;
Ubah properti
HexCell.Elevation
sehingga
HexCell.Elevation
langkah ini ke posisi vertikal sel.
public int Elevation { get { return elevation; } set { elevation = value; Vector3 position = transform.localPosition; position.y = value * HexMetrics.elevationStep; position.y += (HexMetrics.SampleNoise(position).y * 2f - 1f) * HexMetrics.elevationPerturbStrength; transform.localPosition = position; Vector3 uiPosition = uiRect.localPosition; uiPosition.z = -position.y; uiRect.localPosition = uiPosition; } }
Agar langkah diterapkan segera, kita perlu secara eksplisit mengatur ketinggian setiap sel di
HexGrid.CreateCell
. Kalau tidak, grid awalnya akan datar. Mari kita lakukan di bagian akhir, setelah membuat UI.
void CreateCell (int x, int z, int i) { β¦ cell.Elevation = 0; }
Ketinggian pengungsian dengan retakan.Menggunakan ketinggian yang sama
Banyak retakan muncul di mesh, karena ketika kita melakukan triangulasi mesh, kita tidak menggunakan ketinggian sel yang sama. Mari kita tambahkan properti ke
HexCell
untuk mendapatkan posisinya sehingga Anda dapat menggunakannya di mana saja.
public Vector3 Position { get { return transform.localPosition; } }
Sekarang kita bisa menggunakan properti ini di
HexMesh.Triangulate
untuk menentukan pusat sel.
void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.Position; β¦ }
Dan kita dapat menggunakannya dalam
TriangulateConnection
ketika mendefinisikan posisi vertikal sel tetangga.
void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { β¦ Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; v3.y = v4.y = neighbor.Position.y; β¦ HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (direction <= HexDirection.E && nextNeighbor != null) { Vector3 v5 = v2 + HexMetrics.GetBridge(direction.Next()); v5.y = nextNeighbor.Position.y; β¦ } }
Penggunaan tinggi sel yang konsisten.paket unityUnit Tepi Sel
Meskipun sel-sel memiliki variasi yang indah, mereka tetap terlihat seperti segi enam yang jelas. Ini sendiri bukan masalah, tapi kita bisa memperbaiki penampilan mereka.
Sel heksagonal terlihat jelas.Jika kita memiliki lebih banyak simpul, maka akan ada variabilitas lokal yang lebih besar. Jadi mari kita membagi setiap tepi sel menjadi dua bagian dengan menambahkan bagian atas tepi di tengah antara setiap sudut. Ini berarti
HexMesh.Triangulate
harus menambahkan bukan hanya satu, tetapi dua segitiga.
void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.Position; Vector3 v1 = center + HexMetrics.GetFirstSolidCorner(direction); Vector3 v2 = center + HexMetrics.GetSecondSolidCorner(direction); Vector3 e1 = Vector3.Lerp(v1, v2, 0.5f); AddTriangle(center, v1, e1); AddTriangleColor(cell.color); AddTriangle(center, e1, v2); AddTriangleColor(cell.color); if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, v1, v2); } }
Dua belas sisi bukan enam.Menggandakan simpul dan segitiga menambah lebih banyak variabilitas ke tepi sel. Mari kita membuatnya lebih tidak rata dengan melipattigakan jumlah simpul.
Vector3 e1 = Vector3.Lerp(v1, v2, 1f / 3f); Vector3 e2 = Vector3.Lerp(v1, v2, 2f / 3f); AddTriangle(center, v1, e1); AddTriangleColor(cell.color); AddTriangle(center, e1, e2); AddTriangleColor(cell.color); AddTriangle(center, e2, v2); AddTriangleColor(cell.color);
18 sisi.Rib Joint Division
Tentu saja, kita juga perlu membagi tepi sambungan. Oleh karena itu, kami akan meneruskan tepi simpul baru ke
TriangulateConnection
.
if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, v1, e1, e2, v2); }
Tambahkan parameter yang sesuai ke
TriangulateConnection
sehingga dapat bekerja dengan simpul tambahan.
void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 e1, Vector3 e2, Vector3 v2 ) { β¦ }
Kita juga perlu menghitung tepi tambahan tepi untuk sel tetangga. Kita dapat menghitungnya setelah menghubungkan jembatan ke sisi lain.
Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; v3.y = v4.y = neighbor.Position.y; Vector3 e3 = Vector3.Lerp(v3, v4, 1f / 3f); Vector3 e4 = Vector3.Lerp(v3, v4, 2f / 3f);
Selanjutnya kita perlu mengubah triangulasi tulang rusuk. Sampai kita mengabaikan lereng dengan tepian, tambahkan saja tiga bukannya satu quad.
if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(v1, v2, cell, v3, v4, neighbor); } else { AddQuad(v1, e1, v3, e3); AddQuadColor(cell.color, neighbor.color); AddQuad(e1, e2, e3, e4); AddQuadColor(cell.color, neighbor.color); AddQuad(e2, v2, e4, v4); AddQuadColor(cell.color, neighbor.color); }
Koneksi terpecah.Penyatuan ujung-ujungnya
Karena untuk menggambarkan tepi kita sekarang membutuhkan empat simpul, akan logis untuk menggabungkannya menjadi satu set. Ini lebih nyaman daripada bekerja dengan empat simpul independen. Buat struktur
EdgeVertices
sederhana untuk ini. Seharusnya berisi empat simpul searah jarum jam di sepanjang tepi sel.
using UnityEngine; public struct EdgeVertices { public Vector3 v1, v2, v3, v4; }
Bukankah mereka harus serial?Kami akan menggunakan struktur ini hanya untuk triangulasi. Pada tahap ini, kita tidak perlu menyimpan simpul-simpul tepi, sehingga tidak perlu diserialisasi.
Tambahkan metode konstruktor yang nyaman untuk itu, yang akan berurusan dengan perhitungan titik tengah tepi.
public EdgeVertices (Vector3 corner1, Vector3 corner2) { v1 = corner1; v2 = Vector3.Lerp(corner1, corner2, 1f / 3f); v3 = Vector3.Lerp(corner1, corner2, 2f / 3f); v4 = corner2; }
Sekarang kita dapat menambahkan metode triangulasi terpisah ke
HexMesh
untuk membuat penggemar segitiga di antara pusat sel dan salah satu ujungnya.
void TriangulateEdgeFan (Vector3 center, EdgeVertices edge, Color color) { AddTriangle(center, edge.v1, edge.v2); AddTriangleColor(color); AddTriangle(center, edge.v2, edge.v3); AddTriangleColor(color); AddTriangle(center, edge.v3, edge.v4); AddTriangleColor(color); }
Dan metode untuk melakukan triangulasi strip segi empat di antara dua sisi.
void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2 ) { AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); AddQuadColor(c1, c2); AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); AddQuadColor(c1, c2); AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); AddQuadColor(c1, c2); }
Ini akan memungkinkan kita untuk menyederhanakan metode
Triangulate
.
void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.Position; EdgeVertices e = new EdgeVertices( center + HexMetrics.GetFirstSolidCorner(direction), center + HexMetrics.GetSecondSolidCorner(direction) ); TriangulateEdgeFan(center, e, cell.color); if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, e); } }
Mari kita beralih ke
TriangulateConnection
. Sekarang kita bisa menggunakan
TriangulateEdgeStrip
, tetapi penggantian lain harus dilakukan. Di mana kita biasa menggunakan
v1
, kita perlu menggunakan
e1.v1
. Demikian pula,
v2
menjadi
e1.v4
,
v3
menjadi
e2.v1
, dan
v4
menjadi
e2.v4
.
void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { HexCell neighbor = cell.GetNeighbor(direction); if (neighbor == null) { return; } Vector3 bridge = HexMetrics.GetBridge(direction); bridge.y = neighbor.Position.y - cell.Position.y; EdgeVertices e2 = new EdgeVertices( e1.v1 + bridge, e1.v4 + bridge ); if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(e1.v1, e1.v4, cell, e2.v1, e2.v4, neighbor); } else { TriangulateEdgeStrip(e1, cell.color, e2, neighbor.color); } HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (direction <= HexDirection.E && nextNeighbor != null) { Vector3 v5 = e1.v4 + HexMetrics.GetBridge(direction.Next()); v5.y = nextNeighbor.Position.y; if (cell.Elevation <= neighbor.Elevation) { if (cell.Elevation <= nextNeighbor.Elevation) { TriangulateCorner( e1.v4, cell, e2.v4, neighbor, v5, nextNeighbor ); } else { TriangulateCorner( v5, nextNeighbor, e1.v4, cell, e2.v4, neighbor ); } } else if (neighbor.Elevation <= nextNeighbor.Elevation) { TriangulateCorner( e2.v4, neighbor, v5, nextNeighbor, e1.v4, cell ); } else { TriangulateCorner( v5, nextNeighbor, e1.v4, cell, e2.v4, neighbor ); } }
Divisi langkan
Kita perlu membagi tepian. Oleh karena itu, kami meneruskan tepi ke
TriangulateEdgeTerraces
.
if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(e1, cell, e2, neighbor); }
Sekarang kita perlu memodifikasi
TriangulateEdgeTerraces
sehingga interpolasi antara tepi dan bukan antara pasangan simpul. Mari kita asumsikan bahwa
EdgeVertices
memiliki metode statis yang mudah digunakan untuk melakukan ini. Ini akan memungkinkan kami untuk menyederhanakan
TriangulateEdgeTerraces
daripada menyulitkannya.
void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.color, endCell.color, 1); TriangulateEdgeStrip(begin, beginCell.color, e2, c2); for (int i = 2; i < HexMetrics.terraceSteps; i++) { EdgeVertices e1 = e2; Color c1 = c2; e2 = EdgeVertices.TerraceLerp(begin, end, i); c2 = HexMetrics.TerraceLerp(beginCell.color, endCell.color, i); TriangulateEdgeStrip(e1, c1, e2, c2); } TriangulateEdgeStrip(e2, c2, end, endCell.color); }
Metode
EdgeVertices.TerraceLerp
cukup menginterpolasi tepian antara keempat pasang simpul dari dua tepi.
public static EdgeVertices TerraceLerp ( EdgeVertices a, EdgeVertices b, int step) { EdgeVertices result; result.v1 = HexMetrics.TerraceLerp(a.v1, b.v1, step); result.v2 = HexMetrics.TerraceLerp(a.v2, b.v2, step); result.v3 = HexMetrics.TerraceLerp(a.v3, b.v3, step); result.v4 = HexMetrics.TerraceLerp(a.v4, b.v4, step); return result; }
Tepian terbagi.paket unityHubungkan kembali tebing dan tepian
Sejauh ini, kami telah mengabaikan celah di persimpangan tebing dan tepian. Saatnya untuk menyelesaikan masalah ini. Pertama-tama mari kita lihat kasus-kasus lereng-tebing-lereng (OSS) dan lereng-tebing-lereng (SOS).
Lubang jala.Masalah muncul karena puncak perbatasan telah bergerak. Ini berarti bahwa sekarang mereka tidak berbaring persis di sisi tebing, yang mengarah ke celah. Terkadang lubang ini tidak terlihat, dan terkadang mencolok.
Solusinya adalah tidak memindahkan bagian atas perbatasan. Ini berarti bahwa kita perlu mengontrol apakah titik akan dipindahkan. Cara termudah adalah dengan membuat alternatif
AddTriangle
yang tidak memindahkan simpul sama sekali.
void AddTriangleUnperturbed (Vector3 v1, Vector3 v2, Vector3 v3) { int vertexIndex = vertices.Count; vertices.Add(v1); vertices.Add(v2); vertices.Add(v3); triangles.Add(vertexIndex); triangles.Add(vertexIndex + 1); triangles.Add(vertexIndex + 2); }
Ubah
TriangulateBoundaryTriangle
sehingga menggunakan metode ini. Ini berarti bahwa ia harus memindahkan semua simpul secara eksplisit, kecuali untuk yang berbatas.
void TriangulateBoundaryTriangle ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 boundary, Color boundaryColor ) { Vector3 v2 = HexMetrics.TerraceLerp(begin, left, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1); AddTriangleUnperturbed(Perturb(begin), Perturb(v2), boundary); AddTriangleColor(beginCell.color, c2, boundaryColor); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = HexMetrics.TerraceLerp(begin, left, i); c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, i); AddTriangleUnperturbed(Perturb(v1), Perturb(v2), boundary); AddTriangleColor(c1, c2, boundaryColor); } AddTriangleUnperturbed(Perturb(v2), Perturb(left), boundary); AddTriangleColor(c2, leftCell.color, boundaryColor); }
Perlu diperhatikan hal berikut: karena kita tidak menggunakan
v2
untuk mendapatkan poin lain, kita dapat memindahkannya segera. Ini adalah optimasi sederhana dan mengurangi jumlah kode, jadi mari kita perkenalkan.
void TriangulateBoundaryTriangle ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 boundary, Color boundaryColor ) { Vector3 v2 = Perturb(HexMetrics.TerraceLerp(begin, left, 1)); Color c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1); AddTriangleUnperturbed(Perturb(begin), v2, boundary); AddTriangleColor(beginCell.color, c2, boundaryColor); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = Perturb(HexMetrics.TerraceLerp(begin, left, i)); c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, i); AddTriangleUnperturbed(v1, v2, boundary); AddTriangleColor(c1, c2, boundaryColor); } AddTriangleUnperturbed(v2, Perturb(left), boundary); AddTriangleColor(c2, leftCell.color, boundaryColor); }
Perbatasan yang belum dipindahkan.Terlihat lebih baik, tapi kami belum selesai. Di dalam metode
TriangulateCornerTerracesCliff
, titik batas diinterpolasi antara titik kiri dan kanan. Namun, titik-titik ini belum dipindahkan. Agar titik batas sesuai dengan tebing yang dihasilkan, kita perlu melakukan interpolasi antara titik yang dipindahkan.
Vector3 boundary = Vector3.Lerp(Perturb(begin), Perturb(right), b);
Hal yang sama berlaku untuk metode
TriangulateCornerCliffTerraces
.
Vector3 boundary = Vector3.Lerp(Perturb(begin), Perturb(left), b);
Lubangnya sudah hilang.Tebing dan lereng ganda
Dalam semua kasus bermasalah yang tersisa, dua tebing dan satu lereng hadir.
Lubang besar karena satu segitiga.Masalah ini diatasi dengan memindahkan secara manual satu segitiga pada blok yang
else
di akhir
TriangulateCornerTerracesCliff
.
else { AddTriangleUnperturbed(Perturb(left), Perturb(right), boundary); AddTriangleColor(leftCell.color, rightCell.color, boundaryColor); }
Hal yang sama berlaku untuk
TriangulateCornerCliffTerraces
.
else { AddTriangleUnperturbed(Perturb(left), Perturb(right), boundary); AddTriangleColor(leftCell.color, rightCell.color, boundaryColor); }
Singkirkan celah-celah terbaru.paket unityPenyelesaian
Sekarang kami memiliki jaring terdistorsi yang sepenuhnya benar. Penampilannya tergantung pada kebisingan spesifik, skalanya dan kekuatan distorsi. Dalam kasus kami, distorsi mungkin terlihat terlalu kuat. Meskipun ketidakmerataan ini terlihat indah, kami tidak ingin sel terlalu banyak menyimpang dari grid genap. Pada akhirnya, kami masih menggunakannya untuk menentukan ukuran sel yang akan diubah ukurannya. Dan jika ukuran sel terlalu banyak, maka akan lebih sulit bagi kita untuk menempatkan konten di dalamnya.Jerat tidak terdistorsi dan terdistorsi.Tampaknya kekuatan 5 untuk mendistorsi sel terlalu besar.Distorsi sel adalah dari 0 hingga 5.Mari kita kurangi menjadi 4 untuk meningkatkan kenyamanan grid, tanpa membuatnya terlalu benar. Ini memastikan bahwa offset XZ maksimum adalah β32 β 5.66 unit. public const float cellPerturbStrength = 4f;
Kekuatan Distorsi Sel 4.Nilai lain yang dapat diubah adalah koefisien integritas. Jika kita meningkatkannya, maka pusat rata sel akan menjadi lebih besar, yaitu, akan ada lebih banyak ruang untuk konten masa depan. Tentu saja, dengan melakukan itu mereka akan menjadi lebih heksagonal.Koefisien integritas dari 0,75 hingga 0,95.Sedikit peningkatan koefisien integritas ke 0,8 akan sedikit menyederhanakan hidup kita di masa depan. public const float solidFactor = 0.8f;
Koefisien integritas 0.8.Akhirnya, Anda mungkin memperhatikan bahwa perbedaan antara level ketinggian terlalu tajam. Ini nyaman ketika Anda perlu memastikan bahwa jala dihasilkan dengan benar, tetapi kami sudah selesai dengan ini. Mari kita kurangi menjadi 1 unit per langkah, yaitu menjadi 3. public const float elevationStep = 3f;
Pitch dikurangi menjadi 3.Kita juga dapat mengubah kekuatan distorsi pitch. Tetapi sekarang memiliki nilai 1,5, yang sama dengan setengah langkah tingginya, yang cocok untuk kita.Langkah kecil ketinggian memungkinkan penggunaan yang lebih logis dari ketujuh level ketinggian. Ini meningkatkan variabilitas peta.Kami menggunakan tujuh level ketinggian.paket unityBagian 5: kartu yang lebih besar
- Kami membagi grid menjadi beberapa bagian.
- Kami mengontrol kamera.
- Warnai warna dan ketinggian secara terpisah.
- Gunakan sikat sel yang diperbesar.
Sejauh ini kami telah bekerja dengan peta yang sangat kecil. Sudah waktunya untuk meningkatkannya.Sudah waktunya untuk memperbesar.Fragmen jala
Kita tidak dapat membuat grid terlalu besar, karena kita mengalami batasan dari apa yang dapat dimasukkan ke dalam satu mesh. Bagaimana cara mengatasi masalah ini? Gunakan beberapa jerat. Untuk melakukan ini, kita perlu membagi grid kita menjadi beberapa fragmen. Kami menggunakan fragmen persegi panjang dengan ukuran konstan.Membagi grid menjadi 3 segmen 3.Mari kita gunakan 5 oleh 5 blok, yaitu 25 sel per fragmen. Definisikan mereka HexMetrics
. public const int chunkSizeX = 5, chunkSizeZ = 5;
Ukuran fragmen apa yang dapat dianggap cocok?. , . . , (frustum culling), . .
Sekarang kita tidak bisa menggunakan ukuran apa pun untuk jaring, itu harus kelipatan dari ukuran fragmen. Oleh karena itu, mari kita ubah HexGrid
sehingga ia mengatur ukurannya bukan dalam sel yang terpisah, tetapi dalam fragmen. Atur ukuran default menjadi 4 oleh 3 fragmen, yaitu, hanya 12 fragmen atau 300 sel. Jadi kami mendapatkan kartu tes yang nyaman. public int chunkCountX = 4, chunkCountZ = 3;
Kami masih menggunakan width
dan height
, tetapi sekarang mereka harus menjadi pribadi. Dan ganti nama mereka menjadi cellCountX
dan cellCountZ
. Gunakan editor untuk mengganti nama semua kejadian variabel-variabel ini sekaligus. Sekarang akan menjadi jelas ketika kita berurusan dengan jumlah fragmen atau sel.
Tentukan ukuran dalam fragmen.Ubah Awake
sehingga, jika perlu, jumlah sel dihitung dari jumlah fragmen. Kami menyoroti pembuatan sel dalam metode terpisah, agar tidak menyumbat Awake
. void Awake () { HexMetrics.noiseSource = noiseSource; gridCanvas = GetComponentInChildren<Canvas>(); hexMesh = GetComponentInChildren<HexMesh>(); cellCountX = chunkCountX * HexMetrics.chunkSizeX; cellCountZ = chunkCountZ * HexMetrics.chunkSizeZ; CreateCells(); } void CreateCells () { cells = new HexCell[cellCountZ * cellCountX]; for (int z = 0, i = 0; z < cellCountZ; z++) { for (int x = 0; x < cellCountX; x++) { CreateCell(x, z, i++); } } }
Cetakan fragmen
Untuk menggambarkan fragmen mesh, kita membutuhkan tipe komponen baru. using UnityEngine; using UnityEngine.UI; public class HexGridChunk : MonoBehaviour { }
Selanjutnya kita akan membuat fragmen cetakan. Kami akan melakukan ini dengan menduplikasi objek Hex Grid dan menamainya Hex Grid Chunk . Hapus komponennya HexGrid
dan tambahkan komponen HexGridChunk
. Kemudian mengubahnya menjadi cetakan dan menghapus objek dari tempat kejadian.Pabrikan fragmen dengan kanvas dan jaringnya sendiri.Karena dia akan membuat instance dari fragmen-fragmen ini HexGrid
, kami akan memberinya tautan ke prefab fragmen tersebut. public HexGridChunk chunkPrefab;
Sekarang dengan fragmen.Membuat instance fragmen sama seperti membuat instance sel. Kami akan melacak mereka dengan bantuan array, dan menggunakan loop ganda untuk mengisinya. HexGridChunk[] chunks; void Awake () { β¦ CreateChunks(); CreateCells(); } 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(transform); } } }
Menginisialisasi fragmen mirip dengan bagaimana kita menginisialisasi kisi segi enam. Dia mengatur semuanya Awake
dan melakukan triangulasi Start
. Ini membutuhkan referensi ke kanvas dan mesh-nya, serta array untuk sel. Namun, fragmen tidak akan membuat sel-sel ini. Grid akan terus melakukan ini. public class HexGridChunk : MonoBehaviour { HexCell[] cells; HexMesh hexMesh; Canvas gridCanvas; void Awake () { gridCanvas = GetComponentInChildren<Canvas>(); hexMesh = GetComponentInChildren<HexMesh>(); cells = new HexCell[HexMetrics.chunkSizeX * HexMetrics.chunkSizeZ]; } void Start () { hexMesh.Triangulate(cells); } }
Menetapkan Sel ke Fragmen
HexGrid
masih membuat semua sel. Ini normal, tetapi sekarang kita perlu menambahkan setiap sel ke fragmen yang sesuai, dan tidak mengaturnya menggunakan mesh dan kanvas kita sendiri. void CreateCell (int x, int z, int i) { β¦ HexCell cell = cells[i] = Instantiate<HexCell>(cellPrefab);
Kita dapat menemukan fragmen yang benar menggunakan pembagian integer x
dan z
berdasarkan ukuran fragmen. void AddCellToChunk (int x, int z, HexCell cell) { int chunkX = x / HexMetrics.chunkSizeX; int chunkZ = z / HexMetrics.chunkSizeZ; HexGridChunk chunk = chunks[chunkX + chunkZ * chunkCountX]; }
Dengan menggunakan hasil antara, kami juga dapat menentukan indeks lokal sel dalam fragmen ini. Setelah itu, Anda bisa menambahkan sel ke fragmen. void AddCellToChunk (int x, int z, HexCell cell) { int chunkX = x / HexMetrics.chunkSizeX; int chunkZ = z / HexMetrics.chunkSizeZ; HexGridChunk chunk = chunks[chunkX + chunkZ * chunkCountX]; int localX = x - chunkX * HexMetrics.chunkSizeX; int localZ = z - chunkZ * HexMetrics.chunkSizeZ; chunk.AddCell(localX + localZ * HexMetrics.chunkSizeX, cell); }
Kemudian ia HexGridChunk.AddCell
menempatkan sel dalam arraynya sendiri, dan kemudian mengatur elemen induk untuk sel dan UI-nya. public void AddCell (int index, HexCell cell) { cells[index] = cell; cell.transform.SetParent(transform, false); cell.uiRect.SetParent(gridCanvas.transform, false); }
Sapu
Pada titik ini, ia HexGrid
dapat menyingkirkan kanvas anak-anaknya dan hexagon mesh, serta kode.
Karena kita dihilangkan Refresh
, kita seharusnya HexMapEditor
tidak lagi menggunakannya. void EditCell (HexCell cell) { cell.color = activeColor; cell.Elevation = activeElevation;
Kotak hexagon yang dibersihkan.Setelah memulai mode Putar, kartu masih terlihat sama. Tetapi hierarki objek akan berbeda. Hex Grid sekarang membuat fragmen objek anak yang mengandung sel, serta mesh dan kanvas mereka.Fragmen anak dalam mode Putar.Mungkin kami memiliki beberapa masalah dengan label sel. Awalnya, kami menetapkan lebar label menjadi 5. Ini cukup untuk menampilkan dua karakter yang cukup bagi kami di peta kecil. Tetapi sekarang kita dapat memiliki koordinat seperti β10, di mana ada tiga karakter. Mereka tidak akan cocok dan akan dipangkas. Untuk mengatasinya, tambah lebar label sel menjadi 10, atau bahkan lebih.Label sel diperpanjang.Sekarang kita bisa membuat peta yang lebih besar! Karena kami menghasilkan seluruh kisi saat startup, mungkin butuh waktu lama untuk membuat peta besar. Tetapi setelah selesai, kita akan memiliki ruang besar untuk eksperimen.Perbaiki pengeditan sel
Pengeditan tampaknya tidak berfungsi pada tahap saat ini, karena kami tidak lagi memperbarui kisi. Kami fragmen, sehingga menambahkan metode perlu diperbarui Refresh
untuk HexGridChunk
. public void Refresh () { hexMesh.Triangulate(cells); }
Kapan kita harus memanggil metode ini? Kami memperbarui seluruh grid setiap saat karena kami hanya memiliki satu mesh. Tetapi sekarang kami memiliki banyak fragmen. Alih-alih memperbarui semuanya setiap kali, akan jauh lebih efisien untuk memperbarui fragmen yang diubah. Jika tidak, mengganti kartu besar akan menjadi operasi yang sangat lambat.Tapi bagaimana kita tahu bagian mana yang harus diperbarui? Cara termudah adalah membuat setiap sel tahu bagian mana yang dipunyai. Kemudian sel akan dapat memperbarui fragmennya saat mengubah sel ini. Jadi mari kita berikan HexCell
tautan ke fragmennya. public HexGridChunk chunk;
HexGridChunk
dapat menambahkan dirinya ke sel saat menambahkan. public void AddCell (int index, HexCell cell) { cells[index] = cell; cell.chunk = this; cell.transform.SetParent(transform, false); cell.uiRect.SetParent(gridCanvas.transform, false); }
Dengan menghubungkan mereka, kami menambah HexCell
metode Refresh
. Setiap kali sel diperbarui, itu hanya akan memperbarui fragmennya. void Refresh () { chunk.Refresh(); }
Kami tidak perlu membuatnya menjadi HexCell.Refresh
umum, karena sel itu sendiri lebih tahu kapan itu diubah. Misalnya, setelah ketinggiannya diubah. public int Elevation { get { return elevation; } set { β¦ Refresh(); } }
Bahkan, kita perlu memperbaruinya hanya ketika tingginya telah berubah ke nilai yang berbeda. Dia bahkan tidak perlu menghitung ulang apa pun jika kita menetapkan ketinggian yang sama seperti sebelumnya. Karena itu, kita dapat keluar dari awal setter. public int Elevation { get { return elevation; } set { if (elevation == value) { return; } β¦ } }
Namun, kami juga akan melompati perhitungan untuk pertama kalinya ketika tinggi diatur ke 0, karena ini adalah nilai tinggi jala standar. Untuk menghindari ini, kami akan membuat nilai awal seperti yang tidak pernah kami gunakan. int elevation = int.MinValue;
Apa itu int.MinValue?, integer. C# integer β
32- , 2 32 integer, , . .
β β2 31 = β2 147 483 648. !
2 31 β 1 = 2 147 483 647. 2 31 - .
Untuk mengenali perubahan warna sel, kita juga perlu mengubahnya menjadi properti. Ganti nama itu menjadi Color
huruf besar, dan kemudian ubah menjadi properti dengan variabel pribadi color
. Nilai warna default akan menjadi hitam transparan, yang cocok untuk kita. public Color Color { get { return color; } set { if (color == value) { return; } color = value; Refresh(); } } Color color;
Sekarang ketika kita memulai mode Putar, kita mendapatkan pengecualian referensi-nol. Ini terjadi karena kami mengatur warna dan tinggi ke nilai standarnya sebelum menetapkan sel ke fragmennya. Adalah normal bahwa kami tidak memperbarui fragmen pada tahap ini, karena kami melakukan triangulasi setelah semua inisialisasi selesai. Dengan kata lain, kami memperbarui fragmen hanya jika ditugaskan. void Refresh () { if (chunk) { chunk.Refresh(); } }
Kami akhirnya dapat mengubah sel lagi! Namun, muncul masalah. Saat menggambar di sepanjang perbatasan fragmen, lapisan muncul.Kesalahan pada batas fragmen.Ini logis, karena ketika sel tunggal berubah, semua koneksi dengan tetangganya juga berubah. Dan tetangga-tetangga ini mungkin ada di fragmen lain. Solusi paling sederhana adalah memperbarui semua sel tetangga jika berbeda. void Refresh () { if (chunk) { chunk.Refresh(); for (int i = 0; i < neighbors.Length; i++) { HexCell neighbor = neighbors[i]; if (neighbor != null && neighbor.chunk != chunk) { neighbor.chunk.Refresh(); } } } }
Meskipun ini berfungsi, mungkin kami memperbarui satu fragmen beberapa kali. Dan ketika kita mulai mewarnai beberapa sel sekaligus, semuanya akan menjadi lebih buruk.Tetapi kami tidak diharuskan untuk melakukan triangulasi segera setelah memperbarui fragmen. Sebagai gantinya, kami hanya menulis bahwa pembaruan diperlukan dan melakukan pelacakan setelah perubahan selesai.Karena tidak HexGridChunk
melakukan hal lain, kita dapat menggunakan statusnya yang diaktifkan untuk memberi sinyal perlunya pembaruan. Saat memperbarui, kami menyertakan komponen. Menyalakannya beberapa kali tidak akan mengubah apa pun. Komponen kemudian diperbarui. Kami akan melakukan pelacakan pada titik ini dan menonaktifkan komponen lagi.Kami menggunakan LateUpdate
sebagai gantinyaUpdate
untuk memastikan bahwa triangulasi terjadi setelah perubahan selesai untuk kerangka saat ini. public void Refresh () {
Apa perbedaan antara Update dan LateUpdate?Update
- . LateUpdate
. , .
Karena komponen kami diaktifkan secara default, kami tidak perlu lagi melakukan pelacakan secara eksplisit Start
. Karena itu, metode ini dapat dihapus.
Fragmen 20 dengan 20 mengandung 10.000 sel.Daftar Umum
Meskipun kami telah mengubah cara triangulasi grid secara signifikan, HexMesh
tetap saja sama. Yang dia butuhkan untuk bekerja adalah array sel. Dia tidak peduli jika ada satu jala segi enam, atau beberapa dari mereka. Namun kami belum mempertimbangkan untuk menggunakan beberapa jerat. Mungkin ada yang bisa diperbaiki di sini? Daftar yangdigunakan HexMesh
pada dasarnya adalah buffer sementara. Mereka hanya digunakan untuk triangulasi. Dan fragmennya adalah triangulasi satu per satu. Karena itu, pada kenyataannya, kita hanya perlu satu set daftar, dan tidak satu set untuk setiap objek mesh segi enam. Ini dapat dicapai dengan membuat daftar statis. static List<Vector3> vertices = new List<Vector3>(); static List<Color> colors = new List<Color>(); static List<int> triangles = new List<int>(); void Awake () { GetComponent<MeshFilter>().mesh = hexMesh = new Mesh(); meshCollider = gameObject.AddComponent<MeshCollider>(); hexMesh.name = "Hex Mesh";
Apakah daftar statis benar-benar penting?. , , .
, . 20 20 100.
paket unityKontrol kamera
Kamera besar itu luar biasa, tetapi tidak ada gunanya jika kita tidak bisa melihatnya. Untuk memeriksa seluruh peta, kita perlu menggerakkan kamera. Zooming juga bermanfaat. Karena itu, mari kita buat kamera untuk melakukan tindakan ini.Buat objek dummy dan sebut itu Hex Map Camera . Jatuhkan komponen transformasinya sehingga bergerak ke asal tanpa mengubah rotasi dan skalanya. Tambahkan anak ke dalamnya yang disebut Putar , dan tambahkan anak ke Stick . Jadikan kamera utama anak dari Stick, dan reset komponen transformasinya.Hirarki kamera.Tujuan engsel kamera (Putar) adalah untuk mengontrol sudut di mana kamera melihat peta. Mari kita beri giliran (45, 0, 0). Pegangan (Tongkat) mengontrol jarak di mana kamera berada. Mari kita mengaturnya posisi (0, 0, -45).Sekarang kita membutuhkan komponen untuk mengendalikan sistem ini. Tetapkan komponen ini ke akar hierarki kamera. Beri dia tautan ke engsel dan pegangan, masukkan mereka Awake
. using UnityEngine; public class HexMapCamera : MonoBehaviour { Transform swivel, stick; void Awake () { swivel = transform.GetChild(0); stick = swivel.GetChild(0); } }
Kamera peta segi enam.Zoom
Fungsi pertama yang akan kita buat adalah zooming (zoom). Kita dapat mengontrol level zoom saat ini menggunakan variabel float. Nilai 0 berarti kita benar-benar jauh, dan nilai 1 berarti kita benar-benar dekat. Mari kita mulai dengan zoom maksimum. float zoom = 1f;
Zoom biasanya dilakukan dengan roda mouse atau kontrol analog. Kita dapat menerapkannya menggunakan sumbu input Mouse ScrollWheel default. Tambahkan metode Update
yang memeriksa keberadaan input delta, dan jika ada, itu memanggil metode perubahan zoom. void Update () { float zoomDelta = Input.GetAxis("Mouse ScrollWheel"); if (zoomDelta != 0f) { AdjustZoom(zoomDelta); } } void AdjustZoom (float delta) { }
Untuk mengubah level zoom, kita cukup menambahkan delta padanya dan kemudian membatasi nilainya (penjepit) untuk tetap berada dalam kisaran 0-1. void AdjustZoom (float delta) { zoom = Mathf.Clamp01(zoom + delta); }
Saat memperbesar dan memperkecil, jarak ke kamera harus berubah. Ini dapat dilakukan dengan mengubah posisi pegangan di Z. Tambahkan dua variabel float umum untuk menyesuaikan posisi pegangan di zoom minimum dan maksimum. Karena kami mengembangkan peta yang relatif kecil, tetapkan nilainya ke -250 dan -45. public float stickMinZoom, stickMaxZoom;
Setelah mengubah zoom, kami melakukan interpolasi linier antara kedua nilai ini berdasarkan nilai zoom baru. Kemudian perbarui posisi pegangan. void AdjustZoom (float delta) { zoom = Mathf.Clamp01(zoom + delta); float distance = Mathf.Lerp(stickMinZoom, stickMaxZoom, zoom); stick.localPosition = new Vector3(0f, 0f, distance); }
Nilai Stick minimum dan maksimum.Sekarang zoom berfungsi, tetapi sejauh ini tidak terlalu berguna. Biasanya, ketika zoom lebih jauh, kamera masuk ke tampilan atas. Kita bisa menyadari ini dengan memutar engselnya. Oleh karena itu, kami menambahkan variabel min dan maks untuk engsel. Mari kita menetapkan nilai-nilai 90 dan 45. public float swivelMinZoom, swivelMaxZoom;
Seperti halnya posisi pegangan, kami melakukan interpolasi untuk menemukan sudut zoom yang sesuai. Lalu kita atur rotasi engselnya. void AdjustZoom (float delta) { zoom = Mathf.Clamp01(zoom + delta); float distance = Mathf.Lerp(stickMinZoom, stickMaxZoom, zoom); stick.localPosition = new Vector3(0f, 0f, distance); float angle = Mathf.Lerp(swivelMinZoom, swivelMaxZoom, zoom); swivel.localRotation = Quaternion.Euler(angle, 0f, 0f); }
Nilai minimum dan maksimum dari Swivel.Laju perubahan zoom dapat disesuaikan dengan mengubah sensitivitas parameter input roda mouse. Mereka dapat ditemukan di Edit / Pengaturan Proyek / Input . Misalnya, dengan mengubahnya dari 0,1 menjadi 0,025, kami mendapatkan perubahan zoom yang lebih lambat dan lebih lancar.Opsi input roda mouse.Bergerak
Sekarang mari kita beralih ke memindahkan kamera. Pergerakan ke arah X dan Z kita harus menerapkan dalam Update
, seperti dalam kasus zoom. Kita dapat menggunakan sumbu input Horisontal dan Vertikal untuk ini . Ini akan memungkinkan kita untuk memindahkan kamera dengan panah dan tombol WASD. void Update () { float zoomDelta = Input.GetAxis("Mouse ScrollWheel"); if (zoomDelta != 0f) { AdjustZoom(zoomDelta); } float xDelta = Input.GetAxis("Horizontal"); float zDelta = Input.GetAxis("Vertical"); if (xDelta != 0f || zDelta != 0f) { AdjustPosition(xDelta, zDelta); } } void AdjustPosition (float xDelta, float zDelta) { }
Pendekatan paling sederhana adalah mendapatkan posisi saat ini dari sistem kamera, menambahkan delta X dan Z ke sana, dan menetapkan hasilnya ke posisi sistem. void AdjustPosition (float xDelta, float zDelta) { Vector3 position = transform.localPosition; position += new Vector3(xDelta, 0f, zDelta); transform.localPosition = position; }
Karena ini, kamera akan bergerak sambil memegang panah atau WASD, tetapi tidak pada kecepatan konstan. Itu akan tergantung pada frame rate. Untuk menentukan jarak yang Anda perlukan untuk bergerak, kami menggunakan delta waktu, serta kecepatan yang diperlukan. Oleh karena itu, kami menambahkan variabel umum moveSpeed
dan mengaturnya ke 100, dan kemudian mengalikannya dengan delta waktu untuk mendapatkan posisi delta. public float moveSpeed; void AdjustPosition (float xDelta, float zDelta) { float distance = moveSpeed * Time.deltaTime; Vector3 position = transform.localPosition; position += new Vector3(xDelta, 0f, zDelta) * distance; transform.localPosition = position; }
Kecepatan bergerak.Sekarang kita dapat bergerak dengan kecepatan konstan di sepanjang sumbu X atau Z. Tetapi ketika bergerak di kedua sumbu pada saat yang sama (secara diagonal) gerakan akan lebih cepat. Untuk memperbaiki ini, kita perlu menormalkan vektor delta. Ini akan memungkinkan Anda untuk menggunakannya sebagai tujuan. void AdjustPosition (float xDelta, float zDelta) { Vector3 direction = new Vector3(xDelta, 0f, zDelta).normalized; float distance = moveSpeed * Time.deltaTime; Vector3 position = transform.localPosition; position += direction * distance; transform.localPosition = position; }
Gerakan diagonal sekarang diterapkan dengan benar, tetapi tiba-tiba ternyata kamera terus bergerak untuk waktu yang agak lama bahkan setelah melepaskan semua tombol. Ini terjadi karena sumbu input tidak langsung melompat ke nilai batas segera setelah menekan tombol. Mereka butuh waktu untuk ini. Hal yang sama berlaku untuk melepaskan kunci. Butuh waktu untuk kembali ke nilai sumbu nol. Namun, karena kami menormalkan nilai input, kecepatan maksimum tetap dipertahankan.Kita dapat menyesuaikan parameter input untuk menghilangkan penundaan, tetapi mereka memberikan perasaan halus yang layak disimpan. Kita dapat menerapkan nilai sumbu paling ekstrem sebagai koefisien gerak redaman. Vector3 direction = new Vector3(xDelta, 0f, zDelta).normalized; float damping = Mathf.Max(Mathf.Abs(xDelta), Mathf.Abs(zDelta)); float distance = moveSpeed * damping * Time.deltaTime;
Gerakan dengan atenuasi.Sekarang gerakannya bekerja dengan baik, setidaknya dengan peningkatan zoom. Tapi di kejauhan ternyata terlalu lambat. Dengan zoom yang diperkecil, kita perlu mempercepat. Ini dapat dilakukan dengan mengganti satu variabel moveSpeed
dengan dua untuk zoom minimum dan maksimum, dan kemudian interpolasi. Tetapkan nilai 400 dan 100 untuk mereka.
Kecepatan gerakan bervariasi dengan tingkat zoom.Sekarang kita dapat dengan cepat bergerak di sekitar peta! Sebenarnya, kita bisa bergerak jauh melampaui peta, tetapi ini tidak diinginkan. Kamera harus tetap berada di dalam peta. Untuk memastikan ini, kita perlu mengetahui batas-batas peta, sehingga diperlukan tautan ke kisi. Tambahkan dan hubungkan. public HexGrid grid;
Perlu meminta ukuran kisi.Setelah pindah ke posisi baru, kami akan membatasi menggunakan metode baru. void AdjustPosition (float xDelta, float zDelta) { β¦ transform.localPosition = ClampPosition(position); } Vector3 ClampPosition (Vector3 position) { return position; }
Posisi X memiliki nilai minimum 0, dan maksimum ditentukan oleh ukuran peta. Vector3 ClampPosition (Vector3 position) { float xMax = grid.chunkCountX * HexMetrics.chunkSizeX * (2f * HexMetrics.innerRadius); position.x = Mathf.Clamp(position.x, 0f, xMax); return position; }
Hal yang sama berlaku untuk posisi Z. Vector3 ClampPosition (Vector3 position) { float xMax = grid.chunkCountX * HexMetrics.chunkSizeX * (2f * HexMetrics.innerRadius); position.x = Mathf.Clamp(position.x, 0f, xMax); float zMax = grid.chunkCountZ * HexMetrics.chunkSizeZ * (1.5f * HexMetrics.outerRadius); position.z = Mathf.Clamp(position.z, 0f, zMax); return position; }
Sebenarnya, ini sedikit tidak akurat. Titik awalnya adalah di tengah sel, bukan di sebelah kiri. Karena itu, kami ingin kamera berhenti di tengah sel paling kanan. Untuk melakukan ini, kurangi setengah sel dari maksimum X. float xMax = (grid.chunkCountX * HexMetrics.chunkSizeX - 0.5f) * (2f * HexMetrics.innerRadius); position.x = Mathf.Clamp(position.x, 0f, xMax);
Untuk alasan yang sama, kita perlu mengurangi Z maksimum. Karena metriknya sedikit berbeda, kita perlu mengurangi sel penuh. float zMax = (grid.chunkCountZ * HexMetrics.chunkSizeZ - 1) * (1.5f * HexMetrics.outerRadius); position.z = Mathf.Clamp(position.z, 0f, zMax);
Dengan gerakan kita selesai, hanya detail kecil yang tersisa. Terkadang UI bereaksi terhadap tombol panah, dan ini mengarah pada fakta bahwa ketika Anda memindahkan kamera, slider bergerak. Ini terjadi ketika UI menganggap dirinya aktif, setelah Anda mengkliknya dan kursor terus berada di atasnya.Anda dapat mencegah UI mendengarkan input keyboard. Ini dapat dilakukan dengan menginstruksikan objek EventSystem untuk tidak menjalankan Kirim Navigasi Acara .Tidak ada lagi acara navigasi.Putar
Ingin melihat apa yang ada di balik tebing? Akan lebih mudah untuk dapat memutar kamera! Mari kita tambahkan fitur ini.Level zoom tidak penting untuk rotasi, hanya kecepatan yang cukup. Tambahkan variabel umum rotationSpeed
dan atur ke 180 derajat. Periksa delta rotasi Update
dengan mengambil sampel sumbu Rotasi dan ubah rotasi jika perlu. public float rotationSpeed; void Update () { float zoomDelta = Input.GetAxis("Mouse ScrollWheel"); if (zoomDelta != 0f) { AdjustZoom(zoomDelta); } float rotationDelta = Input.GetAxis("Rotation"); if (rotationDelta != 0f) { AdjustRotation(rotationDelta); } float xDelta = Input.GetAxis("Horizontal"); float zDelta = Input.GetAxis("Vertical"); if (xDelta != 0f || zDelta != 0f) { AdjustPosition(xDelta, zDelta); } } void AdjustRotation (float delta) { }
Kecepatan belok.Bahkan, sumbu Rotasi tidak secara default. Kita harus membuatnya sendiri. Pergi ke parameter input dan duplikat entri teratas Vertikal . Ubah nama duplikat menjadi Rotasi dan ubah kunci menjadi QE dan koma (,) dengan titik (.).Putar sumbu input.Saya mengunduh unitypackage, mengapa saya tidak mendapatkan input ini?. Unity. , . , , .
Sudut rotasi yang akan kita lacak dan ubah AdjustRotation
. Setelah itu kami akan memutar seluruh sistem kamera. float rotationAngle; void AdjustRotation (float delta) { rotationAngle += delta * rotationSpeed * Time.deltaTime; transform.localRotation = Quaternion.Euler(0f, rotationAngle, 0f); }
Karena lingkaran penuh adalah 360 derajat, kami menggulung sudut rotasi sehingga berada dalam kisaran dari 0 hingga 360. void AdjustRotation (float delta) { rotationAngle += delta * rotationSpeed * Time.deltaTime; if (rotationAngle < 0f) { rotationAngle += 360f; } else if (rotationAngle >= 360f) { rotationAngle -= 360f; } transform.localRotation = Quaternion.Euler(0f, rotationAngle, 0f); }
Aktifkan aksi.Sekarang rotasi bekerja. Jika Anda memeriksanya, Anda dapat melihat bahwa gerakannya absolut. Karena itu, setelah berbalik 180 derajat, gerakan akan menjadi kebalikan dari apa yang diharapkan. Akan jauh lebih nyaman bagi pengguna bahwa gerakan ini dilakukan relatif terhadap sudut pandang kamera. Kita dapat melakukan ini dengan mengalikan rotasi saat ini dengan arah gerakan. void AdjustPosition (float xDelta, float zDelta) { Vector3 direction = transform.localRotation * new Vector3(xDelta, 0f, zDelta).normalized; β¦ }
Perpindahan relatif.paket unityPengeditan Lanjutan
Sekarang kami memiliki peta yang lebih besar, Anda dapat meningkatkan alat pengeditan peta. Mengubah satu sel pada satu waktu terlalu lama, jadi alangkah baiknya membuat sikat yang lebih besar. Ini juga akan nyaman jika Anda bisa memilih untuk melukis atau mengubah ketinggian, meninggalkan yang lainnya tidak berubah.Warna dan tinggi opsional
Kita dapat membuat warna opsional dengan menambahkan opsi pilihan kosong ke grup sakelar. Gandakan salah satu pengalih warna dan ganti labelnya dengan --- atau sesuatu yang serupa untuk menunjukkan bahwa itu bukan warna. Kemudian ubah argumen event On Value Changed menjadi β1.Indeks warna tidak valid.Tentu saja, indeks ini tidak berlaku untuk berbagai warna. Kita dapat menggunakannya untuk menentukan apakah warna harus diterapkan pada sel. bool applyColor; public void SelectColor (int index) { applyColor = index >= 0; if (applyColor) { activeColor = colors[index]; } } void EditCell (HexCell cell) { if (applyColor) { cell.Color = activeColor; } cell.Elevation = activeElevation; }
Tinggi dikendalikan oleh penggeser, jadi kami tidak bisa menambahkan sakelar padanya. Sebagai gantinya, kita dapat menggunakan sakelar terpisah untuk mengaktifkan atau menonaktifkan pengeditan ketinggian. Secara default, ini akan diaktifkan. bool applyElevation = true; void EditCell (HexCell cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } }
Tambahkan saklar ketinggian baru ke UI. Saya juga akan meletakkan semuanya di panel baru, dan membuat slider ketinggian horizontal sehingga UI lebih indah.Warna dan tinggi opsional.Untuk mengaktifkan ketinggian, kita memerlukan metode baru, yang akan kita sambungkan dengan UI. public void SetApplyElevation (bool toggle) { applyElevation = toggle; }
Dengan menghubungkannya ke sakelar ketinggian, pastikan bahwa metode bool dinamis digunakan di bagian atas daftar metode. Versi yang benar tidak menampilkan tanda centang di inspektur.Kami mentransmisikan status sakelar ketinggian.Sekarang kita dapat memilih hanya pewarnaan dengan bunga atau hanya tinggi. Atau keduanya, seperti biasa. Kita bahkan dapat memilih untuk tidak mengubah yang satu atau yang lain, tetapi sejauh ini tidak terlalu berguna bagi kita.Beralih di antara warna dan tinggi.Mengapa ketinggian mati saat memilih warna?, toggle group. , , toggle group.
Ukuran kuas
Untuk mendukung ukuran kuas yang dapat diubah ukurannya, tambahkan variabel integer brushSize
dan metode untuk menyetelnya melalui UI. Kami akan menggunakan bilah geser, jadi sekali lagi kami harus mengonversi nilai dari float ke int. int brushSize; public void SetBrushSize (float size) { brushSize = (int)size; }
Penggeser ukuran sikat.Anda dapat membuat slider baru dengan menduplikasi slider tinggi. Ubah nilai maksimumnya menjadi 4 dan pasangkan ke metode yang sesuai. Saya juga menambahkan tag padanya.Pengaturan ukuran slider.Sekarang kita dapat mengedit beberapa sel secara bersamaan, kita perlu menggunakan metode ini EditCells
. Metode ini akan memanggil EditCell
semua sel yang terlibat. Sel yang awalnya dipilih akan dianggap sebagai pusat sikat. void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { EditCells(hexGrid.GetCell(hit.point)); } } void EditCells (HexCell center) { } void EditCell (HexCell cell) { β¦ }
Ukuran kuas menentukan radius pengeditan. Dengan jari-jari 0, ini hanya akan menjadi satu sel pusat. Dengan jari-jari 1, ini akan menjadi pusat dan tetangganya. Pada radius 2, tetangga pusat dan tetangga terdekat mereka dihidupkan. Dan sebagainya.
Hingga radius 3.Untuk mengedit sel, Anda harus memutarnya dalam satu lingkaran. Pertama kita membutuhkan koordinat X dan Z dari pusat. void EditCells (HexCell center) { int centerX = center.coordinates.X; int centerZ = center.coordinates.Z; }
Kami menemukan koordinat Z minimum dengan mengurangi jari-jari. Jadi kita mendefinisikan garis nol. Mulai dari baris ini, kita lewati sampai kita menutupi garis di tengah. void EditCells (HexCell center) { int centerX = center.coordinates.X; int centerZ = center.coordinates.Z; for (int r = 0, z = centerZ - brushSize; z <= centerZ; z++, r++) { } }
Sel pertama di baris bawah memiliki koordinat X yang sama dengan sel tengah. Koordinat ini berkurang dengan bertambahnya jumlah baris.Sel terakhir selalu memiliki koordinat X sama dengan koordinat pusat plus jari-jari.Sekarang kita bisa memutari setiap baris dan mendapatkan sel dengan koordinatnya. for (int r = 0, z = centerZ - brushSize; z <= centerZ; z++, r++) { for (int x = centerX - r; x <= centerX + brushSize; x++) { EditCell(hexGrid.GetCell(new HexCoordinates(x, z))); } }
Kami belum memiliki metode HexGrid.GetCell
dengan parameter koordinat, jadi buatlah. Konversikan ke koordinat perpindahan dan dapatkan sel. public HexCell GetCell (HexCoordinates coordinates) { int z = coordinates.Z; int x = coordinates.X + z / 2; return cells[x + z * cellCountX]; }
Bagian bawah sikat, ukuran 2.Kami menutupi sisa sikat, melakukan siklus dari atas ke bawah ke tengah. Dalam hal ini, logikanya dicerminkan dan baris tengah perlu dikecualikan. void EditCells (HexCell center) { int centerX = center.coordinates.X; int centerZ = center.coordinates.Z; for (int r = 0, z = centerZ - brushSize; z <= centerZ; z++, r++) { for (int x = centerX - r; x <= centerX + brushSize; x++) { EditCell(hexGrid.GetCell(new HexCoordinates(x, z))); } } for (int r = 0, z = centerZ + brushSize; z > centerZ; z--, r++) { for (int x = centerX - brushSize; x <= centerX + r; x++) { EditCell(hexGrid.GetCell(new HexCoordinates(x, z))); } } }
Seluruh kuas, ukuran 2.Ini berfungsi, kecuali kuas kami melampaui batas kisi. Ketika ini terjadi, kami mendapatkan pengecualian index-out-of-range. Untuk menghindari ini, periksa batas-batas HexGrid.GetCell
dan kembali null
ketika sel tidak ada diminta. public HexCell GetCell (HexCoordinates coordinates) { int z = coordinates.Z; if (z < 0 || z >= cellCountZ) { return null; } int x = coordinates.X + z / 2; if (x < 0 || x >= cellCountX) { return null; } return cells[x + z * cellCountX]; }
Untuk menghindari null-reference-exception, itu HexMapEditor
harus memeriksa sebelum mengedit apakah sel benar-benar ada. void EditCell (HexCell cell) { if (cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } } }
Menggunakan beberapa ukuran kuas.Toggle visibilitas label sel
Lebih sering daripada tidak, kita tidak perlu melihat label sel. Jadi mari kita membuatnya opsional. Karena setiap fragmen mengontrol kanvasnya sendiri, tambahkan metode ShowUI
ke HexGridChunk
. Ketika UI seharusnya terlihat, kami mengaktifkan kanvas. Jika tidak, nonaktifkan. public void ShowUI (bool visible) { gridCanvas.gameObject.SetActive(visible); }
Mari sembunyikan UI secara default. void Awake () { gridCanvas = GetComponentInChildren<Canvas>(); hexMesh = GetComponentInChildren<HexMesh>(); cells = new HexCell[HexMetrics.chunkSizeX * HexMetrics.chunkSizeZ]; ShowUI(false); }
Karena visibilitas UI dialihkan untuk seluruh peta, kami menambahkan metode ShowUI
ke HexGrid
. Itu hanya meneruskan permintaan ke fragmen-fragmennya. public void ShowUI (bool visible) { for (int i = 0; i < chunks.Length; i++) { chunks[i].ShowUI(visible); } }
HexMapEditor
mendapatkan metode yang sama, meneruskan permintaan ke grid. public void ShowUI (bool visible) { hexGrid.ShowUI(visible); }
Akhirnya, kita bisa menambahkan sakelar ke UI dan menghubungkannya.Tag visibilitas switch.paket unityBagian 6: sungai
- Menambahkan sungai ke sel.
- Seret dan lepas dukungan untuk menggambar sungai.
- Membuat dasar sungai.
- Menggunakan beberapa jerat per fragmen.
- Buat kumpulan daftar bersama.
- Triangulasi dan animasi air yang mengalir.
Pada bagian sebelumnya, kami berbicara tentang mendukung peta besar. Sekarang kita bisa beralih ke elemen bantuan yang lebih besar. Kali ini kita akan berbicara tentang sungai.Sungai mengalir dari pegunungan.Sel sungai
Ada tiga cara untuk menambahkan sungai ke kisi segi enam. Cara pertama adalah membiarkannya mengalir dari sel ke sel. Ini adalah bagaimana ini diterapkan dalam Legenda Tanpa Akhir. Cara kedua adalah membiarkannya mengalir di antara sel, dari ujung ke ujung. Jadi itu diterapkan dalam Peradaban 5. Cara ketiga adalah tidak menciptakan struktur sungai khusus sama sekali, tetapi menggunakan sel air untuk menyarankan mereka. Jadi sungai diimplementasikan dalam Zaman Keajaiban 3.Dalam kasus kami, tepi sel sudah ditempati oleh lereng dan tebing. Ini menyisakan sedikit ruang untuk sungai. Karena itu, kita akan membuatnya mengalir dari sel ke sel. Ini berarti bahwa di setiap sel tidak akan ada sungai, atau sungai akan mengalir di sepanjang itu, atau akan ada awal atau akhir sungai di dalamnya. Di sel-sel di mana sungai mengalir, ia bisa mengalir lurus, berbelok satu langkah atau dua langkah.Lima kemungkinan konfigurasi sungai.Kami tidak akan mendukung percabangan atau penggabungan sungai. Ini akan semakin memperumit masalah, terutama aliran air. Juga, kita tidak akan dibingungkan oleh volume air yang besar. Kami akan mempertimbangkannya di tutorial lain.Pelacakan sungai
Sel di mana aliran sungai dapat dianggap secara bersamaan memiliki sungai yang masuk dan keluar. Jika mengandung awal sungai, maka hanya sungai yang keluar. Dan jika itu berisi ujung sungai, maka itu hanya memiliki sungai yang masuk. Kami dapat menyimpan informasi ini HexCell
menggunakan dua nilai Boolean. bool hasIncomingRiver, hasOutgoingRiver;
Tetapi ini tidak cukup. Kita juga perlu tahu arah sungai-sungai ini. Dalam kasus sungai keluar, ini menunjukkan ke mana ia bergerak. Dalam kasus sungai yang masuk, ini menunjukkan dari mana asalnya. bool hasIncomingRiver, hasOutgoingRiver; HexDirection incomingRiver, outgoingRiver;
Kami akan membutuhkan informasi ini ketika melakukan triangulasi sel, jadi kami akan menambahkan properti agar dapat mengaksesnya. Kami tidak akan mendukung menugaskan mereka secara langsung. Untuk melakukan ini, kami selanjutnya akan menambahkan metode terpisah. public bool HasIncomingRiver { get { return hasIncomingRiver; } } public bool HasOutgoingRiver { get { return hasOutgoingRiver; } } public HexDirection IncomingRiver { get { return incomingRiver; } } public HexDirection OutgoingRiver { get { return outgoingRiver; } }
Sebuah pertanyaan penting adalah apakah ada sungai di dalam sel, terlepas dari detailnya. Karena itu, mari kita tambahkan properti untuk ini juga. public bool HasRiver { get { return hasIncomingRiver || hasOutgoingRiver; } }
Pertanyaan logis lain: adalah awal atau akhir sungai di dalam sel. Jika keadaan sungai yang masuk dan keluar berbeda, maka ini masalahnya. Karena itu, kami akan menjadikan ini properti lain. public bool HasRiverBeginOrEnd { get { return hasIncomingRiver != hasOutgoingRiver; } }
Dan akhirnya, akan berguna untuk mengetahui apakah sungai mengalir melalui punggungan tertentu, apakah itu masuk atau keluar. public bool HasRiverThroughEdge (HexDirection direction) { return hasIncomingRiver && incomingRiver == direction || hasOutgoingRiver && outgoingRiver == direction; }
Pemindahan sungai
Sebelum kita mulai menambahkan sungai ke sel, mari kita terapkan dukungan untuk menghilangkan sungai. Untuk mulai dengan, kami akan menulis metode untuk menghapus hanya bagian keluar sungai.Jika tidak ada sungai keluar di sel, maka tidak ada yang perlu dilakukan. Jika tidak, matikan dan lakukan pembaruan. public void RemoveOutgoingRiver () { if (!hasOutgoingRiver) { return; } hasOutgoingRiver = false; Refresh(); }
Tapi itu belum semuanya. Sungai yang keluar harus bergerak di suatu tempat. Karena itu, harus ada tetangga dengan sungai yang masuk. Kita harus menyingkirkannya juga. public void RemoveOutgoingRiver () { if (!hasOutgoingRiver) { return; } hasOutgoingRiver = false; Refresh(); HexCell neighbor = GetNeighbor(outgoingRiver); neighbor.hasIncomingRiver = false; neighbor.Refresh(); }
Tidak bisakah sungai mengalir keluar dari peta?, . , .
Menghapus sungai dari sel hanya mengubah penampilan sel itu. Tidak seperti mengedit tinggi atau warna, itu tidak mempengaruhi tetangga. Karena itu, kita hanya perlu memperbarui sel itu sendiri, tetapi tidak pada tetangganya. public void RemoveOutgoingRiver () { if (!hasOutgoingRiver) { return; } hasOutgoingRiver = false; RefreshSelfOnly(); HexCell neighbor = GetNeighbor(outgoingRiver); neighbor.hasIncomingRiver = false; neighbor.RefreshSelfOnly(); }
Metode ini RefreshSelfOnly
hanya memperbarui fragmen yang dimiliki sel. Karena kita tidak mengubah sungai selama inisialisasi grid, kita tidak perlu khawatir jika sebuah fragmen telah ditetapkan. void RefreshSelfOnly () { chunk.Refresh(); }
Menghapus sungai yang masuk bekerja dengan cara yang sama. public void RemoveIncomingRiver () { if (!hasIncomingRiver) { return; } hasIncomingRiver = false; RefreshSelfOnly(); HexCell neighbor = GetNeighbor(incomingRiver); neighbor.hasOutgoingRiver = false; neighbor.RefreshSelfOnly(); }
Dan penghapusan seluruh sungai berarti penghapusan bagian sungai yang masuk dan keluar. public void RemoveRiver () { RemoveOutgoingRiver(); RemoveIncomingRiver(); }
Menambah sungai
Untuk mendukung pembuatan sungai, kita membutuhkan metode untuk menentukan sungai keluar sel. Dia harus mendefinisikan ulang semua sungai keluar sebelumnya dan mengatur sungai masuk yang sesuai.Untuk memulainya, kita tidak perlu melakukan apa pun jika sungai sudah ada. public void SetOutgoingRiver (HexDirection direction) { if (hasOutgoingRiver && outgoingRiver == direction) { return; } }
Selanjutnya, kita perlu memastikan bahwa ada tetangga di arah yang benar. Selain itu, sungai tidak bisa mengalir ke atas. Karena itu, kita harus menyelesaikan operasi jika tetangga lebih tinggi. HexCell neighbor = GetNeighbor(direction); if (!neighbor || elevation < neighbor.elevation) { return; }
Selanjutnya kita perlu membersihkan sungai keluar sebelumnya. Dan juga kita perlu menghapus sungai yang masuk, jika ditumpangkan pada sungai keluar baru. RemoveOutgoingRiver(); if (hasIncomingRiver && incomingRiver == direction) { RemoveIncomingRiver(); }
Sekarang kita bisa beralih ke pengaturan sungai keluar. hasOutgoingRiver = true; outgoingRiver = direction; RefreshSelfOnly();
Dan jangan lupa untuk mengatur sungai yang masuk untuk sel lain setelah menghapus sungai yang masuk saat ini, jika ada. neighbor.RemoveIncomingRiver(); neighbor.hasIncomingRiver = true; neighbor.incomingRiver = direction.Opposite(); neighbor.RefreshSelfOnly();
Menyingkirkan sungai yang mengalir
Sekarang kami telah memungkinkan untuk menambahkan hanya sungai yang tepat, tindakan lain masih dapat membuat yang salah. Ketika kita mengubah ketinggian sel, sekali lagi kita harus memastikan bahwa sungai hanya bisa mengalir ke bawah. Semua sungai yang tidak beraturan harus dihilangkan. public int Elevation { get { return elevation; } set { β¦ if ( hasOutgoingRiver && elevation < GetNeighbor(outgoingRiver).elevation ) { RemoveOutgoingRiver(); } if ( hasIncomingRiver && elevation > GetNeighbor(incomingRiver).elevation ) { RemoveIncomingRiver(); } Refresh(); } }
paket unityGanti sungai
Untuk mendukung pengeditan sungai, kita perlu menambahkan sakelar sungai ke UI. Sebenarnya. kami membutuhkan dukungan untuk tiga mode pengeditan. Kita perlu mengabaikan sungai, atau menambahkannya, atau menghapusnya. Kita dapat menggunakan enumerasi helper sederhana untuk melacak status. Karena kita hanya akan menggunakannya di dalam editor, kita dapat mendefinisikannya di dalam kelas HexMapEditor
, bersama dengan bidang mode sungai. enum OptionalToggle { Ignore, Yes, No } OptionalToggle riverMode;
Dan kita membutuhkan metode untuk mengubah rezim sungai melalui UI. public void SetRiverMode (int mode) { riverMode = (OptionalToggle)mode; }
Untuk mengontrol rezim sungai, tambahkan tiga sakelar ke UI dan sambungkan ke kelompok sakelar baru, seperti yang kami lakukan dengan warnanya. Saya mengkonfigurasi switch sehingga label mereka berada di bawah kotak centang. Karena ini, mereka akan tetap cukup tipis agar sesuai dengan ketiga opsi pada satu baris.Sungai UIMengapa tidak menggunakan daftar drop-down?, . dropdown list Unity Play. , .
Seret dan lepas pengenalan
Untuk membuat sungai, kita membutuhkan sel dan arah. Saat ini, HexMapEditor
tidak memberikan kami informasi ini. Karena itu, kita perlu menambahkan dukungan seret dan lepas dari satu sel ke sel lainnya.Kita perlu tahu apakah hambatan ini benar, dan juga menentukan arahnya. Dan untuk mengenali drag dan drop, kita perlu mengingat sel sebelumnya. bool isDrag; HexDirection dragDirection; HexCell previousCell;
Awalnya, ketika seret tidak dilakukan, sel sebelumnya tidak. Yaitu, ketika tidak ada input atau kami tidak berinteraksi dengan kartu, Anda perlu memberikan nilai padanya null
. void Update () { if ( Input.GetMouseButton(0) && !EventSystem.current.IsPointerOverGameObject() ) { HandleInput(); } else { previousCell = null; } } void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { EditCells(hexGrid.GetCell(hit.point)); } else { previousCell = null; } }
Sel saat ini adalah sel yang kami temukan dengan memotong balok dengan jaring. Setelah mengedit sel, itu diperbarui dan menjadi sel sebelumnya untuk pembaruan baru. void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { HexCell currentCell = hexGrid.GetCell(hit.point); EditCells(currentCell); previousCell = currentCell; } else { previousCell = null; } }
Setelah menentukan sel saat ini, kita dapat membandingkannya dengan sel sebelumnya, jika ada. Jika kita mendapatkan dua sel yang berbeda, maka kita mungkin memiliki drag dan drop yang benar dan kita perlu memeriksa ini. Kalau tidak, ini jelas bukan drag and drop. if (Physics.Raycast(inputRay, out hit)) { HexCell currentCell = hexGrid.GetCell(hit.point); if (previousCell && previousCell != currentCell) { ValidateDrag(currentCell); } else { isDrag = false; } EditCells(currentCell); previousCell = currentCell; isDrag = true; }
Bagaimana cara memeriksa seret dan lepas? Memeriksa apakah sel saat ini adalah tetangga dari sel sebelumnya. Kami memeriksa ini dengan menghindari tetangganya dalam satu siklus. Jika kami menemukan kecocokan, kami juga segera mengenali arah seret. void ValidateDrag (HexCell currentCell) { for ( dragDirection = HexDirection.NE; dragDirection <= HexDirection.NW; dragDirection++ ) { if (previousCell.GetNeighbor(dragDirection) == currentCell) { isDrag = true; return; } } isDrag = false; }
Akankah kita membuat dendeng?, . «» , .
, . .
Ubah sel
Sekarang kita bisa mengenali drag dan drop, kita bisa mendefinisikan sungai keluar. Kami juga dapat menghapus sungai, untuk ini, tarik dan jatuhkan dukungan tidak diperlukan. void EditCell (HexCell cell) { if (cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } if (riverMode == OptionalToggle.No) { cell.RemoveRiver(); } else if (isDrag && riverMode == OptionalToggle.Yes) { previousCell.SetOutgoingRiver(dragDirection); } } }
Kode ini akan menarik sungai dari sel sebelumnya ke arus. Tapi dia mengabaikan ukuran kuas. Ini cukup logis, tetapi mari kita menggambar sungai untuk semua sel yang ditutup oleh sikat. Ini dapat dilakukan dengan melakukan operasi pada sel yang diedit. Dalam kasus kami, kami perlu memastikan bahwa sel lain benar-benar ada. else if (isDrag && riverMode == OptionalToggle.Yes) { HexCell otherCell = cell.GetNeighbor(dragDirection.Opposite()); if (otherCell) { otherCell.SetOutgoingRiver(dragDirection); } }
Sekarang kita dapat mengedit sungai, tetapi belum melihatnya. Kami dapat memverifikasi bahwa ini berfungsi dengan memeriksa sel yang dimodifikasi di inspektur debug.Sel dengan sungai di inspektur debug.Apa itu inspektur debug?. . , .
paket unityDasar sungai antar sel
Ketika melakukan triangulasi sungai, kita perlu mempertimbangkan dua bagian: lokasi dasar sungai dan air yang mengalir melaluinya. Pertama, kita akan membuat saluran, dan biarkan air untuk nanti.Bagian paling sederhana dari sungai adalah di mana ia mengalir di persimpangan antar sel. Sementara kami melakukan triangulasi area ini dengan strip tiga quad. Kita dapat menambahkan dasar sungai dengan menurunkan kuadrat tengah dan menambahkan dua dinding saluran.Menambahkan sungai ke strip tulang rusuk.Untuk ini, dalam kasus sungai, dua paha depan akan diperlukan dan saluran dengan dua dinding vertikal akan dibuat. Pendekatan alternatif adalah dengan menggunakan empat quad. Lalu kami menurunkan puncak tengah untuk membuat tempat tidur dengan dinding miring.Selalu empat quad.Penggunaan konstan jumlah quadrangles yang sama adalah mudah, jadi mari kita pilih opsi ini.Menambahkan Tops Edge
Transisi dari tiga ke empat per tepi membutuhkan penciptaan simpul tambahan dari tepi. Kami menulis ulang EdgeVertices
dengan mengubah nama dulu v4
menjadi v5
, dan kemudian mengubah nama v3
menjadi v4
. Tindakan dalam urutan ini memastikan bahwa semua kode terus referensi simpul yang benar. Gunakan opsi rename atau refactor editor Anda untuk ini untuk membuat perubahan berlaku di mana-mana. Jika tidak, Anda harus memeriksa seluruh kode secara manual dan melakukan perubahan. public Vector3 v1, v2, v4, v5;
Setelah mengganti nama semuanya, tambahkan yang baru v3
. public Vector3 v1, v2, v3, v4, v5;
Tambahkan simpul baru ke konstruktor. Terletak di tengah-tengah antara puncak sudut. Selain itu, simpul lain sekarang harus dalam Β½ dan ΒΎ, dan bukan di & frac13; dan & frac23;. public EdgeVertices (Vector3 corner1, Vector3 corner2) { v1 = corner1; v2 = Vector3.Lerp(corner1, corner2, 0.25f); v3 = Vector3.Lerp(corner1, corner2, 0.5f); v4 = Vector3.Lerp(corner1, corner2, 0.75f); v5 = corner2; }
Tambah v3
dan masuk TerraceLerp
. public static EdgeVertices TerraceLerp ( EdgeVertices a, EdgeVertices b, int step) { EdgeVertices result; result.v1 = HexMetrics.TerraceLerp(a.v1, b.v1, step); result.v2 = HexMetrics.TerraceLerp(a.v2, b.v2, step); result.v3 = HexMetrics.TerraceLerp(a.v3, b.v3, step); result.v4 = HexMetrics.TerraceLerp(a.v4, b.v4, step); result.v5 = HexMetrics.TerraceLerp(a.v5, b.v5, step); return result; }
Sekarang saya HexMesh
harus menyertakan simpul tambahan pada penggemar segitiga rusuk. void TriangulateEdgeFan (Vector3 center, EdgeVertices edge, Color color) { AddTriangle(center, edge.v1, edge.v2); AddTriangleColor(color); AddTriangle(center, edge.v2, edge.v3); AddTriangleColor(color); AddTriangle(center, edge.v3, edge.v4); AddTriangleColor(color); AddTriangle(center, edge.v4, edge.v5); AddTriangleColor(color); }
Dan juga dalam garis-garis segi empat. void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2 ) { AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); AddQuadColor(c1, c2); AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); AddQuadColor(c1, c2); AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); AddQuadColor(c1, c2); AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); AddQuadColor(c1, c2); }
Perbandingan empat dan lima simpul per tepi.Ketinggian dasar sungai
Kami membuat saluran dengan menurunkan bagian atas rusuk. Ini menentukan posisi vertikal dari dasar sungai. Meskipun posisi vertikal yang tepat dari setiap sel terdistorsi, kita harus mempertahankan ketinggian sungai yang sama dalam sel dengan ketinggian yang sama. Berkat air ini, tidak harus mengalir ke hulu. Selain itu, tempat tidur harus cukup rendah untuk tetap di bawah, bahkan dalam kasus sel-sel vertikal yang paling menyimpang, sementara pada saat yang sama menyisakan cukup ruang untuk air.Mari atur offset ini ke HexMetrics
dan ekspresikan sebagai tinggi. Offset dari satu level akan cukup. public const float streamBedElevationOffset = -1f;
Kita dapat menggunakan metrik ini untuk menambahkan properti HexCell
untuk mendapatkan posisi vertikal dari dasar sungai sel. public float StreamBedY { get { return (elevation + HexMetrics.streamBedElevationOffset) * HexMetrics.elevationStep; } }
Membuat saluran
Ketika HexMesh
salah satu dari enam bagian segitiga sel adalah triangulasi, kita dapat menentukan apakah sungai mengalir di sepanjang tepinya. Jika demikian, maka kita dapat menurunkan puncak tengah rusuk ke ketinggian dasar sungai. void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.Position; EdgeVertices e = new EdgeVertices( center + HexMetrics.GetFirstSolidCorner(direction), center + HexMetrics.GetSecondSolidCorner(direction) ); if (cell.HasRiverThroughEdge(direction)) { e.v3.y = cell.StreamBedY; } TriangulateEdgeFan(center, e, cell.Color); if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, e); } }
Ubah vertex tengah dari tulang rusuk.Kita bisa melihat bagaimana tanda-tanda pertama sungai muncul, tetapi lubang-lubang muncul pada relief. Untuk menutupnya, kita perlu mengubah tepi yang lain dan kemudian melakukan triangulasi koneksi. void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { HexCell neighbor = cell.GetNeighbor(direction); if (neighbor == null) { return; } Vector3 bridge = HexMetrics.GetBridge(direction); bridge.y = neighbor.Position.y - cell.Position.y; EdgeVertices e2 = new EdgeVertices( e1.v1 + bridge, e1.v5 + bridge ); if (cell.HasRiverThroughEdge(direction)) { e2.v3.y = neighbor.StreamBedY; } β¦ }
Saluran tulang rusuk yang lengkap.paket unityRiverbeds melewati sel
Sekarang kita memiliki dasar sungai yang tepat di antara sel-sel. Tetapi ketika sungai mengalir melalui sel, saluran selalu berakhir di tengahnya. Untuk mengatasi masalah ini harus bekerja. Mari kita mulai dengan kasing ketika sungai mengalir melalui sel secara langsung, dari satu ujung ke ujung.Jika tidak ada sungai, maka setiap bagian sel dapat menjadi penggemar segitiga sederhana. Tetapi ketika sungai mengalir langsung, perlu untuk memasukkan saluran. Faktanya, kita perlu meregangkan simpul tengah menjadi sebuah garis, sehingga mengubah dua segitiga tengah menjadi empat persegi panjang. Kemudian kipas segitiga berubah menjadi trapesium.Kami memasukkan saluran ke dalam segitiga.Saluran semacam itu akan jauh lebih lama daripada saluran yang melewati koneksi sel. Ini menjadi jelas ketika posisi titik terdistorsi. Oleh karena itu, mari kita bagikan trapesium menjadi dua segmen dengan memasukkan satu set ujung vertex di tengah antara tengah dan tepi.Triangulasi saluran.Karena triangulasi dengan sungai akan sangat berbeda dari triangulasi tanpa sungai, mari kita buat metode terpisah untuk itu. Jika kita memiliki sungai, maka kita menggunakan metode ini, jika tidak kita akan meninggalkan penggemar segitiga. void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.Position; EdgeVertices e = new EdgeVertices( center + HexMetrics.GetFirstSolidCorner(direction), center + HexMetrics.GetSecondSolidCorner(direction) ); if (cell.HasRiver) { if (cell.HasRiverThroughEdge(direction)) { e.v3.y = cell.StreamBedY; TriangulateWithRiver(direction, cell, center, e); } } else { TriangulateEdgeFan(center, e, cell.Color); } if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, e); } } void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { }
Lubang di mana harus ada sungai.Untuk melihat dengan lebih baik apa yang terjadi, nonaktifkan sementara distorsi sel. public const float cellPerturbStrength = 0f;
Puncak tidak terdistorsi.Triangulasi langsung melalui sel
Untuk membuat saluran langsung melalui bagian sel, kita perlu meregangkan bagian tengah menjadi satu garis. Baris ini harus memiliki lebar yang sama dengan saluran. Kita dapat menemukan simpul kiri dengan menggerakkan ΒΌ dari jarak dari tengah ke sudut pertama dari bagian sebelumnya. void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { Vector3 centerL = center + HexMetrics.GetFirstSolidCorner(direction.Previous()) * 0.25f; }
Demikian pula untuk vertex yang tepat. Dalam hal ini, kita membutuhkan sudut kedua dari bagian selanjutnya. Vector3 centerL = center + HexMetrics.GetFirstSolidCorner(direction.Previous()) * 0.25f; Vector3 centerR = center + HexMetrics.GetSecondSolidCorner(direction.Next()) * 0.25f;
Garis tengah dapat ditemukan dengan membuat tepi simpul antara pusat dan tepi. EdgeVertices m = new EdgeVertices( Vector3.Lerp(centerL, e.v1, 0.5f), Vector3.Lerp(centerR, e.v5, 0.5f) );
Selanjutnya, ubah vertex tengah dari tulang rusuk tengah, serta pusat, karena mereka akan menjadi titik-titik bawah saluran. m.v3.y = center.y = e.v3.y;
Sekarang kita bisa menggunakan TriangulateEdgeStrip
untuk mengisi ruang antara garis tengah dan garis tepi. TriangulateEdgeStrip(m, cell.Color, e, cell.Color);
Saluran terkompresi.Sayangnya, saluran terlihat terkompresi. Ini terjadi karena simpul tengah tulang rusuk terlalu dekat satu sama lain. Mengapa ini terjadi?Jika kita mengasumsikan bahwa panjang tepi luar adalah 1, maka panjang garis tengah akan Β½. Karena tepi tengah berada di tengah di antara mereka, panjangnya harus sama dengan ΒΎ.Lebar saluran adalah Β½ dan harus tetap konstan. Karena panjang tepi tengah adalah ΒΎ, hanya ΒΌ yang tersisa, menurut & frac18; di kedua sisi saluran.Panjang relatif.Karena panjang tepi tengah adalah ΒΎ, maka & frac18; menjadi relatif terhadap panjang tulang rusuk tengah sama dengan & frac16;. Ini berarti bahwa simpul kedua dan keempatnya harus diinterpolasi dengan perenam, bukan perempat.Kami dapat memberikan dukungan untuk interpolasi alternatif tersebut dengan menambahkan ke EdgeVertices
konstruktor lain. Alih-alih memperbaiki interpolasi untuk v2
dan v4
mari kita gunakan parameter. public EdgeVertices (Vector3 corner1, Vector3 corner2, float outerStep) { v1 = corner1; v2 = Vector3.Lerp(corner1, corner2, outerStep); v3 = Vector3.Lerp(corner1, corner2, 0.5f); v4 = Vector3.Lerp(corner1, corner2, 1f - outerStep); v5 = corner2; }
Sekarang kita dapat menggunakannya dengan & frac16; c HexMesh.TriangulateWithRiver
. EdgeVertices m = new EdgeVertices( Vector3.Lerp(centerL, e.v1, 0.5f), Vector3.Lerp(centerR, e.v5, 0.5f), 1f / 6f );
Saluran langsung.Setelah membuat saluran lurus, kita bisa pergi ke bagian kedua trapesium. Dalam hal ini, kita tidak dapat menggunakan strip rib, jadi kita harus melakukannya secara manual. Pertama mari kita buat segitiga di samping. AddTriangle(centerL, m.v1, m.v2); AddTriangleColor(cell.Color); AddTriangle(centerR, m.v4, m.v5); AddTriangleColor(cell.Color);
Segitiga sisi.Ini terlihat bagus, jadi mari kita isi ruang yang tersisa dengan dua segi empat, membuat bagian terakhir dari saluran. AddTriangle(centerL, m.v1, m.v2); AddTriangleColor(cell.Color); AddQuad(centerL, center, m.v2, m.v3); AddQuadColor(cell.Color); AddQuad(center, centerR, m.v3, m.v4); AddQuadColor(cell.Color); AddTriangle(centerR, m.v4, m.v5); AddTriangleColor(cell.Color);
Faktanya, kami tidak memiliki alternatif AddQuadColor
yang hanya membutuhkan satu parameter. Sementara kami tidak membutuhkannya. Jadi mari kita ciptakan. void AddQuadColor (Color color) { colors.Add(color); colors.Add(color); colors.Add(color); colors.Add(color); }
Saluran lurus lengkap.Mulai dan akhiri triangulasi
Triangulasi bagian yang hanya memiliki awal atau akhir sungai sangat berbeda, dan oleh karena itu memerlukan metode sendiri. Karena itu, kami akan memeriksa ini Triangulate
dan memanggil metode yang sesuai. if (cell.HasRiver) { if (cell.HasRiverThroughEdge(direction)) { e.v3.y = cell.StreamBedY; if (cell.HasRiverBeginOrEnd) { TriangulateWithRiverBeginOrEnd(direction, cell, center, e); } else { TriangulateWithRiver(direction, cell, center, e); } } }
Dalam hal ini, kami ingin menyelesaikan saluran di tengah, tetapi masih menggunakan dua tahap untuk ini. Oleh karena itu, kami akan kembali membuat tepi tengah antara tengah atau tepi. Karena kami ingin menyelesaikan saluran, kami cukup senang bahwa itu akan dikompresi. void TriangulateWithRiverBeginOrEnd ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { EdgeVertices m = new EdgeVertices( Vector3.Lerp(center, e.v1, 0.5f), Vector3.Lerp(center, e.v5, 0.5f) ); }
Agar saluran tidak menjadi terlalu dangkal, kami akan menetapkan ketinggian dasar sungai ke puncak tengah. Tapi pusatnya tidak perlu diubah. m.v3.y = e.v3.y;
Kita bisa melakukan triangulasi dengan satu strip rib dan kipas. TriangulateEdgeStrip(m, cell.Color, e, cell.Color); TriangulateEdgeFan(center, m, cell.Color);
Titik awal dan akhir.Satu Langkah Berubah
Selanjutnya, perhatikan belokan tajam yang zigzag antara sel yang berdekatan. Kami akan menangani mereka juga TriangulateWithRiver
. Oleh karena itu, kita perlu menentukan jenis sungai yang bekerja dengan kita.Sungai Zigzag.Jika sel memiliki sungai yang mengalir ke arah yang berlawanan, serta ke arah yang kita gunakan, maka ini harus berupa sungai lurus. Dalam hal ini, kita dapat menyimpan garis tengah yang telah kita hitung. Kalau tidak, ia kembali ke satu titik, melipat garis tengah. Vector3 centerL, centerR; if (cell.HasRiverThroughEdge(direction.Opposite())) { centerL = center + HexMetrics.GetFirstSolidCorner(direction.Previous()) * 0.25f; centerR = center + HexMetrics.GetSecondSolidCorner(direction.Next()) * 0.25f; } else { centerL = centerR = center; }
Zig-zag melengkung.Kita dapat mengenali belokan tajam dengan memeriksa apakah sel memiliki sungai yang melewati bagian sel berikutnya atau sebelumnya. Jika ada, maka kita perlu menyelaraskan garis tengah dengan tepi antara ini dan bagian tetangga. Kita dapat melakukan ini dengan menempatkan sisi yang sesuai dari garis di tengah antara pusat dan sudut umum. Sisi lain dari garis kemudian menjadi pusat. if (cell.HasRiverThroughEdge(direction.Opposite())) { centerL = center + HexMetrics.GetFirstSolidCorner(direction.Previous()) * 0.25f; centerR = center + HexMetrics.GetSecondSolidCorner(direction.Next()) * 0.25f; } else if (cell.HasRiverThroughEdge(direction.Next())) { centerL = center; centerR = Vector3.Lerp(center, e.v5, 0.5f); } else if (cell.HasRiverThroughEdge(direction.Previous())) { centerL = Vector3.Lerp(center, e.v1, 0.5f); centerR = center; } else { centerL = centerR = center; }
Setelah memutuskan di mana titik kiri dan kanan, kita dapat menentukan pusat yang dihasilkan dengan rata-rata mereka. if (cell.HasRiverThroughEdge(direction.Opposite())) { β¦ } center = Vector3.Lerp(centerL, centerR, 0.5f);
Tulang rusuk tengah offsetMeskipun saluran memiliki lebar yang sama di kedua sisi, tampilannya cukup terkompresi. Ini disebabkan oleh memutar garis tengah 60 Β°. Anda dapat menghaluskan efek ini dengan sedikit meningkatkan lebar garis tengah. Alih-alih interpolasi dengan Β½, kami menggunakan & frac23;. else if (cell.HasRiverThroughEdge(direction.Next())) { centerL = center; centerR = Vector3.Lerp(center, e.v5, 2f / 3f); } else if (cell.HasRiverThroughEdge(direction.Previous())) { centerL = Vector3.Lerp(center, e.v1, 2f / 3f); centerR = center; }
Zigzag tanpa kompresi.Giliran dua tahap
Kasing yang tersisa adalah antara zig-zag dan sungai lurus. Ini adalah belokan dua tahap yang membuat sungai melengkung dengan lembut.Sungai yang berkelok-kelok.Untuk membedakan antara dua orientasi yang mungkin, kita perlu menggunakan direction.Next().Next()
. Tapi mari kita membuatnya lebih nyaman dengan menambahkan HexDirection
metode penyuluhan Next2
dan Previous2
. public static HexDirection Previous2 (this HexDirection direction) { direction -= 2; return direction >= HexDirection.NE ? direction : (direction + 6); } public static HexDirection Next2 (this HexDirection direction) { direction += 2; return direction <= HexDirection.NW ? direction : (direction - 6); }
Kembali ke HexMesh.TriangulateWithRiver
. Sekarang kita bisa mengenali arah sungai kita yang berkelok-kelok direction.Next2()
. if (cell.HasRiverThroughEdge(direction.Opposite())) { centerL = center + HexMetrics.GetFirstSolidCorner(direction.Previous()) * 0.25f; centerR = center + HexMetrics.GetSecondSolidCorner(direction.Next()) * 0.25f; } else if (cell.HasRiverThroughEdge(direction.Next())) { centerL = center; centerR = Vector3.Lerp(center, e.v5, 2f / 3f); } else if (cell.HasRiverThroughEdge(direction.Previous())) { centerL = Vector3.Lerp(center, e.v1, 2f / 3f); centerR = center; } else if (cell.HasRiverThroughEdge(direction.Next2())) { centerL = centerR = center; } else { centerL = centerR = center; }
Dalam dua kasus terakhir ini, kita perlu menggeser garis tengah ke bagian sel yang terletak di bagian dalam kurva. Jika kita memiliki vektor di tengah-tengah tepi yang padat, maka kita dapat menggunakannya untuk memposisikan titik akhir. Mari kita bayangkan bahwa kita memiliki metode untuk ini. else if (cell.HasRiverThroughEdge(direction.Next2())) { centerL = center; centerR = center + HexMetrics.GetSolidEdgeMiddle(direction.Next()) * 0.5f; } else { centerL = center + HexMetrics.GetSolidEdgeMiddle(direction.Previous()) * 0.5f; centerR = center; }
Tentu saja, sekarang kita perlu menambahkan metode seperti itu HexMetrics
. Dia hanya perlu rata-rata dua vektor sudut yang berdekatan dan menerapkan koefisien integritas. public static Vector3 GetSolidEdgeMiddle (HexDirection direction) { return (corners[(int)direction] + corners[(int)direction + 1]) * (0.5f * solidFactor); }
Kurva sedikit terkompresi.Garis tengah kami sekarang diputar dengan benar 30 Β°. Tetapi mereka tidak cukup panjang, itu sebabnya saluran sedikit terkompresi. Ini terjadi karena titik tengah rusuk lebih dekat ke tengah daripada sudut rusuk. Jaraknya sama dengan jari-jari dalam, bukan yang luar. Artinya, kami sedang mengerjakan skala yang salah.Kami sudah mengonversi dari jari-jari eksternal ke internal di HexMetrics
. Kita perlu melakukan operasi terbalik. Jadi, mari kita buat kedua faktor konversi ini tersedia HexMetrics
. public const float outerToInner = 0.866025404f; public const float innerToOuter = 1f / outerToInner; public const float outerRadius = 10f; public const float innerRadius = outerRadius * outerToInner;
Sekarang kita bisa beralih ke skala yang tepat HexMesh.TriangulateWithRiver
. Saluran masih akan tetap sedikit terjepit karena giliran mereka, tetapi ini jauh kurang diucapkan daripada dalam kasus zig-zag. Karena itu, kita tidak perlu memberikan kompensasi untuk ini. else if (cell.HasRiverThroughEdge(direction.Next2())) { centerL = center; centerR = center + HexMetrics.GetSolidEdgeMiddle(direction.Next()) * (0.5f * HexMetrics.innerToOuter); } else { centerL = center + HexMetrics.GetSolidEdgeMiddle(direction.Previous()) * (0.5f * HexMetrics.innerToOuter); centerR = center; }
Kurva halus.paket unityTriangulasi di sekitar sungai
Sungai-sungai kita sudah siap. Tapi kami belum melakukan triangulasi bagian lain dari sel yang mengandung sungai. Sekarang kita akan menutup lubang ini.Lubang di dekat saluran.Jika sel memiliki sungai, tetapi tidak mengalir ke arah saat ini, maka Triangulate
kita akan memanggil metode baru di. if (cell.HasRiver) { if (cell.HasRiverThroughEdge(direction)) { e.v3.y = cell.StreamBedY; if (cell.HasRiverBeginOrEnd) { TriangulateWithRiverBeginOrEnd(direction, cell, center, e); } else { TriangulateWithRiver(direction, cell, center, e); } } else { TriangulateAdjacentToRiver(direction, cell, center, e); } } else { TriangulateEdgeFan(center, e, cell.Color); }
Dalam metode ini, kita mengisi segitiga sel dengan strip dan kipas. Hanya penggemar tidak akan cukup bagi kita, karena puncaknya harus sesuai dengan tepi tengah bagian yang mengandung sungai. void TriangulateAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { EdgeVertices m = new EdgeVertices( Vector3.Lerp(center, e.v1, 0.5f), Vector3.Lerp(center, e.v5, 0.5f) ); TriangulateEdgeStrip(m, cell.Color, e, cell.Color); TriangulateEdgeFan(center, m, cell.Color); }
Hamparan di kurva dan sungai lurus.Cocokkan saluran
Tentu saja, kita perlu membuat pusat yang kita gunakan cocok dengan bagian tengah yang digunakan oleh bagian sungai. Dengan zig-zag, semuanya teratur, dan kurva dan sungai lurus membutuhkan perhatian. Oleh karena itu, kita perlu menentukan jenis sungai dan orientasi relatifnya.Mari kita mulai dengan memeriksa apakah kita ada di dalam kurva. Dalam hal ini, arah sebelumnya dan berikutnya berisi sungai. Jika demikian, maka kita perlu memindahkan pusat ke tepi. if (cell.HasRiverThroughEdge(direction.Next())) { if (cell.HasRiverThroughEdge(direction.Previous())) { center += HexMetrics.GetSolidEdgeMiddle(direction) * (HexMetrics.innerToOuter * 0.5f); } } EdgeVertices m = new EdgeVertices( Vector3.Lerp(center, e.v1, 0.5f), Vector3.Lerp(center, e.v5, 0.5f) );
Memperbaiki kasus di mana sungai mengalir dari kedua sisi.Jika kami memiliki sungai di arah yang berbeda, tetapi tidak di sungai sebelumnya, maka kami memeriksa untuk melihat apakah itu lurus. Jika demikian, maka gerakkan pusat ke sudut pertama. if (cell.HasRiverThroughEdge(direction.Next())) { if (cell.HasRiverThroughEdge(direction.Previous())) { center += HexMetrics.GetSolidEdgeMiddle(direction) * (HexMetrics.innerToOuter * 0.5f); } else if ( cell.HasRiverThroughEdge(direction.Previous2()) ) { center += HexMetrics.GetFirstSolidCorner(direction) * 0.25f; } }
Memperbaiki setengah overlay dengan sungai lurus.Jadi kami memecahkan masalah dengan setengah bagian yang berdekatan dengan sungai lurus. Kasus terakhir - kami memiliki sungai di arah sebelumnya, dan lurus. Dalam hal ini, Anda harus memindahkan tengah ke sudut berikutnya. if (cell.HasRiverThroughEdge(direction.Next())) { if (cell.HasRiverThroughEdge(direction.Previous())) { center += HexMetrics.GetSolidEdgeMiddle(direction) * (HexMetrics.innerToOuter * 0.5f); } else if ( cell.HasRiverThroughEdge(direction.Previous2()) ) { center += HexMetrics.GetFirstSolidCorner(direction) * 0.25f; } } else if ( cell.HasRiverThroughEdge(direction.Previous()) && cell.HasRiverThroughEdge(direction.Next2()) ) { center += HexMetrics.GetSecondSolidCorner(direction) * 0.25f; }
Tidak ada lagi overlay.paket unityGeneralisasi HexMesh
Kami telah menyelesaikan triangulasi saluran. Sekarang kita bisa mengisinya dengan air. Karena air berbeda dari tanah, kita perlu menggunakan jaring yang berbeda dengan data titik dan bahan yang berbeda. Akan sangat nyaman jika kita bisa menggunakan HexMesh
sushi dan air. Jadi mari kita menggeneralisasi HexMesh
dengan mengubahnya menjadi kelas yang berhubungan dengan jerat ini, terlepas dari apa yang digunakan untuk itu. Kami akan meneruskan tugas triangulasi selnya HexGridChunk
.Memindahkan Metode Perturb
Karena metode ini Perturb
cukup digeneralisasi dan akan digunakan di tempat yang berbeda, mari kita pindahkan ke HexMetrics
. Pertama, ubah nama menjadi HexMetrics.Perturb
. Ini adalah nama metode yang tidak benar, tetapi ia mengembalikan semua kode untuk penggunaannya yang benar. Jika editor kode Anda memiliki fungsi khusus untuk memindahkan metode, maka gunakan itu.Dengan memindahkan metode ke dalam HexMetrics
, jadikan umum dan statis, lalu koreksi namanya. public static Vector3 Perturb (Vector3 position) { Vector4 sample = SampleNoise(position); position.x += (sample.x * 2f - 1f) * cellPerturbStrength; position.z += (sample.z * 2f - 1f) * cellPerturbStrength; return position; }
Metode triangulasi bergerak
Di HexGridChunk
ganti variabel hexMesh
dengan variabel umum terrain
. public HexMesh terrain;
Selanjutnya, kami merefleksikan semua metode Addβ¦
dari HexMesh
c terrain.Addβ¦
. Kemudian pindahkan semua metode Triangulateβ¦
ke HexGridChunk
. Anda kemudian dapat memperbaiki nama-nama metode Addβ¦
dalam HexMesh
dan membuat umum mereka. Akibatnya, semua metode triangulasi kompleks akan ditemukan HexGridChunk
, dan metode sederhana untuk menambahkan data ke mesh akan tetap masuk HexMesh
.Kami belum selesai. Sekarang HexGridChunk.LateUpdate
harus memanggil metode sendiri Triangulate
. Selain itu, seharusnya tidak lagi melewati sel sebagai argumen. Karena itu, ia Triangulate
mungkin kehilangan parameternya. Dan dia harus mendelegasikan pembersihan dan aplikasi data mesh HexMesh
. void LateUpdate () { Triangulate();
Menambahkan metode yang dibutuhkan Clear
dan Apply
di HexMesh
. public void Clear () { hexMesh.Clear(); vertices.Clear(); colors.Clear(); triangles.Clear(); } public void Apply () { hexMesh.SetVertices(vertices); hexMesh.SetColors(colors); hexMesh.SetTriangles(triangles, 0); hexMesh.RecalculateNormals(); meshCollider.sharedMesh = hexMesh; }
Bagaimana dengan SetVertices, SetColors, dan SetTriangles?Mesh
. . , .
SetTriangles
integer, . , .
Akhirnya, pasang anak jaring secara manual ke cetakan fragmen. Kami tidak dapat melakukan ini lagi secara otomatis, karena kami akan segera menambahkan anak kedua ke dalam jala. Ubah nama menjadi Terrain untuk menunjukkan tujuannya.Berikan bantuan.Mengganti nama anak prefab tidak berfungsi?. , . , Apply , . .
Membuat Daftar Kolam
Meskipun kami telah memindahkan sedikit kode, peta kami tetap berfungsi seperti sebelumnya. Menambahkan jala lain ke fragmen tidak akan mengubah ini. Tetapi jika kita melakukan ini dengan masa sekarang HexMesh
, maka kesalahan mungkin timbul.Masalahnya adalah kita berasumsi bahwa kita hanya akan bekerja dengan satu mesh pada satu waktu. Ini memungkinkan kami untuk menggunakan daftar statis untuk menyimpan data mesh sementara. Tetapi setelah menambahkan air, kami akan bekerja secara bersamaan dengan dua jerat, sehingga kami tidak dapat lagi menggunakan daftar statis.Namun, kami tidak akan kembali ke set daftar untuk setiap instance HexMesh
. Sebagai gantinya, kami menggunakan kumpulan daftar statis. Secara default, kumpulan ini tidak ada, jadi mari kita mulai dengan membuat sendiri kelas kumpulan daftar umum. public static class ListPool<T> { }
Bagaimana cara kerja ListPool <T>?, List<int>
. <T>
ListPool
, , . , T
( template).
Untuk menyimpan koleksi daftar di kumpulan, kita bisa menggunakan tumpukan. Saya biasanya tidak menggunakan daftar karena Unity tidak membuat cerita bersambung, tetapi dalam hal ini tidak masalah. using System.Collections.Generic; public static class ListPool<T> { static Stack<List<T>> stack = new Stack<List<T>>(); }
Apa artinya tumpukan <daftar <t>>?. , . .
Tambahkan metode statis umum untuk mendapatkan daftar dari kumpulan. Jika tumpukan tidak kosong, kami akan mengekstrak daftar teratas dan mengembalikan yang ini. Kalau tidak, kami akan membuat daftar baru di tempat. public static List<T> Get () { if (stack.Count > 0) { return stack.Pop(); } return new List<T>(); }
Untuk menggunakan kembali daftar, Anda perlu menambahkannya ke kumpulan setelah Anda selesai bekerja dengannya. ListPool
akan menghapus daftar dan mendorongnya ke tumpukan. public static void Add (List<T> list) { list.Clear(); stack.Push(list); }
Sekarang kita bisa menggunakan kolam di HexMesh
. Ganti daftar statis dengan tautan pribadi non-statis. Mari kita tandai mereka NonSerialized
sehingga Unity tidak melindungi mereka selama kompilasi. Atau tulis System.NonSerialized
, atau tambahkan using System;
di awal skrip. [NonSerialized] List<Vector3> vertices; [NonSerialized] List<Color> colors; [NonSerialized] List<int> triangles;
Karena jaring dibersihkan tepat sebelum menambahkan data baru, di sinilah Anda perlu mendapatkan daftar dari kumpulan. public void Clear () { hexMesh.Clear(); vertices = ListPool<Vector3>.Get(); colors = ListPool<Color>.Get(); triangles = ListPool<int>.Get(); }
Setelah menerapkan jerat ini, kita tidak lagi membutuhkannya, jadi di sini kita dapat menambahkannya ke kolam. public void Apply () { hexMesh.SetVertices(vertices); ListPool<Vector3>.Add(vertices); hexMesh.SetColors(colors); ListPool<Color>.Add(colors); hexMesh.SetTriangles(triangles, 0); ListPool<int>.Add(triangles); hexMesh.RecalculateNormals(); meshCollider.sharedMesh = hexMesh; }
Jadi kami menerapkan beberapa penggunaan daftar, terlepas dari berapa banyak jerat yang kami isi pada saat yang sama.Collider opsional
Meskipun medan kita membutuhkan tabrakan, itu tidak benar-benar diperlukan untuk sungai. Sinar hanya akan melewati air dan berpotongan dengan saluran di bawahnya. Mari kita membuatnya sehingga kita dapat mengonfigurasi keberadaan collider untuk HexMesh
. Kami menyadari hal ini dengan menambahkan bidang yang sama bool useCollider
. Untuk medan, kita nyalakan. public bool useCollider;
Menggunakan collider jala.Kita perlu membuat collider dibuat dan ditugaskan hanya ketika dihidupkan. void Awake () { GetComponent<MeshFilter>().mesh = hexMesh = new Mesh(); if (useCollider) { meshCollider = gameObject.AddComponent<MeshCollider>(); } hexMesh.name = "Hex Mesh"; } public void Apply () { β¦ if (useCollider) { meshCollider.sharedMesh = hexMesh; } β¦ }
Warna opsional
Warna vertex mungkin juga opsional. Kami membutuhkan mereka untuk menunjukkan berbagai jenis bantuan, tetapi air tidak berubah warna. Kita bisa menjadikannya opsional sama seperti kita membuat collider menjadi opsional. public bool useCollider, useColors; public void Clear () { hexMesh.Clear(); vertices = ListPool<Vector3>.Get(); if (useColors) { colors = ListPool<Color>.Get(); } triangles = ListPool<int>.Get(); } public void Apply () { hexMesh.SetVertices(vertices); ListPool<Vector3>.Add(vertices); if (useColors) { hexMesh.SetColors(colors); ListPool<Color>.Add(colors); } β¦ }
Tentu saja, medannya harus menggunakan warna titik, jadi nyalakan.Gunakan warna.UV opsional
Sementara kami melakukan ini, kami juga dapat menambahkan dukungan untuk koordinat UV opsional. Meskipun bantuan tidak menggunakannya, kami akan membutuhkannya untuk air. public bool useCollider, useColors, useUVCoordinates; [NonSerialized] List<Vector2> uvs; public void Clear () { hexMesh.Clear(); vertices = ListPool<Vector3>.Get(); if (useColors) { colors = ListPool<Color>.Get(); } if (useUVCoordinates) { uvs = ListPool<Vector2>.Get(); } triangles = ListPool<int>.Get(); } public void Apply () { hexMesh.SetVertices(vertices); ListPool<Vector3>.Add(vertices); if (useColors) { hexMesh.SetColors(colors); ListPool<Color>.Add(colors); } if (useUVCoordinates) { hexMesh.SetUVs(0, uvs); ListPool<Vector2>.Add(uvs); } β¦ }
Kami tidak menggunakan koordinat UV.Untuk menggunakan fungsi ini, buat metode untuk menambahkan koordinat UV ke segitiga dan segi empat. public void AddTriangleUV (Vector2 uv1, Vector2 uv2, Vector3 uv3) { uvs.Add(uv1); uvs.Add(uv2); uvs.Add(uv3); } public void AddQuadUV (Vector2 uv1, Vector2 uv2, Vector3 uv3, Vector3 uv4) { uvs.Add(uv1); uvs.Add(uv2); uvs.Add(uv3); uvs.Add(uv4); }
Mari kita tambahkan metode tambahan AddQuadUV
untuk menambah area UV persegi panjang dengan nyaman. Ini adalah kasus standar ketika quad dan teksturnya sama, kami akan menggunakannya untuk air sungai. public void AddQuadUV (float uMin, float uMax, float vMin, float vMax) { uvs.Add(new Vector2(uMin, vMin)); uvs.Add(new Vector2(uMax, vMin)); uvs.Add(new Vector2(uMin, vMax)); uvs.Add(new Vector2(uMax, vMax)); }
paket unitySungai saat ini
Akhirnya saatnya untuk membuat air! Kami akan melakukan ini dengan quad, yang akan menunjukkan permukaan air. Dan karena kita bekerja dengan sungai, air harus mengalir. Untuk melakukan ini, kami menggunakan koordinat UV yang menunjukkan orientasi sungai. Untuk memvisualisasikan ini, kita memerlukan shader baru. Oleh karena itu, buat shader standar baru dan beri nama River . Ubahlah sehingga koordinat UV direkam dalam saluran albedo hijau dan merah. Shader "Custom/River" { β¦ void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb * IN.color; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; o.Albedo.rg = IN.uv_MainTex; } ENDCG } FallBack "Diffuse" }
Tambahkan ke HexGridChunk
bidang umum HexMesh rivers
. Kami membersihkannya dan menerapkannya dengan cara yang sama seperti dalam kasus bantuan. public HexMesh terrain, rivers; public void Triangulate () { terrain.Clear(); rivers.Clear(); for (int i = 0; i < cells.Length; i++) { Triangulate(cells[i]); } terrain.Apply(); rivers.Apply(); }
Apakah kami akan mendapat panggilan telepon tambahan, bahkan jika kami tidak memiliki sungai?Unity , . , - .
Ubah prefab (melalui instance), menduplikasi objek terainnya, menamainya ulang menjadi Rivers dan menghubungkannya.Potongan cetakan dengan sungai.Buat material Sungai menggunakan shader baru kami dan buat objek Rivers menggunakannya . Kami juga mengatur komponen heksagon segi enam dari objek sehingga menggunakan koordinat UV, tetapi tidak menggunakan warna titik maupun collider.Sungai Subobject.Triangulasi air
Sebelum kita melakukan triangulasi air, kita perlu menentukan tingkat permukaannya. Mari kita buat perubahan ketinggian HexMetrics
, seperti yang kita lakukan dengan dasar sungai. Karena distorsi vertikal sel sama dengan setengah pergeseran ketinggian, mari kita gunakan untuk menggeser permukaan sungai. Jadi kami menjamin bahwa air tidak akan pernah berada di atas topografi sel. public const float riverSurfaceElevationOffset = -0.5f;
Mengapa tidak membuatnya sedikit lebih rendah?, . , .
Tambahkan HexCell
properti untuk mendapatkan posisi vertikal permukaan sungai. public float RiverSurfaceY { get { return (elevation + HexMetrics.riverSurfaceElevationOffset) * HexMetrics.elevationStep; } }
Sekarang kita bisa mulai bekerja HexGridChunk
! Karena kita akan membuat banyak segi empat sungai, mari kita tambahkan metode terpisah untuk ini. Mari kita beri empat simpul dan tinggi sebagai parameter. Ini akan memungkinkan kita untuk dengan mudah mengatur posisi vertikal keempat simpul secara bersamaan sebelum menambahkan quad. void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y ) { v1.y = v2.y = v3.y = v4.y = y; rivers.AddQuad(v1, v2, v3, v4); }
Kami akan menambahkan di sini koordinat UV dari segiempat. Hanya berkeliling dari kiri ke kanan dan dari bawah ke atas. rivers.AddQuad(v1, v2, v3, v4); rivers.AddQuadUV(0f, 1f, 0f, 1f);
TriangulateWithRiver
- Ini adalah metode pertama yang akan kita tambahkan segi empat sungai. Quad pertama adalah antara pusat dan tengah. Yang kedua adalah antara bagian tengah dan tulang rusuk. Kami hanya menggunakan simpul yang sudah kami miliki. Karena puncak-puncak ini akan diremehkan, air sebagai akibatnya akan sebagian di bawah dinding saluran yang miring. Karena itu, kita tidak perlu khawatir dengan posisi tepat tepi air. void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { β¦ TriangulateRiverQuad(centerL, centerR, m.v2, m.v4, cell.RiverSurfaceY); TriangulateRiverQuad(m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY); }
Tanda-tanda pertama air.Mengapa lebar air berubah?, , β . . .
Bergerak mengikuti arus
Saat ini, koordinat UV tidak konsisten dengan arah sungai. Kita perlu menjaga konsistensi di sini. Misalkan koordinat U adalah 0 di sisi kiri sungai, dan 1 di kanan, ketika melihat hilir. Dan koordinat V harus bervariasi dari 0 hingga 1 ke arah sungai.Dengan menggunakan spesifikasi ini, UVs akan benar ketika sungai keluar adalah triangulasi, tetapi mereka akan berubah menjadi tidak benar dan mereka harus dibalik ketika sungai yang masuk adalah triangulasi. Untuk mempermudah pekerjaan, tambahkan ke TriangulateRiverQuad
parameter bool reversed
. Gunakan untuk membalikkan UV jika perlu. void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y, bool reversed ) { v1.y = v2.y = v3.y = v4.y = y; rivers.AddQuad(v1, v2, v3, v4); if (reversed) { rivers.AddQuadUV(1f, 0f, 1f, 0f); } else { rivers.AddQuadUV(0f, 1f, 0f, 1f); } }
Seperti TriangulateWithRiver
kita tahu bahwa kita perlu untuk mengubah arah, ketika berhadapan dengan sungai yang masuk. bool reversed = cell.IncomingRiver == direction; TriangulateRiverQuad( centerL, centerR, m.v2, m.v4, cell.RiverSurfaceY, reversed ); TriangulateRiverQuad( m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY, reversed );
Arah sungai yang disepakati.Awal dan akhir sungai
Di dalam, TriangulateWithRiverBeginOrEnd
kita hanya perlu memeriksa apakah kita memiliki sungai yang masuk untuk menentukan arah aliran. Lalu kita bisa memasukkan sungai quad lain antara tengah dan tulang rusuk. void TriangulateWithRiverBeginOrEnd ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { β¦ bool reversed = cell.HasIncomingRiver; TriangulateRiverQuad( m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY, reversed ); }
Bagian antara pusat dan tengah adalah segitiga, jadi kita tidak bisa menggunakannya TriangulateRiverQuad
. Satu-satunya perbedaan yang signifikan di sini adalah bahwa puncak pusatnya ada di tengah sungai. Karena itu, koordinatnya selalu sama dengan Β½. center.y = m.v2.y = m.v4.y = cell.RiverSurfaceY; rivers.AddTriangle(center, m.v2, m.v4); if (reversed) { rivers.AddTriangleUV( new Vector2(0.5f, 1f), new Vector2(1f, 0f), new Vector2(0f, 0f) ); } else { rivers.AddTriangleUV( new Vector2(0.5f, 0f), new Vector2(0f, 1f), new Vector2(1f, 1f) ); }
Air di awal dan akhir.Apakah ada bagian air yang hilang di ujungnya?, quad , . . .
, . , . .
Aliran antar sel
Saat menambahkan air di antara sel, kita harus berhati-hati tentang perbedaan ketinggian. Agar air dapat mengalir menuruni lereng dan tebing, ia TriangulateRiverQuad
harus mendukung dua parameter ketinggian. Jadi mari kita tambahkan yang kedua. void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y1, float y2, bool reversed ) { v1.y = v2.y = y1; v3.y = v4.y = y2; rivers.AddQuad(v1, v2, v3, v4); if (reversed) { rivers.AddQuadUV(1f, 0f, 1f, 0f); } else { rivers.AddQuadUV(0f, 1f, 0f, 1f); } }
Juga, untuk kenyamanan, mari tambahkan opsi yang akan menerima satu ketinggian. Itu hanya akan memanggil metode lain. void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y, bool reversed ) { TriangulateRiverQuad(v1, v2, v3, v4, y, y, reversed); }
Sekarang kita dapat menambahkan quad Sungai dan TriangulateConnection
. Berada di antara sel-sel, kita tidak bisa segera mengetahui jenis sungai yang kita hadapi. Untuk menentukan apakah diperlukan belokan, kita perlu memeriksa apakah kita memiliki sungai yang masuk dan apakah itu bergerak ke arah kita. if (cell.HasRiverThroughEdge(direction)) { e2.v3.y = neighbor.StreamBedY; TriangulateRiverQuad( e1.v2, e1.v4, e2.v2, e2.v4, cell.RiverSurfaceY, neighbor.RiverSurfaceY, cell.HasIncomingRiver && cell.IncomingRiver == direction ); }
Sungai yang selesai.Peregangan koordinat V
Sejauh ini, di setiap segmen sungai, kami memiliki koordinat V dari 0 hingga 1. Artinya, hanya ada empat dari mereka di sel. Lima jika kita juga menambahkan koneksi antar sel. Apa pun yang kita gunakan untuk memberi tekstur sungai, itu harus diulangi beberapa kali.Kita dapat mengurangi jumlah pengulangan dengan meregangkan koordinat V sehingga mereka berpindah dari 0 ke 1 di seluruh sel ditambah satu koneksi. Ini dapat dilakukan dengan meningkatkan koordinat V di setiap segmen sebesar 0,2. Jika kita menempatkan 0,4 di tengah, maka di tengah akan menjadi 0,6, dan di tepi itu akan mencapai 0,8. Kemudian dalam koneksi sel, nilainya akan menjadi 1.Jika sungai mengalir ke arah yang berlawanan, kita masih bisa menempatkan 0,4 di tengah, tetapi di tengahnya menjadi 0,2, dan di tepi - 0. Jika kita melanjutkan ini sampai sel bergabung, kita mendapatkan -0,2 sebagai hasilnya. Ini normal karena mirip dengan 0,8 untuk tekstur dengan mode penyaringan berulang, sama seperti 0 setara dengan 1.Perubahan koordinat V.Untuk membuat dukungan untuk ini, kita perlu menambahkan TriangulateRiverQuad
satu parameter lagi. void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y, float v, bool reversed ) { TriangulateRiverQuad(v1, v2, v3, v4, y, y, v, reversed); } void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y1, float y2, float v, bool reversed ) { β¦ }
Ketika arah tidak terbalik, kita cukup menggunakan koordinat yang ditransmisikan di bagian bawah segi empat dan menambahkan 0,2 di bagian atas. else { rivers.AddQuadUV(0f, 1f, v, v + 0.2f); }
Kita dapat bekerja dengan arah terbalik dengan mengurangi koordinat dari 0,8 dan 0,6. if (reversed) { rivers.AddQuadUV(1f, 0f, 0.8f - v, 0.6f - v); }
Sekarang kita harus mengirimkan koordinat yang benar, seolah-olah kita berurusan dengan sungai keluar. Mari kita mulai TriangulateWithRiver
. TriangulateRiverQuad( centerL, centerR, m.v2, m.v4, cell.RiverSurfaceY, 0.4f, reversed ); TriangulateRiverQuad( m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY, 0.6f, reversed );
Kemudian TriangulateConnection
ubah sebagai berikut. TriangulateRiverQuad( e1.v2, e1.v4, e2.v2, e2.v4, cell.RiverSurfaceY, neighbor.RiverSurfaceY, 0.8f, cell.HasIncomingRiver && cell.IncomingRiver == direction );
Dan akhirnya TriangulateWithRiverBeginOrEnd
. TriangulateRiverQuad( m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY, 0.6f, reversed ); center.y = m.v2.y = m.v4.y = cell.RiverSurfaceY; rivers.AddTriangle(center, m.v2, m.v4); if (reversed) { rivers.AddTriangleUV( new Vector2(0.5f, 0.4f), new Vector2(1f, 0.2f), new Vector2(0f, 0.2f) ); } else { rivers.AddTriangleUV( new Vector2(0.5f, 0.4f), new Vector2(0f, 0.6f), new Vector2(1f, 0.6f) ); }
Koordinat terentang V.Untuk menampilkan lipatan koordinat V dengan benar, pastikan mereka tetap positif di shader sungai. if (IN.uv_MainTex.y < 0) { IN.uv_MainTex.y += 1; } o.Albedo.rg = IN.uv_MainTex;
Koordinat yang diciutkan V. paketunityAnimasi sungai
Setelah selesai dengan koordinat UV, kita dapat melanjutkan untuk menghidupkan sungai. Shader sungai akan melakukan ini sehingga kita tidak harus terus memperbarui jala.Kami tidak akan membuat shader sungai yang rumit dalam tutorial ini, tetapi akan melakukannya nanti. Untuk saat ini, kami akan membuat efek sederhana yang memberikan pemahaman tentang cara kerja animasi.Animasi dibuat dengan menggeser koordinat V berdasarkan waktu permainan. Unity memungkinkan Anda untuk mendapatkan nilainya menggunakan variabel _Time
. Komponennya Y berisi waktu yang tidak berubah, yang kami gunakan. Komponen lainnya mengandung skala waktu yang berbeda.Kami akan menyingkirkan lipatan di sepanjang V, karena kami tidak lagi membutuhkannya. Sebaliknya, kami mengurangi waktu saat ini dari koordinat V. Ini menggeser koordinat ke bawah, yang menciptakan ilusi arus yang mengalir di hilir sungai. // if (IN.uv_MainTex.y < 0) { // IN.uv_MainTex.y += 1; // } IN.uv_MainTex.y -= _Time.y; o.Albedo.rg = IN.uv_MainTex;
Dalam satu detik, koordinat V di semua titik akan menjadi kurang dari nol, jadi kita tidak akan lagi melihat perbedaannya. Sekali lagi, ini normal ketika menggunakan pemfilteran dalam mode pengulangan tekstur. Tetapi untuk melihat apa yang terjadi, kita dapat mengambil bagian fraksional dari koordinat V. IN.uv_MainTex.y -= _Time.y; IN.uv_MainTex.y = frac(IN.uv_MainTex.y); o.Albedo.rg = IN.uv_MainTex;
Koordinat animasi V.Penggunaan kebisingan
Sekarang sungai kita dianimasikan, tetapi dalam arah dan kecepatan ada transisi yang tajam. Pola UV kami membuatnya sangat jelas, tetapi akan lebih sulit untuk dikenali jika Anda menggunakan pola yang lebih mirip air. Jadi, alih-alih menampilkan UV mentah, mari kita sampel teksturnya. Kita dapat menggunakan tekstur noise yang ada. Kami mengambil sampelnya dan mengalikan warna material dengan saluran noise pertama. void surf (Input IN, inout SurfaceOutputStandard o) { float2 uv = IN.uv_MainTex; uv.y -= _Time.y; float4 noise = tex2D(_MainTex, uv); fixed4 c = _Color * noise.r; o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; }
Tetapkan tekstur kebisingan ke material sungai dan pastikan warnanya putih.Menggunakan tekstur noise.Karena koordinat V sangat memanjang, tekstur kebisingan juga membentang di sepanjang sungai. Sayangnya, jalannya tidak begitu indah. Mari kita coba merentangkannya dengan cara lain - sangat mengurangi skala koordinat U. Satu perenam belas akan cukup. Ini berarti bahwa kami hanya akan mencicipi pita sempit tekstur noise. float2 uv = IN.uv_MainTex; uv.x *= 0.0625; uv.y -= _Time.y;
Meregangkan koordinat U.Mari kita juga memperlambatnya menjadi seperempat per detik sehingga penyelesaian siklus tekstur membutuhkan waktu empat detik. uv.y -= _Time.y * 0.25;
Kebisingan saat ini.Pencampuran kebisingan
Semuanya sudah terlihat jauh lebih baik, tetapi polanya selalu tetap sama. Air tidak berperilaku seperti itu.Karena kami hanya menggunakan pita kecil noise, kami dapat memvariasikan polanya dengan menggeser band ini sepanjang tekstur. Ini dilakukan dengan menambahkan waktu ke koordinat U. Kita harus melakukannya perlahan, kalau tidak sungai akan mengalir ke samping. Mari kita coba koefisien 0,005. Ini berarti bahwa dibutuhkan 200 detik untuk menyelesaikan polanya. uv.x = uv.x * 0.0625 + _Time.y * 0.005;
Bergerak kebisingan.Sayangnya, ini tidak terlihat sangat indah. Air masih tampak statis dan pergeserannya jelas terlihat, meskipun sangat lambat. Kita dapat menyembunyikan shift dengan menggabungkan dua sampel noise, dan menggesernya ke arah yang berlawanan. Dan jika kita menggunakan nilai yang sedikit berbeda untuk memindahkan sampel kedua, kita akan membuat animasi perubahan yang ringan.Sehingga sebagai hasilnya, kami tidak pernah tumpang tindih dengan pola noise yang sama, kami menggunakan saluran yang berbeda untuk sampel kedua. float2 uv = IN.uv_MainTex; uv.x = uv.x * 0.0625 + _Time.y * 0.005; uv.y -= _Time.y * 0.25; float4 noise = tex2D(_MainTex, uv); float2 uv2 = IN.uv_MainTex; uv2.x = uv2.x * 0.0625 - _Time.y * 0.0052; uv2.y -= _Time.y * 0.23; float4 noise2 = tex2D(_MainTex, uv2); fixed4 c = _Color * (noise.r * noise2.a);
Kombinasi dua pola kebisingan bergeser.Air tembus cahaya
Pola kami terlihat cukup dinamis. Langkah selanjutnya adalah membuatnya tembus.Pertama, pastikan bahwa air tidak membuang bayangan. Anda bisa menonaktifkannya melalui komponen renderer dari objek Rivers di prefab.Pengecoran bayangan dinonaktifkan.Sekarang alihkan shader ke mode transparan. Untuk menunjukkan ini, gunakan tag shader. Kemudian tambahkan #pragma surface
kata kunci ke baris alpha
. Saat kami di sini, Anda dapat menghapus kata kunci fullforwardshadows
, karena kami masih tidak memberikan bayangan. Tags { "RenderType"="Transparent" "Queue"="Transparent" } LOD 200 CGPROGRAM #pragma surface surf Standard alpha // fullforwardshadows #pragma target 3.0
Sekarang kita akan mengubah cara kita mengatur warna sungai. Alih-alih mengalikan noise dengan warna, kami akan menambahkan noise ke dalamnya. Kemudian kami menggunakan fungsi saturate
untuk membatasi hasil sehingga tidak melebihi 1. fixed4 c = saturate(_Color + noise.r * noise2.a);
Ini akan memungkinkan kita untuk menggunakan warna material sebagai warna dasar. Kebisingan akan meningkatkan kecerahan dan opasitasnya. Mari kita coba gunakan warna biru dengan opacity yang cukup rendah. Hasilnya, kami mendapatkan air tembus biru dengan cipratan putih.Air berwarna bening.paket unityPenyelesaian
Sekarang semuanya tampaknya berfungsi, saatnya untuk mengubah puncak lagi. Selain merusak tepi sel, ini akan membuat sungai kita tidak rata. public const float cellPerturbStrength = 4f;
Puncak yang terdistorsi dan terdistorsi.Mari kita periksa medan untuk masalah yang muncul karena distorsi. Sepertinya mereka! Mari kita periksa air terjun yang tinggi.Air terpotong oleh tebing.Air yang jatuh dari air terjun yang tinggi menghilang di balik tebing. Ketika ini terjadi, itu sangat nyata, jadi kita perlu melakukan sesuatu.Jauh lebih tidak jelas adalah bahwa air terjun bisa miring, daripada turun lurus ke bawah. Meskipun air pada kenyataannya tidak mengalir seperti itu, itu tidak terlalu terlihat. Otak kita akan menafsirkannya sedemikian rupa sehingga tampak normal bagi kita. Jadi abaikan saja.Cara termudah untuk menghindari kehilangan air adalah dengan memperdalam dasar sungai. Jadi kita akan menciptakan lebih banyak ruang antara permukaan air dan dasar sungai. Ini juga akan membuat dinding saluran lebih vertikal, jadi jangan terlalu dalam. Mari bertanyaHexMetrics.streamBedElevationOffset
nilai -1.75. Ini akan memecahkan sebagian besar masalah, dan tempat tidur tidak akan menjadi terlalu dalam. Sebagian air masih akan terpotong, tetapi tidak seluruh air terjun. public const float streamBedElevationOffset = -1.75f;
Saluran yang dalam.paket unityBagian 7: jalan
- Tambahkan dukungan jalan.
- Triangulasi jalan.
- Kami menggabungkan jalan dan sungai.
- Memperbaiki tampilan jalan.
Tanda-tanda pertama peradaban.Sel dengan jalan
Seperti sungai, jalan pergi dari sel ke sel, melalui bagian tengah tepi sel. Perbedaan besar adalah bahwa tidak ada air yang mengalir di jalan, sehingga mereka dua arah. Selain itu, persimpangan diperlukan untuk jaringan jalan fungsional, jadi kita perlu mendukung lebih dari dua jalan per sel.Jika Anda membiarkan jalan masuk ke semua enam arah, maka sel dapat berisi dari nol hingga enam jalan. Itu total empat belas kemungkinan konfigurasi jalan. Ini jauh lebih dari lima kemungkinan konfigurasi sungai. Untuk menangani ini, kita perlu menggunakan pendekatan yang lebih umum yang dapat menangani semua konfigurasi.14 kemungkinan konfigurasi jalan.Pelacakan jalan
Cara paling sederhana untuk melacak jalan di sel adalah dengan menggunakan array nilai Boolean. Tambahkan bidang pribadi larik ke HexCell
dan buatlah serial agar Anda dapat melihatnya di inspektur. Atur ukuran array melalui prefab sel sehingga mendukung enam jalan. [SerializeField] bool[] roads;
Sel cetakan dengan enam jalan.Tambahkan metode untuk memeriksa apakah sel memiliki jalur ke arah tertentu. public bool HasRoadThroughEdge (HexDirection direction) { return roads[(int)direction]; }
Akan lebih mudah untuk mengetahui jika ada setidaknya satu jalan di dalam sel, jadi kami akan menambahkan properti untuk ini. Hanya berkeliling array dalam loop dan kembali true
segera setelah kami menemukan jalannya. Jika tidak ada jalan, maka kembali false
. public bool HasRoads { get { for (int i = 0; i < roads.Length; i++) { if (roads[i]) { return true; } } return false; } }
Penghapusan jalan
Seperti halnya sungai, kami akan menambahkan metode untuk menghapus semua jalan dari sel. Ini dapat dilakukan dengan loop yang memutus setiap jalan yang sebelumnya diaktifkan. public void RemoveRoads () { for (int i = 0; i < neighbors.Length; i++) { if (roads[i]) { roads[i] = false; } } }
Tentu saja, kita juga perlu menonaktifkan sel-sel mahal yang sesuai di tetangga. if (roads[i]) { roads[i] = false; neighbors[i].roads[(int)((HexDirection)i).Opposite()] = false; }
Setelah itu, kita perlu memperbarui setiap sel. Karena jalan lokal ke sel, kita perlu memperbarui hanya sel sendiri tanpa tetangga mereka. if (roads[i]) { roads[i] = false; neighbors[i].roads[(int)((HexDirection)i).Opposite()] = false; neighbors[i].RefreshSelfOnly(); RefreshSelfOnly(); }
Menambahkan Jalan
Menambahkan jalan sama dengan membuang jalan. Satu-satunya perbedaan adalah bahwa kami memberikan nilai ke Boolean true
, bukan false
. Kami dapat membuat metode pribadi yang dapat melakukan kedua operasi. Maka akan memungkinkan untuk menggunakannya untuk menambah dan menghapus jalan. public void AddRoad (HexDirection direction) { if (!roads[(int)direction]) { SetRoad((int)direction, true); } } public void RemoveRoads () { for (int i = 0; i < neighbors.Length; i++) { if (roads[i]) { SetRoad(i, false); } } } void SetRoad (int index, bool state) { roads[index] = state; neighbors[index].roads[(int)((HexDirection)index).Opposite()] = state; neighbors[index].RefreshSelfOnly(); RefreshSelfOnly(); }
Kita tidak bisa memiliki sungai dan jalan yang menuju ke arah yang sama pada saat yang bersamaan. Karena itu, sebelum menambahkan jalan, kami akan memeriksa apakah ada tempat untuk itu. public void AddRoad (HexDirection direction) { if (!roads[(int)direction] && !HasRiverThroughEdge(direction)) { SetRoad((int)direction, true); } }
Selain itu, jalan tidak dapat digabungkan dengan tebing karena terlalu tajam. Atau mungkin ada baiknya membuka jalan melalui tebing rendah, tetapi tidak melalui tinggi? Untuk menentukan ini, kita perlu membuat metode yang memberitahu kita perbedaan ketinggian dalam arah tertentu. public int GetElevationDifference (HexDirection direction) { int difference = elevation - GetNeighbor(direction).elevation; return difference >= 0 ? difference : -difference; }
Sekarang kita dapat membuat jalan menambah perbedaan ketinggian yang cukup kecil. Saya akan membatasi diri hanya untuk lereng, yaitu maksimum 1 unit. public void AddRoad (HexDirection direction) { if ( !roads[(int)direction] && !HasRiverThroughEdge(direction) && GetElevationDifference(direction) <= 1 ) { SetRoad((int)direction, true); } }
Menghapus jalan yang salah
Kami membuat jalan hanya menambah bila diizinkan. Sekarang kita perlu memastikan bahwa mereka dihapus jika nantinya menjadi salah, misalnya, ketika menambahkan sungai. Kita bisa melarang penempatan sungai di atas jalan, tetapi sungai tidak terganggu oleh jalan. Biarkan mereka mencuci jalan keluar dari jalan.Cukuplah bagi kita untuk menanyakan jalan false
, terlepas dari apakah jalan itu. Akan selalu diperbarui kedua sel, sehingga kita tidak lagi perlu secara eksplisit memanggil RefreshSelfOnly
di SetOutgoingRiver
. public void SetOutgoingRiver (HexDirection direction) { if (hasOutgoingRiver && outgoingRiver == direction) { return; } HexCell neighbor = GetNeighbor(direction); if (!neighbor || elevation < neighbor.elevation) { return; } RemoveOutgoingRiver(); if (hasIncomingRiver && incomingRiver == direction) { RemoveIncomingRiver(); } hasOutgoingRiver = true; outgoingRiver = direction;
Operasi lain yang dapat membuat jalan salah adalah perubahan ketinggian. Dalam hal ini, kita harus memeriksa jalan di semua arah. Jika perbedaan ketinggian terlalu besar, maka jalan yang ada perlu dihapus. public int Elevation { get { return elevation; } set { β¦ for (int i = 0; i < roads.Length; i++) { if (roads[i] && GetElevationDifference((HexDirection)i) > 1) { SetRoad(i, false); } } Refresh(); } }
paket unityPengeditan jalan
Mengedit jalan berfungsi seperti mengedit sungai. Oleh karena itu HexMapEditor
, diperlukan satu sakelar lagi, plus metode untuk mengatur kondisinya. OptionalToggle riverMode, roadMode; public void SetRiverMode (int mode) { riverMode = (OptionalToggle)mode; } public void SetRoadMode (int mode) { roadMode = (OptionalToggle)mode; }
Metode EditCell
sekarang harus mendukung penghapusan dengan penambahan jalan. Ini berarti bahwa ketika menyeret dan menjatuhkan, ia dapat melakukan salah satu dari dua tindakan yang mungkin. Kami merestrukturisasi kode sedikit sehingga ketika melakukan drag dan drop yang benar, keadaan kedua sakelar diperiksa. void EditCell (HexCell cell) { if (cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } if (riverMode == OptionalToggle.No) { cell.RemoveRiver(); } if (roadMode == OptionalToggle.No) { cell.RemoveRoads(); } if (isDrag) { HexCell otherCell = cell.GetNeighbor(dragDirection.Opposite()); if (otherCell) { if (riverMode == OptionalToggle.Yes) { otherCell.SetOutgoingRiver(dragDirection); } if (roadMode == OptionalToggle.Yes) { otherCell.AddRoad(dragDirection); } } } } }
Kita dapat dengan cepat menambahkan bilah jalan ke UI dengan menyalin bilah sungai dan mengubah metode yang disebut oleh sakelar.Hasilnya, kami mendapatkan UI yang cukup tinggi. Untuk memperbaikinya, saya mengubah tata letak panel warna agar sesuai dengan panel jalan dan sungai yang lebih ringkas.UI dengan jalan.Karena sekarang saya menggunakan dua baris dari tiga opsi untuk warna, ada ruang untuk warna lain. Jadi saya menambahkan item untuk oranye.Lima warna: kuning, hijau, biru, oranye dan putih.Sekarang kita bisa mengedit jalan, tetapi sejauh ini tidak terlihat. Anda dapat menggunakan inspektur untuk memastikan semuanya berfungsi.Sel dengan jalan di inspektur.paket unityTriangulasi jalan
Untuk menampilkan jalan, Anda perlu melakukan triangulasi. Ini mirip dengan membuat jaring untuk sungai, hanya dasar sungai tidak akan muncul dalam bantuan.Pertama, buat shader standar baru yang lagi-lagi akan menggunakan koordinat UV untuk mengecat permukaan jalan. Shader "Custom/Road" { Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Standard fullforwardshadows #pragma target 3.0 sampler2D _MainTex; struct Input { float2 uv_MainTex; }; half _Glossiness; half _Metallic; fixed4 _Color; void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = fixed4(IN.uv_MainTex, 1, 1); o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } ENDCG } FallBack "Diffuse" }
Buat material jalan menggunakan shader ini.Jalan Material.Atur prefab fragmen sehingga menerima mesh anak tambahan segi enam untuk jalan. Jala ini tidak boleh membuang bayangan dan harus menggunakan hanya koordinat UV. Cara tercepat untuk melakukan ini adalah melalui contoh cetakan - duplikat objek Rivers dan ganti materialnya.Jalan Obyek Anak.Setelah itu, tambahkan ke HexGridChunk
bidang umum HexMesh roads
dan sertakan dalam Triangulate
. Hubungkan di inspektur dengan objek Roads . public HexMesh terrain, rivers, roads; public void Triangulate () { terrain.Clear(); rivers.Clear(); roads.Clear(); for (int i = 0; i < cells.Length; i++) { Triangulate(cells[i]); } terrain.Apply(); rivers.Apply(); roads.Apply(); }
Objek Jalan terhubung.Jalan antar sel
Pertama mari kita lihat segmen jalan antar sel. Seperti sungai, jalan ditutup oleh dua quad sedang. Kami benar-benar menutupi quadrangles koneksi ini dengan quadrangles jalan sehingga posisi dari enam puncak yang sama dapat digunakan. Tambahkan ini ke HexGridChunk
metode TriangulateRoadSegment
. void TriangulateRoadSegment ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, Vector3 v5, Vector3 v6 ) { roads.AddQuad(v1, v2, v4, v5); roads.AddQuad(v2, v3, v5, v6); }
Karena kita tidak perlu lagi khawatir dengan aliran air, koordinat V tidak diperlukan, jadi kami menetapkan nilai 0 di mana-mana. Kita dapat menggunakan koordinat U untuk menunjukkan apakah kita berada di tengah jalan atau di samping. Biarkan sama dengan 1 di tengah, dan sama dengan 0 di kedua sisi. void TriangulateRoadSegment ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, Vector3 v5, Vector3 v6 ) { roads.AddQuad(v1, v2, v4, v5); roads.AddQuad(v2, v3, v5, v6); roads.AddQuadUV(0f, 1f, 0f, 0f); roads.AddQuadUV(1f, 0f, 0f, 0f); }
Segmen jalan antar sel.Akan logis untuk memanggil metode ini TriangulateEdgeStrip
, tetapi hanya jika memang ada jalan. Tambahkan parameter Boolean ke metode untuk meneruskan informasi ini. void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2, bool hasRoad ) { β¦ }
Tentu saja, sekarang kami akan menerima kesalahan penyusun, karena sejauh ini informasi ini belum dikirim. Sebagai argumen terakhir dalam semua kasus, panggilan TriangulateEdgeStrip
dapat ditambahkan false
. Namun, kami juga dapat menyatakan bahwa nilai default dari parameter ini sama false
. Karena ini, parameter akan menjadi opsional dan kesalahan kompilasi akan hilang. void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2, bool hasRoad = false ) { β¦ }
Bagaimana cara kerja parameter opsional?, . ,
int MyMethod (int x = 1, int y = 2) { return x + y; }
int MyMethod (int x, int y) { return x + y; } int MyMethod (int x) { return MyMethod(x, 2); } int MyMethod () { return MyMethod(1, 2}; }
. . . .
Untuk melakukan triangulasi jalan, panggil saja TriangulateRoadSegment
dengan enam puncak tengah, jika perlu. void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2, bool hasRoad = false ) { terrain.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); terrain.AddQuadColor(c1, c2); terrain.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); terrain.AddQuadColor(c1, c2); terrain.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); terrain.AddQuadColor(c1, c2); terrain.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); terrain.AddQuadColor(c1, c2); if (hasRoad) { TriangulateRoadSegment(e1.v2, e1.v3, e1.v4, e2.v2, e2.v3, e2.v4); } }
Inilah cara kami menangani koneksi sel datar. Untuk mendukung jalan di tepian, kita juga perlu memberi tahu di TriangulateEdgeTerraces
mana jalan harus ditambahkan. Dia dapat dengan mudah menyampaikan informasi ini TriangulateEdgeStrip
. void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell, bool hasRoad ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.Color, endCell.Color, 1); TriangulateEdgeStrip(begin, beginCell.Color, e2, c2, hasRoad); for (int i = 2; i < HexMetrics.terraceSteps; i++) { EdgeVertices e1 = e2; Color c1 = c2; e2 = EdgeVertices.TerraceLerp(begin, end, i); c2 = HexMetrics.TerraceLerp(beginCell.Color, endCell.Color, i); TriangulateEdgeStrip(e1, c1, e2, c2, hasRoad); } TriangulateEdgeStrip(e2, c2, end, endCell.Color, hasRoad); }
TriangulateEdgeTerraces
disebut di dalam TriangulateConnection
. Di sinilah kita dapat menentukan apakah sebenarnya ada jalan menuju arah saat ini, baik selama triangulasi tulang rusuk dan triangulasi tepian. if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces( e1, cell, e2, neighbor, cell.HasRoadThroughEdge(direction) ); } else { TriangulateEdgeStrip( e1, cell.Color, e2, neighbor.Color, cell.HasRoadThroughEdge(direction) ); }
Segmen jalan antar sel.Cell over rendering
Saat menggambar jalan, Anda akan melihat bahwa segmen jalan muncul di antara sel. Bagian tengah segmen ini akan berwarna ungu dengan transisi ke biru di tepinya.Namun, saat Anda menggerakkan kamera, segmen-segmennya mungkin berkedip, dan kadang-kadang hilang sama sekali. Ini karena segitiga jalan persis tumpang tindih dengan segitiga medan. Segitiga untuk rendering dipilih secara acak. Masalah ini dapat diperbaiki dalam dua tahap.Pertama, kami ingin menggambar jalan setelah bantuan ditarik. Ini dapat dicapai dengan rendering mereka setelah rendering geometri biasa, yaitu dengan menempatkan mereka dalam antrian rendering kemudian. Tags { "RenderType"="Opaque" "Queue" = "Geometry+1" }
Kedua, kita perlu memastikan bahwa jalan digambar segitiga di lokasi yang sama. Ini dapat dilakukan dengan menambahkan offset tes kedalaman. Ini akan memungkinkan GPU untuk berasumsi bahwa segitiga lebih dekat ke kamera daripada yang sebenarnya. Tags { "RenderType"="Opaque" "Queue" = "Geometry+1" } LOD 200 Offset -1, -1
Jalan melalui sel
Saat melakukan triangulasi sungai, kami harus berurusan dengan tidak lebih dari dua arah sungai per sel. Kami dapat mengidentifikasi lima opsi yang mungkin dan melakukan triangulasi secara berbeda untuk menciptakan sungai yang tampak benar. Namun, dalam hal jalan, ada empat belas opsi yang memungkinkan. Kami tidak akan menggunakan pendekatan terpisah untuk masing-masing opsi ini. Sebagai gantinya, kami akan memproses masing-masing arah enam sel dengan cara yang sama, terlepas dari konfigurasi jalan tertentu.Ketika sebuah jalan melewati bagian sel, kita akan menariknya langsung ke tengah sel, tanpa meninggalkan zona segitiga. Kami akan menggambar ruas jalan dari tepi ke setengah ke arah tengah. Kemudian kita menggunakan dua segitiga untuk menutup sisanya ke tengah.Triangulasi bagian jalan.Untuk melakukan triangulasi skema ini, kita perlu mengetahui pusat sel, simpul tengah kiri dan kanan dan simpul tepi. Tambahkan metode TriangulateRoad
dengan parameter yang sesuai. void TriangulateRoad ( Vector3 center, Vector3 mL, Vector3 mR, EdgeVertices e ) { }
Untuk membangun ruas jalan, kita membutuhkan satu puncak tambahan. Terletak di antara puncak tengah kiri dan kanan. void TriangulateRoad ( Vector3 center, Vector3 mL, Vector3 mR, EdgeVertices e ) { Vector3 mC = Vector3.Lerp(mL, mR, 0.5f); TriangulateRoadSegment(mL, mC, mR, e.v2, e.v3, e.v4); }
Sekarang kita juga bisa menambahkan dua segitiga yang tersisa. TriangulateRoadSegment(mL, mC, mR, e.v2, e.v3, e.v4); roads.AddTriangle(center, mL, mC); roads.AddTriangle(center, mC, mR);
Kita juga perlu menambahkan koordinat UV dari segitiga. Dua puncaknya ada di tengah jalan, dan sisanya ada di tepi. roads.AddTriangle(center, mL, mC); roads.AddTriangle(center, mC, mR); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(0f, 0f), new Vector2(1f, 0f) ); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(0f, 0f) );
Untuk saat ini, mari kita membatasi diri pada sel-sel di mana tidak ada sungai. Dalam kasus ini, itu Triangulate
hanya menciptakan penggemar segitiga. Pindahkan kode ini ke metode terpisah. Lalu kami menambahkan panggilan TriangulateRoad
saat jalan sebenarnya. Verteks tengah kiri dan kanan dapat ditemukan dengan interpolasi antara simpul tengah dan dua sudut. void Triangulate (HexDirection direction, HexCell cell) { β¦ if (cell.HasRiver) { β¦ } else { TriangulateWithoutRiver(direction, cell, center, e); } β¦ } void TriangulateWithoutRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { TriangulateEdgeFan(center, e, cell.Color); if (cell.HasRoadThroughEdge(direction)) { TriangulateRoad( center, Vector3.Lerp(center, e.v1, 0.5f), Vector3.Lerp(center, e.v5, 0.5f), e ); } }
Jalan melewati sel.Tulang rusuk jalan
Sekarang kita bisa melihat jalan, tetapi lebih dekat ke pusat sel mereka menyempit. Karena kita tidak memeriksa dari empat belas opsi yang kita hadapi, kita tidak dapat menggeser pusat jalan untuk menciptakan bentuk yang lebih indah. Sebagai gantinya, kita dapat menambahkan tepi jalan tambahan di bagian lain sel.Saat jalan melewati sel, tetapi tidak ke arah saat ini, kami akan menambahkan segitiga ke tepi jalan. Segitiga ini ditentukan oleh simpul tengah, kiri dan kanan. Dalam hal ini, hanya puncak pusat yang terletak di tengah jalan. Dua lainnya berbaring di tulang rusuknya. void TriangulateRoadEdge (Vector3 center, Vector3 mL, Vector3 mR) { roads.AddTriangle(center, mL, mR); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(0f, 0f), new Vector2(0f, 0f) ); }
Bagian dari tepi jalan.Ketika kita perlu melakukan triangulasi jalan penuh atau hanya tepi, kita harus membiarkannya TriangulateRoad
. Untuk melakukan ini, metode ini harus tahu jika jalan melewati arah tepi sel saat ini. Karena itu, kami menambahkan parameter untuk ini. void TriangulateRoad ( Vector3 center, Vector3 mL, Vector3 mR, EdgeVertices e, bool hasRoadThroughCellEdge ) { if (hasRoadThroughCellEdge) { Vector3 mC = Vector3.Lerp(mL, mR, 0.5f); TriangulateRoadSegment(mL, mC, mR, e.v2, e.v3, e.v4); roads.AddTriangle(center, mL, mC); roads.AddTriangle(center, mC, mR); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(0f, 0f), new Vector2(1f, 0f) ); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(0f, 0f) ); } else { TriangulateRoadEdge(center, mL, mR); } }
Sekarang TriangulateWithoutRiver
harus menelepon TriangulateRoad
ketika ada jalan yang melewati sel. Dan dia harus mengirimkan informasi tentang apakah jalan melewati tepi saat ini. void TriangulateWithoutRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { TriangulateEdgeFan(center, e, cell.Color); if (cell.HasRoads) { TriangulateRoad( center, Vector3.Lerp(center, e.v1, 0.5f), Vector3.Lerp(center, e.v5, 0.5f), e, cell.HasRoadThroughEdge(direction) ); } }
Jalan dengan tulang rusuk lengkap.Perataan jalan
Jalan sekarang lengkap. Sayangnya, pendekatan ini menciptakan tonjolan di tengah sel. Menempatkan puncak kiri dan kanan di tengah antara tengah dan sudut cocok untuk kita ketika ada jalan yang berdekatan dengan mereka. Tetapi jika tidak, maka ada tonjolan. Untuk menghindari ini, dalam kasus seperti itu kita dapat menempatkan simpul lebih dekat ke tengah. Lebih khusus, lalu interpolasi dengan ΒΌ, bukan dengan Β½.Mari kita buat metode terpisah untuk mencari tahu interpolator mana yang akan digunakan. Karena ada dua dari mereka, kita dapat memasukkan hasilnya Vector2
. Komponen X-nya akan menjadi interpolator dari titik kiri, dan komponen Y akan menjadi interpolator dari titik kanan. Vector2 GetRoadInterpolators (HexDirection direction, HexCell cell) { Vector2 interpolators; return interpolators; }
Jika ada jalan menuju ke arah saat ini, kita bisa menempatkan titik di tengah. Vector2 GetRoadInterpolators (HexDirection direction, HexCell cell) { Vector2 interpolators; if (cell.HasRoadThroughEdge(direction)) { interpolators.x = interpolators.y = 0.5f; } return interpolators; }
Kalau tidak, opsinya mungkin berbeda. Untuk titik kiri, kita bisa menggunakan Β½ jika ada jalan di arah sebelumnya. Jika tidak, maka kita harus menggunakan ΒΌ. Hal yang sama berlaku untuk titik yang tepat, tetapi dengan mempertimbangkan arahan berikut. Vector2 GetRoadInterpolators (HexDirection direction, HexCell cell) { Vector2 interpolators; if (cell.HasRoadThroughEdge(direction)) { interpolators.x = interpolators.y = 0.5f; } else { interpolators.x = cell.HasRoadThroughEdge(direction.Previous()) ? 0.5f : 0.25f; interpolators.y = cell.HasRoadThroughEdge(direction.Next()) ? 0.5f : 0.25f; } return interpolators; }
Anda sekarang dapat menggunakan metode baru ini untuk menentukan interpolator mana yang digunakan. Berkat ini, jalan akan mulus. void TriangulateWithoutRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { TriangulateEdgeFan(center, e, cell.Color); if (cell.HasRoads) { Vector2 interpolators = GetRoadInterpolators(direction, cell); TriangulateRoad( center, Vector3.Lerp(center, e.v1, interpolators.x), Vector3.Lerp(center, e.v5, interpolators.y), e, cell.HasRoadThroughEdge(direction) ); } }
Jalan yang mulus.paket unityKombinasi sungai dan jalan
Pada tahap saat ini, kami memiliki jalan fungsional, tetapi hanya jika tidak ada sungai. Jika ada sungai di dalam sel, maka jalan tidak akan triangulasi.Tidak ada jalan di dekat sungai.Mari kita buat metode TriangulateRoadAdjacentToRiver
untuk menangani situasi ini. Kami mengaturnya ke parameter yang biasa. Kami akan menyebutnya di awal metode TriangulateAdjacentToRiver
. void TriangulateAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { if (cell.HasRoads) { TriangulateRoadAdjacentToRiver(direction, cell, center, e); } β¦ } void TriangulateRoadAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { }
Untuk memulainya, kita akan melakukan hal yang sama seperti untuk jalan tanpa sungai. Kami akan memeriksa apakah jalan melewati tepi saat ini, mendapatkan interpolator, membuat pertengahan puncak dan menelepon TriangulateRoad
. Tetapi karena sungai akan muncul di jalan setapak, kita perlu memindahkan jalan dari mereka. Alhasil, pusat jalan akan berada di posisi yang berbeda. Kami menggunakan variabel untuk menyimpan posisi baru ini roadCenter
. Awalnya, itu akan sama dengan pusat sel. void TriangulateRoadAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { bool hasRoadThroughEdge = cell.HasRoadThroughEdge(direction); Vector2 interpolators = GetRoadInterpolators(direction, cell); Vector3 roadCenter = center; Vector3 mL = Vector3.Lerp(roadCenter, e.v1, interpolators.x); Vector3 mR = Vector3.Lerp(roadCenter, e.v5, interpolators.y); TriangulateRoad(roadCenter, mL, mR, e, hasRoadThroughEdge); }
Jadi kita akan membuat sebagian jalan di sel dengan sungai. Arah yang dilewati sungai akan memotong celah di jalan.Jalan dengan ruang.Awal atau akhir sungai
Pertama-tama mari kita lihat sel-sel yang mengandung awal atau akhir sungai. Agar jalan tidak tumpang tindih dengan air, mari kita pindahkan pusat jalan dari sungai. Untuk mendapatkan arah sungai yang masuk atau keluar, tambahkan HexCell
properti. public HexDirection RiverBeginOrEndDirection { get { return hasIncomingRiver ? incomingRiver : outgoingRiver; } }
Sekarang kita bisa menggunakan properti ini HexGridChunk.TriangulateRoadAdjacentToRiver
untuk memindahkan pusat jalan ke arah yang berlawanan. Ini akan cukup untuk memindahkan sepertiga ke rusuk tengah ke arah ini. bool hasRoadThroughEdge = cell.HasRoadThroughEdge(direction); Vector2 interpolators = GetRoadInterpolators(direction, cell); Vector3 roadCenter = center; if (cell.HasRiverBeginOrEnd) { roadCenter += HexMetrics.GetSolidEdgeMiddle( cell.RiverBeginOrEndDirection.Opposite() ) * (1f / 3f); } Vector3 mL = Vector3.Lerp(roadCenter, e.v1, interpolators.x); Vector3 mR = Vector3.Lerp(roadCenter, e.v5, interpolators.y); TriangulateRoad(roadCenter, mL, mR, e, hasRoadThroughEdge);
Jalan yang dimodifikasi.Selanjutnya kita harus menutup celahnya. Kami akan melakukan ini dengan menambahkan segitiga tambahan ke tepi jalan ketika kami dekat dengan sungai. Jika ada sungai di arah sebelumnya, maka kita tambahkan segitiga antara pusat jalan, pusat sel dan titik kiri tengah. Dan jika sungai berada di arah berikutnya, maka kita tambahkan segitiga di antara tengah jalan, titik kanan tengah dan pusat sel.Kami akan melakukan ini terlepas dari konfigurasi sungai, jadi letakkan kode ini di akhir metode. Vector3 mL = Vector3.Lerp(roadCenter, e.v1, interpolators.x); Vector3 mR = Vector3.Lerp(roadCenter, e.v5, interpolators.y); TriangulateRoad(roadCenter, mL, mR, e, hasRoadThroughEdge); if (cell.HasRiverThroughEdge(direction.Previous())) { TriangulateRoadEdge(roadCenter, center, mL); } if (cell.HasRiverThroughEdge(direction.Next())) { TriangulateRoadEdge(roadCenter, mR, center); }
Tidak bisakah Anda menggunakan pernyataan yang lain?. , .
Jalan siap.Sungai lurus
Sel dengan sungai lurus sangat sulit karena pada dasarnya membagi pusat sel menjadi dua. Kami sudah menambahkan segitiga tambahan untuk mengisi celah di antara sungai, tetapi kami juga harus memutus jalan di sisi yang berlawanan dari sungai.Jalan yang tumpang tindih dengan sungai lurus.Jika sel tidak memiliki awal atau akhir sungai, maka kita dapat memeriksa apakah sungai yang masuk dan keluar menuju arah yang berlawanan. Jika demikian, maka kita memiliki sungai langsung. if (cell.HasRiverBeginOrEnd) { roadCenter += HexMetrics.GetSolidEdgeMiddle( cell.RiverBeginOrEndDirection.Opposite() ) * (1f / 3f); } else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { }
Untuk menentukan di mana sungai relatif terhadap arah saat ini, kita perlu memeriksa arah tetangga. Sungai itu kiri atau kanan. Karena kita melakukan ini di akhir metode, kita menyimpan cache permintaan ini ke dalam variabel Boolean. Ini juga akan menyederhanakan pembacaan kode kita. bool hasRoadThroughEdge = cell.HasRoadThroughEdge(direction); bool previousHasRiver = cell.HasRiverThroughEdge(direction.Previous()); bool nextHasRiver = cell.HasRiverThroughEdge(direction.Next()); Vector2 interpolators = GetRoadInterpolators(direction, cell); Vector3 roadCenter = center; if (cell.HasRiverBeginOrEnd) { roadCenter += HexMetrics.GetSolidEdgeMiddle( cell.RiverBeginOrEndDirection.Opposite() ) * (1f / 3f); } else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { if (previousHasRiver) { } else { } } Vector3 mL = Vector3.Lerp(roadCenter, e.v1, interpolators.x); Vector3 mR = Vector3.Lerp(roadCenter, e.v5, interpolators.y); TriangulateRoad(roadCenter, mL, mR, e, hasRoadThroughEdge); if (previousHasRiver) { TriangulateRoadEdge(roadCenter, center, mL); } if (nextHasRiver) { TriangulateRoadEdge(roadCenter, mR, center); }
Kita perlu menggeser pusat jalan ke vektor sudut yang menunjuk ke arah berlawanan dari sungai. Jika sungai melewati arah sebelumnya, maka ini adalah sudut solid kedua. Kalau tidak, ini adalah sudut solid pertama. else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { Vector3 corner; if (previousHasRiver) { corner = HexMetrics.GetSecondSolidCorner(direction); } else { corner = HexMetrics.GetFirstSolidCorner(direction); } }
Untuk memindahkan jalan sehingga berbatasan dengan sungai, kita perlu memindahkan pusat jalan setengah jarak ke sudut ini. Kemudian kita juga harus memindahkan pusat sel seperempat jarak ke arah itu. else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { Vector3 corner; if (previousHasRiver) { corner = HexMetrics.GetSecondSolidCorner(direction); } else { corner = HexMetrics.GetFirstSolidCorner(direction); } roadCenter += corner * 0.5f; center += corner * 0.25f; }
Jalan yang terbagi.Kami berbagi jaringan jalan di dalam sel ini. Ini normal ketika jalan di kedua sisi sungai. Tetapi jika di satu sisi tidak ada jalan, maka kita akan memiliki bagian kecil dari jalan yang terisolasi. Ini tidak masuk akal, jadi mari kita singkirkan bagian seperti itu.Pastikan bahwa ada jalan menuju ke arah saat ini. Jika tidak, maka periksa arah lain dari sisi sungai yang sama untuk keberadaan jalan. Jika tidak ada jalan yang lewat di sana atau di sana, maka kita keluar dari metode sebelum melakukan triangulasi. if (previousHasRiver) { if ( !hasRoadThroughEdge && !cell.HasRoadThroughEdge(direction.Next()) ) { return; } corner = HexMetrics.GetSecondSolidCorner(direction); } else { if ( !hasRoadThroughEdge && !cell.HasRoadThroughEdge(direction.Previous()) ) { return; } corner = HexMetrics.GetFirstSolidCorner(direction); }
Jalan terputus.Bagaimana dengan jembatan?. .
Sungai Zigzag
Jenis sungai berikutnya adalah zig-zag. Sungai-sungai seperti itu tidak berbagi jaringan jalan, jadi kita hanya perlu memindahkan pusat jalan.Zigzag melewati jalan.Cara termudah untuk memeriksa zig-zag adalah dengan membandingkan arah sungai yang masuk dan keluar. Jika mereka berdekatan, maka kita memiliki zig-zag. Ini mengarah ke dua opsi yang mungkin, tergantung pada arah aliran. if (cell.HasRiverBeginOrEnd) { β¦ } else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { β¦ } else if (cell.IncomingRiver == cell.OutgoingRiver.Previous()) { } else if (cell.IncomingRiver == cell.OutgoingRiver.Next()) { }
Kita bisa memindahkan pusat jalan menggunakan salah satu sudut dari arah sungai yang masuk. Sudut yang Anda pilih tergantung pada arah aliran. Pindahkan bagian tengah jalan dari sudut ini dengan faktor 0,2. else if (cell.IncomingRiver == cell.OutgoingRiver.Previous()) { roadCenter -= HexMetrics.GetSecondCorner(cell.IncomingRiver) * 0.2f; } else if (cell.IncomingRiver == cell.OutgoingRiver.Next()) { roadCenter -= HexMetrics.GetFirstCorner(cell.IncomingRiver) * 0.2f; }
Jalan mendorong menjauh dari zig-zag.Di dalam sungai yang bengkok
Konfigurasi sungai terakhir adalah kurva yang halus. Seperti halnya sungai langsung, sungai ini juga dapat memisahkan jalan. Tetapi dalam hal ini, para pihak akan berbeda. Pertama kita perlu bekerja dengan bagian dalam kurva.Sungai melengkung dengan jalan beraspal.Ketika kita memiliki sungai di kedua sisi dari arah saat ini, maka kita berada di dalam kurva. else if (cell.IncomingRiver == cell.OutgoingRiver.Next()) { β¦ } else if (previousHasRiver && nextHasRiver) { }
Kita perlu memindahkan pusat jalan menuju tepi sel saat ini, memperpendek jalan. Koefisien 0,7 akan dilakukan. Pusat sel juga harus bergeser dengan koefisien 0,5. else if (previousHasRiver && nextHasRiver) { Vector3 offset = HexMetrics.GetSolidEdgeMiddle(direction) * HexMetrics.innerToOuter; roadCenter += offset * 0.7f; center += offset * 0.5f; }
Jalan yang diperpendek.Seperti halnya sungai lurus, kita perlu memotong bagian jalan yang terisolasi. Dalam hal ini, cukup hanya memeriksa arah saat ini. else if (previousHasRiver && nextHasRiver) { if (!hasRoadThroughEdge) { return; } Vector3 offset = HexMetrics.GetSolidEdgeMiddle(direction) * HexMetrics.innerToOuter; roadCenter += offset * 0.7f; center += offset * 0.5f; }
Potong jalan.Di luar sungai yang bengkok
Setelah memeriksa semua kasus sebelumnya, satu-satunya pilihan yang tersisa adalah bagian luar sungai melengkung. Di luar ada tiga bagian sel. Kita perlu menemukan arah tengah. Setelah menerimanya, kita dapat memindahkan pusat jalan menuju tulang rusuk ini dengan faktor 0,25. else if (previousHasRiver && nextHasRiver) { β¦ } else { HexDirection middle; if (previousHasRiver) { middle = direction.Next(); } else if (nextHasRiver) { middle = direction.Previous(); } else { middle = direction; } roadCenter += HexMetrics.GetSolidEdgeMiddle(middle) * 0.25f; }
Mengubah bagian luar jalan.Sebagai langkah terakhir, kita perlu memotong jalan di sisi sungai ini. Cara termudah adalah memeriksa ketiga arah jalan relatif ke tengah. Jika tidak ada jalan, kami berhenti bekerja. else { HexDirection middle; if (previousHasRiver) { middle = direction.Next(); } else if (nextHasRiver) { middle = direction.Previous(); } else { middle = direction; } if ( !cell.HasRoadThroughEdge(middle) && !cell.HasRoadThroughEdge(middle.Previous()) && !cell.HasRoadThroughEdge(middle.Next()) ) { return; } roadCenter += HexMetrics.GetSolidEdgeMiddle(middle) * 0.25f; }
Jalan sebelum dan sesudah kliping.Setelah memproses semua opsi sungai, sungai dan jalan kami dapat hidup berdampingan. Sungai mengabaikan jalan, dan jalan beradaptasi dengan sungai.Kombinasi sungai dan jalan.paket unityPenampilan jalanan
Sampai saat itu, kami menggunakan koordinat UV mereka sebagai warna jalan. Karena hanya koordinat U yang berubah, kami benar-benar menampilkan transisi antara tengah dan tepi jalan.Tampilan koordinat UV.Sekarang jalan benar-benar triangulasi dengan benar, kita dapat mengubah shader jalan sehingga menjadikannya lebih seperti jalan. Seperti dalam kasus sungai, ini akan menjadi visualisasi sederhana, tanpa embel-embel.Kami akan mulai dengan menggunakan warna solid untuk jalan. Cukup gunakan warna materialnya. Saya membuatnya merah. void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = _Color; o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; }
Jalan merah.Dan itu sudah terlihat jauh lebih baik! Tapi mari kita lanjutkan dan mencampur jalan dengan medan, menggunakan koordinat U sebagai faktor pencampuran. void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = _Color; float blend = IN.uv_MainTex.x; o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = blend; }
Tampaknya ini tidak mengubah apa pun. Itu terjadi karena shader kami buram. Sekarang dia membutuhkan alpha blending. Secara khusus, kita membutuhkan shader untuk permukaan decal kawin. Kita bisa mendapatkan shader yang dibutuhkan dengan menambahkan #pragma surface
garis ke arahan decal:blend
. #pragma surface surf Standard fullforwardshadows decal:blend
Campuran jalan.Jadi kami membuat perpaduan linier yang halus dari pertengahan ke tepi yang tidak terlihat sangat cantik. Agar terlihat seperti jalan, kita membutuhkan area padat, diikuti dengan transisi cepat ke area buram. Anda dapat menggunakan fungsi ini smoothstep
. Ini mengubah perkembangan linier dari 0 ke 1 menjadi kurva berbentuk S.Perkembangan linier dan langkah mulus.Fungsi ini smoothstep
memiliki parameter minimum dan maksimum agar sesuai dengan kurva dalam interval sewenang-wenang. Nilai input di luar rentang dibatasi untuk menjaga kurva tetap rata. Mari kita gunakan 0,4 di awal kurva dan 0,7 di akhir. Ini berarti bahwa koordinat U dari 0 hingga 0.4 akan sepenuhnya transparan. Dan koordinat U dari 0,7 ke 1 akan sepenuhnya buram. Transisi terjadi antara 0,4 dan 0,7. float blend = IN.uv_MainTex.x; blend = smoothstep(0.4, 0.7, blend);
Transisi cepat antara area buram dan transparan.Jalan dengan kebisingan
Karena jalan jalan akan terdistorsi, jalan memiliki lebar yang bervariasi. Oleh karena itu, lebar transisi di tepi juga akan bervariasi. Terkadang buram, terkadang kasar. Keragaman seperti itu cocok untuk kita, jika kita menganggap jalan itu berpasir atau bersahaja.Mari kita mengambil langkah selanjutnya dan menambahkan suara ke tepi jalan. Ini akan membuat mereka lebih tidak rata dan lebih sedikit poligonal. Kita dapat melakukan ini dengan mengambil sampel tekstur suara. Untuk pengambilan sampel, Anda dapat menggunakan koordinat dunia XZ, seperti yang kami lakukan saat mendistorsi simpul sel.Untuk mendapatkan akses ke posisi dunia dalam shader permukaan, tambahkan ke struktur input float3 worldPos
. struct Input { float2 uv_MainTex; float3 worldPos; };
Sekarang kita dapat menggunakan posisi ini surf
untuk mencicipi tekstur utama. Perkecil juga, jika tidak tekstur akan berulang terlalu sering. float4 noise = tex2D(_MainTex, IN.worldPos.xz * 0.025); fixed4 c = _Color; float blend = IN.uv_MainTex.x;
Kami mendistorsi transisi dengan mengalikan koordinat U dengan noise.x
. Tetapi karena nilai kebisingan rata-rata 0,5, sebagian besar jalan akan hilang. Untuk menghindari ini, tambahkan 0,5 ke noise sebelum perkalian. float blend = IN.uv_MainTex.x; blend *= noise.x + 0.5; blend = smoothstep(0.4, 0.7, blend);
Tepi jalan yang terdistorsi.Untuk mengakhiri ini, kami juga akan mengubah warna jalan. Ini akan memberi jalan sensasi tanah yang sesuai dengan tepi fuzzy.Lipat gandakan warna dengan saluran noise lain, ucapkan oleh noise.y
. Jadi kami mendapatkan rata-rata setengah nilai warna. Karena ini terlalu banyak, kami akan sedikit mengurangi skala kebisingan dan menambahkan konstanta sehingga jumlahnya bisa mencapai 1. fixed4 c = _Color * (noise.y * 0.75 + 0.25);
Jalan yang kasar.paket unity