Hexagon maps in Unity: kabut perang, riset peta, pembuatan prosedural

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

Bagian 4-7: gundukan, sungai, dan jalan

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

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

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

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

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

Bagian 20: kabut perang


  • Simpan data sel dalam tekstur.
  • Ubah jenis bantuan tanpa triangulasi.
  • Kami melacak visibilitas.
  • Menggelapkan segalanya yang tak terlihat.

Di bagian ini, kita akan menambahkan efek kabut perang ke peta.

Sekarang seri akan dibuat pada Unity 2017.1.0.


Sekarang kita melihat bahwa kita dapat dan tidak dapat melihat.

Data Sel di Shader


Banyak game strategi menggunakan konsep kabut perang. Ini berarti bahwa visi pemain terbatas. Dia hanya bisa melihat apa yang dekat dengan unitnya atau area yang dikendalikannya. Meskipun kami bisa melihat kelegaan, kami tidak tahu apa yang terjadi di sana. Biasanya medan yang tak terlihat menjadi lebih gelap. Untuk mewujudkan ini, kita perlu melacak visibilitas sel dan membuatnya sesuai.

Cara paling sederhana untuk mengubah tampilan sel tersembunyi adalah menambahkan metrik visibilitas ke data mesh. Namun, pada saat yang sama, kami harus meluncurkan triangulasi bantuan baru ketika visibilitas berubah. Ini adalah keputusan yang buruk karena visibilitas terus berubah selama pertandingan.

Teknik render di atas topografi permukaan yang tembus sering digunakan, yang sebagian menutupi sel yang tidak terlihat oleh pemain. Metode ini cocok untuk medan yang relatif datar dalam kombinasi dengan sudut pandang terbatas. Tetapi karena medan kami dapat berisi ketinggian dan objek yang sangat bervariasi yang dapat dilihat dari sudut yang berbeda, untuk ini kami membutuhkan jaring yang sangat detail yang cocok dengan bentuk medan. Metode ini akan lebih mahal daripada pendekatan paling sederhana yang disebutkan di atas.

Pendekatan lain adalah untuk mentransfer data sel ke shader ketika merender secara terpisah dari relief mesh. Ini akan memungkinkan kita untuk melakukan triangulasi hanya sekali. Data sel dapat ditransfer menggunakan tekstur. Mengubah tekstur adalah proses yang jauh lebih sederhana daripada melakukan triangulasi medan. Selain itu, mengeksekusi beberapa sampel tekstur tambahan lebih cepat daripada merender lapisan tembus tunggal.

Bagaimana dengan menggunakan array shader?
Anda juga dapat mentransfer data sel ke shader menggunakan array vektor. Namun, array shader memiliki batas ukuran, diukur dalam ribuan byte, dan tekstur dapat mengandung jutaan piksel. Untuk mendukung peta besar, kami akan menggunakan tekstur.

Manajemen Data Sel


Kami membutuhkan cara untuk mengontrol tekstur yang berisi data sel. Mari kita membuat komponen HexCellShaderData baru yang akan melakukan ini.

 using UnityEngine; public class HexCellShaderData : MonoBehaviour { Texture2D cellTexture; } 

Saat membuat atau memuat peta baru, kita perlu membuat tekstur baru dengan ukuran yang benar. Oleh karena itu, kami menambahkan metode inisialisasi yang menciptakan tekstur padanya. Kami menggunakan tekstur RGBA tanpa tekstur mip dan ruang warna linier. Kami tidak perlu mencampur data sel, jadi kami menggunakan pemfilteran titik. Selain itu, data tidak boleh diciutkan. Setiap piksel dalam tekstur akan berisi data dari satu sel.

  public void Initialize (int x, int z) { cellTexture = new Texture2D( x, z, TextureFormat.RGBA32, false, true ); cellTexture.filterMode = FilterMode.Point; cellTexture.wrapMode = TextureWrapMode.Clamp; } 

Haruskah ukuran tekstur cocok dengan ukuran peta?
Tidak, itu hanya perlu memiliki piksel yang cukup untuk menampung semua sel. Dengan kecocokan persis dengan ukuran peta, tekstur dengan ukuran yang bukan kekuatan dua (non-kekuatan-dua, NPOT) kemungkinan besar akan dibuat, dan format tekstur ini bukan yang paling efektif. Meskipun kita dapat mengonfigurasi kode untuk bekerja dengan tekstur ukuran kekuatan dua, ini adalah optimasi kecil, yang mempersulit akses ke data sel.

Faktanya, kita tidak perlu membuat tekstur baru setiap kali kita membuat peta baru. Cukup untuk mengubah ukuran tekstur jika sudah ada. Kami bahkan tidak perlu memeriksa apakah kami sudah memiliki ukuran yang tepat, karena Texture2D.Resize cukup pintar untuk melakukan ini untuk kami.

  public void Initialize (int x, int z) { if (cellTexture) { cellTexture.Resize(x, z); } else { cellTexture = new Texture2D( cellCountX, cellCountZ, TextureFormat.RGBA32, false, true ); cellTexture.filterMode = FilterMode.Point; cellTexture.wrapMode = TextureWrapMode.Clamp; } } 

Alih-alih menerapkan data sel satu piksel pada satu waktu, kami menggunakan buffer warna dan menerapkan data semua sel sekaligus. Untuk melakukan ini, kita akan menggunakan larik Color32 . Jika perlu, kami akan membuat instance array baru di akhir Initialize . Jika kita sudah memiliki array dengan ukuran yang benar. lalu kita bersihkan isinya.

  Texture2D cellTexture; Color32[] cellTextureData; public void Initialize () { … if (cellTextureData == null || cellTextureData.Length != x * z) { cellTextureData = new Color32[x * z]; } else { for (int i = 0; i < cellTextureData.Length; i++) { cellTextureData[i] = new Color32(0, 0, 0, 0); } } } 

Apa itu color32?
Tekstur RGBA tanpa kompresi standar mengandung piksel empat-byte. Masing-masing dari empat saluran warna menerima byte, yaitu, mereka memiliki 256 nilai yang mungkin. Saat menggunakan struktur Color Persatuan, komponen titik apung dalam interval 0–1 dikonversi menjadi byte dalam interval 0–255. Saat pengambilan sampel, GPU melakukan transformasi terbalik.

Struktur Color32 bekerja secara langsung dengan byte, sehingga mereka mengambil lebih sedikit ruang dan tidak memerlukan konversi, yang meningkatkan efisiensi penggunaannya. Karena kami menyimpan data sel alih-alih warna, akan lebih logis untuk bekerja secara langsung dengan data tekstur mentah, dan bukan dengan Color .

HexGrid harus berurusan dengan pembuatan dan inisialisasi sel-sel ini di shader. Oleh karena itu, kami akan menambahkan bidang cellShaderData ke cellShaderData dan membuat komponen di dalam Awake .

  HexCellShaderData cellShaderData; void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; cellShaderData = gameObject.AddComponent<HexCellShaderData>(); CreateMap(cellCountX, cellCountZ); } 

Saat membuat peta baru, cellShaderData juga harus cellShaderData .

  public bool CreateMap (int x, int z) { … cellCountX = x; cellCountZ = z; chunkCountX = cellCountX / HexMetrics.chunkSizeX; chunkCountZ = cellCountZ / HexMetrics.chunkSizeZ; cellShaderData.Initialize(cellCountX, cellCountZ); CreateChunks(); CreateCells(); return true; } 

Mengedit Data Sel


Sampai sekarang, ketika mengubah properti sel, perlu memperbarui satu atau beberapa fragmen, tetapi sekarang mungkin perlu memperbarui data sel. Ini berarti bahwa sel harus memiliki tautan ke data sel di shader. Untuk melakukan ini, tambahkan properti ke HexCell .

  public HexCellShaderData ShaderData { get; set; } 

Di HexGrid.CreateCell kami HexGrid.CreateCell menetapkan komponen data shader ke properti ini.

  void CreateCell (int x, int z, int i) { … HexCell cell = cells[i] = Instantiate<HexCell>(cellPrefab); cell.transform.localPosition = position; cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); cell.ShaderData = cellShaderData; … } 

Sekarang kita bisa mendapatkan sel untuk memperbarui data shader mereka. Meskipun kami tidak melacak visibilitas, kami dapat menggunakan data shader untuk hal lain. Jenis relief sel menentukan tekstur yang digunakan untuk membuatnya. Itu tidak mempengaruhi geometri sel, jadi kita bisa menyimpan indeks tipe elevasi dalam data sel, dan tidak dalam data mesh. Ini akan memungkinkan kita untuk menyingkirkan kebutuhan akan triangulasi ketika mengubah jenis relief sel.

Tambahkan metode HexCellShaderData ke RefreshTerrain untuk menyederhanakan tugas ini untuk sel tertentu. Biarkan metode ini kosong untuk saat ini.

  public void RefreshTerrain (HexCell cell) { } 

Ubah HexCell.TerrainTypeIndex sehingga HexCell.TerrainTypeIndex metode ini, dan tidak memesan untuk memperbarui fragmen.

  public int TerrainTypeIndex { get { return terrainTypeIndex; } set { if (terrainTypeIndex != value) { terrainTypeIndex = value; // Refresh(); ShaderData.RefreshTerrain(this); } } } 

Kami juga akan memanggilnya di HexCell.Load setelah menerima jenis topografi sel.

  public void Load (BinaryReader reader) { terrainTypeIndex = reader.ReadByte(); ShaderData.RefreshTerrain(this); elevation = reader.ReadByte(); RefreshPosition(); … } 

Indeks sel


Untuk mengubah sel-sel ini, kita perlu mengetahui indeks sel. Cara termudah untuk melakukan ini adalah dengan menambahkan properti Index ke HexCell . Ini akan menunjukkan indeks sel dalam daftar sel di peta, yang sesuai dengan indeksnya dalam sel yang diberikan dalam shader.

  public int Index { get; set; } 

Indeks ini sudah ada di HexGrid.CreateCell , jadi tetapkan saja ke sel yang dibuat.

  void CreateCell (int x, int z, int i) { … cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); cell.Index = i; cell.ShaderData = cellShaderData; … } 

Sekarang HexCellShaderData.RefreshTerrain dapat menggunakan indeks ini untuk menentukan data sel. Mari kita simpan indeks tipe elevasi dalam komponen alfa pikselnya dengan hanya mengonversi tipe menjadi byte. Ini akan mendukung hingga 256 jenis medan, yang akan cukup bagi kami.

  public void RefreshTerrain (HexCell cell) { cellTextureData[cell.Index].a = (byte)cell.TerrainTypeIndex; } 

Untuk menerapkan data ke tekstur dan meneruskannya ke GPU, kita perlu memanggil Texture2D.SetPixels32 , dan kemudian Texture2D.Apply . Seperti dalam kasus fragmen, kami akan menunda operasi ini di LateUpdate sehingga mereka dapat dilakukan tidak lebih dari sekali per frame, terlepas dari jumlah sel yang diubah.

  public void RefreshTerrain (HexCell cell) { cellTextureData[cell.Index].a = (byte)cell.TerrainTypeIndex; enabled = true; } void LateUpdate () { cellTexture.SetPixels32(cellTextureData); cellTexture.Apply(); enabled = false; } 

Untuk memastikan bahwa data akan diperbarui setelah membuat peta baru, aktifkan komponen setelah inisialisasi.

  public void Initialize (int x, int z) { … enabled = true; } 

Triangulasi indeks sel


Karena kami sekarang menyimpan indeks tipe elevasi dalam sel-sel ini, kami tidak perlu lagi memasukkannya dalam proses triangulasi. Tetapi untuk menggunakan data sel, shader harus tahu indeks mana yang harus digunakan. Oleh karena itu, Anda perlu menyimpan indeks sel dalam data mesh, menggantikan indeks tipe ketinggian. Selain itu, kita masih membutuhkan saluran warna mesh untuk mencampur sel saat menggunakan sel-sel ini.

Kami HexMesh bidang umum usang useColors dan useTerrainTypes . Ganti dengan satu bidang useCellData .

 // public bool useCollider, useColors, useUVCoordinates, useUV2Coordinates; // public bool useTerrainTypes; public bool useCollider, useCellData, useUVCoordinates, useUV2Coordinates; 

Kami mengubah nama dari daftar cellIndices menjadi cellIndices . Mari juga refactor-rename colors ke cellWeights - nama ini akan lebih baik.

 // [NonSerialized] List<Vector3> vertices, terrainTypes; // [NonSerialized] List<Color> colors; [NonSerialized] List<Vector3> vertices, cellIndices; [NonSerialized] List<Color> cellWeights; [NonSerialized] List<Vector2> uvs, uv2s; [NonSerialized] List<int> triangles; 

Ubah Clear sehingga saat menggunakan sel-sel ini, ia mengumpulkan dua daftar, dan tidak secara terpisah.

  public void Clear () { hexMesh.Clear(); vertices = ListPool<Vector3>.Get(); if (useCellData) { cellWeights = ListPool<Color>.Get(); cellIndices = ListPool<Vector3>.Get(); } // if (useColors) { // colors = ListPool<Color>.Get(); // } if (useUVCoordinates) { uvs = ListPool<Vector2>.Get(); } if (useUV2Coordinates) { uv2s = ListPool<Vector2>.Get(); } // if (useTerrainTypes) { // terrainTypes = ListPool<Vector3>.Get(); // } triangles = ListPool<int>.Get(); } 

Lakukan pengelompokan yang sama di Apply .

  public void Apply () { hexMesh.SetVertices(vertices); ListPool<Vector3>.Add(vertices); if (useCellData) { hexMesh.SetColors(cellWeights); ListPool<Color>.Add(cellWeights); hexMesh.SetUVs(2, cellIndices); ListPool<Vector3>.Add(cellIndices); } // if (useColors) { // hexMesh.SetColors(colors); // ListPool<Color>.Add(colors); // } if (useUVCoordinates) { hexMesh.SetUVs(0, uvs); ListPool<Vector2>.Add(uvs); } if (useUV2Coordinates) { hexMesh.SetUVs(1, uv2s); ListPool<Vector2>.Add(uv2s); } // if (useTerrainTypes) { // hexMesh.SetUVs(2, terrainTypes); // ListPool<Vector3>.Add(terrainTypes); // } hexMesh.SetTriangles(triangles, 0); ListPool<int>.Add(triangles); hexMesh.RecalculateNormals(); if (useCollider) { meshCollider.sharedMesh = hexMesh; } } 

Mari kita hapus semua AddTriangleTerrainTypes dan AddTriangleTerrainTypes . Ganti mereka dengan metode AddTriangleCellData sesuai, yang menambahkan indeks dan bobot sekaligus.

  public void AddTriangleCellData ( Vector3 indices, Color weights1, Color weights2, Color weights3 ) { cellIndices.Add(indices); cellIndices.Add(indices); cellIndices.Add(indices); cellWeights.Add(weights1); cellWeights.Add(weights2); cellWeights.Add(weights3); } public void AddTriangleCellData (Vector3 indices, Color weights) { AddTriangleCellData(indices, weights, weights, weights); } 

Lakukan hal yang sama dalam metode AddQuad sesuai.

  public void AddQuadCellData ( Vector3 indices, Color weights1, Color weights2, Color weights3, Color weights4 ) { cellIndices.Add(indices); cellIndices.Add(indices); cellIndices.Add(indices); cellIndices.Add(indices); cellWeights.Add(weights1); cellWeights.Add(weights2); cellWeights.Add(weights3); cellWeights.Add(weights4); } public void AddQuadCellData ( Vector3 indices, Color weights1, Color weights2 ) { AddQuadCellData(indices, weights1, weights1, weights2, weights2); } public void AddQuadCellData (Vector3 indices, Color weights) { AddQuadCellData(indices, weights, weights, weights, weights); } 

HexGridChunk Refactoring


Pada tahap ini, kami mendapatkan banyak kesalahan kompiler di HexGridChunk yang perlu HexGridChunk . Tapi pertama-tama, demi konsistensi, kami mengubah nama-warna statis menjadi bobot.

  static Color weights1 = new Color(1f, 0f, 0f); static Color weights2 = new Color(0f, 1f, 0f); static Color weights3 = new Color(0f, 0f, 1f); 

Mari kita mulai dengan memperbaiki TriangulateEdgeFan . Dia dulu butuh tipe, tapi sekarang dia butuh indeks sel. AddTriangleColor dan AddTriangleTerrainTypes dengan kode AddTriangleTerrainTypes yang sesuai.

  void TriangulateEdgeFan (Vector3 center, EdgeVertices edge, float index) { terrain.AddTriangle(center, edge.v1, edge.v2); terrain.AddTriangle(center, edge.v2, edge.v3); terrain.AddTriangle(center, edge.v3, edge.v4); terrain.AddTriangle(center, edge.v4, edge.v5); Vector3 indices; indices.x = indices.y = indices.z = index; terrain.AddTriangleCellData(indices, weights1); terrain.AddTriangleCellData(indices, weights1); terrain.AddTriangleCellData(indices, weights1); terrain.AddTriangleCellData(indices, weights1); // terrain.AddTriangleColor(weights1); // terrain.AddTriangleColor(weights1); // terrain.AddTriangleColor(weights1); // terrain.AddTriangleColor(weights1); // Vector3 types; // types.x = types.y = types.z = type; // terrain.AddTriangleTerrainTypes(types); // terrain.AddTriangleTerrainTypes(types); // terrain.AddTriangleTerrainTypes(types); // terrain.AddTriangleTerrainTypes(types); } 

Metode ini disebut di beberapa tempat. Mari kita telaah mereka dan pastikan bahwa indeks sel ditransfer di sana, dan bukan jenis medan.

  TriangulateEdgeFan(center, e, cell.Index); 

Berikutnya adalah TriangulateEdgeStrip . Semuanya sedikit lebih rumit di sini, tetapi kami menggunakan pendekatan yang sama. Juga refactor-rename nama parameter c1 dan c2 ke w1 dan w2 .

  void TriangulateEdgeStrip ( EdgeVertices e1, Color w1, float index1, EdgeVertices e2, Color w2, float index2, bool hasRoad = false ) { terrain.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); terrain.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); terrain.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); terrain.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); Vector3 indices; indices.x = indices.z = index1; indices.y = index2; terrain.AddQuadCellData(indices, w1, w2); terrain.AddQuadCellData(indices, w1, w2); terrain.AddQuadCellData(indices, w1, w2); terrain.AddQuadCellData(indices, w1, w2); // terrain.AddQuadColor(c1, c2); // terrain.AddQuadColor(c1, c2); // terrain.AddQuadColor(c1, c2); // terrain.AddQuadColor(c1, c2); // Vector3 types; // types.x = types.z = type1; // types.y = type2; // terrain.AddQuadTerrainTypes(types); // terrain.AddQuadTerrainTypes(types); // terrain.AddQuadTerrainTypes(types); // terrain.AddQuadTerrainTypes(types); if (hasRoad) { TriangulateRoadSegment(e1.v2, e1.v3, e1.v4, e2.v2, e2.v3, e2.v4); } } 

Ubah panggilan ke metode ini sehingga indeks sel diteruskan ke mereka. Kami juga menjaga agar nama variabel tetap konsisten.

  TriangulateEdgeStrip( m, weights1, cell.Index, e, weights1, cell.Index ); … TriangulateEdgeStrip( e1, weights1, cell.Index, e2, weights2, neighbor.Index, hasRoad ); … void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell, bool hasRoad ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color w2 = HexMetrics.TerraceLerp(weights1, weights2, 1); float i1 = beginCell.Index; float i2 = endCell.Index; TriangulateEdgeStrip(begin, weights1, i1, e2, w2, i2, hasRoad); for (int i = 2; i < HexMetrics.terraceSteps; i++) { EdgeVertices e1 = e2; Color w1 = w2; e2 = EdgeVertices.TerraceLerp(begin, end, i); w2 = HexMetrics.TerraceLerp(weights1, weights2, i); TriangulateEdgeStrip(e1, w1, i1, e2, w2, i2, hasRoad); } TriangulateEdgeStrip(e2, w2, i1, end, weights2, i2, hasRoad); } 

Sekarang kita beralih ke metode sudut. Perubahan ini sederhana, tetapi harus dibuat dalam jumlah besar kode. Pertama di TriangulateCorner .

  void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … else { terrain.AddTriangle(bottom, left, right); Vector3 indices; indices.x = bottomCell.Index; indices.y = leftCell.Index; indices.z = rightCell.Index; terrain.AddTriangleCellData(indices, weights1, weights2, weights3); // terrain.AddTriangleColor(weights1, weights2, weights3); // Vector3 types; // types.x = bottomCell.TerrainTypeIndex; // types.y = leftCell.TerrainTypeIndex; // types.z = rightCell.TerrainTypeIndex; // terrain.AddTriangleTerrainTypes(types); } features.AddWall(bottom, bottomCell, left, leftCell, right, rightCell); } 

Datang ke TriangulateCornerTerraces .

  void TriangulateCornerTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { Vector3 v3 = HexMetrics.TerraceLerp(begin, left, 1); Vector3 v4 = HexMetrics.TerraceLerp(begin, right, 1); Color w3 = HexMetrics.TerraceLerp(weights1, weights2, 1); Color w4 = HexMetrics.TerraceLerp(weights1, weights3, 1); Vector3 indices; indices.x = beginCell.Index; indices.y = leftCell.Index; indices.z = rightCell.Index; terrain.AddTriangle(begin, v3, v4); terrain.AddTriangleCellData(indices, weights1, w3, w4); // terrain.AddTriangleColor(weights1, w3, w4); // terrain.AddTriangleTerrainTypes(indices); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v3; Vector3 v2 = v4; Color w1 = w3; Color w2 = w4; v3 = HexMetrics.TerraceLerp(begin, left, i); v4 = HexMetrics.TerraceLerp(begin, right, i); w3 = HexMetrics.TerraceLerp(weights1, weights2, i); w4 = HexMetrics.TerraceLerp(weights1, weights3, i); terrain.AddQuad(v1, v2, v3, v4); terrain.AddQuadCellData(indices, w1, w2, w3, w4); // terrain.AddQuadColor(w1, w2, w3, w4); // terrain.AddQuadTerrainTypes(indices); } terrain.AddQuad(v3, v4, left, right); terrain.AddQuadCellData(indices, w3, w4, weights2, weights3); // terrain.AddQuadColor(w3, w4, weights2, weights3); // terrain.AddQuadTerrainTypes(indices); } 

Kemudian di TriangulateCornerTerracesCliff .

  void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } Vector3 boundary = Vector3.Lerp( HexMetrics.Perturb(begin), HexMetrics.Perturb(right), b ); Color boundaryWeights = Color.Lerp(weights1, weights3, b); Vector3 indices; indices.x = beginCell.Index; indices.y = leftCell.Index; indices.z = rightCell.Index; TriangulateBoundaryTriangle( begin, weights1, left, weights2, boundary, boundaryWeights, indices ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, weights2, right, weights3, boundary, boundaryWeights, indices ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleCellData( indices, weights2, weights3, boundaryWeights ); // terrain.AddTriangleColor(weights2, weights3, boundaryColor); // terrain.AddTriangleTerrainTypes(indices); } } 

Dan sedikit berbeda di TriangulateCornerCliffTerraces .

  void TriangulateCornerCliffTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (leftCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } Vector3 boundary = Vector3.Lerp( HexMetrics.Perturb(begin), HexMetrics.Perturb(left), b ); Color boundaryWeights = Color.Lerp(weights1, weights2, b); Vector3 indices; indices.x = beginCell.Index; indices.y = leftCell.Index; indices.z = rightCell.Index; TriangulateBoundaryTriangle( right, weights3, begin, weights1, boundary, boundaryWeights, indices ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, weights2, right, weights3, boundary, boundaryWeights, indices ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleCellData( indices, weights2, weights3, boundaryWeights ); // terrain.AddTriangleColor(weights2, weights3, boundaryWeights); // terrain.AddTriangleTerrainTypes(indices); } } 

Dua metode sebelumnya menggunakan TriangulateBoundaryTriangle , yang juga membutuhkan pembaruan.

  void TriangulateBoundaryTriangle ( Vector3 begin, Color beginWeights, Vector3 left, Color leftWeights, Vector3 boundary, Color boundaryWeights, Vector3 indices ) { Vector3 v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, 1)); Color w2 = HexMetrics.TerraceLerp(beginWeights, leftWeights, 1); terrain.AddTriangleUnperturbed(HexMetrics.Perturb(begin), v2, boundary); terrain.AddTriangleCellData(indices, beginWeights, w2, boundaryWeights); // terrain.AddTriangleColor(beginColor, c2, boundaryColor); // terrain.AddTriangleTerrainTypes(types); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color w1 = w2; v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, i)); w2 = HexMetrics.TerraceLerp(beginWeights, leftWeights, i); terrain.AddTriangleUnperturbed(v1, v2, boundary); terrain.AddTriangleCellData(indices, w1, w2, boundaryWeights); // terrain.AddTriangleColor(c1, c2, boundaryColor); // terrain.AddTriangleTerrainTypes(types); } terrain.AddTriangleUnperturbed(v2, HexMetrics.Perturb(left), boundary); terrain.AddTriangleCellData(indices, w2, leftWeights, boundaryWeights); // terrain.AddTriangleColor(c2, leftColor, boundaryColor); // terrain.AddTriangleTerrainTypes(types); } 

Metode terakhir yang perlu diubah adalah TriangulateWithRiver .

  void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … terrain.AddTriangle(centerL, m.v1, m.v2); terrain.AddQuad(centerL, center, m.v2, m.v3); terrain.AddQuad(center, centerR, m.v3, m.v4); terrain.AddTriangle(centerR, m.v4, m.v5); Vector3 indices; indices.x = indices.y = indices.z = cell.Index; terrain.AddTriangleCellData(indices, weights1); terrain.AddQuadCellData(indices, weights1); terrain.AddQuadCellData(indices, weights1); terrain.AddTriangleCellData(indices, weights1); // terrain.AddTriangleColor(weights1); // terrain.AddQuadColor(weights1); // terrain.AddQuadColor(weights1); // terrain.AddTriangleColor(weights1); // Vector3 types; // types.x = types.y = types.z = cell.TerrainTypeIndex; // terrain.AddTriangleTerrainTypes(types); // terrain.AddQuadTerrainTypes(types); // terrain.AddQuadTerrainTypes(types); // terrain.AddTriangleTerrainTypes(types); … } 

Agar semuanya berfungsi, kita perlu menunjukkan bahwa kita akan menggunakan data sel untuk elemen anak dari relief fragmen cetakan.


Relief menggunakan data sel.

Pada tahap ini, mesh berisi indeks sel, bukan indeks tipe elevasi. Karena shader elevasi masih menafsirkannya sebagai indeks elevasi, kita akan melihat bahwa sel pertama dirender dengan tekstur pertama dan seterusnya hingga tekstur relief terakhir tercapai.


Menggunakan indeks sel sebagai indeks tekstur ketinggian.

Saya tidak dapat mengaktifkan kode refactored. Apa yang saya lakukan salah?
Pada suatu waktu kami mengubah sejumlah besar kode triangulasi, sehingga ada kemungkinan kesalahan atau kelalaian yang tinggi. Jika Anda tidak dapat menemukan kesalahan, coba unduh paket dari bagian ini dan ekstrak file yang sesuai. Anda dapat mengimpornya ke proyek terpisah dan membandingkannya dengan kode Anda sendiri.

Transfer data sel ke shader


Untuk menggunakan sel-sel ini, shader medan harus memiliki akses ke mereka. Ini dapat diimplementasikan melalui properti shader. Ini akan membutuhkan HexCellShaderData mengatur properti material lega. Atau kita dapat membuat tekstur sel-sel ini terlihat secara global oleh semua shader. Ini nyaman karena kami membutuhkannya dalam beberapa shader, jadi kami akan menggunakan pendekatan ini.

Setelah membuat tekstur sel, panggil metode Shader.SetGlobalTexture statis untuk membuatnya terlihat secara global sebagai _HexCellData .

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

Saat menggunakan properti shader, Unity membuat ukuran tekstur tersedia untuk shader melalui variabel teksturName_TexelSize . Ini adalah vektorizer empat komponen yang berisi nilai yang terbalik dengan lebar dan tinggi, serta lebar dan tinggi itu sendiri. Tetapi ketika mengatur tekstur global, ini tidak dilakukan. Oleh karena itu, kami akan melakukannya sendiri menggunakan Shader.SetGlobalVector setelah membuat atau mengubah ukuran tekstur.

  else { cellTexture = new Texture2D( x, z, TextureFormat.RGBA32, false, true ); cellTexture.filterMode = FilterMode.Point; cellTexture.wrapMode = TextureWrapMode.Clamp; Shader.SetGlobalTexture("_HexCellData", cellTexture); } Shader.SetGlobalVector( "_HexCellData_TexelSize", new Vector4(1f / x, 1f / z, x, z) ); 

Akses Data Shader


Buat shader baru sertakan file dalam folder bahan yang disebut HexCellData . Di dalamnya, kami mendefinisikan variabel untuk informasi tentang tekstur dan ukuran sel-sel ini. Kami juga membuat fungsi untuk mendapatkan data sel untuk data mesh vertex yang diberikan.

 sampler2D _HexCellData; float4 _HexCellData_TexelSize; float4 GetCellData (appdata_full v) { } 


Sertakan file baru.

Indeks sel disimpan dalam v.texcoord2 , seperti halnya dengan jenis medan. Mari kita mulai dengan indeks pertama - v.texcoord2.x . Sayangnya, kami tidak dapat langsung menggunakan indeks untuk sampel tekstur sel-sel ini. Kami harus mengubahnya menjadi koordinat UV.

Langkah pertama dalam membuat koordinat U adalah membagi indeks sel dengan lebar tekstur. Kita dapat melakukan ini dengan mengalikannya dengan _HexCellData_TexelSize.x .

 float4 GetCellData (appdata_full v) { float2 uv; uv.x = v.texcoord2.x * _HexCellData_TexelSize.x; } 

Hasilnya akan berupa angka dalam bentuk ZU, di mana Z adalah indeks baris dan U adalah koordinat dari sel U. Kita dapat mengekstraksi string dengan membulatkan angka ke bawah dan kemudian mengurangkannya dari angka untuk mendapatkan koordinat U.

 float4 GetCellData (appdata_full v) { float2 uv; uv.x = v.texcoord2.x * _HexCellData_TexelSize.x; float row = floor(uv.x); uv.x -= row; } 

Koordinat V membagi garis dengan ketinggian tekstur.

 float4 GetCellData (appdata_full v) { float2 uv; uv.x = v.texcoord2.x * _HexCellData_TexelSize.x; float row = floor(uv.x); uv.x -= row; uv.y = row * _HexCellData_TexelSize.y; } 

Karena kita mengambil sampel tekstur, kita perlu menggunakan koordinat di pusat piksel, bukan di tepinya. Dengan cara ini kami menjamin bahwa piksel yang benar disampel. Karena itu, setelah membaginya dengan ukuran tekstur, tambahkan ½.

 float4 GetCellData (appdata_full v) { float2 uv; uv.x = (v.texcoord2.x + 0.5) * _HexCellData_TexelSize.x; float row = floor(uv.x); uv.x -= row; uv.y = (row + 0.5) * _HexCellData_TexelSize.y; } 

Ini memberi kita koordinat UV yang benar untuk indeks sel pertama yang disimpan dalam data titik. Tetapi di atas kita dapat memiliki hingga tiga indeks berbeda. Karenanya, kami akan membuatnya GetCellDataberfungsi untuk indeks apa pun. Tambahkan parameter integer ke dalamnya index, yang akan kita gunakan untuk mengakses komponen vektor dengan indeks sel.

 float4 GetCellData (appdata_full v, int index) { float2 uv; uv.x = (v.texcoord2[index] + 0.5) * _HexCellData_TexelSize.x; float row = floor(uv.x); uv.x -= row; uv.y = (row + 0.5) * _HexCellData_TexelSize.y; } 

Sekarang kita memiliki semua koordinat yang diperlukan untuk sel-sel ini, kita dapat mencicipi _HexCellData. Karena kita mengambil sampel tekstur dalam program vertex, kita perlu secara eksplisit memberi tahu shader yang menggunakan tekstur mip. Ini dapat dilakukan dengan menggunakan fungsi tex2Dlodyang membutuhkan koordinat empat tekstur. Karena sel-sel ini tidak memiliki tekstur-mip, kami menetapkan nilai nol ke koordinat tambahan.

 float4 GetCellData (appdata_full v, int index) { float2 uv; uv.x = (v.texcoord2[index] + 0.5) * _HexCellData_TexelSize.x; float row = floor(uv.x); uv.x -= row; uv.y = (row + 0.5) * _HexCellData_TexelSize.y; float4 data = tex2Dlod(_HexCellData, float4(uv, 0, 0)); } 

Komponen data keempat berisi indeks tipe elevasi, yang kami simpan langsung sebagai byte. Namun, GPU secara otomatis mengubahnya menjadi nilai floating point di kisaran 0-1. Untuk mengubahnya kembali ke nilai yang benar, kalikan dengan 255. Setelah itu, Anda dapat mengembalikan data.

  float4 data = tex2Dlod(_HexCellData, float4(uv, 0, 0)); data.w *= 255; return data; 

Untuk menggunakan fungsi ini, aktifkan HexCellData di Terrain shader . Karena saya menempatkan shader ini di Material / Terrain , saya perlu menggunakan path relatif ../HexCellData.cginc .

  #include "../HexCellData.cginc" UNITY_DECLARE_TEX2DARRAY(_MainTex); 

Dalam program titik, kami memperoleh data sel untuk ketiga indeks sel yang disimpan dalam data titik. Kemudian tetapkan data.terrainindeks ketinggian mereka.

  void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); // data.terrain = v.texcoord2.xyz; float4 cell0 = GetCellData(v, 0); float4 cell1 = GetCellData(v, 1); float4 cell2 = GetCellData(v, 2); data.terrain.x = cell0.w; data.terrain.y = cell1.w; data.terrain.z = cell2.w; } 

Pada titik ini, peta kembali mulai menampilkan medan yang benar. Perbedaan besar adalah bahwa pengeditan hanya tipe medan tidak lagi mengarah ke triangulasi baru. Jika selama mengedit data sel lainnya diubah, maka triangulasi akan dilakukan seperti biasa.

paket unity

Visibilitas


Setelah menciptakan dasar sel-sel ini, kita dapat melanjutkan untuk mendukung visibilitas. Untuk melakukan ini, kami menggunakan shader, sel itu sendiri, dan objek yang menentukan visibilitas. Perhatikan bahwa proses triangulasi sama sekali tidak mengetahui tentang ini.

Shader


Mari kita mulai dengan memberi tahu shader Terrain tentang visibilitas. Ini akan menerima data visibilitas dari program vertex dan meneruskannya ke program fragmen menggunakan struktur Input. Karena kami melewati tiga indeks ketinggian yang terpisah, kami akan melewati tiga nilai visibilitas juga.

  struct Input { float4 color : COLOR; float3 worldPos; float3 terrain; float3 visibility; }; 

Untuk menyimpan visibilitas, kami menggunakan komponen pertama sel-sel ini.

  void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); float4 cell0 = GetCellData(v, 0); float4 cell1 = GetCellData(v, 1); float4 cell2 = GetCellData(v, 2); data.terrain.x = cell0.w; data.terrain.y = cell1.w; data.terrain.z = cell2.w; data.visibility.x = cell0.x; data.visibility.y = cell1.x; data.visibility.z = cell2.x; } 

Visibilitas 0 berarti sel saat ini tidak terlihat. Jika itu terlihat, itu akan memiliki nilai visibilitas 1. Oleh karena itu, kita dapat menggelapkan medan dengan mengalikan hasilnya GetTerrainColordengan vektor visibilitas yang sesuai. Jadi, kami secara individual memodulasi warna relief dari setiap sel campuran.

  float4 GetTerrainColor (Input IN, int index) { float3 uvw = float3(IN.worldPos.xz * 0.02, IN.terrain[index]); float4 c = UNITY_SAMPLE_TEX2DARRAY(_MainTex, uvw); return c * (IN.color[index] * IN.visibility[index]); } 


Sel-sel menjadi hitam.

Tidak bisakah kita menggabungkan visibilitas dalam program vertex?
, . . . , . , .

Gelap total adalah penghalang bagi sel-sel yang tidak terlihat sementara. Agar kita masih bisa melihat kelegaan, kita perlu meningkatkan indikator yang digunakan untuk sel-sel tersembunyi. Mari kita beralih dari 0-1 ke ¼ - 1, yang dapat dilakukan dengan menggunakan fungsi lerpdi akhir program vertex.

  void vert (inout appdata_full v, out Input data) { … data.visibility.x = cell0.x; data.visibility.y = cell1.x; data.visibility.z = cell2.x; data.visibility = lerp(0.25, 1, data.visibility); } 


Sel teduh.

Pelacakan visibilitas sel


Agar visibilitas berfungsi, sel harus melacak visibilitasnya. Tetapi bagaimana sebuah sel menentukan apakah itu terlihat? Kita dapat melakukan ini dengan melacak jumlah entitas yang melihatnya. Ketika seseorang mulai melihat sel, ia harus melaporkan sel ini. Dan ketika seseorang berhenti melihat sel, dia juga harus memberi tahu dia tentang hal itu. Sel hanya melacak jumlah pengamat, apa pun entitas itu. Jika sel memiliki nilai visibilitas minimal 1, maka itu terlihat, jika tidak maka tidak terlihat. Untuk menerapkan perilaku ini, kami menambahkan HexCelldua metode dan properti ke variabel.

  public bool IsVisible { get { return visibility > 0; } } … int visibility; … public void IncreaseVisibility () { visibility += 1; } public void DecreaseVisibility () { visibility -= 1; } 

Selanjutnya, tambahkan ke HexCellShaderDatametode RefreshVisibility, yang melakukan hal yang sama RefreshTerrain, hanya demi visibilitas. Menyimpan data dalam komponen R dari sel data. Karena kami bekerja dengan byte yang dikonversi ke nilai 0-1, kami menggunakan untuk menunjukkan visibilitas (byte)255.

  public void RefreshVisibility (HexCell cell) { cellTextureData[cell.Index].r = cell.IsVisible ? (byte)255 : (byte)0; enabled = true; } 

Kami akan memanggil metode ini dengan meningkatkan dan menurunkan visibilitas, mengubah nilai antara 0 dan 1.

  public void IncreaseVisibility () { visibility += 1; if (visibility == 1) { ShaderData.RefreshVisibility(this); } } public void DecreaseVisibility () { visibility -= 1; if (visibility == 0) { ShaderData.RefreshVisibility(this); } } 

Menciptakan visibilitas pasukan


Mari kita membuatnya sehingga unit dapat melihat sel yang mereka tempati. Ini dilakukan dengan menggunakan panggilan IncreaseVisibilityke lokasi baru unit selama tugas HexUnit.Location. Kami juga memanggil lokasi lama (jika ada) DecreaseVisibility.

  public HexCell Location { get { return location; } set { if (location) { location.DecreaseVisibility(); location.Unit = null; } location = value; value.Unit = this; value.IncreaseVisibility(); transform.localPosition = value.Position; } } 


Unit dapat melihat di mana mereka berada.

Akhirnya kami menggunakan visibilitas! Saat ditambahkan ke peta, unit membuat sel mereka terlihat. Selain itu, ruang lingkup mereka diteleportasi ketika pindah ke lokasi baru mereka. Tetapi ruang lingkup mereka tetap aktif saat mengeluarkan unit dari peta. Untuk memperbaiki ini, kami akan mengurangi visibilitas lokasi mereka saat menghancurkan unit.

  public void Die () { if (location) { location.DecreaseVisibility(); } location.Unit = null; Destroy(gameObject); } 

Rentang visibilitas


Sejauh ini, kita hanya melihat sel tempat detasemen itu berada, dan ini membatasi kemungkinan. Setidaknya kita perlu melihat sel tetangga. Dalam kasus umum, unit dapat melihat semua sel dalam jarak tertentu, yang tergantung pada unit.

Mari kita tambahkan ke HexGridmetode untuk menemukan semua sel terlihat dari satu sel dengan mempertimbangkan rentang. Kita dapat membuat metode ini dengan menduplikasi dan mengubah Search. Ubah parameternya dan buat kembali daftar sel tempat Anda bisa menggunakan kumpulan daftar.

Pada setiap iterasi, sel saat ini ditambahkan ke daftar. Tidak ada lagi sel akhir, jadi pencarian tidak akan pernah berakhir ketika mencapai titik ini. Kami juga menyingkirkan logika bergerak dan biaya bergerak. Buat propertiPathFrommereka tidak lagi ditanya karena kita tidak membutuhkannya, dan kita tidak ingin mengganggu jalan di sepanjang grid.

Pada setiap langkah, jarak hanya meningkat dengan 1. Jika melebihi kisaran, maka sel ini dilewati. Dan kami tidak memerlukan pencarian heuristik, jadi kami menginisialisasi dengan nilai 0. Artinya, pada dasarnya, kami kembali ke algoritma Dijkstra.

  List<HexCell> GetVisibleCells (HexCell fromCell, int range) { List<HexCell> visibleCells = ListPool<HexCell>.Get(); searchFrontierPhase += 2; if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } else { searchFrontier.Clear(); } fromCell.SearchPhase = searchFrontierPhase; fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); while (searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.SearchPhase += 1; visibleCells.Add(current); // if (current == toCell) { // return true; // } // int currentTurn = (current.Distance - 1) / speed; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if ( neighbor == null || neighbor.SearchPhase > searchFrontierPhase ) { continue; } // … // int moveCost; // … int distance = current.Distance + 1; if (distance > range) { continue; } // int turn = (distance - 1) / speed; // if (turn > currentTurn) { // distance = turn * speed + moveCost; // } if (neighbor.SearchPhase < searchFrontierPhase) { neighbor.SearchPhase = searchFrontierPhase; neighbor.Distance = distance; // neighbor.PathFrom = current; neighbor.SearchHeuristic = 0; searchFrontier.Enqueue(neighbor); } else if (distance < neighbor.Distance) { int oldPriority = neighbor.SearchPriority; neighbor.Distance = distance; // neighbor.PathFrom = current; searchFrontier.Change(neighbor, oldPriority); } } } return visibleCells; } 

Tidak bisakah kita menggunakan algoritma yang lebih sederhana untuk menemukan semua sel dalam jangkauan?
, , .

Juga tambahkan HexGridmetode IncreaseVisibilitydan DecreaseVisibility. Mereka mendapatkan sel dan rentang, mengambil daftar sel yang sesuai dan meningkatkan / menurunkan visibilitas mereka. Setelah selesai, mereka harus mengembalikan daftar itu ke kumpulannya.

  public void IncreaseVisibility (HexCell fromCell, int range) { List<HexCell> cells = GetVisibleCells(fromCell, range); for (int i = 0; i < cells.Count; i++) { cells[i].IncreaseVisibility(); } ListPool<HexCell>.Add(cells); } public void DecreaseVisibility (HexCell fromCell, int range) { List<HexCell> cells = GetVisibleCells(fromCell, range); for (int i = 0; i < cells.Count; i++) { cells[i].DecreaseVisibility(); } ListPool<HexCell>.Add(cells); } 

Untuk menggunakan metode ini HexUnitmemerlukan akses ke kisi, jadi tambahkan properti ke dalamnya Grid.

  public HexGrid Grid { get; set; } 

Saat Anda menambahkan skuad ke kisi, ia akan menetapkan kisi ke properti ini HexGrid.AddUnit.

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

Untuk mulai dengan, kisaran visibilitas tiga sel akan cukup. Untuk melakukan ini, kita tambahkan ke HexUnitkonstanta, yang di masa depan selalu dapat berubah menjadi variabel. Kemudian kita akan membuat metode memanggil regu untuk grid IncreaseVisibilitydan DecreaseVisibility, mentransmisikan juga jangkauan visibilitasnya, dan tidak hanya pergi ke tempat ini.

  const int visionRange = 3; … public HexCell Location { get { return location; } set { if (location) { // location.DecreaseVisibility(); Grid.DecreaseVisibility(location, visionRange); location.Unit = null; } location = value; value.Unit = this; // value.IncreaseVisibility(); Grid.IncreaseVisibility(value, visionRange); transform.localPosition = value.Position; } } … public void Die () { if (location) { // location.DecreaseVisibility(); Grid.DecreaseVisibility(location, visionRange); } location.Unit = null; Destroy(gameObject); } 


Unit dengan rentang visibilitas yang dapat tumpang tindih.

Visibilitas saat bergerak


Saat ini, area visibilitas skuad setelah perintah bergerak segera dipindahkan ke titik akhir. Akan terlihat lebih baik jika unit dan bidang visibilitasnya bergerak bersama. Langkah pertama untuk ini adalah bahwa kami tidak akan lagi menetapkan properti Locationc HexUnit.Travel. Sebagai gantinya, kami akan langsung mengubah bidang location, menghindari kode properti. Karenanya, kami akan menghapus lokasi lama secara manual dan mengonfigurasi lokasi baru. Visibilitas akan tetap tidak berubah.

  public void Travel (List<HexCell> path) { // Location = path[path.Count - 1]; location.Unit = null; location = path[path.Count - 1]; location.Unit = this; pathToTravel = path; StopAllCoroutines(); StartCoroutine(TravelPath()); } 

Di dalam coroutine, TravelPathkita akan mengurangi visibilitas sel pertama hanya setelah selesai LookAt. Setelah itu, sebelum pindah ke sel baru, kami akan meningkatkan visibilitas dari sel ini. Setelah selesai dengan ini, kami kembali mengurangi visibilitas darinya. Akhirnya, tingkatkan visibilitas dari sel terakhir.

  IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; // transform.localPosition = c; yield return LookAt(pathToTravel[1].Position); Grid.DecreaseVisibility(pathToTravel[0], visionRange); float t = Time.deltaTime * travelSpeed; for (int i = 1; i < pathToTravel.Count; i++) { a = c; b = pathToTravel[i - 1].Position; c = (b + pathToTravel[i].Position) * 0.5f; Grid.IncreaseVisibility(pathToTravel[i], visionRange); for (; t < 1f; t += Time.deltaTime * travelSpeed) { … } Grid.DecreaseVisibility(pathToTravel[i], visionRange); t -= 1f; } a = c; b = location.Position; // We can simply use the destination here. c = b; Grid.IncreaseVisibility(location, visionRange); for (; t < 1f; t += Time.deltaTime * travelSpeed) { … } … } 


Visibilitas saat bepergian.

Semua ini berfungsi, kecuali ketika pesanan baru dikeluarkan pada saat detasemen bergerak. Ini mengarah pada teleportasi, yang juga harus berlaku untuk visibilitas. Untuk mewujudkan ini, kita perlu melacak lokasi skuad saat ini saat bergerak.

  HexCell location, currentTravelLocation; 

Kami akan memperbarui lokasi ini setiap kali kami menekan sel baru saat bergerak, hingga skuad mencapai sel terakhir. Maka harus diatur ulang.

  IEnumerator TravelPath () { … for (int i = 1; i < pathToTravel.Count; i++) { currentTravelLocation = pathToTravel[i]; a = c; b = pathToTravel[i - 1].Position; c = (b + currentTravelLocation.Position) * 0.5f; Grid.IncreaseVisibility(pathToTravel[i], visionRange); for (; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); Vector3 d = Bezier.GetDerivative(a, b, c, t); dy = 0f; transform.localRotation = Quaternion.LookRotation(d); yield return null; } Grid.DecreaseVisibility(pathToTravel[i], visionRange); t -= 1f; } currentTravelLocation = null; … } 

Sekarang setelah menyelesaikan pergantian, TravelPathkita dapat memeriksa apakah lokasi perantara yang lama diketahui. Jika ya, maka Anda perlu mengurangi visibilitas dalam sel ini, dan tidak di awal jalan.

  IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; yield return LookAt(pathToTravel[1].Position); Grid.DecreaseVisibility( currentTravelLocation ? currentTravelLocation : pathToTravel[0], visionRange ); … } 

Kita juga perlu memperbaiki visibilitas setelah kompilasi yang terjadi selama pergerakan pasukan. Jika lokasi perantara masih diketahui, maka kurangi visibilitas di dalamnya dan tingkatkan visibilitas di titik akhir, lalu atur ulang lokasi perantara.

  void OnEnable () { if (location) { transform.localPosition = location.Position; if (currentTravelLocation) { Grid.IncreaseVisibility(location, visionRange); Grid.DecreaseVisibility(currentTravelLocation, visionRange); currentTravelLocation = null; } } } 

paket unity

Visibilitas jalan dan air


Meskipun perubahan warna bantuan didasarkan pada visibilitas, ini tidak mempengaruhi jalan dan air. Mereka terlihat terlalu cerah untuk sel yang tidak terlihat. Untuk menerapkan visibilitas ke jalan dan air, kita perlu menambahkan indeks sel dan mencampur bobot ke data mesh mereka. Oleh karena itu, kami akan memeriksa anak-anak dari Data Sel Penggunaan untuk Sungai , Jalan , Air , Perairan , dan Muara fragmen cetakan.

Jalan


Kami akan mulai dari jalan. Metode ini HexGridChunk.TriangulateRoadEdgedigunakan untuk membuat sebagian kecil jalan di tengah sel, sehingga perlu satu indeks sel. Tambahkan parameter ke sana dan buat data sel untuk segitiga.

  void TriangulateRoadEdge ( Vector3 center, Vector3 mL, Vector3 mR, float index ) { roads.AddTriangle(center, mL, mR); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(0f, 0f), new Vector2(0f, 0f) ); Vector3 indices; indices.x = indices.y = indices.z = index; roads.AddTriangleCellData(indices, weights1); } 

Cara mudah lain untuk membuat jalan adalah TriangulateRoadSegment. Ini digunakan baik di dalam maupun di antara sel, sehingga harus bekerja dengan dua indeks yang berbeda. Untuk ini, nyaman untuk menggunakan parameter indeks vektor. Karena ruas jalan dapat menjadi bagian dari tepian, bobot juga harus melewati parameter.

  void TriangulateRoadSegment ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, Vector3 v5, Vector3 v6, Color w1, Color w2, Vector3 indices ) { roads.AddQuad(v1, v2, v4, v5); roads.AddQuad(v2, v3, v5, v6); roads.AddQuadUV(0f, 1f, 0f, 0f); roads.AddQuadUV(1f, 0f, 0f, 0f); roads.AddQuadCellData(indices, w1, w2); roads.AddQuadCellData(indices, w1, w2); } 

Sekarang mari kita beralih ke TriangulateRoad, yang menciptakan jalan di dalam sel. Itu juga membutuhkan parameter indeks. Dia melewati data ini ke metode jalan yang dia panggil, dan menambahkannya ke segitiga yang dia buat.

  void TriangulateRoad ( Vector3 center, Vector3 mL, Vector3 mR, EdgeVertices e, bool hasRoadThroughCellEdge, float index ) { if (hasRoadThroughCellEdge) { Vector3 indices; indices.x = indices.y = indices.z = index; Vector3 mC = Vector3.Lerp(mL, mR, 0.5f); TriangulateRoadSegment( mL, mC, mR, e.v2, e.v3, e.v4, weights1, weights1, indices ); 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) ); roads.AddTriangleCellData(indices, weights1); roads.AddTriangleCellData(indices, weights1); } else { TriangulateRoadEdge(center, mL, mR, index); } } 

Tetap menambahkan argumen metode yang diperlukan untuk TriangulateRoad, TriangulateRoadEdgedan TriangulateRoadSegment, untuk memperbaiki semua kesalahan kompiler.

  void TriangulateWithoutRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { TriangulateEdgeFan(center, e, cell.Index); 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), cell.Index ); } } … void TriangulateRoadAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … TriangulateRoad(roadCenter, mL, mR, e, hasRoadThroughEdge, cell.Index); if (previousHasRiver) { TriangulateRoadEdge(roadCenter, center, mL, cell.Index); } if (nextHasRiver) { TriangulateRoadEdge(roadCenter, mR, center, cell.Index); } } … void TriangulateEdgeStrip () { … if (hasRoad) { TriangulateRoadSegment( e1.v2, e1.v3, e1.v4, e2.v2, e2.v3, e2.v4, w1, w2, indices ); } } 

Sekarang data mesh sudah benar, dan kita akan beralih ke Road shader . Perlu program vertex dan harus mengandung HexCellData .

  #pragma surface surf Standard fullforwardshadows decal:blend vertex:vert #pragma target 3.0 #include "HexCellData.cginc" 

Karena kami tidak mencampurkan beberapa bahan, cukup bagi kami untuk memasukkan satu indikator visibilitas ke dalam program fragmen.

  struct Input { float2 uv_MainTex; float3 worldPos; float visibility; }; 

Cukup untuk program vertex baru untuk menerima data dari dua sel. Kami segera mencampur visibilitas mereka, menyesuaikannya dan menambah output.

  void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); float4 cell0 = GetCellData(v, 0); float4 cell1 = GetCellData(v, 1); data.visibility = cell0.x * v.color.x + cell1.x * v.color.y; data.visibility = lerp(0.25, 1, data.visibility); } 

Dalam program fragmen, kita hanya perlu menambahkan visibilitas ke warna.

  void surf (Input IN, inout SurfaceOutputStandard o) { float4 noise = tex2D(_MainTex, IN.worldPos.xz * 0.025); fixed4 c = _Color * ((noise.y * 0.75 + 0.25) * IN.visibility); … } 


Jalan dengan jarak pandang.

Air terbuka


Mungkin terlihat bahwa visibilitas telah mempengaruhi air, tetapi ini hanya permukaan medan yang terbenam dalam air. Mari kita mulai dengan menerapkan visibilitas ke perairan terbuka. Untuk ini kita perlu berubah HexGridChunk.TriangulateOpenWater.

  void TriangulateOpenWater ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { … water.AddTriangle(center, c1, c2); Vector3 indices; indices.x = indices.y = indices.z = cell.Index; water.AddTriangleCellData(indices, weights1); if (direction <= HexDirection.SE && neighbor != null) { … water.AddQuad(c1, c2, e1, e2); indices.y = neighbor.Index; water.AddQuadCellData(indices, weights1, weights2); if (direction <= HexDirection.E) { … water.AddTriangle( c2, e2, c2 + HexMetrics.GetWaterBridge(direction.Next()) ); indices.z = nextNeighbor.Index; water.AddTriangleCellData( indices, weights1, weights2, weights3 ); } } } 

Kita juga perlu menambahkan data sel ke penggemar segitiga di dekat pantai.

  void TriangulateWaterShore ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { … water.AddTriangle(center, e1.v1, e1.v2); water.AddTriangle(center, e1.v2, e1.v3); water.AddTriangle(center, e1.v3, e1.v4); water.AddTriangle(center, e1.v4, e1.v5); Vector3 indices; indices.x = indices.y = indices.z = cell.Index; water.AddTriangleCellData(indices, weights1); water.AddTriangleCellData(indices, weights1); water.AddTriangleCellData(indices, weights1); water.AddTriangleCellData(indices, weights1); … } 

Shader Air perlu diubah dengan cara yang sama dengan Road shader , tetapi perlu menggabungkan visibilitas tidak hanya dua, tetapi tiga sel.

  #pragma surface surf Standard alpha vertex:vert #pragma target 3.0 #include "Water.cginc" #include "HexCellData.cginc" sampler2D _MainTex; struct Input { float2 uv_MainTex; float3 worldPos; float visibility; }; … void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); float4 cell0 = GetCellData(v, 0); float4 cell1 = GetCellData(v, 1); float4 cell2 = GetCellData(v, 2); data.visibility = cell0.x * v.color.x + cell1.x * v.color.y + cell2.x * v.color.z; data.visibility = lerp(0.25, 1, data.visibility); } void surf (Input IN, inout SurfaceOutputStandard o) { float waves = Waves(IN.worldPos.xz, _MainTex); fixed4 c = saturate(_Color + waves); o.Albedo = c.rgb * IN.visibility; … } 


Air terbuka dengan visibilitas.

Pantai dan muara


Untuk mendukung pantai, kita perlu berubah lagi HexGridChunk.TriangulateWaterShore. Kami sudah membuat vektor indeks, tetapi kami hanya menggunakan satu indeks sel untuk perairan terbuka. Pantai juga membutuhkan indeks tetangga, jadi ubah kodenya.

  Vector3 indices; // indices.x = indices.y = indices.z = cell.Index; indices.x = indices.z = cell.Index; indices.y = neighbor.Index; 

Tambahkan data sel ke paha depan dan segitiga pantai. Kami juga menyampaikan indeks panggilan TriangulateEstuary.

  if (cell.HasRiverThroughEdge(direction)) { TriangulateEstuary( e1, e2, cell.IncomingRiver == direction, indices ); } else { … waterShore.AddQuadUV(0f, 0f, 0f, 1f); waterShore.AddQuadCellData(indices, weights1, weights2); waterShore.AddQuadCellData(indices, weights1, weights2); waterShore.AddQuadCellData(indices, weights1, weights2); waterShore.AddQuadCellData(indices, weights1, weights2); } HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor != null) { … waterShore.AddTriangleUV( … ); indices.z = nextNeighbor.Index; waterShore.AddTriangleCellData( indices, weights1, weights2, weights3 ); } 

Tambahkan parameter yang diperlukan ke TriangulateEstuarydan rawat sel-sel ini untuk pantai dan mulut. Jangan lupa bahwa mulutnya terbuat dari trapesium dengan dua segitiga pantai di sisinya. Kami memastikan bahwa bobot ditransfer dalam urutan yang benar.

  void TriangulateEstuary ( EdgeVertices e1, EdgeVertices e2, bool incomingRiver, Vector3 indices ) { waterShore.AddTriangle(e2.v1, e1.v2, e1.v1); waterShore.AddTriangle(e2.v5, e1.v5, e1.v4); waterShore.AddTriangleUV( new Vector2(0f, 1f), new Vector2(0f, 0f), new Vector2(0f, 0f) ); waterShore.AddTriangleUV( new Vector2(0f, 1f), new Vector2(0f, 0f), new Vector2(0f, 0f) ); waterShore.AddTriangleCellData(indices, weights2, weights1, weights1); waterShore.AddTriangleCellData(indices, weights2, weights1, weights1); estuaries.AddQuad(e2.v1, e1.v2, e2.v2, e1.v3); estuaries.AddTriangle(e1.v3, e2.v2, e2.v4); estuaries.AddQuad(e1.v3, e1.v4, e2.v4, e2.v5); estuaries.AddQuadUV( new Vector2(0f, 1f), new Vector2(0f, 0f), new Vector2(1f, 1f), new Vector2(0f, 0f) ); estuaries.AddTriangleUV( new Vector2(0f, 0f), new Vector2(1f, 1f), new Vector2(1f, 1f) ); estuaries.AddQuadUV( new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(1f, 1f), new Vector2(0f, 1f) ); estuaries.AddQuadCellData( indices, weights2, weights1, weights2, weights1 ); estuaries.AddTriangleCellData(indices, weights1, weights2, weights2); estuaries.AddQuadCellData(indices, weights1, weights2); … } 

Di waterShore shader, Anda perlu membuat perubahan yang sama seperti di Water shader , mencampurkan visibilitas tiga sel.

  #pragma surface surf Standard alpha vertex:vert #pragma target 3.0 #include "Water.cginc" #include "HexCellData.cginc" sampler2D _MainTex; struct Input { float2 uv_MainTex; float3 worldPos; float visibility; }; … void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); float4 cell0 = GetCellData(v, 0); float4 cell1 = GetCellData(v, 1); float4 cell2 = GetCellData(v, 2); data.visibility = cell0.x * v.color.x + cell1.x * v.color.y + cell2.x * v.color.z; data.visibility = lerp(0.25, 1, data.visibility); } void surf (Input IN, inout SurfaceOutputStandard o) { … fixed4 c = saturate(_Color + max(foam, waves)); o.Albedo = c.rgb * IN.visibility; … } 

Scheider Muara campuran visibilitas dari dua sel, serta shader Jalan Umum Deskripsi . Dia sudah memiliki program vertex, karena kita membutuhkannya untuk mengirimkan koordinat UV sungai.

  #include "Water.cginc" #include "HexCellData.cginc" sampler2D _MainTex; struct Input { float2 uv_MainTex; float2 riverUV; float3 worldPos; float visibility; }; half _Glossiness; half _Metallic; fixed4 _Color; void vert (inout appdata_full v, out Input o) { UNITY_INITIALIZE_OUTPUT(Input, o); o.riverUV = v.texcoord1.xy; float4 cell0 = GetCellData(v, 0); float4 cell1 = GetCellData(v, 1); o.visibility = cell0.x * v.color.x + cell1.x * v.color.y; o.visibility = lerp(0.25, 1, o.visibility); } void surf (Input IN, inout SurfaceOutputStandard o) { … fixed4 c = saturate(_Color + water); o.Albedo = c.rgb * IN.visibility; … } 


Pantai dan muara dengan visibilitas.

Sungai


Daerah air terakhir yang bekerja adalah sungai. Tambahkan HexGridChunk.TriangulateRiverQuadvektor indeks ke parameter dan menambahkannya ke jala sehingga dapat mempertahankan visibilitas dua sel.

  void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y, float v, bool reversed, Vector3 indices ) { TriangulateRiverQuad(v1, v2, v3, v4, y, y, v, reversed, indices); } void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y1, float y2, float v, bool reversed, Vector3 indices ) { … rivers.AddQuadCellData(indices, weights1, weights2); } 

TriangulateWithRiverBeginOrEndmenciptakan titik akhir sungai dengan quad dan segitiga di tengah sel. Tambahkan data sel yang diperlukan untuk ini.

  void TriangulateWithRiverBeginOrEnd ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … if (!cell.IsUnderwater) { bool reversed = cell.HasIncomingRiver; Vector3 indices; indices.x = indices.y = indices.z = cell.Index; TriangulateRiverQuad( m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY, 0.6f, reversed, indices ); center.y = m.v2.y = m.v4.y = cell.RiverSurfaceY; rivers.AddTriangle(center, m.v2, m.v4); … rivers.AddTriangleCellData(indices, weights1); } } 

Kami sudah memiliki indeks sel ini TriangulateWithRiver, jadi kami hanya meneruskannya saat menelepon TriangulateRiverQuad.

  void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … if (!cell.IsUnderwater) { bool reversed = cell.IncomingRiver == direction; TriangulateRiverQuad( centerL, centerR, m.v2, m.v4, cell.RiverSurfaceY, 0.4f, reversed, indices ); TriangulateRiverQuad( m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY, 0.6f, reversed, indices ); } } 

Kami juga menambahkan dukungan indeks ke air terjun yang mengalir ke air yang dalam.

  void TriangulateWaterfallInWater ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y1, float y2, float waterY, Vector3 indices ) { … rivers.AddQuadCellData(indices, weights1, weights2); } 

Dan akhirnya, ubahlah TriangulateConnectionsehingga melewati indeks yang diperlukan untuk metode sungai dan air terjun.

  void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { … if (hasRiver) { e2.v3.y = neighbor.StreamBedY; Vector3 indices; indices.x = indices.z = cell.Index; indices.y = neighbor.Index; if (!cell.IsUnderwater) { if (!neighbor.IsUnderwater) { TriangulateRiverQuad( e1.v2, e1.v4, e2.v2, e2.v4, cell.RiverSurfaceY, neighbor.RiverSurfaceY, 0.8f, cell.HasIncomingRiver && cell.IncomingRiver == direction, indices ); } else if (cell.Elevation > neighbor.WaterLevel) { TriangulateWaterfallInWater( e1.v2, e1.v4, e2.v2, e2.v4, cell.RiverSurfaceY, neighbor.RiverSurfaceY, neighbor.WaterSurfaceY, indices ); } } else if ( !neighbor.IsUnderwater && neighbor.Elevation > cell.WaterLevel ) { TriangulateWaterfallInWater( e2.v4, e2.v2, e1.v4, e1.v2, neighbor.RiverSurfaceY, cell.RiverSurfaceY, cell.WaterSurfaceY, indices ); } } … } 

Shader Sungai perlu membuat perubahan yang sama dengan Road shader .

  #pragma surface surf Standard alpha vertex:vert #pragma target 3.0 #include "Water.cginc" #include "HexCellData.cginc" sampler2D _MainTex; struct Input { float2 uv_MainTex; float visibility; }; … void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); float4 cell0 = GetCellData(v, 0); float4 cell1 = GetCellData(v, 1); data.visibility = cell0.x * v.color.x + cell1.x * v.color.y; data.visibility = lerp(0.25, 1, data.visibility); } void surf (Input IN, inout SurfaceOutputStandard o) { float river = River(IN.uv_MainTex, _MainTex); fixed4 c = saturate(_Color + river); o.Albedo = c.rgb * IN.visibility; … } 


Sungai dengan visibilitas.

paket unity

Objek dan Visibilitas


Sekarang visibilitas berfungsi untuk seluruh medan yang dihasilkan secara prosedural, tetapi sejauh ini tidak memengaruhi fitur medan. Bangunan, pertanian, dan pohon dibuat dari prefab, dan bukan dari geometri prosedural, jadi kami tidak dapat menambahkan indeks sel dan mencampur bobot dengan simpulnya. Karena masing-masing objek ini hanya dimiliki oleh satu sel, kita perlu menentukan di mana sel mereka berada. Jika kita bisa melakukan ini, maka kita akan mendapatkan akses ke data sel yang sesuai dan menerapkan visibilitas.

Kita sudah bisa mengubah posisi XZ dunia menjadi indeks sel. Transformasi ini digunakan untuk mengedit medan dan mengelola regu. Namun, kode yang sesuai adalah nontrivial. Ia menggunakan operasi integer dan membutuhkan logika untuk bekerja dengan edge. Ini tidak praktis untuk shader, jadi kita dapat memanggang sebagian besar logika dalam tekstur dan menggunakannya.

Kami sudah menggunakan tekstur dengan pola heksagonal untuk memproyeksikan kisi di atas topografi. Tekstur ini mendefinisikan area sel 2 × 2. Karena itu, kita dapat dengan mudah menghitung di area mana kita berada. Setelah itu, Anda bisa menerapkan tekstur yang mengandung offset X dan Z untuk sel di area ini dan menggunakan data ini untuk menghitung sel tempat kami berada.

Berikut adalah tekstur yang serupa. X offset disimpan di saluran merahnya, dan offset Z disimpan di saluran hijau. Karena mencakup area sel 2 × 2, kita perlu offset dari 0 dan 2. Data tersebut tidak dapat disimpan dalam saluran warna, sehingga offset dikurangi setengahnya. Kita tidak membutuhkan tepi sel yang jelas, jadi tekstur kecil sudah cukup.


Tekstur kisi koordinat.

Tambahkan tekstur ke proyek. Atur Wrap Mode-nya ke Repeat , sama seperti tekstur mesh lainnya. Kami tidak membutuhkan pencampuran, jadi untuk Blend Mode kami akan memilih Point . Matikan juga Kompresi agar data tidak terdistorsi. Matikan mode sRGB sehingga ketika merender dalam mode linear, tidak ada konversi ruang warna yang dilakukan. Dan akhirnya, kita tidak perlu tekstur mip.


Opsi impor tekstur.

Obyek Shader dengan Visibilitas


Buat shader Fitur baru untuk menambahkan dukungan visibilitas ke objek. Ini adalah shader permukaan sederhana dengan program titik. Tambahkan HexCellData padanya dan berikan indikator visibilitas ke program fragmen, dan seperti biasa, pertimbangkan dalam warna. Perbedaannya di sini adalah bahwa kita tidak dapat menggunakannya GetCellDatakarena data mesh yang diperlukan tidak ada. Sebaliknya, kita memiliki posisi di dunia. Tetapi untuk sekarang, biarkan visibilitas sama dengan 1.

 Shader "Custom/Feature" { 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 [NoTilingOffset] _GridCoordinates ("Grid Coordinates", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Standard fullforwardshadows vertex:vert #pragma target 3.0 #include "../HexCellData.cginc" sampler2D _MainTex, _GridCoordinates; half _Glossiness; half _Metallic; fixed4 _Color; struct Input { float2 uv_MainTex; float visibility; }; void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); float3 pos = mul(unity_ObjectToWorld, v.vertex); data.visibility = 1; } void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb * IN.visibility; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } ENDCG } FallBack "Diffuse" } 

Ubah semua bahan objek sehingga mereka menggunakan shader baru dan berikan mereka tekstur koordinat grid.


Perkotaan dengan tekstur jala.

Akses data sel


Untuk sampel tekstur koordinat grid dalam program vertex, kita lagi membutuhkan tex2Dlodvektor koordinat tekstur empat komponen. Dua koordinat pertama adalah posisi dunia XZ. Dua lainnya sama dengan nol seperti sebelumnya.

  void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); float3 pos = mul(unity_ObjectToWorld, v.vertex); float4 gridUV = float4(pos.xz, 0, 0); data.visibility = 1; } 

Seperti pada Terrain shader , kami meregangkan koordinat UV sehingga tekstur memiliki rasio aspek yang benar sesuai dengan kisi segi enam.

  float4 gridUV = float4(pos.xz, 0, 0); gridUV.x *= 1 / (4 * 8.66025404); gridUV.y *= 1 / (2 * 15.0); 

Kita dapat mengetahui di bagian mana sel 2 × 2 kita berada dengan mengambil nilai koordinat UV dibulatkan ke bawah. Ini membentuk dasar untuk koordinat sel.

  float4 gridUV = float4(pos.xz, 0, 0); gridUV.x *= 1 / (4 * 8.66025404); gridUV.y *= 1 / (2 * 15.0); float2 cellDataCoordinates = floor(gridUV.xy); 

Untuk menemukan koordinat sel tempat kami berada, kami menambahkan perpindahan yang disimpan dalam tekstur.

  float2 cellDataCoordinates = floor(gridUV.xy) + tex2Dlod(_GridCoordinates, gridUV).rg; 

Karena bagian dari kisi berukuran 2 × 2, dan offset dibagi dua, kita perlu menggandakan hasilnya untuk mendapatkan koordinat akhir.

  float2 cellDataCoordinates = floor(gridUV.xy) + tex2Dlod(_GridCoordinates, gridUV).rg; cellDataCoordinates *= 2; 

Sekarang kita memiliki koordinat XZ dari kisi sel yang perlu kita konversi menjadi koordinat UV sel-sel ini. Ini dapat dilakukan dengan hanya berpindah ke pusat piksel dan kemudian membaginya menjadi ukuran tekstur. Jadi mari kita tambahkan fungsi untuk ini ke file HexCellData termasuk yang juga akan menangani pengambilan sampel.

 float4 GetCellData (float2 cellDataCoordinates) { float2 uv = cellDataCoordinates + 0.5; uv.x *= _HexCellData_TexelSize.x; uv.y *= _HexCellData_TexelSize.y; return tex2Dlod(_HexCellData, float4(uv, 0, 0)); } 

Sekarang kita dapat menggunakan ini dalam program vertex shader Fitur yang .

  cellDataCoordinates *= 2; data.visibility = GetCellData(cellDataCoordinates).x; data.visibility = lerp(0.25, 1, data.visibility); 


Objek dengan visibilitas.

Akhirnya, visibilitas mempengaruhi seluruh peta, dengan pengecualian unit yang selalu terlihat. Karena kita menentukan visibilitas objek untuk setiap verteks, maka untuk objek yang melintasi batas sel, visibilitas sel yang ditutup akan bercampur. Tetapi objek sangat kecil sehingga mereka terus-menerus tetap di dalam sel mereka, bahkan dengan mempertimbangkan distorsi posisi. Namun, beberapa mungkin merupakan bagian dari simpul di sel lain. Karena itu, pendekatan kami murah, tetapi tidak sempurna. Ini paling terlihat dalam kasus dinding, visibilitasnya bervariasi antara visibilitas dari sel tetangga.


Dinding dengan perubahan visibilitas.

Karena segmen dinding dihasilkan secara prosedural, kita dapat menambahkan data sel ke mesh mereka dan menggunakan pendekatan yang kita gunakan untuk bantuan. Sayangnya, menara adalah cetakan, jadi kami masih akan memiliki inkonsistensi. Secara umum, pendekatan yang ada terlihat cukup baik untuk geometri sederhana yang kami gunakan. Di masa depan, kami akan mempertimbangkan model dan dinding yang lebih rinci, oleh karena itu, kami akan meningkatkan metode pencampuran visibilitas mereka.

paket unity

Bagian 21: penelitian peta


  • Kami menampilkan semuanya selama mengedit.
  • Kami melacak sel yang diselidiki.
  • Kami menyembunyikan apa yang masih belum diketahui.
  • Kami memaksa unit untuk menghindari area yang belum dijelajahi.

Pada bagian sebelumnya, kami menambahkan kabut perang, yang sekarang akan kami sempurnakan untuk mengimplementasikan penelitian peta.


Kami siap menjelajah dunia.

Tampilkan seluruh peta dalam mode edit


Arti dari penelitian ini adalah bahwa sampai sel-sel tidak terlihat dianggap tidak diketahui, dan karena itu tidak terlihat. Mereka seharusnya tidak dikaburkan, tetapi tidak ditampilkan sama sekali. Karena itu, sebelum menambahkan dukungan penelitian, kami akan mengaktifkan visibilitas dalam mode edit.

Pengalihan Visibilitas


Kami dapat mengontrol apakah shader menggunakan visibilitas menggunakan kata kunci, seperti yang dilakukan pada hamparan di grid. Mari kita gunakan kata kunci HEX_MAP_EDIT_MODE untuk menunjukkan status mode pengeditan. Karena beberapa shader harus tahu tentang kata kunci ini, kami akan mendefinisikannya secara global menggunakan metode statis Shader.EnableKeyWorddan Shader.DisableKeyword. Kami akan memanggil metode yang sesuai HexGameUI.SetEditModeketika mengubah mode pengeditan.

  public void SetEditMode (bool toggle) { enabled = !toggle; grid.ShowUI(!toggle); grid.ClearPath(); if (toggle) { Shader.EnableKeyword("HEX_MAP_EDIT_MODE"); } else { Shader.DisableKeyword("HEX_MAP_EDIT_MODE"); } } 

Edit mode shader


Ketika HEX_MAP_EDIT_MODE didefinisikan, shader akan mengabaikan visibilitas. Ini bermuara pada kenyataan bahwa visibilitas sel akan selalu dianggap sama dengan 1. Mari kita tambahkan fungsi untuk memfilter data sel tergantung pada kata kunci di awal file-file HexCellData .

 sampler2D _HexCellData; float4 _HexCellData_TexelSize; float4 FilterCellData (float4 data) { #if defined(HEX_MAP_EDIT_MODE) data.x = 1; #endif return data; } 

Kami melewati fungsi ini hasil dari kedua fungsi GetCellDatasebelum mengembalikannya.

 float4 GetCellData (appdata_full v, int index) { … return FilterCellData(data); } float4 GetCellData (float2 cellDataCoordinates) { … return FilterCellData(tex2Dlod(_HexCellData, float4(uv, 0, 0))); } 

Agar semuanya berfungsi, semua shader yang relevan harus menerima arahan multi_compile untuk membuat opsi seandainya kata kunci HEX_MAP_EDIT_MODE ditentukan. Tambahkan garis yang sesuai ke Estuary shaders , Feature , River , Road , Terrain , Water dan Water Shore , antara arahan target dan yang pertama termasuk arahan.

  #pragma multi_compile _ HEX_MAP_EDIT_MODE 

Sekarang, ketika beralih ke mode edit peta, kabut perang akan hilang.

paket unity

Penelitian sel


Secara default, sel harus dianggap belum dijelajahi. Mereka menjadi dieksplorasi ketika sebuah regu melihat mereka. Setelah itu, mereka terus tetap diselidiki jika detasemen dapat melihat mereka.

Melacak Status Studi


Untuk menambahkan dukungan untuk memantau status studi, kami menambah HexCellproperti umum IsExplored.

  public bool IsExplored { get; set; } 

Keadaan penelitian ditentukan oleh sel itu sendiri. Karena itu, properti ini hanya boleh disetel HexCell. Untuk menambahkan batasan ini, kami akan menetapkan setter pribadi.

  public bool IsExplored { get; private set; } 

Pertama kali visibilitas sel menjadi lebih besar dari nol, sel mulai dianggap diselidiki, dan karenanya IsExplorednilai harus ditetapkan true. Bahkan, cukup bagi kita untuk hanya menandai sel saat diperiksa ketika visibilitas meningkat menjadi 1. Ini harus dilakukan sebelum panggilan RefreshVisibility.

  public void IncreaseVisibility () { visibility += 1; if (visibility == 1) { IsExplored = true; ShaderData.RefreshVisibility(this); } } 

Mentransfer status penelitian ke shader


Seperti dalam kasus dengan visibilitas sel, kami mentransfer status penelitian mereka ke shader melalui data shader. Pada akhirnya, itu hanyalah jenis visibilitas lainnya. HexCellShaderData.RefreshVisibilitymenyimpan status visibilitas dalam saluran data R. Mari kita jaga keadaan penelitian di saluran G data.

  public void RefreshVisibility (HexCell cell) { int index = cell.Index; cellTextureData[index].r = cell.IsVisible ? (byte)255 : (byte)0; cellTextureData[index].g = cell.IsExplored ? (byte)255 : (byte)0; enabled = true; } 

Relief hitam yang belum dijelajahi


Sekarang kita dapat menggunakan shader untuk memvisualisasikan keadaan penelitian sel. Untuk memastikan semuanya berjalan sebagaimana mestinya, kami hanya membuat medan yang tidak dijelajahi menjadi hitam. Tetapi pertama-tama, untuk membuat mode pengeditan berfungsi, ubahlah FilterCellDatasehingga memfilter data penelitian.

 float4 FilterCellData (float4 data) { #if defined(HEX_MAP_EDIT_MODE) data.xy = 1; #endif return data; } 

Terrain shader meneruskan data visibilitas ketiga sel yang mungkin ke program fragmen. Dalam kasus keadaan penelitian, kami menggabungkan mereka dalam program vertex dan mentransfer satu-satunya nilai ke program fragmen. Tambahkan visibilitykomponen keempat ke input sehingga kita punya tempat untuk ini.

  struct Input { float4 color : COLOR; float3 worldPos; float3 terrain; float4 visibility; }; 

Sekarang, dalam program vertex, ketika kita mengubah indeks visibilitas, kita harus secara eksplisit mengakses data.visibility.xyz.

  void vert (inout appdata_full v, out Input data) { … data.visibility.xyz = lerp(0.25, 1, data.visibility.xyz); } 

Setelah itu, kami menggabungkan keadaan penelitian dan menulis hasilnya data.visibility.w. Ini mirip dengan menggabungkan visibilitas dalam shader lain, tetapi menggunakan komponen Y dari sel-sel ini.

  data.visibility.xyz = lerp(0.25, 1, data.visibility.xyz); data.visibility.w = cell0.y * v.color.x + cell1.y * v.color.y + cell2.y * v.color.z; 

Status penelitian sekarang tersedia dalam program fragmen melalui IN.visibility.w. Pertimbangkan itu dalam perhitungan albedo.

  void surf (Input IN, inout SurfaceOutputStandard o) { … float explored = IN.visibility.w; o.Albedo = c.rgb * grid * _Color * explored; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } 


Topografi yang belum dijelajahi sekarang berwarna hitam.

Relief sel yang belum dijelajahi sekarang memiliki warna hitam. Tapi ini belum mempengaruhi benda, jalan, dan air. Namun, ini cukup untuk memastikan bahwa penelitian ini berhasil.

Menyimpan dan memuat status penelitian


Sekarang kami telah menambahkan dukungan penelitian, kami perlu memastikan bahwa status penelitian diperhitungkan saat menyimpan dan memuat peta. Karena itu, kita perlu meningkatkan versi file peta menjadi 3. Untuk membuat perubahan ini lebih nyaman, mari kita tambahkan SaveLoadMenukonstanta untuk ini .

  const int mapFileVersion = 3; 

Kami akan menggunakan konstanta ini ketika menulis versi file ke Savedan ketika memeriksa dukungan file di Load.

  void Save (string path) { using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(mapFileVersion); hexGrid.Save(writer); } } void Load (string path) { if (!File.Exists(path)) { Debug.LogError("File does not exist " + path); return; } using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { int header = reader.ReadInt32(); if (header <= mapFileVersion) { hexGrid.Load(reader, header); HexMapCamera.ValidatePosition(); } else { Debug.LogWarning("Unknown map format " + header); } } } 

Sebagai langkah terakhir, HexCell.Savekami mencatat status penelitian.

  public void Save (BinaryWriter writer) { … writer.Write(IsExplored); } 

Dan kita akan membacanya di bagian akhir Load. Setelah itu, kami akan memanggil RefreshVisibilityjika keadaan penelitian berbeda dari yang sebelumnya.

  public void Load (BinaryReader reader) { … IsExplored = reader.ReadBoolean(); ShaderData.RefreshVisibility(this); } 

Untuk menjaga kompatibilitas ke belakang dengan file penyimpanan lama, kita harus melewati membaca status penyimpanan jika versi file kurang dari 3. Dalam kasus ini, secara default, sel-sel akan memiliki status "belum dijelajahi". Untuk melakukan ini, kita perlu menambahkan Loaddata header sebagai parameter .

  public void Load (BinaryReader reader, int header) { … IsExplored = header >= 3 ? reader.ReadBoolean() : false; ShaderData.RefreshVisibility(this); } 

Sekarang HexGrid.Loadharus melewati HexCell.Loaddata header.

  public void Load (BinaryReader reader, int header) { … for (int i = 0; i < cells.Length; i++) { cells[i].Load(reader, header); } … } 

Sekarang, saat menyimpan dan memuat peta, keadaan eksplorasi sel akan diperhitungkan.

paket unity

Sembunyikan sel yang tidak dikenal


Pada tahap saat ini, sel-sel yang belum dijelajahi secara visual diindikasikan oleh relief hitam. Namun pada kenyataannya, kami ingin sel-sel ini tidak terlihat karena tidak diketahui. Kita dapat membuat geometri buram transparan sehingga tidak terlihat. Namun, kerangka shader permukaan Unity dikembangkan tanpa mempertimbangkan kemungkinan ini. Alih-alih menggunakan transparansi sejati, kami akan mengubah shader agar sesuai dengan latar belakang, yang juga akan membuatnya tidak terlihat.

Membuat lega benar-benar hitam


Meskipun relief yang dipelajari berwarna hitam, kita masih bisa mengenalinya karena masih memiliki pencahayaan specular. Untuk menghilangkan pencahayaan, kita harus membuatnya hitam sempurna. Agar tidak mempengaruhi sifat permukaan lainnya, paling mudah untuk mengubah warna specular menjadi hitam. Ini dimungkinkan jika Anda menggunakan permukaan shader yang bekerja dengan specular, tapi sekarang kami menggunakan logam standar. Jadi mari kita mulai dengan mengganti Terrain shader ke specular.

Ganti properti warna _Metallic pada properti _Specular . Secara default, nilai warnanya harus sama dengan (0,2, 0,2, 0,2). Jadi kami menjamin bahwa itu akan cocok dengan penampilan versi logam.

  Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Terrain Texture Array", 2DArray) = "white" {} _GridTex ("Grid Texture", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 // _Metallic ("Metallic", Range(0,1)) = 0.0 _Specular ("Specular", Color) = (0.2, 0.2, 0.2) } 

Juga ubah variabel shader yang sesuai. Warna permukaan specular shader didefinisikan sebagai fixed3, jadi mari kita gunakan.

  half _Glossiness; // half _Metallic; fixed3 _Specular; fixed4 _Color; 

Ubah permukaan pragma surf dari Standard ke StandardSpecular . Ini akan memaksa Unity untuk menghasilkan shader menggunakan specular.

  #pragma surface surf StandardSpecular fullforwardshadows vertex:vert 

Sekarang fungsi tersebut surfmembutuhkan parameter kedua bertipe SurfaceOutputStandardSpecular. Selain itu, sekarang Anda harus menetapkan nilai bukan o.Metallic, tetapi o.Specular.

  void surf (Input IN, inout SurfaceOutputStandardSpecular o) { … float explored = IN.visibility.w; o.Albedo = c.rgb * grid * _Color * explored; // o.Metallic = _Metallic; o.Specular = _Specular; o.Smoothness = _Glossiness; o.Alpha = ca; } 

Sekarang kita dapat mengaburkan highlight dengan mempertimbangkan exploredwarna specular.

  o.Specular = _Specular * explored; 


Medan yang belum dijelajahi tanpa pencahayaan yang dipantulkan.

Seperti yang dapat Anda lihat dalam gambar, sekarang relief yang belum dijelajahi tampak hitam pudar. Namun, ketika dilihat pada sudut singgung, permukaan berubah menjadi cermin, karena itu relief mulai mencerminkan lingkungan, yaitu skybox.

Mengapa permukaan menjadi cermin?
. . Rendering .


Area yang belum dijelajahi masih mencerminkan lingkungan.

Untuk menghilangkan refleksi ini, kami akan mempertimbangkan bantuan yang belum dijelajahi sepenuhnya teduh. Ini dilakukan dengan menetapkan nilai ke exploredparameter oklusi, yang kami gunakan sebagai topeng refleksi.

  float explored = IN.visibility.w; o.Albedo = c.rgb * grid * _Color * explored; o.Specular = _Specular * explored; o.Smoothness = _Glossiness; o.Occlusion = explored; o.Alpha = ca; 


Belum dijelajahi tanpa refleksi.

Latar Belakang Pencocokan


Sekarang karena medan yang belum dijelajahi mengabaikan semua pencahayaan, Anda harus menyesuaikannya dengan latar belakang. Karena kamera kami selalu terlihat dari atas, latar belakang selalu abu-abu. Untuk memberi tahu shader Terrain warna yang akan digunakan, tambahkan properti _BackgroundColor , yang defaultnya menjadi hitam.

  Properties { … _BackgroundColor ("Background Color", Color) = (0,0,0) } … half _Glossiness; fixed3 _Specular; fixed4 _Color; half3 _BackgroundColor; 

Untuk menggunakan warna ini, kami akan menambahkannya sebagai cahaya memancarkan. Ini o.Emissiondilakukan dengan menetapkan nilai warna latar belakang dikalikan dengan satu dikurangi dieksplorasi.

  o.Occlusion = explored; o.Emission = _BackgroundColor * (1 - explored); 

Karena kami menggunakan skybox default, warna latar belakang yang terlihat sebenarnya tidak sama. Secara umum, abu-abu yang sedikit kemerahan akan menjadi warna terbaik. Saat menyiapkan bahan bantuan, Anda dapat menggunakan kode 68615BFF untuk Hex Color .


Bahan bantuan dengan warna latar belakang abu-abu.

Secara umum, ini berhasil, meskipun jika Anda tahu ke mana harus mencari, Anda akan melihat siluet yang sangat lemah. Agar pemain tidak dapat melihatnya, Anda dapat menetapkan warna latar belakang seragam 68615BFF ke kamera alih-alih skybox.


Kamera dengan warna latar yang seragam.

Mengapa tidak menghapus skybox?
, , environmental lighting . , .

Sekarang kita tidak dapat menemukan perbedaan antara latar belakang dan sel yang belum dijelajahi. Topografi tinggi yang belum dijelajahi masih dapat mengaburkan topografi yang dieksplorasi rendah pada sudut kamera rendah. Selain itu, bagian yang belum dijelajahi masih memberikan bayangan pada yang dieksplorasi. Tapi petunjuk minimal ini bisa diabaikan.


Sel yang belum dijelajahi tidak lagi terlihat.

Bagaimana jika Anda tidak menggunakan warna latar belakang yang seragam?
, , . . , . , , , UV- .

Sembunyikan benda bantuan


Sekarang kami hanya memiliki lubang bantuan yang tersembunyi. Sisa keadaan penelitian belum terpengaruh.


Sejauh ini, hanya bantuan yang disembunyikan.

Mari kita mengubah shader Fitur , yang merupakan shader buram seperti Terrain . Ubah itu menjadi shader specular dan tambahkan warna latar belakang ke dalamnya. Mari kita mulai dengan propertinya.

  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 _Specular ("Specular", Color) = (0.2, 0.2, 0.2) _BackgroundColor ("Background Color", Color) = (0,0,0) [NoScaleOffset] _GridCoordinates ("Grid Coordinates", 2D) = "white" {} } 

Lebih lanjut permukaan dan variabel pragma, seperti sebelumnya.

  #pragma surface surf StandardSpecular fullforwardshadows vertex:vert … half _Glossiness; // half _Metallic; fixed3 _Specular; fixed4 _Color; half3 _BackgroundColor; 

visibilitysatu komponen lagi juga diperlukan. Karena Fitur menggabungkan visibilitas untuk setiap titik, itu hanya membutuhkan satu nilai float. Sekarang kita butuh dua.

  struct Input { float2 uv_MainTex; float2 visibility; }; 

Ubah vertsehingga digunakan secara eksplisit untuk data visibilitas data.visibility.x, dan kemudian tetapkan data.visibility.ynilai data penelitian.

  void vert (inout appdata_full v, out Input data) { … float4 cellData = GetCellData(cellDataCoordinates); data.visibility.x = cellData.x; data.visibility.x = lerp(0.25, 1, data.visibility.x); data.visibility.y = cellData.y; } 

Ubah surfsehingga menggunakan data baru, seperti Terrain .

  void surf (Input IN, inout SurfaceOutputStandardSpecular o) { fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color; float explored = IN.visibility.y; o.Albedo = c.rgb * (IN.visibility.x * explored); // o.Metallic = _Metallic; o.Specular = _Specular * explored; o.Smoothness = _Glossiness; o.Occlusion = explored; o.Emission = _BackgroundColor * (1 - explored); o.Alpha = ca; } 


Benda bantuan tersembunyi.

Sembunyikan air


Selanjutnya adalah Water and Water Shore shaders . Mari kita mulai dengan mengubahnya menjadi specular shaders. Namun, mereka tidak membutuhkan warna latar belakang karena mereka adalah shader transparan.

Setelah konversi, tambahkan visibilitysatu komponen lagi dan ubah sesuai vert. Kedua shader menggabungkan data dari tiga sel.

  struct Input { … float2 visibility; }; … void vert (inout appdata_full v, out Input data) { … data.visibility.x = cell0.x * v.color.x + cell1.x * v.color.y + cell2.x * v.color.z; data.visibility.x = lerp(0.25, 1, data.visibility.x); data.visibility.y = cell0.y * v.color.x + cell1.y * v.color.y + cell2.y * v.color.z; } 

Air dan Pesisir Air melakukan surfoperasi yang berbeda, tetapi mengatur sifat permukaannya dengan cara yang sama. Karena transparan, kami akan mempertimbangkan exploresaluran alpha, dan kami tidak akan menetapkan emisi.

  void surf (Input IN, inout SurfaceOutputStandardSpecular o) { … float explored = IN.visibility.y; o.Albedo = c.rgb * IN.visibility.x; o.Specular = _Specular * explored; o.Smoothness = _Glossiness; o.Occlusion = explored; o.Alpha = ca * explored; } 


Air yang tersembunyi.

Kami menyembunyikan muara, sungai, dan jalan


Kami masih memiliki shader untuk Muara , Sungai, dan Jalan . Ketiganya transparan dan menggabungkan data dua sel. Alihkan semuanya ke specular, dan kemudian tambahkan ke visibilitydata penelitian.

  struct Input { … float2 visibility; }; … void vert (inout appdata_full v, out Input data) { … data.visibility.x = cell0.x * v.color.x + cell1.x * v.color.y; data.visibility.x = lerp(0.25, 1, data.visibility.x); data.visibility.y = cell0.y * v.color.x + cell1.y * v.color.y; } 

Ubah fungsi Estuary dan Riversurf shaders sehingga menggunakan data baru. Keduanya perlu melakukan perubahan yang sama.

  void surf (Input IN, inout SurfaceOutputStandardSpecular o) { … float explored = IN.visibility.y; fixed4 c = saturate(_Color + water); o.Albedo = c.rgb * IN.visibility.x; o.Specular = _Specular * explored; o.Smoothness = _Glossiness; o.Occlusion = explored; o.Alpha = ca * explored; } 

Shader Jalan sedikit berbeda karena menggunakan indikator tambahan pencampuran.

  void surf (Input IN, inout SurfaceOutputStandardSpecular o) { float4 noise = tex2D(_MainTex, IN.worldPos.xz * 0.025); fixed4 c = _Color * ((noise.y * 0.75 + 0.25) * IN.visibility.x); float blend = IN.uv_MainTex.x; blend *= noise.x + 0.5; blend = smoothstep(0.4, 0.7, blend); float explored = IN.visibility.y; o.Albedo = c.rgb; o.Specular = _Specular * explored; o.Smoothness = _Glossiness; o.Occlusion = explored; o.Alpha = blend * explored; } 


Semuanya tersembunyi.

paket unity

Menghindari Sel yang Belum Dieksplorasi


Meskipun segala sesuatu yang tidak diketahui secara visual disembunyikan, sedangkan keadaan penelitian tidak diperhitungkan saat mencari jalan. Akibatnya, unit dapat dipesan untuk bergerak melalui dan melalui sel yang belum dijelajahi, secara ajaib menentukan cara untuk bergerak. Kita perlu memaksa unit untuk menghindari sel yang belum dijelajahi.


Menavigasi sel yang belum dijelajahi.

Pasukan menentukan biaya bergerak


Sebelum menangani sel yang belum dijelajahi, mari kita ulangi kode untuk mentransfer biaya pindah dari HexGridke HexUnit. Ini akan menyederhanakan dukungan untuk unit dengan aturan pergerakan yang berbeda.

Tambahkan ke HexUnitmetode umum GetMoveCostuntuk menentukan biaya pemindahan. Dia perlu tahu sel mana yang bergerak di antara mereka, serta arahnya. Kami menyalin kode yang sesuai untuk biaya pindah dari HexGrid.Searchke metode ini dan mengubah nama variabel.

  public int GetMoveCost ( HexCell fromCell, HexCell toCell, HexDirection direction) { HexEdgeType edgeType = fromCell.GetEdgeType(toCell); if (edgeType == HexEdgeType.Cliff) { continue; } int moveCost; if (fromCell.HasRoadThroughEdge(direction)) { moveCost = 1; } else if (fromCell.Walled != toCell.Walled) { continue; } else { moveCost = edgeType == HexEdgeType.Flat ? 5 : 10; moveCost += toCell.UrbanLevel + toCell.FarmLevel + toCell.PlantLevel; } } 

Metode harus mengembalikan biaya pemindahan. Saya menggunakan kode lama untuk melewati gerakan yang tidak valid continue, tetapi pendekatan ini tidak akan berfungsi di sini. Jika pergerakan tidak memungkinkan, maka kami akan mengembalikan biaya bergerak negatif.

  public int GetMoveCost ( HexCell fromCell, HexCell toCell, HexDirection direction) { HexEdgeType edgeType = fromCell.GetEdgeType(toCell); if (edgeType == HexEdgeType.Cliff) { return -1; } int moveCost; if (fromCell.HasRoadThroughEdge(direction)) { moveCost = 1; } else if (fromCell.Walled != toCell.Walled) { return -1; } else { moveCost = edgeType == HexEdgeType.Flat ? 5 : 10; moveCost += toCell.UrbanLevel + toCell.FarmLevel + toCell.PlantLevel; } return moveCost; } 

Sekarang kita perlu tahu kapan menemukan jalan, tidak hanya kecepatan, tetapi juga unit yang dipilih. Ubah sesuai HexGameUI.DoPathFinding.

  void DoPathfinding () { if (UpdateCurrentCell()) { if (currentCell && selectedUnit.IsValidDestination(currentCell)) { grid.FindPath(selectedUnit.Location, currentCell, selectedUnit); } else { grid.ClearPath(); } } } 

Karena kami masih membutuhkan akses ke kecepatan regu, kami akan menambah ke HexUnitproperti Speed. Sementara itu akan mengembalikan nilai konstan 24.

  public int Speed { get { return 24; } } 

Dalam HexGridperubahan, FindPathdan Searchagar mereka dapat bekerja dengan pendekatan baru kami.

  public void FindPath (HexCell fromCell, HexCell toCell, HexUnit unit) { ClearPath(); currentPathFrom = fromCell; currentPathTo = toCell; currentPathExists = Search(fromCell, toCell, unit); ShowPath(unit.Speed); } bool Search (HexCell fromCell, HexCell toCell, HexUnit unit) { int speed = unit.Speed; … } 

Sekarang kita akan menghapus dari Searchkode lama yang menentukan apakah mungkin untuk pindah ke sel berikutnya dan berapa biaya untuk pindah. Sebaliknya, kami akan menelepon HexUnit.IsValidDestinationdan HexUnit.GetMoveCost. Kami akan melewati sel jika biaya bergerak negatif.

  for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if ( neighbor == null || neighbor.SearchPhase > searchFrontierPhase ) { continue; } // if (neighbor.IsUnderwater || neighbor.Unit) { // continue; // } // HexEdgeType edgeType = current.GetEdgeType(neighbor); // if (edgeType == HexEdgeType.Cliff) { // continue; // } // int moveCost; // if (current.HasRoadThroughEdge(d)) { // moveCost = 1; // } // else if (current.Walled != neighbor.Walled) { // continue; // } // else { // moveCost = edgeType == HexEdgeType.Flat ? 5 : 10; // moveCost += neighbor.UrbanLevel + neighbor.FarmLevel + // neighbor.PlantLevel; // } if (!unit.IsValidDestination(neighbor)) { continue; } int moveCost = unit.GetMoveCost(current, neighbor, d); if (moveCost < 0) { continue; } int distance = current.Distance + moveCost; int turn = (distance - 1) / speed; if (turn > currentTurn) { distance = turn * speed + moveCost; } … } 

Lewati area yang belum dijelajahi


Untuk menghindari sel yang belum dijelajahi, cukup bagi kami untuk memastikan bahwa kami HexUnit.IsValidDestinationmemeriksa apakah sel tersebut diperiksa.

  public bool IsValidDestination (HexCell cell) { return cell.IsExplored && !cell.IsUnderwater && !cell.Unit; } 


Lebih banyak unit tidak akan dapat membuka sel yang belum dijelajahi.

Karena sel yang belum dijelajahi tidak lagi merupakan titik akhir yang valid, regu akan menghindarinya ketika pindah ke titik akhir. Artinya, area yang belum dijelajahi bertindak sebagai penghalang yang memperpanjang jalan atau bahkan membuatnya tidak mungkin. Kami harus membawa unit lebih dekat ke medan yang tidak diketahui untuk menjelajahi area terlebih dahulu.

Bagaimana jika jalur yang lebih pendek muncul selama perpindahan?
. , . .

, , . , .

paket unity

Bagian 22: Peningkatan Visibilitas


  • Ubah visibilitas dengan lancar.
  • Gunakan ketinggian sel untuk menentukan ruang lingkup.
  • Sembunyikan tepi peta.

Dengan menambahkan dukungan untuk eksplorasi peta, kami akan meningkatkan perhitungan dan transisi dari ruang lingkup.


Untuk melihat lebih jauh, naik lebih tinggi.

Transisi Visibilitas


Sel itu terlihat atau tidak terlihat, karena ia berada dalam ruang lingkup detasemen atau tidak. Sekalipun unit itu membutuhkan waktu untuk bergerak di antara sel, bidang pandangnya melompat dari sel ke sel secara instan. Akibatnya, visibilitas sel-sel di sekitarnya berubah secara dramatis. Pergerakan pasukan tampak halus, tetapi perubahan visibilitas tiba-tiba.

Idealnya, visibilitas juga harus berubah dengan lancar. Begitu berada di bidang visibilitas, sel-sel harus diterangi secara bertahap, dan meninggalkannya, secara bertahap menjadi gelap. Atau mungkin Anda lebih suka transisi instan? Mari kita tambahkan ke HexCellShaderDataproperti yang beralih transisi instan. Secara default, transisi akan mulus.

  public bool ImmediateMode { get; set; } 

Pelacakan Sel Transisi


Bahkan ketika menampilkan transisi yang halus, data visibilitas sebenarnya masih tetap biner, yaitu, efeknya hanya visual. Ini berarti bahwa transisi visibilitas harus ditangani HexCellShaderData. Kami akan memberikan daftar sel di mana transisi dilakukan. Pastikan bahwa pada setiap inisialisasi itu kosong.

 using System.Collections.Generic; using UnityEngine; public class HexCellShaderData : MonoBehaviour { Texture2D cellTexture; Color32[] cellTextureData; List<HexCell> transitioningCells = new List<HexCell>(); public bool ImmediateMode { get; set; } public void Initialize (int x, int z) { … transitioningCells.Clear(); enabled = true; } … } 

Saat ini, kami sedang mengatur data sel RefreshVisibilitysecara langsung. Ini masih benar untuk mode transisi instan, tetapi ketika dinonaktifkan, kita harus menambahkan sel ke daftar sel transisi.

  public void RefreshVisibility (HexCell cell) { int index = cell.Index; if (ImmediateMode) { cellTextureData[index].r = cell.IsVisible ? (byte)255 : (byte)0; cellTextureData[index].g = cell.IsExplored ? (byte)255 : (byte)0; } else { transitioningCells.Add(cell); } enabled = true; } 

Visibilitas tampaknya tidak berfungsi lagi, karena untuk saat ini, kami tidak melakukan apa pun dengan sel dalam daftar.

Loop melalui sel dalam satu lingkaran


Alih-alih langsung mengatur nilai yang sesuai ke 255 atau 0, kami akan menambah / mengurangi nilai-nilai ini secara bertahap. Kelancaran transisi tergantung pada tingkat perubahan. Seharusnya tidak terlalu cepat dan tidak terlalu lambat. Kompromi yang baik antara transisi yang indah dan kenyamanan gim adalah berubah dalam satu detik. Mari kita tetapkan konstanta untuk ini agar lebih mudah diubah.

  const float transitionSpeed = 255f; 

Sekarang LateUpdatekita dapat mendefinisikan delta yang diterapkan pada nilai-nilai. Untuk melakukan ini, kalikan delta waktu dengan kecepatan. Itu harus bilangan bulat karena kita tidak tahu seberapa besar itu. Penurunan tajam dalam frame rate dapat membuat delta lebih dari 255.

Selain itu, kami perlu memperbarui sementara ada sel transisi. Karena itu, kode harus dimasukkan ketika ada sesuatu dalam daftar.

  void LateUpdate () { int delta = (int)(Time.deltaTime * transitionSpeed); cellTexture.SetPixels32(cellTextureData); cellTexture.Apply(); enabled = transitioningCells.Count > 0; } 

Secara teori, kemungkinan frame rate sangat tinggi. Dalam kombinasi dengan kecepatan transisi yang rendah, ini dapat memberi kita delta 0. Agar perubahan terjadi, kami memaksakan delta minimum menjadi 1.

  int delta = (int)(Time.deltaTime * transitionSpeed); if (delta == 0) { delta = 1; } 

Setelah menerima delta, kita dapat memutar semua sel transisi dan memperbarui datanya. Misalkan kita memiliki metode untuk ini UpdateCellData, parameternya adalah sel dan delta yang sesuai.

  int delta = (int)(Time.deltaTime * transitionSpeed); if (delta == 0) { delta = 1; } for (int i = 0; i < transitioningCells.Count; i++) { UpdateCellData(transitioningCells[i], delta); } 

Pada titik tertentu, transisi sel harus selesai. Asumsikan bahwa metode ini mengembalikan informasi tentang apakah transisi masih berlangsung. Ketika berhenti, kita dapat menghapus sel dari daftar. Setelah itu, kita harus mengurangi iterator agar tidak melewatkan sel.

  for (int i = 0; i < transitioningCells.Count; i++) { if (!UpdateCellData(transitioningCells[i], delta)) { transitioningCells.RemoveAt(i--); } } 

Urutan di mana sel transisi diproses tidak penting. Karenanya, kita tidak perlu menghapus sel pada indeks saat ini, yang akan memaksa RemoveAtsemua sel untuk bergerak setelahnya. Alih-alih, kami memindahkan sel terakhir ke indeks saat ini, lalu menghapus yang terakhir.

  if (!UpdateCellData(transitioningCells[i], delta)) { transitioningCells[i--] = transitioningCells[transitioningCells.Count - 1]; transitioningCells.RemoveAt(transitioningCells.Count - 1); } 

Sekarang kita harus membuat metode UpdateCellData. Untuk melakukan pekerjaannya, dia akan membutuhkan data indeks dan sel, jadi mari kita mulai dengan mendapatkannya. Itu juga harus menentukan apakah akan terus memperbarui sel. Secara default, kami akan menganggap bahwa itu tidak perlu. Setelah menyelesaikan pekerjaan, perlu untuk menerapkan data yang diubah dan mengembalikan status "pembaruan sedang berlangsung".

  bool UpdateCellData (HexCell cell, int delta) { int index = cell.Index; Color32 data = cellTextureData[index]; bool stillUpdating = false; cellTextureData[index] = data; return stillUpdating; } 

Memperbarui Data Sel


Pada tahap ini, kita memiliki sel yang sedang dalam proses transisi atau telah menyelesaikannya. Pertama, mari kita periksa status probe sel. Jika sel diperiksa, tetapi nilai G-nya belum sama dengan 255, maka ia sedang dalam proses transisi, jadi kami akan memantau ini.

  bool stillUpdating = false; if (cell.IsExplored && data.g < 255) { stillUpdating = true; } cellTextureData[index] = data; 

Untuk melakukan transisi, kami akan menambahkan delta ke nilai G sel. Operasi aritmatika tidak bekerja dengan byte, mereka pertama kali dikonversi ke integer. Oleh karena itu, penjumlahan akan memiliki format integer, yang harus dikonversi ke byte.

  if (cell.IsExplored && data.g < 255) { stillUpdating = true; int t = data.g + delta; data.g = (byte)t; } 

Tetapi sebelum konversi, Anda harus memastikan bahwa nilainya tidak melebihi 255.

  int t = data.g + delta; data.g = t >= 255 ? (byte)255 : (byte)t; 

Selanjutnya, kita perlu melakukan hal yang sama untuk visibilitas, yang menggunakan nilai R.

  if (cell.IsExplored && data.g < 255) { … } if (cell.IsVisible && data.r < 255) { stillUpdating = true; int t = data.r + delta; data.r = t >= 255 ? (byte)255 : (byte)t; } 

Karena sel dapat menjadi tidak terlihat lagi, kita perlu memeriksa apakah perlu untuk mengurangi nilai R. Ini terjadi ketika sel tidak terlihat, tetapi R lebih besar dari nol.

  if (cell.IsVisible) { if (data.r < 255) { stillUpdating = true; int t = data.r + delta; data.r = t >= 255 ? (byte)255 : (byte)t; } } else if (data.r > 0) { stillUpdating = true; int t = data.r - delta; data.r = t < 0 ? (byte)0 : (byte)t; } 

Sekarang sudah UpdateCellDatasiap dan transisi visibilitas dilakukan dengan benar.


Transisi Visibilitas.

Perlindungan terhadap elemen transisi duplikat


Transisi berfungsi, tetapi item duplikat mungkin muncul dalam daftar. Ini terjadi jika keadaan visibilitas sel berubah saat masih dalam transisi. Misalnya, ketika sel terlihat selama pergerakan skuad hanya untuk waktu yang singkat.

Sebagai hasil dari penampilan elemen yang digandakan, transisi sel diperbarui beberapa kali per frame, yang mengarah pada transisi yang lebih cepat dan kerja ekstra. Kami dapat mencegah ini dengan memeriksa sebelum menambahkan sel apakah sudah ada dalam daftar. Namun, pencarian daftar pada setiap panggilanRefreshVisibilitymahal, terutama ketika beberapa transisi sel dilakukan. Sebagai gantinya, mari kita gunakan saluran lain yang belum digunakan untuk menunjukkan apakah sel sedang dalam proses transisi, misalnya nilai B. Saat menambahkan sel ke daftar, kami akan menetapkan nilai 255, dan menambahkan hanya sel-sel yang nilainya tidak sama dengan 255.

  public void RefreshVisibility (HexCell cell) { int index = cell.Index; if (ImmediateMode) { cellTextureData[index].r = cell.IsVisible ? (byte)255 : (byte)0; cellTextureData[index].g = cell.IsExplored ? (byte)255 : (byte)0; } else if (cellTextureData[index].b != 255) { cellTextureData[index].b = 255; transitioningCells.Add(cell); } enabled = true; } 

Agar ini berfungsi, kita perlu mengatur ulang nilai B setelah sel transisi selesai.

  bool UpdateCellData (HexCell cell, int delta) { … if (!stillUpdating) { data.b = 0; } cellTextureData[index] = data; return stillUpdating; } 


Transisi tanpa duplikat.

Seketika memuat visibilitas


Perubahan visibilitas sekarang selalu bertahap, bahkan saat memuat peta. Ini tidak logis, karena peta menggambarkan keadaan di mana sel-sel sudah terlihat, sehingga transisi tidak sesuai di sini. Selain itu, melakukan transisi untuk banyak sel yang terlihat dari peta besar dapat memperlambat permainan setelah memuat. Karena itu, sebelum memuat sel dan regu, mari kita beralih HexGrid.Loadke mode transisi instan.

  public void Load (BinaryReader reader, int header) { … cellShaderData.ImmediateMode = true; for (int i = 0; i < cells.Length; i++) { cells[i].Load(reader, header); } … } 

Jadi kami mendefinisikan kembali pengaturan awal mode transisi instan, apa pun itu. Mungkin itu sudah dimatikan, atau membuat opsi konfigurasi, jadi kita akan mengingat mode awal dan akan beralih ke sana setelah selesai bekerja.

  public void Load (BinaryReader reader, int header) { … bool originalImmediateMode = cellShaderData.ImmediateMode; cellShaderData.ImmediateMode = true; … cellShaderData.ImmediateMode = originalImmediateMode; } 

paket unity

Lingkup tergantung tinggi


Sejauh ini kami telah menggunakan ruang lingkup tiga untuk semua unit, tetapi pada kenyataannya itu lebih rumit. Dalam kasus umum, kita tidak dapat melihat objek karena dua alasan: beberapa hambatan menghalangi kita untuk melihatnya, atau objek itu terlalu kecil atau jauh. Dalam permainan kami, kami hanya menerapkan batasan ruang lingkup.

Kita tidak dapat melihat apa yang ada di sisi berlawanan dari Bumi, karena planet ini mengaburkan kita. Kami hanya bisa melihat ke cakrawala. Karena planet ini dapat dianggap sebagai bola, semakin tinggi sudut pandangnya, semakin banyak permukaan yang bisa kita lihat, yaitu cakrawala tergantung pada ketinggian.


Cakrawala tergantung pada ketinggian sudut pandang.

Visibilitas terbatas dari unit kami meniru efek cakrawala yang diciptakan oleh kelengkungan Bumi. Kisaran ulasan mereka tergantung pada ukuran planet dan skala peta. Setidaknya itulah penjelasan logisnya. Tapi alasan utama untuk mengurangi ruang lingkup adalah gameplay, ini adalah batasan yang disebut kabut perang. Namun, memahami fisika yang mendasari bidang pandang, kita dapat menyimpulkan bahwa sudut pandang yang tinggi harus memiliki nilai strategis, karena itu menjauhkan cakrawala dan memungkinkan Anda untuk melihat hambatan yang lebih rendah. Namun sejauh ini kami belum mengimplementasikannya.

Tinggi untuk ditinjau


Untuk memperhitungkan ketinggian saat menentukan ruang lingkup, kita perlu mengetahui ketinggiannya. Ini akan menjadi ketinggian atau tingkat air yang biasa, tergantung pada apakah sel tanah atau air. Mari kita tambahkan ini ke HexCellproperti.

  public int ViewElevation { get { return elevation >= waterLevel ? elevation : waterLevel; } } 

Tetapi jika tinggi mempengaruhi ruang lingkup, maka dengan perubahan ketinggian tampilan sel, situasi visibilitas juga dapat berubah. Karena sel telah diblokir atau sekarang memblokir ruang lingkup beberapa unit, tidaklah mudah untuk menentukan apa yang perlu diubah. Sel itu sendiri tidak akan dapat menyelesaikan masalah ini, jadi biarkan ia melaporkan perubahan dalam situasi ini HexCellShaderData. Misalkan Anda HexCellShaderDatamemiliki metode untuk ini ViewElevationChanged. Kami akan menyebutnya atas penugasan HexCell.Elevation, jika perlu.

  public int Elevation { get { return elevation; } set { if (elevation == value) { return; } int originalViewElevation = ViewElevation; elevation = value; if (ViewElevation != originalViewElevation) { ShaderData.ViewElevationChanged(); } … } } 

Hal yang sama berlaku untuk WaterLevel.

  public int WaterLevel { get { return waterLevel; } set { if (waterLevel == value) { return; } int originalViewElevation = ViewElevation; waterLevel = value; if (ViewElevation != originalViewElevation) { ShaderData.ViewElevationChanged(); } ValidateRivers(); Refresh(); } } 

Atur ulang visibilitas


Sekarang kita perlu membuat metode HexCellShaderData.ViewElevationChanged. Menentukan bagaimana perubahan situasi visibilitas umum adalah tugas yang kompleks, terutama ketika mengubah banyak sel secara bersamaan. Karena itu, kami tidak akan menemukan trik apa pun, tetapi cukup merencanakan untuk mengatur ulang visibilitas semua sel. Tambahkan bidang boolean untuk melacak apakah akan melakukan ini. Di dalam metode, kita hanya akan mengaturnya menjadi true dan memasukkan komponen. Terlepas dari jumlah sel yang telah berubah secara bersamaan, ini akan mengarah ke pengaturan ulang tunggal.

  bool needsVisibilityReset; … public void ViewElevationChanged () { needsVisibilityReset = true; enabled = true; } 

Untuk mengatur ulang nilai visibilitas semua sel, Anda harus memiliki akses ke sana, yang HexCellShaderDatatidak Anda miliki. Jadi mari kita mendelegasikan tanggung jawab ini HexGrid. Untuk melakukan ini, Anda perlu menambahkan ke HexCellShaderDataproperti, yang akan memungkinkan Anda untuk merujuk ke kisi. Kemudian kita bisa menggunakannya LateUpdateuntuk meminta reset.

  public HexGrid Grid { get; set; } … void LateUpdate () { if (needsVisibilityReset) { needsVisibilityReset = false; Grid.ResetVisibility(); } … } 

Mari kita beralih ke HexGrid: mengatur tautan ke kisi HexGrid.Awakesetelah membuat data shader.

  void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; cellShaderData = gameObject.AddComponent<HexCellShaderData>(); cellShaderData.Grid = this; CreateMap(cellCountX, cellCountZ); } 

HexGridjuga harus mendapatkan metode ResetVisibilityuntuk membuang semua sel. Hanya membuatnya berkeliling semua sel dalam loop dan mendelegasikan reset ke dirinya sendiri.

  public void ResetVisibility () { for (int i = 0; i < cells.Length; i++) { cells[i].ResetVisibility(); } } 

Sekarang kita perlu menambahkan HexCellmetode ResetVisibilty. Ini hanya akan nol visibilitas dan memicu pembaruan visibilitas. Ini harus dilakukan ketika visibilitas sel lebih besar dari nol.

  public void ResetVisibility () { if (visibility > 0) { visibility = 0; ShaderData.RefreshVisibility(this); } } 

Setelah mengatur ulang semua data visibilitas, HexGrid.ResetVisibilityia harus kembali menerapkan visibilitas ke semua regu, untuk itu ia perlu mengetahui ruang lingkup masing-masing regu. Misalkan dapat diperoleh menggunakan properti VisionRange.

  public void ResetVisibility () { for (int i = 0; i < cells.Length; i++) { cells[i].ResetVisibility(); } for (int i = 0; i < units.Count; i++) { HexUnit unit = units[i]; IncreaseVisibility(unit.Location, unit.VisionRange); } } 

Untuk ini untuk bekerja, mengubah nama refactor- HexUnit.visionRangedi HexUnit.VisionRangedan mengubahnya menjadi fitur. Meskipun akan menerima nilai konstan 3, tetapi di masa depan itu akan berubah.

  public int VisionRange { get { return 3; } } 

Karena ini, data visibilitas akan diatur ulang dan tetap benar setelah mengubah ketinggian tampilan sel. Tetapi kemungkinan kami akan mengubah aturan untuk menentukan ruang lingkup dan menjalankan kompilasi dalam mode Putar. Agar cakupan berubah secara independen, mari jalankan reset HexGrid.OnEnableketika kompilasi terdeteksi.

  void OnEnable () { if (!HexMetrics.noiseSource) { … ResetVisibility(); } } 

Sekarang Anda dapat mengubah kode lingkup dan melihat hasilnya, sambil tetap dalam mode Putar.

Memperluas cakrawala


Perhitungan ruang lingkup ditentukan HexGrid.GetVisibleCells. Agar ketinggian memengaruhi ruang lingkup, kita cukup menggunakan ketinggian tampilan dengan fromCellmendefinisikan kembali sementara area yang ditransmisikan. Jadi kita dapat dengan mudah memeriksa apakah ini berhasil.

  List<HexCell> GetVisibleCells (HexCell fromCell, int range) { … range = fromCell.ViewElevation; fromCell.SearchPhase = searchFrontierPhase; fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); … } 


Gunakan ketinggian sebagai ruang lingkup.

Hambatan untuk Visibilitas


Menerapkan ketinggian tampilan sebagai ruang lingkup hanya berfungsi dengan benar ketika semua sel lain berada pada ketinggian nol. Tetapi jika semua sel memiliki ketinggian yang sama dengan sudut pandang, maka bidang pandang harus nol. Selain itu, sel dengan ketinggian tinggi harus memblokir visibilitas sel-sel rendah di belakangnya. Sejauh ini, belum ada yang diterapkan.


Lingkup tidak mengganggu.

Cara yang paling benar untuk menentukan ruang lingkup adalah dengan mengecek dengan emisi sinar, tetapi dengan cepat akan menjadi mahal dan masih menghasilkan hasil yang aneh. Kami membutuhkan solusi cepat yang menciptakan hasil yang cukup baik yang tidak harus sempurna. Selain itu, penting bahwa aturan untuk menentukan ruang lingkup sederhana, intuitif, dan dapat diprediksi untuk pemain.

Solusi kami adalah sebagai berikut - saat menentukan visibilitas sel, kami akan menambahkan ketinggian tampilan sel tetangga ke jarak yang dicakup. Bahkan, ini mengurangi ruang lingkup ketika kita melihat sel-sel ini, dan jika mereka dilewati, ini tidak akan memungkinkan kita untuk mencapai sel-sel di belakangnya.

  int distance = current.Distance + 1; if (distance + neighbor.ViewElevation > range) { continue; } 


Sel-sel tinggi memblokir tampilan.

Tidakkah seharusnya kita melihat sel-sel tinggi di kejauhan?
, , , . , .

Jangan melihat-lihat sudut


Sekarang tampaknya sel-sel tinggi menghalangi pandangan ke rendah, tetapi kadang-kadang ruang lingkup menerobosnya, meskipun tampaknya tidak seharusnya demikian. Ini terjadi karena algoritma pencarian masih menemukan jalur ke sel-sel ini, melewati sel yang menghalangi. Alhasil, area visibilitas kami seolah bisa melewati rintangan. Untuk menghindari hal ini, kita perlu memastikan bahwa hanya jalur terpendek yang diperhitungkan saat menentukan visibilitas sel. Ini bisa dilakukan dengan menjatuhkan jalur yang menjadi lebih panjang dari yang diperlukan.

  HexCoordinates fromCoordinates = fromCell.coordinates; while (searchFrontier.Count > 0) { … for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … int distance = current.Distance + 1; if (distance + neighbor.ViewElevation > range || distance > fromCoordinates.DistanceTo(neighbor.coordinates) ) { continue; } … } } 


Kami hanya menggunakan jalur terpendek.

Jadi kami memperbaiki sebagian besar kasus yang keliru. Untuk sel di dekatnya, ini bekerja dengan baik, karena hanya ada jalur terpendek ke sana. Sel yang lebih jauh memiliki lebih banyak opsi untuk jalur, oleh karena itu, jarak yang jauh, sebuah amplop visibilitas masih dapat terjadi. Ini tidak akan menjadi masalah jika area visibilitas tetap kecil dan perbedaan ketinggian yang berdekatan tidak terlalu besar.

Dan akhirnya, alih-alih mengganti bidang tampilan yang ditransmisikan, kami menambahkan ketinggian tampilan. Bidang pandang regu sendiri menunjukkan kemampuan ketinggian, ketinggian penerbangan, atau pengintaian.

  range += fromCell.ViewElevation; 


Lihat dengan bidang pandang penuh pada titik pandang rendah.

Artinya, aturan final visibilitas berlaku untuk penglihatan ketika bergerak di sepanjang jalur terpendek ke bidang pandang, dengan mempertimbangkan perbedaan ketinggian sel relatif terhadap sudut pandang. Ketika sel di luar ruang lingkup, itu memblokir semua jalur melaluinya. Akibatnya, titik pengamatan tinggi, yang tidak menghalangi pandangan, menjadi bernilai strategis.

Bagaimana dengan menghalangi visibilitas objek?
, , . , , . .

paket unity

Sel yang tidak bisa dieksplorasi


Masalah terakhir dengan visibilitas menyangkut tepi peta. Kelegaan tiba-tiba dan tanpa transisi berakhir, karena sel-sel di tepi tidak memiliki tetangga.


Tepi peta yang ditandai.

Idealnya, tampilan visual area dan tepi peta yang belum dijelajahi harus sama. Kita dapat mencapai ini dengan menambahkan case khusus ketika melakukan triangulasi edge, ketika mereka tidak memiliki tetangga, tetapi ini akan membutuhkan logika tambahan, dan kita harus bekerja dengan sel yang hilang. Oleh karena itu, solusi semacam itu tidak trivial. Pendekatan alternatif adalah memaksa sel-sel batas peta untuk tidak dieksplorasi, bahkan jika mereka berada dalam lingkup skuad. Pendekatan ini jauh lebih sederhana, jadi mari kita gunakan. Ini juga memungkinkan Anda untuk menandai sebagai sel yang belum dijelajahi dan lainnya, membuatnya lebih mudah untuk mencapai penciptaan tepi peta yang tidak rata. Selain itu, sel-sel tersembunyi di tepi memungkinkan Anda untuk membuat jalan dan sungai yang masuk dan meninggalkan peta sungai dan jalan, karena titik akhir mereka akan berada di luar ruang lingkup.Juga, dengan bantuan solusi ini, Anda dapat menambahkan unit yang masuk dan keluar peta.

Kami menandai sel yang diselidiki


Untuk menunjukkan bahwa sel dapat diperiksa, tambahkan ke HexCellproperti Explorable.

  public bool Explorable { get; set; } 

Sekarang sel dapat terlihat jika itu adalah sel yang diselidiki, jadi IsVisiblekami akan mengubah properti untuk mempertimbangkan ini.

  public bool IsVisible { get { return visibility > 0 && Explorable; } } 

Hal yang sama berlaku untuk IsExplored. Namun, untuk ini kami menyelidiki properti standar. Kita perlu mengubahnya menjadi properti eksplisit agar dapat mengubah logika pengambilnya.

  public bool IsExplored { get { return explored && Explorable; } private set { explored = value; } } … bool explored; 

Sembunyikan tepi peta


Anda bisa menyembunyikan tepi peta persegi panjang dalam metode ini HexGrid.CreateCell. Sel-sel yang tidak ada di tepi diselidiki, semua sisanya belum diselidiki.

  void CreateCell (int x, int z, int i) { … HexCell cell = cells[i] = Instantiate<HexCell>(cellPrefab); cell.transform.localPosition = position; cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); cell.Index = i; cell.ShaderData = cellShaderData; cell.Explorable = x > 0 && z > 0 && x < cellCountX - 1 && z < cellCountZ - 1; … } 

Sekarang kartu-kartu itu digelapkan di sekitar tepinya, bersembunyi di belakangnya ruang-ruang besar yang belum dijelajahi. Akibatnya, ukuran area peta yang dipelajari berkurang di setiap dimensi oleh dua dimensi.


Tepi peta yang belum dijelajahi.

Apakah mungkin untuk membuat status penelitian dapat diedit?
, , . .

Sel yang belum dijelajahi menghalangi visibilitas


Akhirnya, jika sel tidak dapat diperiksa, maka itu harus mengganggu visibilitas. Ubah HexGrid.GetVisibleCellsuntuk mempertimbangkan ini.

  if ( neighbor == null || neighbor.SearchPhase > searchFrontierPhase || !neighbor.Explorable ) { continue; } 

paket unity

Bagian 23: menghasilkan tanah


  • Isi peta baru dengan lanskap yang dihasilkan.
  • Kami mengangkat tanah di atas air, kami membanjiri sebagian.
  • Kami mengontrol jumlah tanah yang dibuat, ketinggian dan ketidakrataannya.
  • Kami menambahkan dukungan untuk berbagai opsi konfigurasi untuk membuat peta variabel.
  • Kami membuatnya sehingga peta yang sama dapat dihasilkan lagi.

Bagian tutorial ini akan menjadi awal dari rangkaian pembuatan peta prosedural.

Bagian ini dibuat di Unity 2017.1.0.


Salah satu dari banyak peta yang dihasilkan.

Pembuatan kartu


Meskipun kita dapat membuat peta apa pun, butuh banyak waktu. Akan lebih mudah jika aplikasi tersebut dapat membantu perancang dengan membuat kartu untuknya, yang kemudian dapat dimodifikasi sesuai selera. Anda dapat mengambil langkah lain dan sepenuhnya menghilangkan membuat desain secara manual, sepenuhnya mentransfer tanggung jawab untuk menghasilkan peta yang sudah selesai ke aplikasi. Karena ini, permainan dapat dimainkan setiap kali dengan kartu baru dan setiap sesi permainan akan berbeda. Agar semua ini memungkinkan, kita harus membuat algoritma pembuatan peta.

Jenis algoritma pembangkitan yang Anda butuhkan tergantung pada jenis kartu yang Anda butuhkan. Tidak ada pendekatan yang benar, Anda selalu harus mencari kompromi antara kredibilitas dan kemampuan bermain.

Agar sebuah kartu dapat dipercaya, itu harus tampak sangat mungkin dan nyata bagi pemain. Ini tidak berarti bahwa peta harus terlihat seperti bagian dari planet kita. Ini mungkin planet yang berbeda atau realitas yang sama sekali berbeda. Tetapi jika itu harus menunjukkan relief Bumi, maka setidaknya harus sebagian menyerupai itu.

Pemutaran terkait dengan bagaimana kartu sesuai dengan gameplay. Terkadang bertentangan dengan kepercayaan. Misalnya, meskipun pegunungan dapat terlihat indah, pada saat yang sama mereka sangat membatasi pergerakan dan tampilan unit. Jika ini tidak diinginkan, maka Anda harus melakukannya tanpa gunung, yang akan mengurangi kredibilitas dan membatasi ekspresif permainan. Atau kita bisa menyelamatkan pegunungan, tetapi mengurangi dampaknya pada gameplay, yang juga dapat mengurangi kredibilitas.

Selain itu, kelayakan harus dipertimbangkan. Misalnya, Anda dapat membuat planet mirip bumi yang sangat realistis dengan mensimulasikan lempeng tektonik, erosi, hujan, letusan gunung berapi, efek meteorit dan bulan, dan sebagainya. Tetapi pengembangan sistem seperti itu akan membutuhkan banyak waktu. Selain itu, dibutuhkan waktu lama untuk menghasilkan planet seperti itu, dan para pemain tidak akan mau menunggu beberapa menit sebelum memulai permainan baru. Artinya, simulasi adalah alat yang ampuh, tetapi memiliki harga.

Game sering menggunakan pertukaran antara kredibilitas, kemampuan bermain, dan kelayakan. Terkadang kompromi semacam itu tidak terlihat dan tampak sepenuhnya normal, dan terkadang kompromi itu terlihat acak, tidak konsisten, atau kacau, tergantung pada keputusan yang diambil selama proses pengembangan. Ini tidak hanya berlaku untuk pembuatan kartu, tetapi ketika mengembangkan generator kartu prosedural, Anda perlu memberikan perhatian khusus pada ini. Anda dapat menghabiskan banyak waktu membuat algoritme yang menghasilkan kartu-kartu indah yang ternyata tidak berguna untuk gim yang Anda buat.

Dalam seri tutorial ini, kami akan membuat relief mirip tanah. Seharusnya terlihat menarik, dengan variabilitas besar dan tidak adanya area homogen yang besar. Skala bantuan akan besar, peta akan mencakup satu atau lebih benua, wilayah lautan, atau bahkan seluruh planet. Kita memerlukan kendali atas geografi, termasuk massa tanah, iklim, jumlah daerah, dan benjolan di daratan. Pada bagian ini kita akan meletakkan dasar untuk pembuatan sushi.

Memulai dalam mode edit


Kami akan fokus pada peta, bukan pada gameplay, sehingga akan lebih mudah untuk meluncurkan aplikasi dalam mode edit. Berkat ini, kita dapat langsung melihat kartunya. Oleh karena itu, kami akan mengubah dengan HexMapEditor.Awakemengatur mode pengeditan ke true dan mengaktifkan kata kunci shader dari mode ini.

  void Awake () { terrainMaterial.DisableKeyword("GRID_ON"); Shader.EnableKeyword("HEX_MAP_EDIT_MODE"); SetEditMode(true); } 

Generator kartu


Karena cukup banyak kode diperlukan untuk menghasilkan peta prosedural, kami tidak akan menambahkannya langsung ke HexGrid. Sebaliknya, kami akan membuat komponen baru HexMapGenerator, dan HexGridtidak akan mengetahuinya. Ini akan menyederhanakan transisi ke algoritma lain jika kita membutuhkannya.

Generator membutuhkan tautan ke grid, jadi kami akan menambahkan bidang umum ke situ. Selain itu, kami menambahkan metode umum GenerateMapyang akan menangani pekerjaan algoritma. Kami akan memberikannya dimensi peta sebagai parameter, dan kemudian memaksanya untuk digunakan untuk membuat peta kosong baru.

 using System.Collections.Generic; using UnityEngine; public class HexMapGenerator : MonoBehaviour { public HexGrid grid; public void GenerateMap (int x, int z) { grid.CreateMap(x, z); } } 

Tambahkan objek dengan komponen ke adegan HexMapGeneratordan hubungkan ke grid.


Peta objek generator.

Ubah menu peta baru


Kami akan mengubahnya NewMapMenusehingga dapat menghasilkan kartu, bukan hanya membuat yang kosong. Kami akan mengontrol fungsinya melalui bidang Boolean generateMaps, yang secara default memiliki nilai true. Mari kita buat metode umum untuk mengatur bidang ini, seperti yang kita lakukan untuk beralih opsi HexMapEditor. Tambahkan saklar yang sesuai ke menu dan hubungkan ke metode.

  bool generateMaps = true; public void ToggleMapGeneration (bool toggle) { generateMaps = toggle; } 


Menu kartu baru dengan sakelar.

Berikan menu tautan ke generator peta. Kemudian kita akan memaksanya untuk memanggil metode GenerateMapgenerator jika perlu , dan tidak hanya menjalankan CreateMapgrid.

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


Koneksi ke generator.

Akses sel


Agar generator berfungsi, ia membutuhkan akses ke sel. Kami HexGridsudah memiliki metode umum GetCellyang memerlukan atau vektor posisi, atau koordinat segi enam. Generator tidak perlu bekerja dengan satu atau yang lain, jadi kami menambahkan dua metode HexGrid.GetCellyang nyaman yang akan bekerja dengan koordinat offset atau indeks sel.

  public HexCell GetCell (int xOffset, int zOffset) { return cells[xOffset + zOffset * cellCountX]; } public HexCell GetCell (int cellIndex) { return cells[cellIndex]; } 

Sekarang HexMapGeneratordapat menerima sel secara langsung. Misalnya, setelah membuat peta baru, ia dapat menggunakan koordinat rumput untuk mengatur rumput sebagai relief kolom tengah sel.

  public void GenerateMap (int x, int z) { grid.CreateMap(x, z); for (int i = 0; i < z; i++) { grid.GetCell(x / 2, i).TerrainTypeIndex = 1; } } 


Kolom rumput di peta kecil.

paket unity

Membuat sushi


Saat membuat peta, kami memulai sepenuhnya tanpa tanah. Orang dapat membayangkan bahwa seluruh dunia dibanjiri oleh satu samudera luas. Sebuah daratan tercipta ketika bagian dasar samudera terdorong begitu tinggi hingga naik di atas air. Kita perlu memutuskan berapa banyak tanah yang harus dibuat dengan cara ini, di mana akan muncul dan apa bentuknya.

Naikkan kelegaan


Mari kita mulai dari yang kecil - angkat satu bidang tanah di atas air. Kami membuat metode ini RaiseTerraindengan parameter untuk mengontrol ukuran plot. Panggil metode ini GenerateMap, ganti kode tes sebelumnya. Mari kita mulai dengan sebidang tanah kecil yang terdiri dari tujuh sel.

  public void GenerateMap (int x, int z) { grid.CreateMap(x, z); // for (int i = 0; i < z; i++) { // grid.GetCell(x / 2, i).TerrainTypeIndex = 1; // } RaiseTerrain(7); } void RaiseTerrain (int chunkSize) {} 

Sejauh ini, kami menggunakan jenis bantuan “rumput” untuk menunjukkan tanah yang ditinggikan, dan bantuan “pasir” asli mengacu pada lautan. Buat kami RaiseTerrainmengambil sel acak dan mengubah jenis bantuannya sampai kami mendapatkan jumlah lahan yang tepat.

Untuk mendapatkan sel acak, kami menambahkan metode GetRandomCellyang menentukan indeks sel acak dan memperoleh sel yang sesuai dari kisi.

  void RaiseTerrain (int chunkSize) { for (int i = 0; i < chunkSize; i++) { GetRandomCell().TerrainTypeIndex = 1; } } HexCell GetRandomCell () { return grid.GetCell(Random.Range(0, grid.cellCountX * grid.cellCountZ)); } 


Tujuh sel sushi acak.

Karena pada akhirnya kita mungkin membutuhkan banyak sel acak atau loop melalui semua sel beberapa kali, mari kita melacak jumlah sel dalam sel itu sendiri HexMapGenerator.

  int cellCount; public void GenerateMap (int x, int z) { cellCount = x * z; … } … HexCell GetRandomCell () { return grid.GetCell(Random.Range(0, cellCount)); } 

Pembuatan satu situs


Sejauh ini, kami mengubah tujuh sel acak menjadi daratan, dan mereka dapat berada di mana saja. Kemungkinan besar mereka tidak membentuk satu wilayah lahan tunggal. Selain itu, kami dapat memilih sel yang sama beberapa kali, jadi kami mendapat lebih sedikit lahan. Untuk mengatasi kedua masalah, tanpa batasan, kami hanya akan memilih sel pertama. Setelah itu, kita harus memilih hanya sel-sel yang berada di sebelah yang dipilih sebelumnya. Pembatasan ini mirip dengan pembatasan pencarian jalur, jadi kami menggunakan pendekatan yang sama di sini.

Kami menambahkan HexMapGeneratorproperti kami sendiri dan penghitung dari fase batas pencarian, seperti di HexGrid.

  HexCellPriorityQueue searchFrontier; int searchFrontierPhase; 

Pastikan antrian prioritas ada sebelum kita membutuhkannya.

  public void GenerateMap (int x, int z) { cellCount = x * z; grid.CreateMap(x, z); if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } RaiseTerrain(7); } 

Setelah membuat peta baru, batas pencarian untuk semua sel adalah nol. Tetapi jika kita akan mencari sel dalam proses pembuatan peta, kita akan meningkatkan batas pencarian mereka dalam proses ini. Jika kami melakukan banyak operasi pencarian, mereka mungkin berada di depan fase batas pencarian yang direkam HexGrid. Ini dapat mengganggu pencarian jalur unit. Untuk menghindari hal ini, pada akhir proses pembuatan peta, kami akan mengatur ulang fase pencarian semua sel menjadi nol.

  RaiseTerrain(7); for (int i = 0; i < cellCount; i++) { grid.GetCell(i).SearchPhase = 0; } 

Sekarang saya RaiseTerrainharus mencari sel yang sesuai, dan tidak memilihnya secara acak. Proses ini sangat mirip dengan metode pencarian di HexGrid. Namun, kami tidak akan mengunjungi sel lebih dari satu kali, sehingga cukup bagi kami untuk meningkatkan fase batas pencarian sebanyak 1 alih-alih 2. Kemudian kami menginisialisasi perbatasan dengan sel pertama, yang dipilih secara acak. Seperti biasa, selain mengatur fase pencariannya, kami menetapkan jarak dan heuristik ke nol.

  void RaiseTerrain (int chunkSize) { // for (int i = 0; i < chunkSize; i++) { // GetRandomCell().TerrainTypeIndex = 1; // } searchFrontierPhase += 1; HexCell firstCell = GetRandomCell(); firstCell.SearchPhase = searchFrontierPhase; firstCell.Distance = 0; firstCell.SearchHeuristic = 0; searchFrontier.Enqueue(firstCell); } 

Setelah itu, loop pencarian sebagian besar akan akrab bagi kita. Selain itu, untuk melanjutkan pencarian sampai perbatasan kosong, kita perlu berhenti ketika fragmen mencapai ukuran yang diinginkan, jadi kita akan melacaknya. Pada setiap iterasi, kita akan mengekstrak sel berikutnya dari antrian, mengatur jenis reliefnya, menambah ukurannya, dan kemudian memotong tetangga sel ini. Semua tetangga ditambahkan ke perbatasan jika mereka belum ditambahkan di sana. Kami tidak perlu melakukan perubahan atau perbandingan apa pun. Setelah selesai, Anda harus menghapus batas.

  searchFrontier.Enqueue(firstCell); int size = 0; while (size < chunkSize && searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.TerrainTypeIndex = 1; size += 1; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if (neighbor && neighbor.SearchPhase < searchFrontierPhase) { neighbor.SearchPhase = searchFrontierPhase; neighbor.Distance = 0; neighbor.SearchHeuristic = 0; searchFrontier.Enqueue(neighbor); } } } searchFrontier.Clear(); 


Garis sel.

Kami mendapat satu plot dengan ukuran yang tepat. Ini akan lebih kecil hanya jika tidak ada jumlah sel yang cukup. Karena cara perbatasan diisi, plot selalu terdiri dari garis yang membentang ke barat laut. Itu mengubah arah hanya ketika mencapai tepi peta.

Kami menghubungkan sel


Area tanah jarang menyerupai garis, dan jika mereka lakukan, mereka tidak selalu berorientasi dengan cara yang sama. Untuk mengubah bentuk situs, kita perlu mengubah prioritas sel. Sel acak pertama dapat digunakan sebagai pusat plot. Maka jarak ke semua sel lain akan relatif ke titik ini. Jadi kami akan memberikan prioritas lebih tinggi ke sel yang lebih dekat ke pusat, sehingga situs tidak akan tumbuh sebagai garis, tetapi di sekitar pusat.

  searchFrontier.Enqueue(firstCell); HexCoordinates center = firstCell.coordinates; int size = 0; while (size < chunkSize && searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.TerrainTypeIndex = 1; size += 1; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if (neighbor && neighbor.SearchPhase < searchFrontierPhase) { neighbor.SearchPhase = searchFrontierPhase; neighbor.Distance = neighbor.coordinates.DistanceTo(center); neighbor.SearchHeuristic = 0; searchFrontier.Enqueue(neighbor); } } } 


Akumulasi sel.

Dan faktanya, sekarang tujuh sel kita dikemas dengan indah di area heksagonal yang kompak jika sel pusat tidak muncul di tepi peta. Mari kita coba sekarang untuk menggunakan ukuran plot 30.

  RaiseTerrain(30); 


Sushi massa dalam 30 sel.

Kami kembali mendapatkan bentuk yang sama, meskipun tidak ada cukup sel untuk mendapatkan segi enam yang tepat. Karena jari-jari plot lebih besar, lebih cenderung dekat dengan tepi peta, yang akan memaksanya mengambil bentuk yang berbeda.

Pengacakan sushi


Kami tidak ingin semua area terlihat sama, jadi kami akan sedikit mengubah prioritas sel. Setiap kali kita menambahkan sel tetangga ke perbatasan, jika angka berikutnya Random.valuekurang dari nilai ambang tertentu, maka heuristik sel ini menjadi bukan 0, tetapi 1. Mari kita gunakan nilai 0,5 sebagai ambang, artinya, kemungkinan besar akan mempengaruhi setengah sel.

  neighbor.Distance = neighbor.coordinates.DistanceTo(center); neighbor.SearchHeuristic = Random.value < 0.5f ? 1: 0; searchFrontier.Enqueue(neighbor); 


Area terdistorsi.

Dengan meningkatkan heuristik pencarian sel, kami membuatnya mengunjungi lebih lambat dari yang diharapkan. Pada saat yang sama, sel-sel lain yang terletak satu langkah lebih jauh dari pusat akan dikunjungi lebih awal, kecuali jika mereka juga meningkatkan heuristik. Ini berarti bahwa jika kita meningkatkan heuristik semua sel dengan satu nilai, maka ini tidak akan mempengaruhi peta. Artinya, ambang 1 tidak akan berpengaruh, seperti ambang 0. Dan ambang 0.8 akan setara dengan 0.2. Artinya, probabilitas 0,5 membuat proses pencarian paling "gemetar."

Jumlah osilasi yang tepat tergantung pada jenis medan yang diinginkan, jadi mari kita ubahsuaikan. Tambahkan bidang float generik jitterProbabilitydengan atribut ke generatorRangeterbatas pada kisaran 0-0,5. Mari kita beri nilai default yang sama dengan rata-rata interval ini, mis. 0,25. Ini akan memungkinkan kita untuk mengonfigurasi generator di jendela inspektur Unity.

  [Range(0f, 0.5f)] public float jitterProbability = 0.25f; 


Kemungkinan fluktuasi.

Bisakah Anda membuatnya dapat disesuaikan di game UI?
, . UI, . , UI. , . , .

Sekarang, untuk membuat keputusan tentang kapan heuristik harus sama dengan 1, kita menggunakan probabilitas daripada nilai konstan.

  neighbor.SearchHeuristic = Random.value < jitterProbability ? 1: 0; 

Kami menggunakan nilai heuristik 0 dan 1. Meskipun nilai yang lebih besar dapat digunakan, ini akan sangat memperburuk deformasi bagian, kemungkinan besar mengubahnya menjadi banyak garis.

Naikkan beberapa tanah


Kami tidak akan terbatas pada pembuatan satu bidang tanah saja. Misalnya, kami melakukan panggilan RaiseTerraindi dalam satu lingkaran untuk mendapatkan lima bagian.

  for (int i = 0; i < 5; i++) { RaiseTerrain(30); } 


Lima bidang tanah.

Meskipun sekarang kami menghasilkan lima plot masing-masing 30 sel, tetapi belum tentu mendapatkan 150 sel tanah. Karena setiap situs dibuat secara terpisah, mereka tidak saling mengenal, sehingga mereka dapat bersinggungan. Ini normal karena dapat menciptakan lanskap yang lebih menarik daripada hanya seperangkat bagian yang terisolasi.

Untuk meningkatkan variabilitas lahan, kami juga dapat mengubah ukuran setiap plot. Tambahkan dua bidang bilangan bulat untuk mengontrol ukuran minimum dan maksimum plot. Tetapkan mereka interval yang cukup besar, misalnya, 20-200. Saya akan membuat standar minimum sama dengan 30, dan standar maksimum - 100.

  [Range(20, 200)] public int chunkSizeMin = 30; [Range(20, 200)] public int chunkSizeMax = 100; 


Interval ukuran.

Kami menggunakan bidang ini untuk secara acak menentukan ukuran area saat dipanggil RaiseTerrain.

  RaiseTerrain(Random.Range(chunkSizeMin, chunkSizeMax + 1)); 


Lima bagian berukuran acak di peta tengah.

Buat cukup sushi


Meskipun kami tidak dapat secara khusus mengontrol jumlah lahan yang dihasilkan. Meskipun kita dapat menambahkan opsi konfigurasi untuk jumlah plot, plot itu sendiri berukuran acak dan mungkin tumpang tindih sedikit atau kuat. Oleh karena itu, jumlah situs tidak menjamin tanda terima pada peta dari jumlah lahan yang diperlukan. Mari tambahkan opsi untuk secara langsung mengontrol persentase lahan yang dinyatakan sebagai bilangan bulat. Karena 100% tanah atau air tidak terlalu menarik, kami membatasinya pada interval 5–95, dengan nilai 50 secara default.

  [Range(5, 95)] public int landPercentage = 50; 


Persentase sushi.

Untuk menjamin terciptanya jumlah lahan yang tepat, kita hanya perlu terus meningkatkan area dataran sampai kita mendapatkan jumlah yang cukup. Untuk melakukan ini, kita perlu mengendalikan prosesnya, yang akan menyulitkan pembuatan tanah. Karena itu, mari kita ganti siklus yang ada untuk meningkatkan situs dengan memanggil metode baru CreateLand. Hal pertama yang dilakukan metode ini adalah menghitung jumlah sel yang seharusnya menjadi daratan. Jumlah ini akan menjadi jumlah total sel sushi kami.

  public void GenerateMap (int x, int z) { … // for (int i = 0; i < 5; i++) { // RaiseTerrain(Random.Range(chunkSizeMin, chunkSizeMax + 1)); // } CreateLand(); for (int i = 0; i < cellCount; i++) { grid.GetCell(i).SearchPhase = 0; } } void CreateLand () { int landBudget = Mathf.RoundToInt(cellCount * landPercentage * 0.01f); } 

CreateLandakan menyebabkan RaiseTerrainsampai kita menghabiskan seluruh jumlah sel. Agar tidak melebihi jumlah, kami mengubah RaiseTerrainsehingga menerima jumlah sebagai parameter tambahan. Setelah selesai bekerja, ia harus mengembalikan jumlah yang tersisa.

 // void RaiseTerrain (int chunkSize) { int RaiseTerrain (int chunkSize, int budget) { … return budget; } 

Jumlahnya harus berkurang setiap kali sel dikeluarkan dari perbatasan dan dikonversi menjadi daratan. Jika setelah ini seluruh jumlah dihabiskan, maka kita harus menghentikan pencarian dan menyelesaikan situs. Selain itu, ini harus dilakukan hanya ketika sel saat ini belum mendarat.

  while (size < chunkSize && searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); if (current.TerrainTypeIndex == 0) { current.TerrainTypeIndex = 1; if (--budget == 0) { break; } } size += 1; … } 

Sekarang CreateLanddapat meningkatkan lahan sampai menghabiskan seluruh sel.

  void CreateLand () { int landBudget = Mathf.RoundToInt(cellCount * landPercentage * 0.01f); while (landBudget > 0) { landBudget = RaiseTerrain( Random.Range(chunkSizeMin, chunkSizeMax + 1), landBudget ); } } 


Setengah peta menjadi tanah.

paket unity

Mempertimbangkan ketinggian


Tanah bukan hanya pelat datar, dibatasi oleh garis pantai. Dia memiliki ketinggian yang berubah, mengandung bukit, gunung, lembah, danau, dan sebagainya. Perbedaan besar dalam ketinggian ada karena interaksi lempeng tektonik yang bergerak lambat. Meskipun kami tidak akan mensimulasikannya, area tanah kami harus menyerupai pelat tersebut. Situs tidak bergerak, tetapi dapat berpotongan. Dan kita bisa memanfaatkan ini.

Dorong tanah ke atas


Setiap plot mewakili bagian tanah yang didorong keluar dari dasar lautan. Karena itu, mari kita terus-menerus meningkatkan ketinggian sel saat ini RaiseTerraindan melihat apa yang terjadi.

  HexCell current = searchFrontier.Dequeue(); current.Elevation += 1; if (current.TerrainTypeIndex == 0) { … } 


Mendarat dengan ketinggian.

Kami punya ketinggian, tapi sulit dilihat. Anda dapat membuatnya lebih mudah dibaca jika menggunakan jenis medan Anda sendiri untuk setiap tingkat ketinggian, seperti pelapisan geografis. Kami hanya akan melakukan ini agar ketinggian lebih terlihat, sehingga Anda cukup menggunakan level ketinggian sebagai indeks ketinggian.

Apa yang terjadi jika ketinggian melebihi jumlah jenis medan?
. , .

Alih-alih memperbarui jenis medan sel dengan setiap perubahan ketinggian, mari kita buat metode terpisah SetTerrainTypeuntuk mengatur semua jenis medan hanya sekali.

  void SetTerrainType () { for (int i = 0; i < cellCount; i++) { HexCell cell = grid.GetCell(i); cell.TerrainTypeIndex = cell.Elevation; } } 

Kami akan memanggil metode ini setelah membuat sushi.

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

Sekarang dia RaiseTerraintidak bisa menangani jenis bantuan, dan fokus pada ketinggian. Untuk melakukan ini, Anda perlu mengubah logikanya. Jika tinggi baru sel saat ini adalah 1, maka itu baru saja menjadi kering, sehingga jumlah sel telah menurun, yang dapat menyebabkan selesainya pertumbuhan situs.

  HexCell current = searchFrontier.Dequeue(); current.Elevation += 1; if (current.Elevation == 1 && --budget == 0) { break; } // if (current.TerrainTypeIndex == 0) { // current.TerrainTypeIndex = 1; // if (--budget == 0) { // break; // } // } 


Stratifikasi lapisan.

Tambahkan air


Mari kita secara eksplisit menunjukkan sel mana yang air atau tanah, mengatur level air untuk semua sel menjadi 1. Mari kita lakukan ini GenerateMapsebelum membuat tanah.

  public void GenerateMap (int x, int z) { cellCount = x * z; grid.CreateMap(x, z); if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } for (int i = 0; i < cellCount; i++) { grid.GetCell(i).WaterLevel = 1; } CreateLand(); … } 

Sekarang untuk penunjukan lapisan tanah kita bisa menggunakan semua jenis medan. Semua sel kapal selam akan tetap berpasir, demikian pula sel-sel daratan terendah. Ini dapat dilakukan dengan mengurangi ketinggian air dari ketinggian dan menggunakan nilai sebagai indeks dari jenis bantuan.

  void SetTerrainType () { for (int i = 0; i < cellCount; i++) { HexCell cell = grid.GetCell(i); if (!cell.IsUnderwater) { cell.TerrainTypeIndex = cell.Elevation - cell.WaterLevel; } } } 


Tanah dan air.

Naikkan level air


Kami tidak terbatas pada satu permukaan air saja. Mari kita membuatnya dapat disesuaikan menggunakan bidang umum dengan interval 1–5 dan nilai default 3. Gunakan level ini saat menginisialisasi sel.

  [Range(1, 5)] public int waterLevel = 3; … public void GenerateMap (int x, int z) { … for (int i = 0; i < cellCount; i++) { grid.GetCell(i).WaterLevel = waterLevel; } … } 



Ketinggian air 3.

Ketika ketinggian airnya 3, kita mendapatkan tanah yang kurang dari yang kita harapkan. Ini karena RaiseTerrainmasih percaya bahwa ketinggian air adalah 1. Mari kita perbaiki.

  HexCell current = searchFrontier.Dequeue(); current.Elevation += 1; if (current.Elevation == waterLevel && --budget == 0) { break; } 

Menggunakan level air yang lebih tinggi mengarah ke sana. bahwa sel tidak segera menjadi daratan. Ketika level air 2, bagian pertama akan tetap berada di bawah air. Dasar laut telah naik, tetapi masih tetap di bawah air. Sebuah tanah hanya terbentuk di persimpangan setidaknya dua bagian. Semakin tinggi permukaan air, semakin banyak situs yang harus dilintasi untuk menciptakan tanah. Karena itu, dengan naiknya permukaan air, tanah menjadi lebih kacau. Selain itu, ketika lebih banyak plot diperlukan, lebih mungkin mereka akan berpotongan di lahan yang sudah ada, itulah sebabnya gunung akan lebih umum dan tanah datar lebih jarang, seperti dalam kasus menggunakan plot yang lebih kecil.





Tingkat airnya 2-5, sushi selalu 50%.

paket unity

Gerakan vertikal


Sejauh ini kami telah menaikkan plot satu tingkat pada satu waktu, tetapi kami tidak harus membatasi diri untuk ini.

Situs tinggi


Meskipun setiap bagian meningkatkan tinggi selnya satu level, kliping dapat terjadi. Ini terjadi ketika tepi dua bagian bersentuhan. Ini dapat membuat tebing yang terisolasi, tetapi garis tebing yang panjang akan jarang. Kita dapat meningkatkan frekuensi penampilan mereka dengan meningkatkan ketinggian plot lebih dari satu langkah. Tetapi ini perlu dilakukan hanya untuk sebagian situs saja. Jika semua daerah naik tinggi, akan sangat sulit untuk bergerak di sepanjang medan. Jadi mari kita buat parameter ini dapat dikustomisasi menggunakan bidang probabilitas dengan nilai default 0,25.

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


Kemungkinan kenaikan kuat dalam sel.

Meskipun kita dapat menggunakan peningkatan ketinggian apa pun untuk area tinggi, ini dengan cepat menjadi tidak terkendali. Perbedaan ketinggian 2 sudah membuat tebing, jadi ini sudah cukup. Karena Anda dapat melewati ketinggian yang sama dengan ketinggian air, kami perlu mengubah cara kami menentukan apakah sebuah sel telah menjadi daratan. Jika berada di bawah permukaan air, dan sekarang berada pada tingkat yang sama atau lebih tinggi, maka kami membuat sel tanah baru.

  int rise = Random.value < highRiseProbability ? 2 : 1; int size = 0; while (size < chunkSize && searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); int originalElevation = current.Elevation; current.Elevation = originalElevation + rise; if ( originalElevation < waterLevel && current.Elevation >= waterLevel && --budget == 0 ) { break; } size += 1; … } 





Probabilitas peningkatan tinggi yang kuat adalah 0,25, 0,50, 0,75 dan 1.

Turunkan tanah


Tanah tidak selalu naik, kadang jatuh. Ketika tanah jatuh cukup rendah, air mengisinya dan hilang. Sejauh ini kami tidak melakukan ini. Karena kami hanya mendorong area ke atas, tanah biasanya terlihat seperti satu set area yang agak bulat bercampur menjadi satu. Jika kita terkadang menurunkan area ke bawah, kita mendapatkan bentuk yang lebih bervariasi.


Peta besar tanpa sushi cekung.

Kami dapat mengontrol frekuensi penurunan tanah menggunakan bidang probabilitas lain. Karena penurunan dapat menghancurkan tanah, probabilitas penurunan harus selalu lebih rendah daripada probabilitas peningkatan. Kalau tidak, mungkin butuh waktu yang sangat lama untuk mendapatkan persentase lahan yang tepat. Oleh karena itu, mari kita gunakan probabilitas penurunan maksimum 0,4 dengan nilai default 0,2.

  [Range(0f, 0.4f)] public float sinkProbability = 0.2f; 


Kemungkinan menurunkan.

Menurunkan situs mirip dengan menaikkan, dengan beberapa perbedaan. Karenanya, kami menduplikasi metode RaiseTerraindan mengubah namanya menjadi SinkTerrain. Alih-alih menentukan besarnya kenaikan, kita membutuhkan nilai yang lebih rendah yang dapat menggunakan logika yang sama. Pada saat yang sama, perbandingan untuk memeriksa apakah kita telah melewati permukaan air perlu dibalik. Selain itu, ketika menurunkan relief, kita tidak terbatas pada jumlah sel. Sebaliknya, setiap sel sushi yang hilang mengembalikan jumlah yang dihabiskan untuknya, jadi kami meningkatkannya dan terus bekerja.

  int SinkTerrain (int chunkSize, int budget) { … int sink = Random.value < highRiseProbability ? 2 : 1; int size = 0; while (size < chunkSize && searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); int originalElevation = current.Elevation; current.Elevation = originalElevation - sink; if ( originalElevation >= waterLevel && current.Elevation < waterLevel // && --budget == 0 ) { // break; budget += 1; } size += 1; … } searchFrontier.Clear(); return budget; } 

Sekarang, pada setiap iterasi di dalam, CreateLandkita harus menurunkan atau menaikkan lahan, tergantung pada probabilitas penurunan.

  void CreateLand () { int landBudget = Mathf.RoundToInt(cellCount * landPercentage * 0.01f); while (landBudget > 0) { int chunkSize = Random.Range(chunkSizeMin, chunkSizeMax - 1); if (Random.value < sinkProbability) { landBudget = SinkTerrain(chunkSize, landBudget); } else { landBudget = RaiseTerrain(chunkSize, landBudget); } } } 





Peluang jatuh adalah 0,1, 0,2, 0,3, dan 0,4.

Batasi ketinggian


Pada tahap saat ini, kita bisa berpotensi tumpang tindih banyak bagian, kadang-kadang dengan beberapa peningkatan ketinggian, beberapa di antaranya bisa turun dan kemudian naik lagi. Pada saat yang sama, kita dapat menciptakan ketinggian yang sangat tinggi, dan kadang-kadang sangat rendah, terutama ketika dibutuhkan persentase tanah yang tinggi.


Ketinggian besar di tanah 90%.

Untuk membatasi ketinggian, mari tambahkan kustom minimum dan maksimum. Minimum yang wajar akan berada di suatu tempat antara −4 dan 0, dan maksimum yang dapat diterima mungkin dalam kisaran 6-10. Biarkan nilai default menjadi −2 dan 8. Saat mengedit peta secara manual, mereka akan berada di luar batas yang dapat diterima, sehingga Anda dapat mengubah bilah geser UI editor, atau membiarkannya apa adanya.

  [Range(-4, 0)] public int elevationMinimum = -2; [Range(6, 10)] public int elevationMaximum = 8; 


Tinggi minimum dan maksimum.

Sekarang RaiseTerrainkita harus memastikan bahwa ketinggiannya tidak melebihi batas maksimum yang diizinkan. Ini dapat dilakukan dengan memeriksa apakah sel saat ini terlalu tinggi. Jika demikian, maka kami melewati mereka tanpa mengubah ketinggian dan menambahkan tetangga mereka. Ini akan mengarah pada fakta bahwa area daratan akan menghindari area yang telah mencapai ketinggian maksimum, dan tumbuh di sekitarnya.

  HexCell current = searchFrontier.Dequeue(); int originalElevation = current.Elevation; int newElevation = originalElevation + rise; if (newElevation > elevationMaximum) { continue; } current.Elevation = newElevation; if ( originalElevation < waterLevel && newElevation >= waterLevel && --budget == 0 ) { break; } size += 1; 

Lakukan hal yang sama SinkTerrain, tetapi untuk ketinggian minimum.

  HexCell current = searchFrontier.Dequeue(); int originalElevation = current.Elevation; int newElevation = current.Elevation - sink; if (newElevation < elevationMinimum) { continue; } current.Elevation = newElevation; if ( originalElevation >= waterLevel && newElevation < waterLevel ) { budget += 1; } size += 1; 


Ketinggian terbatas dengan tanah 90%.

Pelestarian Ketinggian Negatif


Pada titik ini, kode simpan dan muat tidak dapat menangani ketinggian negatif karena kami menyimpan ketinggian sebagai byte. Angka negatif dikonversi ketika disimpan ke positif besar. Karena itu, ketika menyimpan dan memuat peta yang dihasilkan, yang sangat tinggi mungkin muncul sebagai pengganti sel bawah air asli.

Kami dapat menambahkan dukungan untuk ketinggian negatif dengan menyimpannya sebagai integer, bukan byte. Namun, kita masih tidak perlu mendukung berbagai level ketinggian. Selain itu, kami dapat mengimbangi nilai yang disimpan dengan menambahkan 127. Ini akan memungkinkan kami untuk menyimpan ketinggian dengan benar di kisaran −127–128 dalam satu byte. Ubah HexCell.Savesesuai.

  public void Save (BinaryWriter writer) { writer.Write((byte)terrainTypeIndex); writer.Write((byte)(elevation + 127)); … } 

Karena kami mengubah cara kami menyimpan data peta, kami menambahnya SaveLoadMenu.mapFileVersionmenjadi 4.

  const int mapFileVersion = 4; 

Dan akhirnya, ubah HexCell.Loadsehingga mengurangi 127 dari ketinggian yang dimuat dari file versi 4.

  public void Load (BinaryReader reader, int header) { terrainTypeIndex = reader.ReadByte(); ShaderData.RefreshTerrain(this); elevation = reader.ReadByte(); if (header >= 4) { elevation -= 127; } … } 

paket unity

Menciptakan peta yang sama


Sekarang kita dapat membuat berbagai macam peta. Saat menghasilkan setiap hasil baru akan acak. Kami dapat mengontrol menggunakan opsi konfigurasi hanya karakteristik kartu, tetapi bukan bentuk yang paling akurat. Tetapi kadang-kadang kita perlu membuat kembali peta yang sama persis lagi. Misalnya, untuk berbagi peta yang indah dengan teman, atau mulai lagi setelah mengeditnya secara manual. Ini juga berguna dalam proses pengembangan game, jadi mari kita tambahkan fitur ini.

Menggunakan Benih


Untuk membuat proses pembuatan peta tidak dapat diprediksi, kami menggunakan Random.Rangedan Random.value. Untuk mendapatkan urutan angka pseudo-acak yang sama lagi, Anda perlu menggunakan nilai seed yang sama. Kami telah mengambil pendekatan serupa sebelumnya, di HexMetrics.InitializeHashGrid. Pertama-tama ia menyimpan keadaan saat ini dari generator nomor yang diinisialisasi dengan nilai benih tertentu, dan kemudian mengembalikan keadaan aslinya. Kita dapat menggunakan pendekatan yang sama untuk HexMapGenerator.GenerateMap. Kita dapat mengingat kembali kondisi lama dan mengembalikannya setelah selesai, agar tidak mengganggu apa pun yang menggunakan Random.

  public void GenerateMap (int x, int z) { Random.State originalRandomState = Random.state; … Random.state = originalRandomState; } 

Selanjutnya, kita perlu menyediakan seed yang digunakan untuk menghasilkan kartu terakhir. Ini dilakukan dengan menggunakan bidang bilangan bulat umum.

  public int seed; 


Benih tampilan.

Sekarang kita membutuhkan nilai seed untuk diinisialisasi Random. Untuk membuat kartu acak, Anda perlu menggunakan seed acak. Pendekatan paling sederhana adalah dengan menggunakan nilai seed arbitrary untuk menghasilkan Random.Range. Agar tidak mempengaruhi keadaan acak awal, kita perlu melakukan ini setelah menyimpannya.

  public void GenerateMap (int x, int z) { Random.State originalRandomState = Random.state; seed = Random.Range(0, int.MaxValue); Random.InitState(seed); … } 

Karena setelah selesai kami mengembalikan keadaan acak, jika kami segera menghasilkan kartu lain, sebagai hasilnya kami mendapatkan nilai seed yang sama. Selain itu, kita tidak tahu bagaimana keadaan acak awal diinisialisasi. Oleh karena itu, meskipun dapat berfungsi sebagai titik awal yang sewenang-wenang, kami membutuhkan sesuatu yang lebih untuk mengacaknya dengan setiap panggilan.

Ada berbagai cara untuk menginisialisasi generator angka acak. Dalam hal ini, Anda dapat dengan mudah menggabungkan beberapa nilai arbitrer yang bervariasi pada rentang yang luas, yaitu kemungkinan menghasilkan kembali kartu yang sama akan rendah. Sebagai contoh, kami menggunakan 32 bit yang lebih rendah dari waktu sistem, dinyatakan dalam siklus, ditambah runtime aplikasi saat ini. Gabungkan nilai-nilai ini menggunakan operasi ATAU bitwise eksklusif sehingga hasilnya tidak terlalu besar.

  seed = Random.Range(0, int.MaxValue); seed ^= (int)System.DateTime.Now.Ticks; seed ^= (int)Time.unscaledTime; Random.InitState(seed); 

Angka yang dihasilkan mungkin negatif, yang untuk nilai benih publik tidak terlihat bagus. Kita bisa membuatnya benar-benar positif dengan menggunakan masking bitwise dengan nilai integer maksimum yang akan mereset bit sign.

  seed ^= (int)Time.unscaledTime; seed &= int.MaxValue; Random.InitState(seed); 

Benih yang dapat digunakan kembali


Kami masih menghasilkan kartu acak, tetapi sekarang kami bisa melihat nilai seed apa yang digunakan untuk masing-masing kartu. Untuk membuat kembali peta yang sama lagi, kita harus memesan generator untuk menggunakan nilai seed yang sama lagi, daripada membuat yang baru. Kami akan melakukan ini dengan menambahkan saklar menggunakan bidang Boolean.

  public bool useFixedSeed; 


Opsi untuk menggunakan seed konstan.

Jika sebuah seed konstan dipilih, maka kami hanya melewatkan menghasilkan seed baru di GenerateMap. Jika kami tidak mengubah bidang benih secara manual, hasilnya akan menjadi peta yang sama lagi.

  Random.State originalRandomState = Random.state; if (!useFixedSeed) { seed = Random.Range(0, int.MaxValue); seed ^= (int)System.DateTime.Now.Ticks; seed ^= (int)Time.time; seed &= int.MaxValue; } Random.InitState(seed); 

Sekarang kita dapat menyalin nilai seed dari peta yang kita sukai dan menyimpannya di suatu tempat, untuk menghasilkannya lagi di masa depan. Jangan lupa bahwa kita akan mendapatkan kartu yang sama hanya jika kita menggunakan parameter generator yang sama persis, yaitu, ukuran kartu yang sama, serta semua opsi konfigurasi lainnya. Bahkan perubahan kecil dalam probabilitas ini dapat membuat peta yang sama sekali berbeda. Karena itu, selain seed, kita perlu mengingat semua pengaturan.



Kartu besar dengan nilai seed 0 dan 929396788, parameter standar.

paket unity

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


All Articles