الخرائط السداسية في الوحدة: ضباب الحرب ، وبحوث الخرائط ، والتوليد الإجرائي

الأجزاء 1-3: الشبكة والألوان وارتفاعات الخلية

الأجزاء 4-7: المطبات والأنهار والطرق

الأجزاء 8-11: الماء والأشكال الأرضية والأسوار

الأجزاء 12-15: الحفظ والتحميل ، القوام ، المسافات

الأجزاء 16-19: إيجاد الطريق وفرق اللاعبين والرسوم المتحركة

الأجزاء 20-23: ضباب الحرب ، بحث الخرائط ، الجيل الإجرائي

الأجزاء 24-27: دورة الماء ، التآكل ، المناطق الأحيائية ، الخريطة الأسطوانية

الجزء 20: ضباب الحرب


  • حفظ بيانات الخلية في النسيج.
  • تغيير أنواع الإغاثة دون التثليث.
  • نحن نتتبع الرؤية.
  • تعتيم كل شيء غير مرئي.

في هذا الجزء ، سنضيف ضباب تأثير الحرب إلى الخريطة.

الآن سيتم إنشاء المسلسل على Unity 2017.1.0.


الآن نرى أننا نستطيع ولا نستطيع أن نرى.

بيانات الخلية في Shader


تستخدم العديد من الألعاب الإستراتيجية مفهوم ضباب الحرب. هذا يعني أن رؤية اللاعب محدودة. يمكنه فقط رؤية ما هو قريب من وحداته أو المنطقة الخاضعة للسيطرة. على الرغم من أننا نرى الارتياح ، إلا أننا لا نعرف ما يحدث هناك. عادة ما تصبح التضاريس غير المرئية أكثر قتامة. لتحقيق ذلك ، نحتاج إلى تتبع رؤية الخلية وتقديمها وفقًا لذلك.

إن أبسط طريقة لتغيير مظهر الخلايا المخفية هي إضافة مقياس للرؤية إلى بيانات الشبكة. ومع ذلك ، في الوقت نفسه ، سيتعين علينا بدء تثليث إغاثة جديد مع تغيير في الرؤية. هذا قرار سيئ لأن الرؤية تتغير باستمرار أثناء اللعبة.

غالبًا ما يتم استخدام تقنية العرض على تضاريس سطح شفاف ، والتي تخفي جزئيًا الخلايا غير المرئية للاعب. هذه الطريقة مناسبة للتضاريس المسطحة نسبيًا مع زاوية عرض محدودة. ولكن نظرًا لأن تضاريسنا يمكن أن تحتوي على ارتفاعات وأشياء مختلفة جدًا يمكن رؤيتها من زوايا مختلفة ، لذلك نحتاج إلى شبكة مفصلة للغاية تتناسب مع شكل التضاريس. ستكون هذه الطريقة أكثر تكلفة من أبسط النهج المذكور أعلاه.

نهج آخر هو نقل بيانات الخلايا إلى التظليل عند التقديم بشكل منفصل عن شبكة الإغاثة. هذا سيسمح لنا بإجراء التثليث مرة واحدة فقط. يمكن نقل بيانات الخلية باستخدام مادة. يعد تغيير النسيج عملية أبسط بكثير من تثليث التضاريس. بالإضافة إلى ذلك ، تنفيذ عدة عينات نسيج إضافية أسرع من تقديم طبقة شفافة واحدة.

ماذا عن استخدام مصفوفات تظليل؟
يمكنك أيضًا نقل بيانات الخلية إلى جهاز تظليل باستخدام مجموعة من المتجهات. ومع ذلك ، فإن صفائف التظليل لها حد حجم ، مقاسة بآلاف البايت ، ويمكن أن تحتوي الأنسجة على ملايين البكسل. لدعم الخرائط الكبيرة ، سنستخدم القوام.

إدارة بيانات الخلية


نحتاج إلى طريقة للتحكم في النسيج الذي يحتوي على بيانات الخلية. لنقم بإنشاء مكون HexCellShaderData جديد HexCellShaderData بذلك.

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

عند إنشاء أو تحميل خريطة جديدة ، نحتاج إلى إنشاء نسيج جديد بالحجم الصحيح. لذلك ، نضيف طريقة التهيئة التي تخلق نسيجًا لها. نحن نستخدم نسيج RGBA بدون زخارف صغيرة ومساحة لونية خطية. لسنا بحاجة إلى خلط بيانات الخلية ، لذلك نستخدم تصفية النقاط. بالإضافة إلى ذلك ، لا ينبغي طي البيانات. سيحتوي كل بكسل في المادة على بيانات من خلية واحدة.

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

هل يجب أن يتطابق حجم النسيج مع حجم الخريطة؟
لا ، إنها تحتاج فقط إلى وجود وحدات بكسل كافية لاستيعاب جميع الخلايا. مع التطابق التام مع حجم الخريطة ، من المرجح أن يتم إنشاء نسيج بأحجام ليست قوة من اثنين (غير قوة من اثنين ، NPOT) ، وليس تنسيق النسيج هذا هو الأكثر فعالية. على الرغم من أنه يمكننا تكوين الكود للعمل مع مواد بحجم قوة اثنين ، فهذا تحسين ثانوي ، مما يعقد الوصول إلى بيانات الخلية.

في الواقع ، لا يتعين علينا إنشاء نسيج جديد في كل مرة نقوم فيها بإنشاء خريطة جديدة. يكفي تغيير حجم النسيج إذا كان موجودًا بالفعل. لا نحتاج حتى إلى التحقق مما إذا كان لدينا الحجم الصحيح بالفعل ، لأن Texture2D.Resize ذكي بما يكفي للقيام بذلك من أجلنا.

  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; } } 

بدلاً من تطبيق بيانات الخلية بكسل واحد في كل مرة ، نستخدم مخزن الألوان ونطبق بيانات جميع الخلايا في كل مرة. للقيام بذلك ، سوف نستخدم مجموعة Color32 . إذا لزم الأمر ، فسننشئ نسخة مصفوفة جديدة في نهاية Initialize . إذا كان لدينا بالفعل مجموعة من الحجم الصحيح. ثم نقوم بمسح محتوياته.

  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); } } } 

ما هو color32؟
تحتوي زخارف RGBA القياسية غير المضغوطة على وحدات بكسل بأربعة بايت. تتلقى كل قناة من قنوات الألوان الأربعة بايتًا ، أي أنها تحتوي على 256 قيمة محتملة. عند استخدام بنية Unity Color ، يتم تحويل مكونات الفاصلة العائمة في الفاصل الزمني 0-1 إلى وحدات بايت في الفاصل الزمني 0-255. عند أخذ العينات ، يقوم GPU بالتحويل العكسي.

تعمل بنية Color32 مباشرة مع وحدات البايت ، لذا فهي Color32 مساحة أقل ولا تتطلب التحويل ، مما يزيد من كفاءة استخدامها. نظرًا لأننا نقوم بتخزين بيانات الخلية بدلاً من الألوان ، فسيكون من المنطقي العمل مباشرة مع بيانات النسيج الخام ، وليس مع Color .

يجب أن تتعامل HexGrid مع إنشاء وتهيئة هذه الخلايا في تظليل. لذلك ، سنقوم بإضافة حقل cellShaderData وإنشاء مكون داخل Awake .

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

عند إنشاء خريطة جديدة ، يجب أيضًا 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; } 

تحرير بيانات الخلية


حتى الآن ، عند تغيير خصائص الخلية ، كان من الضروري تحديث جزء واحد أو عدة أجزاء ، ولكن قد يكون من الضروري الآن تحديث بيانات الخلايا. هذا يعني أن الخلايا يجب أن يكون لها ارتباط ببيانات الخلية في التظليل. للقيام بذلك ، قم بإضافة خاصية إلى HexCell .

  public HexCellShaderData ShaderData { get; set; } 

في HexGrid.CreateCell سنقوم بتعيين مكون بيانات تظليل لهذه الخاصية.

  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; … } 

يمكننا الآن الحصول على الخلايا لتحديث بيانات التظليل الخاصة بها. بينما نحن لا نتتبع الرؤية ، يمكننا استخدام بيانات تظليل لشيء آخر. يحدد نوع الإغاثة للخلية النسيج المستخدم لتقديمها. لا يؤثر على هندسة الخلية ، لذلك يمكننا تخزين فهرس نوع الارتفاع في بيانات الخلية ، وليس في بيانات الشبكة. سيسمح لنا هذا بالتخلص من الحاجة إلى التثليث عند تغيير نوع راحة الخلية.

أضف طريقة HexCellShaderData إلى RefreshTerrain لتبسيط هذه المهمة لخلية معينة. لنترك هذه الطريقة فارغة الآن.

  public void RefreshTerrain (HexCell cell) { } 

قم بتغيير HexCell.TerrainTypeIndex بحيث HexCell.TerrainTypeIndex هذه الطريقة ، ولا تأمر بتحديث الأجزاء.

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

سوف نطلق عليه أيضًا اسم HexCell.Load بعد استلام نوع تضاريس الخلية.

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

مؤشر الخلية


لتغيير هذه الخلايا ، نحتاج إلى معرفة فهرس الخلية. أسهل طريقة للقيام بذلك هي بإضافة خاصية Index إلى HexCell . سيشير إلى فهرس الخلية في قائمة الخلايا في الخريطة ، والذي يتوافق مع فهرسها في الخلايا المحددة في التظليل.

  public int Index { get; set; } 

هذا الفهرس موجود بالفعل في HexGrid.CreateCell ، لذا قم بتعيينه إلى الخلية التي تم إنشاؤها.

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

الآن يمكن لـ HexCellShaderData.RefreshTerrain استخدام هذا الفهرس لتحديد بيانات الخلية. لنحفظ فهرس نوع الارتفاع في مكون ألفا من بكسله ببساطة عن طريق تحويل النوع إلى بايت. وهذا سيدعم ما يصل إلى 256 نوعًا من التضاريس ، وهو ما سيكفي لنا.

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

لتطبيق البيانات على مادة Texture2D.SetPixels32 إلى GPU ، نحتاج إلى استدعاء Texture2D.SetPixels32 ، ثم Texture2D.Apply . كما هو الحال في الأجزاء ، سنقوم بتأجيل هذه العمليات على LateUpdate بحيث لا يمكن إجراؤها أكثر من مرة لكل إطار ، بغض النظر عن عدد الخلايا المتغيرة.

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

لضمان تحديث البيانات بعد إنشاء خريطة جديدة ، قم بتمكين المكون بعد التهيئة.

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

تثليث مؤشرات الخلية


نظرًا لأننا نخزن الآن مؤشر نوع الارتفاع في هذه الخلايا ، لم نعد بحاجة إلى تضمينها في عملية التثليث. ولكن من أجل استخدام بيانات الخلية ، يجب أن يعرف جهاز تظليل أي الفهارس لاستخدامها. لذلك ، تحتاج إلى تخزين مؤشرات الخلايا في بيانات الشبكة ، واستبدال مؤشرات نوع الارتفاع. بالإضافة إلى ذلك ، ما زلنا بحاجة إلى قناة اللون للشبكة لخلط الخلايا عند استخدام هذه الخلايا.

HexMesh الحقول الشائعة المتقادمة useColors و useTerrainTypes . useCellData بحقل useCellData واحد useCellData .

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

نحن terrainTypes قائمة cellIndices إلى cellIndices . لنقم أيضًا بإعادة تسمية colors إلى cellWeights - cellWeights هذا الاسم بشكل أفضل.

 // [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; 

تغيير Clear بحيث تحصل عند استخدام هذه الخلايا على قائمتين معًا ، وليس بشكل منفصل.

  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(); } 

تنفيذ نفس التجميع في 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; } } 

دعنا نزيل كل AddTriangleTerrainTypes و AddTriangleTerrainTypes . AddTriangleCellData بأساليب AddTriangleCellData المناسبة ، والتي تضيف الفهارس والأوزان في كل مرة.

  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); } 

افعل نفس الشيء في طريقة AddQuad المناسبة.

  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 التي يجب HexGridChunk . ولكن أولاً ، من أجل الاتساق ، نعيد تسمية الألوان الثابتة للأوزان.

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

لنبدأ بإصلاح TriangulateEdgeFan . كان يحتاج إلى نوع ، لكنه الآن بحاجة إلى فهرس خلية. AddTriangleColor رمز AddTriangleTerrainTypes و AddTriangleTerrainTypes برمز AddTriangleCellData المقابل.

  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); } 

تسمى هذه الطريقة في عدة أماكن. دعونا نراجعها ونتأكد من أن فهرس الخلية ينقل هناك ، وليس نوع التضاريس.

  TriangulateEdgeFan(center, e, cell.Index); 

التالي هو TriangulateEdgeStrip . كل شيء أكثر تعقيدًا هنا ، لكننا نستخدم نفس النهج. أيضا إعادة تسمية refactor أسماء المعلمات c1 و c2 إلى w1 و 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); } } 

قم بتغيير المكالمات إلى هذه الطريقة بحيث يتم تمرير فهرس الخلية إليها. نحافظ أيضًا على تناسق أسماء المتغيرات.

  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); } 

الآن ننتقل إلى طرق الزاوية. هذه التغييرات بسيطة ، ولكن يجب إجراؤها في كمية كبيرة من التعليمات البرمجية. أولاً في 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); } 

القادمة إلى 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); } 

ثم في 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); } } 

وبشكل مختلف قليلا في 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); } } 

تستخدم الطريقتان TriangulateBoundaryTriangle ، والذي يتطلب أيضًا التحديث.

  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); } 

الطريقة الأخيرة التي تحتاج إلى تغيير هي 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); … } 

لكي يعمل كل شيء ، نحتاج إلى الإشارة إلى أننا سنستخدم بيانات الخلية للعنصر الفرعي لتخفيف الجزء الجاهز.


يستخدم الإغاثة بيانات الخلية.

في هذه المرحلة ، تحتوي الشبكة على فهارس الخلايا بدلاً من مؤشرات نوع الارتفاع. نظرًا لأن تظليل الارتفاع لا يزال يفسرها كمؤشرات ارتفاع ، سنرى أن الخلية الأولى يتم تقديمها مع النسيج الأول وهكذا حتى يتم الوصول إلى نسيج الإغاثة الأخير.


استخدام مؤشرات الخلية كمؤشرات نسيج الارتفاع.

لا يمكنني الحصول على رمز إعادة التشغيل للعمل. ماذا أفعل خطأ؟
في وقت واحد ، قمنا بتغيير كمية كبيرة من رمز التثليث ، لذلك هناك احتمال كبير للأخطاء أو عمليات المراقبة. إذا لم تتمكن من العثور على الخطأ ، فحاول تنزيل الحزمة من هذا القسم واستخراج الملفات المناسبة. يمكنك استيرادها إلى مشروع منفصل ومقارنتها بالرمز الخاص بك.

نقل بيانات الخلية إلى تظليل


لاستخدام هذه الخلايا ، يجب أن يكون بإمكان جهاز تظليل التضاريس الوصول إليها. يمكن تنفيذ ذلك من خلال خاصية تظليل. سيتطلب هذا HexCellShaderData لتعيين خاصية المواد للإغاثة. أو يمكننا جعل نسيج هذه الخلايا مرئيًا عالميًا لجميع التظليل. هذا مناسب لأننا نحتاجه في العديد من التظليل ، لذلك سنستخدم هذا النهج.

بعد إنشاء نسيج الخلية ، قم باستدعاء طريقة Shader.SetGlobalTexture الثابتة لجعلها مرئية عالميًا باسم _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); } … } 

عند استخدام خاصية التظليل ، تجعل Unity حجم المادة متاحًا للتظليل عبر متغير نسيج الاسم _exelSize . هذا عبارة عن أداة ناقل مكونة من أربعة مكونات تحتوي على قيم معكوسة للعرض والارتفاع ، وكذلك العرض والارتفاع. ولكن عند تحديد الملمس العالمي ، لا يتم تنفيذ ذلك. لذلك ، سنفعل ذلك بأنفسنا باستخدام Shader.SetGlobalVector بعد إنشاء أو تغيير حجم النسيج.

  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) ); 

الوصول إلى بيانات شادر


قم بإنشاء ملف تظليل جديد في ملف المواد يسمى HexCellData . داخله ، نحدد المتغيرات للحصول على معلومات حول نسيج وحجم هذه الخلايا. نقوم أيضًا بإنشاء وظيفة للحصول على بيانات الخلية لبيانات شبكة الرأس المحددة.

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


ملف تضمين جديد.

يتم تخزين مؤشرات الخلايا في v.texcoord2 ، كما كان الحال مع أنواع التضاريس. لنبدأ بالمؤشر الأول - v.texcoord2.x . لسوء الحظ ، لا يمكننا استخدام الفهرس مباشرةً لاختبار عينة نسيج هذه الخلايا. سيتعين علينا تحويله إلى إحداثيات للأشعة فوق البنفسجية.

الخطوة الأولى في إنشاء إحداثيات U هي تقسيم فهرس الخلية حسب عرض النسيج. يمكننا القيام بذلك عن طريق _HexCellData_TexelSize.x .

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

ستكون النتيجة رقمًا في النموذج ZU ، حيث Z هو فهرس الصف و U هو إحداثيات الخلية U. يمكننا استخراج السلسلة عن طريق تقريب الرقم لأسفل ثم طرحه من الرقم للحصول على إحداثيات U.

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

الإحداثيات V تقسم الخط على ارتفاع النسيج.

 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; } 

نظرًا لأننا نأخذ عينات من النسيج ، نحتاج إلى استخدام الإحداثيات في مراكز البكسل ، وليس على حوافها. بهذه الطريقة نضمن أن يتم أخذ عينات البكسل الصحيحة. لذلك ، بعد القسمة على حجم النسيج ، أضف ½.

 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; } 

هذا يعطينا إحداثيات الأشعة فوق البنفسجية الصحيحة لمؤشر الخلية الأولى المخزنة في بيانات القمة. ولكن في الأعلى يمكن أن يكون لدينا ما يصل إلى ثلاثة مؤشرات مختلفة. لذلك ، سنجعلها GetCellDataتعمل لأي فهرس. أضف معلمة عدد صحيح إليها index، والتي سنستخدمها للوصول إلى مكون المتجه مع فهرس الخلية.

 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; } 

الآن لدينا جميع الإحداثيات اللازمة لهذه الخلايا ، يمكننا أخذ العينات _HexCellData. نظرًا لأننا نأخذ عينات من النسيج في برنامج vertex ، فنحن بحاجة إلى أن نقول للتظليل بشكل صريح أي نوع من الملمس يجب استخدامه. يمكن القيام بذلك باستخدام وظيفة tex2Dlodتتطلب إحداثيات أربعة مواد. نظرًا لأن هذه الخلايا لا تحتوي على مواد mip ، فإننا نعين قيمًا صفرية للإحداثيات الإضافية.

 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)); } 

يحتوي مكون البيانات الرابع على فهرس نوع الارتفاع ، والذي نقوم بتخزينه مباشرةً كبايت. ومع ذلك ، فإن GPU يحولها تلقائيًا إلى قيمة نقطة عائمة في النطاق 0-1. لتحويلها مرة أخرى إلى القيمة الصحيحة ، اضربها في 255. بعد ذلك ، يمكنك إرجاع البيانات.

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

لاستخدام هذه الوظيفة ، قم بتمكين HexCellData في Terrain shader . نظرًا لأنني وضعت هذا التظليل في المواد / التضاريس ، أحتاج إلى استخدام المسار النسبي ../HexCellData.cginc .

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

في برنامج القمة ، نحصل على بيانات الخلية لجميع مؤشرات الخلايا الثلاثة المخزنة في بيانات القمة. ثم قم بتعيين data.terrainمؤشرات الارتفاع الخاصة بهم.

  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; } 

عند هذه النقطة ، بدأت الخريطة مرة أخرى في عرض التضاريس الصحيحة. الاختلاف الكبير هو أن تحرير أنواع التضاريس فقط لم يعد يؤدي إلى تثليث جديد. إذا تم تغيير أي بيانات خلية أخرى أثناء التحرير ، فسيتم إجراء التثليث كالمعتاد.

حزمة الوحدة

الرؤية


بعد إنشاء أساس هذه الخلايا ، يمكننا الانتقال لدعم الرؤية. للقيام بذلك ، نستخدم التظليل والخلايا نفسها والأشياء التي تحدد الرؤية. لاحظ أن عملية التثليث لا تعرف شيئًا على الإطلاق عن هذا.

شادر


لنبدأ بإخبار جهاز تظليل التضاريس عن الرؤية. سيتلقى بيانات الرؤية من برنامج vertex ويمررها إلى برنامج التجزئة باستخدام البنية Input. نظرًا لأننا نجتاز ثلاثة مؤشرات ارتفاع منفصلة ، فإننا سنمرر ثلاث قيم رؤية أيضًا.

  struct Input { float4 color : COLOR; float3 worldPos; float3 terrain; float3 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.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; } 

تعني رؤية 0 أن الخلية غير مرئية حاليًا. إذا كانت مرئية ، فسيكون لها قيمة الرؤية 1. وبالتالي ، يمكننا تعتيم التضاريس بضرب النتيجة GetTerrainColorفي المتجه المقابل للرؤية. وبالتالي ، نقوم بتعديل لون الإغاثة لكل خلية مختلطة بشكل فردي.

  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]); } 


تحولت الخلايا إلى اللون الأسود.

ألا يمكننا بدلاً من ذلك دمج الرؤية في برنامج القمة؟
, . . . , . , .

الظلام الكامل هو تمثال نصفي للخلايا غير المرئية مؤقتًا. حتى نتمكن من رؤية الارتياح ، نحتاج إلى زيادة المؤشر المستخدم للخلايا المخفية. دعنا ننتقل من 0-1 إلى ¼ - 1 ، والذي يمكن القيام به باستخدام الوظيفة lerpفي نهاية برنامج القمة.

  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); } 


خلايا مظللة.

تتبع رؤية الخلية


لكي تعمل الرؤية ، يجب على الخلايا تتبع رؤيتها. ولكن كيف تحدد الخلية ما إذا كانت مرئية؟ يمكننا القيام بذلك عن طريق تتبع عدد الكيانات التي تراه. عندما يبدأ شخص في رؤية خلية ، يجب عليه الإبلاغ عن هذه الخلية. وعندما يتوقف شخص عن رؤية الزنزانة ، يجب عليه أيضًا إخطارها بذلك. الخلية ببساطة تتبع عدد المراقبين ، مهما كانت تلك الكيانات. إذا كان للخلية قيمة رؤية 1 على الأقل ، تكون مرئية ، وإلا فهي غير مرئية. لتطبيق هذا السلوك ، نضيف HexCellطريقتين وخاصية إلى المتغير.

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

بعد ذلك ، أضف إلى HexCellShaderDataالطريقة RefreshVisibility، التي تفعل نفس الشيء RefreshTerrain، فقط من أجل الرؤية. حفظ البيانات في المكون R لخلايا البيانات. نظرًا لأننا نعمل مع وحدات البايت التي يتم تحويلها إلى القيم من 0 إلى 1 ، فإننا نستخدمها للإشارة إلى الرؤية (byte)255.

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

سوف نسمي هذه الطريقة بزيادة الرؤية وتناقصها ، وتغيير القيمة بين 0 و 1.

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

خلق رؤية الفريق


دعونا نجعلها حتى تتمكن الوحدات من رؤية الخلية التي تشغلها. يتم تحقيق ذلك باستخدام مكالمة IncreaseVisibilityإلى الموقع الجديد للوحدة أثناء المهمة HexUnit.Location. ندعو أيضًا إلى الموقع القديم (إن وجد) 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; } } 


يمكن للوحدات معرفة مكانها.

أخيرا استخدمنا الرؤية! عند إضافتها إلى خريطة ، تجعل الوحدات خليتها مرئية. بالإضافة إلى ذلك ، يتم نقل نطاقهم عن بعد عند الانتقال إلى موقعهم الجديد. لكن نطاقها يظل نشطًا عند إزالة الوحدات من الخريطة. لإصلاح ذلك ، سنقلل من وضوح مواقعها عند تدمير الوحدات.

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

نطاق الرؤية


حتى الآن ، نرى فقط الخلية التي تقع فيها مفرزة ، وهذا يحد من الاحتمالات. على الأقل نحتاج لرؤية الخلايا المجاورة. في الحالة العامة ، يمكن للوحدات رؤية جميع الخلايا ضمن مسافة معينة ، والتي تعتمد على الوحدة.

دعنا نضيف إلى HexGridالطريقة للعثور على جميع الخلايا المرئية من خلية واحدة مع مراعاة النطاق. يمكننا إنشاء هذه الطريقة عن طريق التكرار والتغيير Search. قم بتغيير معلماته واجعله يعيد قائمة بالخلايا التي يمكنك استخدام تجمع القوائم لها.

عند كل تكرار ، تتم إضافة الخلية الحالية إلى القائمة. لم تعد هناك أي خلية نهائية ، لذلك لن ينتهي البحث أبدًا عندما يصل إلى هذه النقطة. نتخلص أيضًا من منطق التحركات وتكلفة الانتقال. جعل الخصائصPathFromلم تعد تُسأل لأننا لسنا بحاجة إليها ، ولا نريد التدخل في المسار على طول الشبكة.

في كل خطوة ، تزيد المسافة ببساطة بمقدار 1. إذا تجاوزت النطاق ، يتم تخطي هذه الخلية. ولسنا بحاجة إلى البحث عن مجريات البحث ، لذلك نقوم بتهيئته بقيمة 0. أي ، في الجوهر ، عدنا إلى خوارزمية 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; } 

ألا يمكننا استخدام خوارزمية أبسط للعثور على جميع الخلايا داخل النطاق؟
, , .

أضف أيضا HexGridالأساليب IncreaseVisibilityو DecreaseVisibility. يحصلون على الخلية والنطاق ، ويأخذون قائمة بالخلايا المقابلة ويزيدون / يقللون من رؤيتهم. عند الانتهاء ، يجب عليهم إعادة القائمة إلى مجموعتها.

  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); } 

HexUnitيتطلب استخدام هذه الطرق الوصول إلى الشبكة ، لذا أضف خاصية إليها Grid.

  public HexGrid Grid { get; set; } 

عند إضافة فرقة إلى شبكة ، سيتم تعيين شبكة لهذه الخاصية 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; } 

بادئ ذي بدء ، فإن مدى رؤية ثلاث خلايا سيكون كافيًا. للقيام بذلك ، نضيف إلى HexUnitالثابت ، والذي يمكن أن يتحول دائمًا في المستقبل إلى متغير. ثم القيام بذلك إلى انفصال تسببت لشبكة تقنيات IncreaseVisibilityو DecreaseVisibility، ويمر كما وضوحه، لم يجتز فقط في هذا المكان.

  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); } 


الوحدات ذات نطاق الرؤية التي يمكن أن تتداخل.

الرؤية عند الحركة


في الوقت الحالي ، يتم نقل منطقة رؤية الفريق بعد أمر النقل فورًا إلى نقطة النهاية. كان سيبدو أفضل إذا تحركت الوحدة ومجال الرؤية معًا. الخطوة الأولى هي أننا لن نعد خاصية Locationc HexUnit.Travel. بدلاً من ذلك ، سنقوم بتغيير الحقل مباشرة location، وتجنب رمز الملكية. لذلك ، سنقوم بمسح الموقع القديم يدويًا وتكوين موقع جديد. ستبقى الرؤية دون تغيير.

  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()); } 

داخل الكورونات ، سنقلل TravelPathمن رؤية الخلية الأولى فقط بعد الانتهاء LookAt. بعد ذلك ، قبل الانتقال إلى خلية جديدة ، سنزيد الرؤية من هذه الخلية. بعد الانتهاء من هذا ، قللنا من ظهوره مرة أخرى. أخيرًا ، قم بزيادة الرؤية من الخلية الأخيرة.

  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) { … } … } 


الرؤية أثناء الحركة.

كل هذا يعمل ، باستثناء عندما يتم إصدار أمر جديد في الوقت الذي تتحرك فيه مفرزة. هذا يؤدي إلى النقل الفوري ، والذي يجب أن ينطبق أيضًا على الرؤية. لتحقيق ذلك ، نحتاج إلى تتبع الموقع الحالي للفريق أثناء التحرك.

  HexCell location, currentTravelLocation; 

سنقوم بتحديث هذا الموقع في كل مرة نصل فيها إلى خلية جديدة أثناء التحرك ، حتى تصل الفرقة إلى الخلية الأخيرة. ثم يجب إعادة تعيينه.

  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; … } 

الآن بعد إكمال التسليم ، TravelPathيمكننا التحقق مما إذا كان الموقع الوسيط القديم للمسار معروفًا أم لا. إذا كانت الإجابة بنعم ، فأنت بحاجة إلى تقليل الرؤية في هذه الخلية ، وليس في بداية المسار.

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

نحتاج أيضًا إلى تصحيح الرؤية بعد إعادة التجميع التي حدثت أثناء حركة الفرقة. إذا كان الموقع الوسيط لا يزال معروفًا ، فقم بتقليل الرؤية فيه وزيادة الرؤية عند نقطة النهاية ، ثم قم بإعادة تعيين الموقع الوسيط.

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

حزمة الوحدة

وضوح الطرق والمياه


على الرغم من أن تغيرات ألوان الإغاثة تعتمد على الرؤية ، إلا أن هذا لا يؤثر على الطرق والمياه. تبدو مشرقة للغاية بالنسبة للخلايا غير المرئية. لتطبيق الرؤية على الطرق والمياه ، نحتاج إلى إضافة مؤشرات الخلايا ومزج الأوزان إلى بيانات الشبكة الخاصة بها. لذلك تحقق العناصر التابعة لل بيانات استخدام خلية لل أنهار ، الطرق ، المياه ، شور المياه و مصبات جزء الجاهزة.

الطرق


سنبدأ من الطرق. تُستخدم الطريقة HexGridChunk.TriangulateRoadEdgeلإنشاء جزء صغير من الطريق في منتصف الخلية ، لذا فهي تحتاج إلى فهرس خلية واحد. أضف معلمة إليه وقم بإنشاء بيانات الخلية للمثلث.

  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); } 

طريقة أخرى سهلة لإنشاء الطرق TriangulateRoadSegment. يتم استخدامه داخل الخلايا وفيما بينها ، لذلك يجب أن يعمل مع فهرسين مختلفين. لهذا ، من الملائم استخدام معلمة متجه الفهرس. نظرًا لأن أجزاء الطريق يمكن أن تكون أجزاء من الحواف ، يجب أيضًا تمرير الأوزان من خلال المعلمات.

  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); } 

الآن دعنا ننتقل إلى TriangulateRoadالأمر الذي يخلق الطرق داخل الخلايا. يحتاج أيضا إلى معلمة فهرس. يمرر هذه البيانات إلى طرق الطريق التي يسميها ، ويضيفها إلى المثلثات التي ينشئها.

  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); } } 

بقي أن أضيف الحجج طريقة المطلوبة TriangulateRoad، TriangulateRoadEdgeو TriangulateRoadSegmentلتصحيح كافة الأخطاء مترجم.

  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 ); } } 

الآن بيانات الشبكة صحيحة ، وسننتقل إلى تظليل الطريق . يحتاج برنامج vertex ويجب أن يحتوي على HexCellData .

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

نظرًا لأننا لا نمزج العديد من المواد ، فسيكون كافياً بالنسبة لنا لتمرير مؤشر واحد للرؤية إلى برنامج الجزء.

  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); 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) { float4 noise = tex2D(_MainTex, IN.worldPos.xz * 0.025); fixed4 c = _Color * ((noise.y * 0.75 + 0.25) * IN.visibility); … } 


الطرق ذات الرؤية.

المياه المفتوحة


قد يبدو أن الرؤية قد أثرت بالفعل على المياه ، ولكن هذا هو مجرد سطح تضاريس مغمورة في الماء. لنبدأ بتطبيق الرؤية على المياه المفتوحة. لهذا نحن بحاجة إلى التغيير 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 ); } } } 

نحتاج أيضًا إلى إضافة بيانات الخلية إلى مراوح المثلثات بالقرب من السواحل.

  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 Water بنفس الطريقة مثل Shader Road ، ولكنه يحتاج إلى الجمع بين رؤية ليس خليتين ، ولكن ثلاث خلايا.

  #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; … } 


المياه المفتوحة مع الرؤية.

الساحل والمصب


لدعم الساحل ، نحتاج إلى التغيير مرة أخرى HexGridChunk.TriangulateWaterShore. لقد أنشأنا بالفعل متجهًا فهرسًا ، لكننا استخدمنا فهرس خلية واحد فقط للمياه المفتوحة. يحتاج الساحل أيضًا إلى فهرس الجوار ، لذا قم بتغيير الرمز.

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

إضافة بيانات الخلية إلى الكواد ومثلث الساحل. نقوم بتمرير الفهارس على المكالمة 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 ); } 

إضافة المعلمة اللازمة TriangulateEstuaryوالعناية بهذه الخلايا للسواحل والفم. لا تنس أن الفم مصنوع من شبه منحرف مع مثلثين من الساحل على الجانبين. نتأكد من نقل الأوزان بالترتيب الصحيح.

  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); … } 

في تظليل WaterShore ، تحتاج إلى إجراء نفس التغييرات كما هو الحال في تظليل الماء ، وخلط رؤية الخلايا الثلاث.

  #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; … } 

شايدر مصب يمزج الرؤية من خليتين، وكذلك تظليل الوصف الطريق العام . لديه بالفعل برنامج قمة ، لأننا نحتاج إليه لإرسال إحداثيات الأشعة فوق البنفسجية للأنهار.

  #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; … } 


الساحل والمصب مع الرؤية.

الأنهار


آخر مناطق المياه للعمل معها هي الأنهار. أضف HexGridChunk.TriangulateRiverQuadمتجه فهرس إلى المعلمة وأضفه إلى الشبكة حتى يتمكن من الحفاظ على رؤية خليتين.

  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); } 

TriangulateWithRiverBeginOrEndيخلق نقاط نهاية نهرية مع رباعي ومثلث في وسط الخلية. أضف بيانات الخلية اللازمة لذلك.

  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); } } 

لدينا بالفعل مؤشرات الخلايا هذه TriangulateWithRiver، لذلك نقوم فقط بتمريرها على المكالمة 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 ); } } 

كما نضيف دعمًا قياسيًا للشلالات التي تصب في المياه العميقة.

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

وأخيرًا ، قم بتغييره TriangulateConnectionبحيث يمرر الفهارس اللازمة لأساليب الأنهار والشلالات.

  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 ); } } … } 

يحتاج River Shader إلى إجراء نفس التغييرات مثل Shader Road .

  #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; … } 


الأنهار برؤية.

حزمة الوحدة

الأشياء والرؤية


تعمل الرؤية الآن على كامل التضاريس التي تم إنشاؤها إجرائيًا ، لكنها حتى الآن لا تؤثر على ميزات التضاريس. يتم إنشاء المباني والمزارع والأشجار من المباني الجاهزة ، وليس من الهندسة الإجرائية ، لذلك لا يمكننا إضافة مؤشرات الخلايا ومزج الأوزان مع رؤوسهم. نظرًا لأن كل من هذه الكائنات ينتمي إلى خلية واحدة فقط ، نحتاج إلى تحديد الخلية التي توجد فيها. إذا استطعنا القيام بذلك ، فسوف نحصل على بيانات الخلايا المقابلة ونطبق الرؤية.

يمكننا بالفعل تحويل مواقع XZ للعالم إلى مؤشرات خلوية. تم استخدام هذا التحول لتحرير التضاريس وإدارة الفرق. ومع ذلك ، فإن الرمز المقابل غير بديهي. يستخدم عمليات عدد صحيح ويتطلب المنطق للعمل مع الحواف. هذا غير عملي بالنسبة للتظليل ، لذلك يمكننا خبز الجزء الأكبر من المنطق في نسيج واستخدامه.

نحن نستخدم بالفعل نسيجًا بنمط سداسي لإسقاط الشبكة فوق التضاريس. يحدد هذا النسيج مساحة الخلية 2 × 2. لذلك ، يمكننا بسهولة الحساب في أي منطقة نحن. بعد ذلك ، يمكنك تطبيق نسيج يحتوي على تعويضات X و Z للخلايا في هذه المنطقة واستخدام هذه البيانات لحساب الخلية التي نحن فيها.

هنا نسيج مماثل. يتم تخزين الإزاحة X في قناتها الحمراء ، ويتم تخزين الإزاحة Z في القناة الخضراء. نظرًا لأنها تغطي مساحة خلايا 2 × 2 ، نحتاج إلى تعويضات من 0 و 2. لا يمكن تخزين هذه البيانات في قناة الألوان ، لذلك يتم تقليل الإزاحات بمقدار النصف. نحن لا نحتاج إلى حواف واضحة للخلايا ، لذلك يكفي نسيج صغير.


نسيج إحداثيات الشبكة.

أضف نسيج للمشروع. اضبط وضع الالتفاف على التكرار ، تمامًا مثل نسيج الشبكة الآخر. نحن لا نحتاج إلى أي خلط ، لذلك في وضع Blend ، سنختار Point . قم أيضًا بإيقاف الضغط حتى لا يتم تشويه البيانات. قم بإيقاف تشغيل وضع sRGB بحيث لا يتم إجراء تحويلات لمساحة الألوان عند العرض في الوضع الخطي. وأخيرًا ، لا نحتاج إلى مواد mip.


خيارات استيراد الملمس.

تظليل الكائن مع الرؤية


قم بإنشاء تظليل ميزة جديد لإضافة دعم الرؤية للكائنات. هذا هو تظليل سطح بسيط مع برنامج قمة. أضف HexCellData إليه وقم بتمرير مؤشر الرؤية إلى برنامج الجزء ، وكالعادة ، اعتبره بالألوان. الاختلاف هنا هو أنه لا يمكننا استخدامها GetCellDataلعدم وجود بيانات الشبكة المطلوبة. بدلاً من ذلك ، لدينا موقع في العالم. ولكن في الوقت الحالي ، اترك مستوى الرؤية يساوي 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" } 

قم بتغيير جميع مواد الكائنات بحيث تستخدم تظليل جديد وتعيين نسيج إحداثيات الشبكة لها.


الحضرية مع نسيج شبكي.

الوصول إلى بيانات الخلية


لأخذ عينة من إحداثيات الشبكة في برنامج القمة ، نحتاج مرة أخرى إلى tex2Dlodناقل إحداثيات مكون من أربعة مكونات. الإحداثيات الأولى والثانية هي موقع عالم XZ. الاثنان الآخران يساويان الصفر كما كان من قبل.

  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; } 

كما هو الحال في تظليل التضاريس ، نقوم بتمديد إحداثيات الأشعة فوق البنفسجية بحيث يكون للنسيج نسبة العرض إلى الارتفاع المناسبة لشبكة السداسيات.

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

يمكننا معرفة أي جزء من خلايا 2 × 2 التي نأخذها عن طريق أخذ قيمة إحداثيات الأشعة فوق البنفسجية مقربة. هذا يشكل الأساس لإحداثيات الخلايا.

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

للعثور على إحداثيات الخلية التي نحن فيها ، نضيف الإزاحة المخزنة في النسيج.

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

نظرًا لأن جزءًا من الشبكة يبلغ حجمه 2 × 2 ، ويتم تعويض التعويضات إلى النصف ، فنحن بحاجة إلى مضاعفة النتيجة للحصول على الإحداثيات النهائية.

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

الآن لدينا إحداثيات XZ لشبكة الخلايا التي نحتاج إلى تحويلها إلى إحداثيات الأشعة فوق البنفسجية لهذه الخلايا. يمكن القيام بذلك عن طريق الانتقال ببساطة إلى مراكز البكسل ثم تقسيمها إلى أحجام نسيج. لذا دعنا نضيف دالة لهذا إلى ملف HexCellData الذي سيتناول أيضًا أخذ العينات.

 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)); } 

الآن يمكننا استخدام هذا في برنامج قمة تظليل ميزة .

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


كائنات ذات رؤية.

أخيرًا ، تؤثر الرؤية على الخريطة بأكملها ، باستثناء الوحدات التي تكون مرئية دائمًا. نظرًا لأننا نحدد رؤية الكائنات لكل قمة ، ثم بالنسبة للكائن الذي يعبر حدود الخلية ، فإن رؤية الخلايا التي يتم إغلاقها سيتم مزجها. لكن الأجسام صغيرة جدًا بحيث تبقى باستمرار داخل زنزانتها ، حتى مع مراعاة تشويه المواضع. ومع ذلك ، قد يكون البعض جزءًا من القمم في خلية أخرى. لذلك ، نهجنا رخيص ، لكنه غير كامل. هذا هو أكثر ما يلاحظ في حالة الجدران ، والتي تختلف رؤيتها بين رؤى الخلايا المجاورة.


الجدران ذات الرؤية المتغيرة.

نظرًا لأن مقاطع الجدار يتم إنشاؤها إجرائيًا ، يمكننا إضافة بيانات الخلية إلى شبكتها واستخدام النهج الذي استخدمناه للتخفيف. لسوء الحظ ، فإن الأبراج سابقة التجهيز ، لذلك لا يزال لدينا تناقضات. بشكل عام ، يبدو المنهج الحالي جيدًا بما يكفي للهندسة البسيطة التي نستخدمها. في المستقبل ، سننظر في نماذج وجدران أكثر تفصيلاً ، لذلك ، سنحسن طريقة مزج رؤيتها.

حزمة الوحدة

الجزء 21: بحث الخرائط


  • نعرض كل شيء أثناء التحرير.
  • نحن نتتبع الخلايا التي تم التحقيق فيها.
  • نخفي ما لا يزال مجهولا.
  • نحن نجبر الوحدات على تجنب المناطق غير المستكشفة.

في الجزء السابق ، أضفنا ضباب الحرب ، والذي سنقوم الآن بتنقيحه لتنفيذ أبحاث الخرائط.


نحن مستعدون لاستكشاف العالم.

اعرض الخريطة بالكامل في وضع التحرير


معنى الدراسة هو أنه حتى لا ترى الخلايا تعتبر غير معروفة ، وبالتالي غير مرئية. لا يجب حجبها ، ولكن لا يتم عرضها على الإطلاق. لذلك ، قبل إضافة دعم البحث ، سنقوم بتمكين الرؤية في وضع التحرير.

تبديل الرؤية


يمكننا التحكم فيما إذا كان التظليل يستخدم الرؤية باستخدام الكلمة الرئيسية ، كما حدث مع التراكب على الشبكة. لنستخدم الكلمة الرئيسية HEX_MAP_EDIT_MODE للإشارة إلى حالة وضع التحرير. نظرًا لأن العديد من التظليل يجب أن يعرفوا عن هذه الكلمة الرئيسية ، فسوف نحددها عالميًا باستخدام الأساليب الثابتة Shader.EnableKeyWordو Shader.DisableKeyword. سنستدعي الطريقة المناسبة HexGameUI.SetEditModeعند تغيير وضع التحرير.

  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"); } } 

تعديل تظليل الوضع


عند تحديد HEX_MAP_EDIT_MODE ، ستتجاهل التظليل الرؤية. هذا يتلخص في حقيقة أن رؤية الخلية ستعتبر دائمًا مساوية لـ 1. دعنا نضيف وظيفة لتصفية بيانات الخلايا اعتمادًا على الكلمة الرئيسية في بداية ملف التضمين HexCellData .

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

نقوم بتمرير هذه الوظيفة نتيجة كلتا الوظيفتين GetCellDataقبل إعادتها.

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

لكي يعمل كل شيء ، يجب أن يتلقى جميع أجهزة التظليل ذات الصلة التوجيه متعدد_الملفات لإنشاء خيارات في حالة تحديد الكلمة الرئيسية HEX_MAP_EDIT_MODE . أضف السطر المناسب في تظليل مصب ، ميزة ، نهر ، الطريق ، التضاريس ، المياه و الشاطئ المياه ، بين الهدف وأول توجيه التوجيهي تشمل.

  #pragma multi_compile _ HEX_MAP_EDIT_MODE 

الآن ، عند التبديل إلى وضع تحرير الخريطة ، سيختفي ضباب الحرب.

حزمة الوحدة

البحث الخلوي


بشكل افتراضي ، يجب اعتبار الخلايا غير مستكشفة. يتم استكشافهم عندما تراهم فرقة. بعد ذلك ، يستمرون في التحقيق إذا كان بإمكان مفرزة رؤيتهم.

تتبع حالة الدراسة


لإضافة دعم لرصد حالة الدراسات ، نضيف إلى HexCellالممتلكات العامة IsExplored.

  public bool IsExplored { get; set; } 

يتم تحديد حالة الدراسة من قبل الخلية نفسها. لذلك ، يجب تعيين هذه الخاصية فقط HexCell. لإضافة هذا التقييد ، سنقوم بتعيين الضابط خاص.

  public bool IsExplored { get; private set; } 

في المرة الأولى التي تصبح فيها رؤية الخلية أكبر من الصفر ، تبدأ الخلية في التحقيق ، وبالتالي IsExploredيجب تعيين قيمة true. في الواقع ، سيكون كافياً بالنسبة لنا ببساطة وضع علامة على الخلية على أنها تم فحصها عند زيادة الرؤية إلى 1. يجب أن يتم ذلك قبل المكالمة RefreshVisibility.

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

نقل حالة البحث إلى تظليل


كما هو الحال في رؤية الخلايا ، ننقل حالة البحث الخاصة بهم إلى تظليل من خلال بيانات تظليل. في النهاية ، إنه مجرد نوع آخر من الرؤية. HexCellShaderData.RefreshVisibilityيخزن حالة الرؤية في قناة البيانات ر. دعونا نحافظ على حالة الدراسة في بيانات القناة G.

  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; } 

راحة سوداء غير مستكشفة


يمكننا الآن استخدام أجهزة تظليل لتصور حالة أبحاث الخلايا. للتأكد من أن كل شيء يعمل كما ينبغي ، نحن فقط نجعل التضاريس غير المستكشفة سوداء. ولكن أولاً ، لجعل وضع التحرير يعمل ، قم بتغييره FilterCellDataبحيث يقوم بتصفية بيانات البحث.

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

يقوم جهاز تظليل التضاريس بتمرير بيانات الرؤية للخلايا الثلاث الممكنة إلى برنامج الأجزاء. في حالة البحث ، نقوم بدمجها في برنامج القمة وننقل القيمة الوحيدة إلى برنامج التجزئة. أضف visibilityالمكون الرابع إلى المدخلات بحيث يكون لدينا مكان لذلك.

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

الآن ، في برنامج القمة ، عندما نغير مؤشر الرؤية ، يجب أن نصل صراحة data.visibility.xyz.

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

بعد ذلك ، نجمع بين حالات الدراسة ونكتب النتيجة data.visibility.w. هذا مشابه للجمع بين الرؤية في تظليل أخرى ، ولكن باستخدام المكون Y لهذه الخلايا.

  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; 

حالة البحث متاحة الآن في برنامج الجزء من خلال IN.visibility.w. اعتبرها في حساب البياض.

  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; } 


التضاريس غير المكتشفة أصبحت الآن سوداء.

إن تخفيف الخلايا غير المكتشفة أصبح الآن لونه أسود. لكن هذا لم يؤثر بعد على الأشياء والطرق والمياه. ومع ذلك ، هذا يكفي للتأكد من أن الدراسة تعمل.

حفظ وتحميل حالة البحث


الآن بعد أن أضفنا دعم البحث ، نحتاج إلى التأكد من أخذ حالة البحث بعين الاعتبار عند حفظ الخرائط وتحميلها. لذلك ، نحتاج إلى زيادة إصدار ملفات الخرائط إلى 3. لجعل هذه التغييرات أكثر ملاءمة ، دعنا نضيف SaveLoadMenuثابتًا لذلك .

  const int mapFileVersion = 3; 

سنستخدم هذا الثابت عند كتابة إصدار الملف إلى Saveوعند التحقق من دعم الملف 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); } } } 

كخطوة أخيرة ، HexCell.Saveنسجل حالة الدراسة.

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

وسنقرأه في النهاية Load. بعد ذلك ، سنتصل RefreshVisibilityفي حالة اختلاف حالة الدراسة عن الحالة السابقة.

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

للحفاظ على التوافق مع ملفات الحفظ القديمة ، نحتاج إلى تخطي قراءة حالة الحفظ إذا كان إصدار الملف أقل من 3. في هذه الحالة ، بشكل افتراضي ، سيكون للخلايا الحالة "غير مستكشفة". للقيام بذلك ، نحتاج إلى إضافة Loadبيانات رأس كمعلمة .

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

الآن HexGrid.Loadيجب أن تمر في HexCell.Loadبيانات الرأس.

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

الآن ، عند حفظ وتحميل الخرائط ، سيتم أخذ حالة استكشاف الخلايا بعين الاعتبار.

حزمة الوحدة

إخفاء الخلايا غير المعروفة


في المرحلة الحالية ، يتم تحديد الخلايا غير المستكشفة بصريًا بواسطة ارتياح أسود. لكن في الواقع ، نريد أن تكون هذه الخلايا غير مرئية لأنها غير معروفة. يمكننا أن نجعل الهندسة غير الشفافة شفافة بحيث لا تكون مرئية. ومع ذلك ، تم تطوير إطار تظليل سطح الوحدة دون وضع هذه الإمكانية في الاعتبار. بدلاً من استخدام الشفافية الحقيقية ، سنقوم بتغيير الظلال لتتناسب مع الخلفية ، مما سيجعلها أيضًا غير مرئية.

جعل الإغاثة سوداء حقا


على الرغم من أن الإغاثة المدروسة سوداء ، لا يزال بإمكاننا التعرف عليها لأنها لا تزال تحتوي على إضاءة براق. للتخلص من الإضاءة ، نحتاج إلى جعلها سوداء غير لامعة تمامًا. لكي لا تؤثر على خصائص السطح الأخرى ، من الأسهل تغيير لون المرآة إلى الأسود. هذا ممكن إذا كنت تستخدم تظليل سطح يعمل مع براق ، ولكن الآن نستخدم المعدني القياسي. لنبدأ من خلال تبديل جهاز تظليل Terrain إلى منظاري.

استبدال الممتلكات اللون _Metallic على الممتلكات _Specular . بشكل افتراضي ، يجب أن تكون قيمة لونه تساوي (0.2 ، 0.2 ، 0.2). لذلك نحن نضمن أنها ستطابق مظهر النسخة المعدنية.

  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) } 

قم أيضًا بتغيير متغيرات التظليل المقابلة. يتم تعريف لون تظليل السطح المرآوي على أنه fixed3، لذلك دعونا نستخدمه.

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

تغيير تصفح سطح pragma من Standard إلى StandardSpecular . هذا سيجبر الوحدة على توليد تظليل باستخدام براق.

  #pragma surface surf StandardSpecular fullforwardshadows vertex:vert 

surfتحتاج الوظيفة الآن إلى المعلمة الثانية من النوع SurfaceOutputStandardSpecular. بالإضافة إلى ذلك ، تحتاج الآن إلى تعيين القيمة لا o.Metallic، ولكن 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; } 

الآن يمكننا إخفاء النقاط البارزة من خلال النظر في exploredاللون المرآوي.

  o.Specular = _Specular * explored; 


تضاريس غير مستكشفة بدون إضاءة عاكسة.

كما ترى في الصورة ، تبدو النقوش غير المكتشفة الآن سوداء باهتة. ومع ذلك ، عند عرضه بزاوية مماسة ، تتحول الأسطح إلى مرآة ، والتي بسببها يبدأ الارتخاء في عكس البيئة ، أي صندوق السماء.

لماذا تصبح الأسطح مرايا؟
. . Rendering .


لا تزال المناطق غير المستكشفة تعكس البيئة.

للتخلص من هذه الانعكاسات ، سننظر في الإغاثة غير المستكشفة مظللة تمامًا. يتم تحقيق ذلك عن طريق تعيين قيمة exploredلمعلمة الإطباق ، والتي نستخدمها كقناع انعكاس.

  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; 


غير مستكشفة بدون تأملات.

مطابقة الخلفية


الآن بعد أن تتجاهل التضاريس غير المستكشفة جميع الإضاءة ، تحتاج إلى جعلها تتناسب مع الخلفية. نظرًا لأن الكاميرا الخاصة بنا تبدو دائمًا من الأعلى ، فإن الخلفية دائمًا ما تكون رمادية. لإخبار أداة Terrain shader عن اللون الذي يجب استخدامه ، أضف خاصية _BackgroundColor ، التي يتم تعيينها افتراضيًا إلى اللون الأسود.

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

لاستخدام هذا اللون ، سنضيفه كضوء ينبعث منه. يتم o.Emissionتحقيق ذلك عن طريق تعيين قيمة لون الخلفية مضروبة في ناقص واحد تم استكشافه.

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

نظرًا لأننا نستخدم صندوق Skybox الافتراضي ، فإن لون الخلفية المرئي ليس هو نفسه في الواقع. بشكل عام ، يكون اللون الرمادي المحمر قليلاً هو أفضل لون. عند إعداد مواد الإغاثة ، يمكنك استخدام الرمز 68615BFF لـ Hex Color .


مواد الإغاثة بلون الخلفية الرمادية.

بشكل عام ، يعمل هذا ، على الرغم من أنك إذا كنت تعرف إلى أين تبحث ، ستلاحظ الصور الظلية ضعيفة للغاية. حتى لا يتمكن اللاعب من رؤيتها ، يمكنك تعيين لون خلفية موحد يبلغ 68615BFF للكاميرا بدلاً من skybox.


كاميرا بلون خلفية موحد.

لماذا لا تقم بإزالة صندوق Skybox؟
, , environmental lighting . , .

الآن لا يمكننا العثور على الفرق بين الخلفية والخلايا غير المستكشفة. لا يزال بإمكان التضاريس العالية غير المكتشفة أن تحجب التضاريس المنخفضة المستكشفة بزوايا الكاميرا المنخفضة. بالإضافة إلى ذلك ، لا تزال الأجزاء غير المستكشفة تلقي بظلالها على المستكشف. ولكن يمكن تجاهل هذه القرائن الدنيا.


الخلايا غير المستكشفة لم تعد مرئية.

ماذا لو كنت لا تستخدم لون خلفية موحد؟
, , . . , . , , , UV- .

إخفاء كائنات الإغاثة


الآن لدينا فقط شبكة الإغاثة المخفية. ما تبقى من حالة الدراسة لم تتأثر بعد.


حتى الآن ، تم إخفاء الإغاثة فقط.

دعنا نغير Shader Feature ، وهو تظليل غير شفاف مثل Terrain . حولها إلى تظليل براق وأضف لون الخلفية إليها. لنبدأ بالخصائص.

  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" {} } 

مزيد من السطح والمتغيرات براغما ، كما كان من قبل.

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

visibilityمطلوب مكون واحد آخر أيضا. نظرًا لأن الميزة تجمع بين الرؤية لكل قمة ، فإنها تحتاج فقط إلى قيمة تعويم واحدة. الآن نحن بحاجة إلى اثنين.

  struct Input { float2 uv_MainTex; float2 visibility; }; 

قم بتغييره vertبحيث يستخدم بشكل صريح لبيانات الرؤية data.visibility.x، ثم قم بتعيين data.visibility.yقيمة بيانات الدراسة.

  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; } 

قم بتغييره surfبحيث يستخدم البيانات الجديدة ، مثل 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; } 


كائنات الإغاثة المخفية.

إخفاء المياه


فيما يلي تظليل المياه و الشاطئ المياه . لنبدأ بتحويلها إلى تظليل براق. ومع ذلك ، لا يحتاجون إلى لون خلفية لأنهم تظليل شفاف.

بعد التحويل ، أضف visibilityمكونًا آخر وقم بتغييره وفقًا لذلك vert. يجمع كلا الظلال بيانات من ثلاث خلايا.

  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; } 

تقوم Water and Water Shore بعمليات surfمختلفة ، ولكنها تحدد خصائصها السطحية بنفس الطريقة. نظرًا لأنها شفافة ، سنأخذ في الاعتبار exploreفي قناة ألفا ، ولن نقوم بتعيين الانبعاثات.

  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; } 


المياه الخفية.

نخفي مصبات الأنهار والأنهار والطرق


بقينا تظليل مصب ، نهر و الوصف الطريق العام . الثلاثة جميعها شفافة وتجمع بيانات خليتين. قم بتحويلها جميعًا إلى specular ، ثم أضفها إلى visibilityبيانات البحث.

  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; } 

تعديل الدالة surfتظليل مصب و نهر بحيث يستخدم البيانات الجديدة. كلاهما بحاجة إلى إجراء نفس التغييرات.

  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 قليلاً لأنه يستخدم مقياس مزج إضافي.

  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; } 


كل شيء مخفي.

حزمة الوحدة

تجنب الخلايا غير المستكشفة


على الرغم من أن كل شيء غير معروف مخفي بصريًا ، في حين أن حالة الدراسة لا تؤخذ في الاعتبار عند البحث عن مسار. ونتيجة لذلك ، يمكن أن تأمر الوحدات بالتحرك من خلال الخلايا غير المستكشفة وعبرها ، وتحديد طريقة التنقل بطريقة سحرية. نحن بحاجة إلى قوة الوحدات لتجنب الخلايا غير المستكشفة.


التنقل في الخلايا غير المستكشفة.

تحدد الفرق تكلفة النقل


قبل معالجة الخلايا غير المستكشفة ، دعنا نعيد الرمز لنقل تكلفة الانتقال من HexGridإلى HexUnit. سيؤدي ذلك إلى تبسيط الدعم للوحدات ذات قواعد الحركة المختلفة.

أضف إلى HexUnitالطريقة العامة GetMoveCostلتحديد تكلفة النقل. إنه بحاجة إلى معرفة الخلايا التي تتحرك بينها ، وكذلك الاتجاه. نقوم بنسخ الكود المقابل لتكاليف الانتقال من HexGrid.Searchهذه الطريقة وتغيير أسماء المتغيرات.

  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; } } 

يجب أن تعيد الطريقة تكلفة النقل. استخدمت الرمز القديم لتخطي التحركات غير الصالحة continue، ولكن هذا النهج لن يعمل هنا. إذا كانت الحركة غير ممكنة ، فسنعيد التكاليف السلبية للتحرك.

  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; } 

الآن نحن بحاجة إلى معرفة عند العثور على المسار ، ليس فقط السرعة ، ولكن أيضًا الوحدة المحددة. حسب التغيير HexGameUI.DoPathFinding.

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

نظرًا لأننا ما زلنا بحاجة إلى الوصول إلى سرعة الفريق ، فسنضيف إلى HexUnitالموقع Speed. بينما ستعود بقيمة ثابتة 24.

  public int Speed { get { return 24; } } 

و HexGridالتغيير FindPath، و Searchحتى يتمكنوا من العمل مع نهجنا الجديد.

  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; … } 

الآن سنزيل من Searchالكود القديم الذي حدد ما إذا كان من الممكن الانتقال إلى الخلية التالية وما هي تكاليف النقل. بدلا من ذلك ، سنتصل HexUnit.IsValidDestinationو HexUnit.GetMoveCost. سوف نتخطى الخلية إذا كانت تكلفة النقل سالبة.

  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; } … } 

تجاوز المناطق غير المستكشفة


لتجنب الخلايا غير المستكشفة ، يكفي لنا التأكد من HexUnit.IsValidDestinationالتحقق مما إذا كانت الخلية قد تم فحصها.

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


لن تتمكن المزيد من الوحدات من الوصول إلى الخلايا غير المكتشفة.

نظرًا لأن الخلايا غير المكتشفة لم تعد نقاطًا نهائية صالحة ، ستتجنبها الفرق عند الانتقال إلى نقطة النهاية. أي أن المناطق غير المستكشفة تعمل كحواجز تطيل المسار أو تجعله مستحيلاً. سيتعين علينا تقريب الوحدات من منطقة غير معروفة لاستكشاف المنطقة أولاً.

ماذا لو ظهر مسار أقصر أثناء الحركة؟
. , . .

, , . , .

حزمة الوحدة

الجزء 22: رؤية محسنة


  • تغيير الرؤية بسلاسة.
  • استخدم ارتفاع الخلية لتحديد النطاق.
  • إخفاء حافة الخريطة.

من خلال إضافة دعم لاستكشاف الخرائط ، سنعمل على تحسين الحسابات والانتقالات في النطاق.


لرؤية المزيد ، تسلق أعلى.

انتقالات الرؤية


الخلية إما مرئية أو غير مرئية ، لأنها إما في نطاق مفرزة أم لا. حتى إذا كان يبدو أن الأمر يستغرق وحدة من الوقت للتنقل بين الخلايا ، فإن نطاقها ينتقل من خلية إلى أخرى على الفور. ونتيجة لذلك ، تتغير رؤية الخلايا المحيطة بشكل كبير. تبدو حركة الفرقة سلسة ، لكن التغييرات في الرؤية مفاجئة.

من الناحية المثالية ، يجب أن تتغير الرؤية أيضًا بسلاسة. مرة واحدة في مجال الرؤية ، يجب أن تضيء الخلايا تدريجيا ، وتركها ، قتامة تدريجيا. أو ربما تفضل التحولات الفورية؟ دعنا نضيف إلى HexCellShaderDataالخاصية التي تقوم بتبديل التحولات الفورية. بشكل افتراضي ، ستكون الانتقالات سلسة.

  public bool ImmediateMode { get; set; } 

تتبع الخلية الانتقالية


حتى عند عرض التحولات السلسة ، تظل بيانات الرؤية الحقيقية ثنائية ، أي أن التأثير مرئي فقط. هذا يعني أنه يجب التعامل مع انتقالات الرؤية HexCellShaderData. سنقدم لها قائمة بالخلايا التي يتم فيها إجراء الانتقال. تأكد من أنها فارغة عند كل تهيئة.

 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; } … } 

في الوقت الحالي ، نقوم بتعيين بيانات الخلية RefreshVisibilityمباشرة. لا يزال هذا صحيحًا لوضع الانتقال الفوري ، ولكن عندما يتم تعطيله ، يجب أن نضيف خلية إلى قائمة الخلايا الانتقالية.

  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; } 

يبدو أن الرؤية لم تعد تعمل ، لأننا في الوقت الحالي لا نفعل أي شيء مع الخلايا في القائمة.

حلقة عبر الخلايا في حلقة


بدلاً من تعيين القيم المقابلة على الفور إلى 255 أو 0 ، سنقوم بزيادة / تقليل هذه القيم تدريجيًا. تعتمد سلاسة الانتقال على معدل التغيير. لا يجب أن تكون سريعة جدًا وليست بطيئة جدًا. حل وسط جيد بين التحولات الجميلة وراحة اللعبة هو التغيير في غضون ثانية واحدة. دعنا نضع ثابتًا لهذا لتسهيل التغيير.

  const float transitionSpeed = 255f; 

الآن LateUpdateيمكننا تحديد دلتا المطبقة على القيم. للقيام بذلك ، اضرب دلتا الوقت بالسرعة. يجب أن يكون عددًا صحيحًا لأننا لا نعرف حجمه. يمكن أن يؤدي الانخفاض الحاد في معدل الإطارات إلى جعل الدلتا أكثر من 255.

بالإضافة إلى ذلك ، نحتاج إلى التحديث أثناء وجود خلايا انتقالية. لذلك ، يجب تضمين الرمز أثناء وجود شيء في القائمة.

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

أيضا من الناحية النظرية معدلات الإطار عالية جدا. بالاقتران مع سرعة انتقال منخفضة ، يمكن أن يعطينا هذا دلتا 0. لكي يحدث التغيير ، نفرض أن يكون الحد الأدنى للدلتا 1.

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

بعد تلقي الدلتا ، يمكننا الالتفاف حول جميع الخلايا الانتقالية وتحديث بياناتها. لنفترض أن لدينا طريقة لذلك UpdateCellData، ومعلماتها هي الخلية المقابلة والدلتا.

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

في مرحلة ما ، يجب أن يكتمل انتقال الخلية. افترض أن الطريقة تقوم بإرجاع معلومات حول ما إذا كان الانتقال لا يزال مستمرًا. عندما تتوقف ، يمكننا إزالة الخلية من القائمة. بعد ذلك ، يجب علينا إنقاص المكرر حتى لا نتخطى الخلايا.

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

الترتيب الذي تتم فيه معالجة الخلايا الانتقالية ليس مهمًا. لذلك ، لا يتعين علينا حذف الخلية في الفهرس الحالي ، مما سيجبر RemoveAtجميع الخلايا على التحرك بعدها. بدلاً من ذلك ، ننقل الخلية الأخيرة إلى الفهرس الحالي ، ثم نحذف الخلية الأخيرة.

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

الآن علينا إنشاء طريقة UpdateCellData. للقيام بعمله ، سيحتاج إلى فهرس وبيانات الخلية ، لذلك دعونا نبدأ بالحصول عليها. يجب أن تحدد أيضًا ما إذا كانت ستستمر في تحديث الخلية. افتراضيًا ، سنفترض أنه ليس ضروريًا. بعد الانتهاء من العمل ، من الضروري تطبيق البيانات التي تم تغييرها وإعادة الحالة "التحديث مستمر".

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

تحديث بيانات الخلية


في هذه المرحلة ، لدينا خلية في مرحلة انتقال أو أكملت بالفعل. أولاً ، دعنا نتحقق من حالة مسبار الخلية. إذا تم فحص الخلية ، ولكن قيمة G الخاصة بها لا تساوي 255 بعد ، فهي في عملية انتقال ، لذلك سنراقب ذلك.

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

لإجراء الانتقال ، سنضيف دلتا إلى قيمة G للخلية. لا تعمل العمليات الحسابية مع وحدات البايت ، يتم تحويلها أولاً إلى عدد صحيح. لذلك ، سيكون المجموع بتنسيق صحيح ، والذي يجب تحويله إلى بايت.

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

ولكن قبل التحويل ، تحتاج إلى التأكد من أن القيمة لا تتجاوز 255.

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

بعد ذلك ، نحتاج إلى القيام بنفس الشيء من أجل الرؤية ، التي تستخدم قيمة 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; } 

نظرًا لأن الخلية يمكن أن تصبح غير مرئية مرة أخرى ، نحتاج إلى التحقق مما إذا كان من الضروري تقليل قيمة R. يحدث هذا عندما تكون الخلية غير مرئية ، ولكن R أكبر من الصفر.

  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; } 

الآن أصبح UpdateCellDataجاهزًا ويتم تنفيذ انتقالات الرؤية بشكل صحيح.


انتقالات الرؤية.

الحماية ضد العناصر الانتقالية المكررة


تعمل الانتقالات ، ولكن قد تظهر العناصر المكررة في القائمة. يحدث هذا إذا تغيرت حالة رؤية الخلية أثناء انتقالها. على سبيل المثال ، عندما تكون الخلية مرئية أثناء تحرك الفرقة لفترة قصيرة فقط.

نتيجة لظهور العناصر المكررة ، يتم تحديث انتقال الخلية عدة مرات لكل إطار ، مما يؤدي إلى انتقالات أسرع وعمل إضافي. يمكننا منع ذلك عن طريق التحقق قبل إضافة خلية ما إذا كانت موجودة بالفعل في القائمة. ومع ذلك ، بحث قائمة في كل مكالمةRefreshVisibilityمكلفة ، خاصة عندما يتم تنفيذ عمليات انتقال متعددة للخلايا. بدلاً من ذلك ، دعنا نستخدم قناة أخرى لم يتم استخدامها حتى الآن للإشارة إلى ما إذا كانت الخلية في عملية انتقال ، على سبيل المثال ، بواسطة B. عندما نضيف خلية إلى القائمة ، فسنعينها بقيمة 255 ، ونضيف فقط تلك الخلايا التي لا تكون قيمتها 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; } 

لكي يعمل هذا ، نحتاج إلى إعادة تعيين قيمة B بعد الانتهاء من انتقال الخلية.

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


انتقالات بدون تكرارات.

تحميل الرؤية على الفور


دائمًا ما تكون تغييرات الرؤية تدريجية ، حتى عند تحميل خريطة. هذا غير منطقي ، لأن الخريطة تصف الحالة التي تكون فيها الخلايا مرئية بالفعل ، لذا فإن الانتقال غير مناسب هنا. بالإضافة إلى ذلك ، يمكن أن يؤدي إجراء التحولات للعديد من الخلايا المرئية لخريطة كبيرة إلى إبطاء اللعبة بعد التحميل. لذلك ، قبل تحميل الخلايا والفرق ، دعنا ننتقل HexGrid.Loadإلى وضع الانتقال الفوري.

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

لذا فإننا نعيد تعريف الإعداد الأولي لوضع الانتقال الفوري ، مهما كان. ربما تم إيقاف تشغيله بالفعل ، أو جعل خيار التكوين ، لذلك سنتذكر الوضع الأولي وسنتحول إليه بعد الانتهاء من العمل.

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

حزمة الوحدة

نطاق يعتمد على الارتفاع


حتى الآن استخدمنا نطاقًا ثابتًا من ثلاثة لجميع الوحدات ، ولكنه في الواقع أكثر تعقيدًا. في الحالة العامة ، لا يمكننا رؤية الكائن لسببين: إما أن هناك عقبة تمنعنا من رؤيته ، أو أن الكائن صغير جدًا أو بعيد جدًا. في لعبتنا ، نقوم فقط بتطبيق حدود النطاق.

لا يمكننا أن نرى ما هو على الجانب الآخر من الأرض ، لأن الكوكب يحجبنا. يمكننا أن نرى فقط في الأفق. نظرًا لأن الكوكب يمكن اعتباره كرويًا تقريبًا ، كلما ارتفعت وجهة النظر ، كلما رأينا المزيد من السطح ، أي أن الأفق يعتمد على الارتفاع.


يعتمد الأفق على ارتفاع وجهة النظر.

يحاكي الرؤية المحدودة لوحداتنا تأثير الأفق الناتج عن انحناء الأرض. يعتمد مدى مراجعتهم على حجم الكوكب وحجم الخريطة. على الأقل هذا هو التفسير المنطقي. لكن السبب الرئيسي لتقليل النطاق هو أسلوب اللعب ، وهذا تقييد يسمى ضباب الحرب. ومع ذلك ، من خلال فهم الفيزياء الكامنة في مجال الرؤية ، يمكننا أن نستنتج أن وجهة نظر عالية يجب أن يكون لها قيمة استراتيجية ، لأنها تحرك الأفق بعيدًا وتسمح لك بالنظر إلى العقبات الأقل. لكن حتى الآن لم ننفذها.

الارتفاع للمراجعة


لأخذ الارتفاع في الاعتبار عند تحديد النطاق ، نحتاج إلى معرفة الارتفاع. سيكون هذا هو الارتفاع أو مستوى الماء المعتاد ، اعتمادًا على ما إذا كانت الخلية الأرضية أو الماء. دعنا نضيف هذا إلى HexCellالممتلكات.

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

ولكن إذا كان الارتفاع يؤثر على النطاق ، فعندئذٍ مع تغيير في ارتفاع عرض الخلية ، فقد يتغير وضع الرؤية أيضًا. نظرًا لأن الخلية حظرت نطاق عدة وحدات أو تحظره الآن ، فليس من السهل تحديد ما يجب تغييره. لن تتمكن الخلية نفسها من حل هذه المشكلة ، لذا دعها تبلغ عن تغيير في الموقف HexCellShaderData. افترض أن HexCellShaderDataلديك طريقة لذلك ViewElevationChanged. سوف نطلق عليه عند التعيين HexCell.Elevation، إذا لزم الأمر.

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

وينطبق نفس الشيء WaterLevel.

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

إعادة تعيين الرؤية


الآن نحن بحاجة إلى إنشاء طريقة HexCellShaderData.ViewElevationChanged. يعد تحديد كيفية تغير حالة الرؤية العامة مهمة معقدة ، خاصة عند تغيير خلايا متعددة في نفس الوقت. لذلك ، لن نخرج بأي حيل ، ولكن ببساطة نخطط لإعادة تعيين رؤية جميع الخلايا. أضف حقلًا منطقيًا لتتبع ما إذا كنت ستفعل ذلك أم لا. داخل الطريقة ، سوف نقوم ببساطة بتعيينها على true وتضمين المكون. بغض النظر عن عدد الخلايا التي تغيرت في وقت واحد ، سيؤدي ذلك إلى إعادة تعيين واحدة.

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

لإعادة تعيين قيم الرؤية لجميع الخلايا ، يجب أن يكون لديك حق الوصول إليها ، الذي HexCellShaderDataليس لديك. لذا دعونا نفوض هذه المسؤولية HexGrid. للقيام بذلك ، تحتاج إلى الإضافة إلى HexCellShaderDataالخاصية ، والتي ستسمح لك بالرجوع إلى الشبكة. ثم يمكننا استخدامه LateUpdateلطلب إعادة تعيين.

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

دعنا ننتقل إلى HexGrid: تعيين الرابط إلى الشبكة HexGrid.Awakeبعد إنشاء بيانات التظليل.

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

HexGridيجب أن تحصل أيضًا على طريقة ResetVisibilityللتخلص من جميع الخلايا. فقط اجعلها تدور حول جميع الخلايا في الحلقة وتفويض إعادة التعيين لنفسها.

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

الآن نحن بحاجة إلى إضافة إلى HexCellالأسلوب ResetVisibilty. ستقوم ببساطة بإلغاء تحديد الرؤية وتشغيل تحديث الرؤية. يجب أن يتم ذلك عندما تكون رؤية الخلية أكبر من الصفر.

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

بعد إعادة تعيين جميع بيانات الرؤية ، HexGrid.ResetVisibilityيجب عليه مرة أخرى تطبيق الرؤية على جميع الفرق ، والتي يحتاج إلى معرفة نطاق كل فرقة. افترض أنه يمكن الحصول عليها باستخدام الخاصية 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); } } 

لهذا للعمل، وإعادة تسمية refactor- HexUnit.visionRangeفي HexUnit.VisionRangeوتحويلها إلى ميزة. بينما ستحصل على قيمة ثابتة 3 ، لكنها ستتغير في المستقبل.

  public int VisionRange { get { return 3; } } 

ونتيجة لذلك ، سيتم إعادة تعيين بيانات الرؤية وستظل صحيحة بعد تغيير ارتفاع عرض الخلية. ولكن من المحتمل أننا سنغير قواعد تحديد النطاق وتشغيل إعادة الترجمة في وضع التشغيل. بحيث يتغير النطاق بشكل مستقل ، فلنقم بإعادة تعيين HexGrid.OnEnableعند اكتشاف إعادة الترجمة.

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

الآن يمكنك تغيير رمز النطاق ورؤية النتائج ، بينما تبقى في وضع التشغيل.

توسيع الأفق


يتم تحديد حساب النطاق HexGrid.GetVisibleCells. بحيث يؤثر الارتفاع على النطاق ، يمكننا ببساطة استخدام ارتفاع العرض fromCellبإعادة تعريف المنطقة المرسلة مؤقتًا. حتى نتمكن من التحقق بسهولة مما إذا كان هذا يعمل.

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


استخدم الارتفاع كنطاق.

معوقات الرؤية


تطبيق ارتفاع العرض كنطاق يعمل بشكل صحيح فقط عندما تكون جميع الخلايا الأخرى عند ارتفاع صفري. ولكن إذا كان لجميع الخلايا نفس ارتفاع وجهة النظر ، فيجب أن يكون مجال الرؤية صفرًا. بالإضافة إلى ذلك ، يجب أن تمنع الخلايا ذات المرتفعات العالية رؤية الخلايا المنخفضة خلفها. حتى الآن ، لم يتم تنفيذ أي من هذا.


لا يتدخل النطاق.

الطريقة الأكثر صحة لتحديد النطاق هي التحقق عن طريق انبعاث الأشعة ، ولكنها ستصبح مكلفة للغاية وستظل تنتج نتائج غريبة. نحن بحاجة إلى حل سريع يخلق نتائج جيدة بما فيه الكفاية والتي لا يجب أن تكون مثالية. بالإضافة إلى ذلك ، من المهم أن تكون قواعد تحديد النطاق بسيطة وبديهية ويمكن التنبؤ بها للاعبين.

سيكون حلنا على النحو التالي - عند تحديد مدى رؤية الخلية ، سنضيف ارتفاع عرض الخلية المجاورة إلى المسافة المغطاة. في الواقع ، هذا يقلل من النطاق عندما ننظر إلى هذه الخلايا ، وإذا تم تخطيها ، فلن يسمح لنا هذا بالوصول إلى الخلايا خلفها.

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


الخلايا العالية تمنع الرؤية.

ألا يجب أن نرى خلايا طويلة في المسافة؟
, , , . , .

لا تنظر حول الزوايا


الآن يبدو أن الخلايا المرتفعة تمنع العرض إلى الانخفاض ، ولكن في بعض الأحيان يخترق النطاق هذه ، على الرغم من أنه يبدو أنه لا ينبغي أن يكون هذا. يحدث هذا لأن خوارزمية البحث لا تزال تجد مسارًا لهذه الخلايا ، متجاوزة الخلايا المحظورة. ونتيجة لذلك ، يبدو كما لو أن مجال رؤيتنا يمكن أن يدور حول العقبات. لتجنب ذلك ، نحتاج إلى التأكد من أن أقصر المسارات فقط تؤخذ في الاعتبار عند تحديد رؤية الخلية. يمكن القيام بذلك عن طريق إسقاط المسارات التي تصبح أطول من اللازم.

  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; } … } } 


نستخدم أقصر الطرق فقط.

لذلك أصلحنا معظم الحالات الخاطئة بشكل واضح. بالنسبة للخلايا المجاورة ، يعمل هذا بشكل جيد ، لأنه لا يوجد سوى أقصر المسارات لها. تحتوي الخلايا البعيدة على المزيد من الخيارات للمسارات ؛ وبالتالي ، عبر مسافات طويلة ، لا يزال يمكن أن يحدث ظرف للرؤية. لن يكون هذا مشكلة إذا ظلت مناطق الرؤية صغيرة والاختلافات في الارتفاعات المجاورة ليست كبيرة جدًا.

وأخيرًا ، بدلاً من استبدال مجال الرؤية المرسَل ، نضيف إليه ارتفاع العرض. يشير مجال رؤية الفريق إلى ارتفاعه أو ارتفاع طيرانه أو قدراته الاستطلاعية.

  range += fromCell.ViewElevation; 


عرض مع مجال رؤية كامل من وجهة نظر منخفضة.

أي أن القواعد النهائية للرؤية تنطبق على الرؤية عند التحرك على طول أقصر مسار إلى مجال الرؤية ، مع مراعاة الاختلاف في ارتفاع الخلية بالنسبة لوجهة النظر. عندما تكون الخلية خارج النطاق ، فإنها تحجب جميع المسارات عبرها. ونتيجة لذلك ، تصبح نقاط المراقبة العالية ، التي لا شيء يمنع الرؤية ، ذات قيمة استراتيجية.

ماذا عن إعاقة رؤية الأشياء؟
, , . , , . .

حزمة الوحدة

الخلايا التي لا يمكن استكشافها


المشكلة الأخيرة مع الرؤية تتعلق بحواف الخريطة. يتوقف الانتعاش فجأة وبدون انتقالات ، لأن الخلايا الموجودة على الحافة ليس لها جيران.


حافة ملحوظة للخريطة.

من الناحية المثالية ، يجب أن يكون العرض المرئي للمناطق والحواف غير المكتشفة هو نفسه. يمكننا تحقيق ذلك عن طريق إضافة حالات خاصة عند تثليث الحواف ، عندما لا يكون لديهم جيران ، ولكن هذا سيتطلب منطقًا إضافيًا ، وسيكون علينا العمل مع الخلايا المفقودة. لذلك ، مثل هذا الحل غير بديهي. نهج بديل هو إجبار الخلايا الحدودية للخريطة على أن تكون غير مستكشفة ، حتى لو كانت في نطاق الفرقة. هذا النهج أبسط بكثير ، لذلك دعونا نستخدمه. كما يسمح لك بوضع علامة على أنها خلايا غير مستكشفة وخلايا أخرى ، مما يسهل تحقيق إنشاء حواف غير مستوية للخريطة. بالإضافة إلى ذلك ، تسمح لك الخلايا المخفية عند الحواف بإنشاء الطرق والأنهار التي تدخل وتخرج من خريطة النهر والطريق ، لأن نقاط النهاية ستكون خارج النطاق.أيضًا ، بمساعدة هذا الحل ، يمكنك إضافة وحدات تدخل وتخرج من الخريطة.

نضع علامة على الخلايا كما تم التحقيق فيها


للإشارة إلى إمكانية فحص الخلية ، أضف إلى HexCellالخاصية Explorable.

  public bool Explorable { get; set; } 

الآن يمكن أن تكون الخلية مرئية إذا تم التحقيق فيها ، لذلك IsVisibleسنقوم بتغيير الموقع لأخذ ذلك في الاعتبار.

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

الأمر نفسه ينطبق على IsExplored. ومع ذلك ، قمنا بالتحقيق في الخاصية القياسية. نحن بحاجة إلى تحويلها إلى خاصية صريحة حتى نتمكن من تغيير منطق getter الخاص بها.

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

إخفاء حافة الخريطة


يمكنك إخفاء حافة خريطة مستطيلة في الطريقة HexGrid.CreateCell. يتم التحقيق في الخلايا غير الموجودة على الحافة ، وكل الباقي غير مكتشف.

  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; … } 

الآن أصبحت البطاقات مظلمة حول الحواف ، مختبئة خلفها مساحات ضخمة غير مكتشفة. ونتيجة لذلك ، يقل حجم منطقة الخرائط المدروسة في كل بعد بمقدار اثنين.


حافة غير مستكشفة للخريطة.

هل من الممكن جعل حالة البحث قابلة للتحرير؟
, , . .

الخلايا غير المستكشفة تعوق الرؤية


أخيرًا ، إذا كان لا يمكن فحص الخلية ، فيجب أن تتداخل مع الرؤية. التغيير HexGrid.GetVisibleCellsلأخذ هذا في الاعتبار.

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

حزمة الوحدة

الجزء 23: توليد الأرض


  • املأ الخرائط الجديدة بالمناظر الطبيعية المولدة.
  • نرفع الأرض فوق الماء ، ونغرق بعضًا.
  • نحن نتحكم في مساحة الأرض المخلوقة وارتفاعها وعدم تكافؤها.
  • نضيف دعمًا لخيارات التكوين المختلفة لإنشاء خرائط متغيرة.
  • نصنعها بحيث يمكن إنشاء نفس الخريطة مرة أخرى.

سيكون هذا الجزء من البرنامج التعليمي بداية سلسلة حول إنشاء الخريطة الإجرائية.

تم إنشاء هذا الجزء في Unity 2017.1.0.


واحدة من الخرائط العديدة التي تم إنشاؤها.

توليد البطاقة


على الرغم من أنه يمكننا إنشاء أي خريطة ، إلا أنها تستغرق الكثير من الوقت. سيكون من الملائم إذا كان التطبيق يمكن أن يساعد المصمم من خلال إنشاء بطاقات له ، والتي يمكنه بعد ذلك تعديلها حسب ذوقه. يمكنك اتخاذ خطوة أخرى والتخلص تمامًا من إنشاء التصميم يدويًا ، ونقل المسؤولية الكاملة عن إنشاء الخريطة النهائية إلى التطبيق. ونتيجة لذلك ، يمكن لعب اللعبة في كل مرة ببطاقة جديدة وستكون كل جلسة لعبة مختلفة. لكي يكون كل هذا ممكنًا ، يجب أن ننشئ خوارزمية لتوليد الخرائط.

يعتمد نوع خوارزمية التوليد التي تحتاجها على نوع البطاقة التي تحتاجها. لا يوجد نهج واحد صحيح ، عليك دائمًا البحث عن حل وسط بين المصداقية وقابلية اللعب.

لكي تكون البطاقة قابلة للتصديق ، يجب أن تبدو ممكنة تمامًا وحقيقية للاعب. هذا لا يعني أن الخريطة يجب أن تبدو وكأنها جزء من كوكبنا. قد يكون كوكبًا مختلفًا أو حقيقة مختلفة تمامًا. ولكن إذا كان يجب أن يشير إلى ارتياح الأرض ، فيجب أن يشبهها جزئيًا على الأقل.

ترتبط إمكانية التشغيل بكيفية تطابق البطاقات مع طريقة اللعب. في بعض الأحيان يتعارض مع المصداقية. على سبيل المثال ، على الرغم من أن السلاسل الجبلية يمكن أن تبدو جميلة ، إلا أنها في نفس الوقت تحد بشكل كبير من حركة الوحدات وعرضها. إذا كان هذا غير مرغوب فيه ، فعليك الاستغناء عن الجبال ، مما سيقلل من المصداقية ويحد من تعبيرات اللعبة. أو يمكننا إنقاذ الجبال ، ولكن تقليل تأثيرها على اللعب ، والذي يمكن أن يقلل أيضًا من المصداقية.

بالإضافة إلى ذلك ، يجب النظر في الجدوى. على سبيل المثال ، يمكنك إنشاء كوكب واقعي للغاية يشبه الأرض من خلال محاكاة الصفائح التكتونية والتآكل والأمطار والانفجارات البركانية وتأثيرات النيازك والقمر وما إلى ذلك. لكن تطوير مثل هذا النظام سوف يتطلب الكثير من الوقت. بالإضافة إلى ذلك ، قد يستغرق إنشاء مثل هذا الكوكب وقتًا طويلاً ، ولن يرغب اللاعبون في الانتظار بضع دقائق قبل بدء لعبة جديدة. أي أن المحاكاة أداة قوية ، ولكن لها ثمن.

غالبًا ما تستخدم الألعاب مقايضات بين المصداقية وإمكانية اللعب والجدوى. في بعض الأحيان تكون مثل هذه التنازلات غير مرئية وتبدو طبيعية تمامًا ، وأحيانًا تبدو عشوائية أو غير متسقة أو فوضوية ، اعتمادًا على القرارات المتخذة أثناء عملية التطوير. لا ينطبق هذا فقط على إنشاء البطاقة ، ولكن عند تطوير مولد بطاقة إجرائي ، تحتاج إلى إيلاء اهتمام خاص لذلك. يمكنك قضاء الكثير من الوقت في إنشاء خوارزمية تولد بطاقات جميلة تبين أنها غير مجدية للعبة التي تقوم بإنشائها.

في هذه السلسلة التعليمية ، سنقوم بعمل إغاثة تشبه الأرض. يجب أن تبدو مثيرة للاهتمام ، مع تغير كبير وغياب مناطق متجانسة كبيرة. سيكون حجم الإغاثة كبيرًا ، وستغطي الخرائط قارة واحدة أو أكثر ، أو مناطق من المحيطات ، أو حتى كوكبًا كاملاً. نحن بحاجة إلى السيطرة على الجغرافيا ، بما في ذلك الكتل الأرضية والمناخ وعدد المناطق ومضخات التضاريس. في هذا الجزء ، سنضع الأساس لإنشاء السوشي.

الشروع في وضع التحرير


سنركز على الخريطة ، وليس على طريقة اللعب ، لذلك سيكون من الأنسب تشغيل التطبيق في وضع التحرير. بفضل هذا ، يمكننا رؤية البطاقات على الفور. لذلك ، سوف نتغير عن طريق HexMapEditor.Awakeضبط وضع التحرير على "true" وتشغيل الكلمة الأساسية للتظليل في هذا الوضع.

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

مولد البطاقة


نظرًا لأن هناك حاجة إلى الكثير من التعليمات البرمجية لإنشاء خرائط إجرائية ، فلن نضيفها مباشرةً إلى HexGrid. بدلاً من ذلك ، سنقوم بإنشاء مكون جديد HexMapGenerator، HexGridولن نعرف عنه. سيؤدي ذلك إلى تبسيط الانتقال إلى خوارزمية أخرى إذا كنا بحاجة إليها.

يحتاج المولد إلى ارتباط بالشبكة ، لذلك سنضيف حقلاً عامًا إليه. بالإضافة إلى ذلك ، نضيف طريقة عامة GenerateMapتتعامل مع عمل الخوارزمية. سنعطيها أبعاد الخريطة كمعلمات ، ثم نجبرها على استخدامها لإنشاء خريطة فارغة جديدة.

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

أضف كائنًا مع مكون إلى المشهد HexMapGeneratorوقم بتوصيله بالشبكة.


كائن منشئ الخريطة.

تغيير قائمة خريطة جديدة


سنقوم بتغييرها NewMapMenuبحيث يمكنها إنشاء بطاقات ، وليس فقط إنشاء بطاقات فارغة. سنتحكم في وظائفه من خلال حقل منطقي generateMaps، والذي له قيمة بشكل افتراضي true. لنقم بإنشاء طريقة عامة لتعيين هذا الحقل ، كما فعلنا للتبديل بين الخيارات HexMapEditor. أضف المفتاح المناسب إلى القائمة وقم بتوصيله بالطريقة.

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


قائمة بطاقة جديدة بمفتاح.

امنح القائمة رابطًا لمولد الخريطة. ثم سنضطر إلى استدعاء طريقة GenerateMapالمولد إذا لزم الأمر ، وليس فقط تنفيذ CreateMapالشبكة.

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


توصيل المولد.

الوصول إلى الخلية


لكي يعمل المولد ، يحتاج إلى الوصول إلى الخلايا. نحن HexGridبالفعل الطرق الشائعة GetCellالتي تتطلب أو ناقلات الموقف، أو الإحداثيات مسدس. لا يحتاج المولد للعمل مع أحدهما أو الآخر ، لذلك نضيف طريقتين مناسبتين ستعملان HexGrid.GetCellمع إحداثيات الإزاحة أو فهرس الخلية.

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

الآن HexMapGeneratorيمكنه استقبال الخلايا مباشرة. على سبيل المثال ، بعد إنشاء خريطة جديدة ، يمكنه استخدام إحداثيات العشب لتعيين العشب كتخفيف العمود الأوسط من الخلايا.

  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مع معلمة للتحكم في حجم المؤامرة. استدعاء هذه الطريقة في GenerateMapاستبدال رمز الاختبار السابق. لنبدأ بقطعة أرض صغيرة تتكون من سبع خلايا.

  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) {} 

حتى الآن ، نستخدم نوع الإغاثة "العشب" للإشارة إلى الأرض المرتفعة ، ونقش "الرمال" الأصلي يشير إلى المحيط. اجعلنا RaiseTerrainنأخذ خلية عشوائية ونغير نوع ارتياحها حتى نحصل على المساحة المناسبة من الأرض.

للحصول على خلية عشوائية ، نضيف طريقة GetRandomCellتحدد فهرس خلية عشوائية وتحصل على الخلية المقابلة من الشبكة.

  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)); } 


سبع خلايا سوشي عشوائية.

نظرًا لأنه في النهاية قد نحتاج إلى الكثير من الخلايا العشوائية أو التكرار عبر جميع الخلايا عدة مرات ، دعنا نتتبع عدد الخلايا في الخلية نفسها HexMapGenerator.

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

إنشاء موقع واحد


حتى الآن ، نقوم بتحويل سبع خلايا عشوائية إلى أرض ، ويمكن أن تكون في أي مكان. على الأرجح أنهم لا يشكلون مساحة أرض واحدة. بالإضافة إلى ذلك ، يمكننا تحديد الخلايا نفسها عدة مرات ، حتى نحصل على مساحة أقل. لحل كلتا المشكلتين ، بدون قيود ، سنختار الخلية الأولى فقط. بعد ذلك ، يجب أن نختار فقط الخلايا المجاورة للخلايا المحددة سابقًا. تشبه هذه القيود قيود البحث عن المسار ، لذلك نستخدم نفس النهج هنا.

نضيف HexMapGeneratorممتلكاتنا الخاصة وعداد مرحلة حدود البحث ، كما كانت HexGrid.

  HexCellPriorityQueue searchFrontier; int searchFrontierPhase; 

تحقق من وجود قائمة انتظار الأولوية قبل أن نحتاج إليها.

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

بعد إنشاء خريطة جديدة ، يكون حد البحث لجميع الخلايا صفرًا. ولكن إذا أردنا البحث عن الخلايا في عملية إنشاء الخريطة ، فسنزيد حدود البحث في هذه العملية. إذا قمنا بإجراء العديد من عمليات البحث ، فقد تكون قبل مرحلة حدود البحث المسجلة HexGrid. قد يتداخل هذا مع البحث عن مسارات الوحدة. لتجنب ذلك ، في نهاية عملية إنشاء الخريطة ، سنعيد ضبط مرحلة البحث لجميع الخلايا إلى صفر.

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

الآن RaiseTerrainيجب أن أبحث عن الخلايا المناسبة ، وأن لا أختارها بشكل عشوائي. هذه العملية تشبه إلى حد كبير طريقة البحث في HexGrid. ومع ذلك ، لن نقوم بزيارة الخلايا أكثر من مرة ، لذا سيكون كافياً بالنسبة لنا لزيادة مرحلة حد البحث بمقدار 1 بدلاً من 2. ثم نقوم بتهيئة الحدود بالخلية الأولى ، التي يتم تحديدها عشوائيًا. كالعادة ، بالإضافة إلى تحديد مرحلة البحث ، فإننا نخصص المسافة والموجه إلى الصفر.

  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); } 

بعد ذلك ، ستكون حلقة البحث مألوفة لنا في الغالب. بالإضافة إلى ذلك ، لمواصلة البحث حتى تصبح الحدود فارغة ، نحتاج إلى التوقف عندما يصل الجزء إلى الحجم المطلوب ، لذلك سنتتبعه. في كل تكرار ، سنقوم باستخراج الخلية التالية من قائمة الانتظار ، وتعيين نوع ارتياحها ، وزيادة الحجم ، ثم تجاوز جيران هذه الخلية. تتم إضافة جميع الجيران ببساطة إلى الحدود إذا لم تتم إضافتهم هناك بعد. لسنا بحاجة لإجراء أي تغييرات أو مقارنات. بعد الانتهاء ، تحتاج إلى مسح الحدود.

  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(); 


خط من الخلايا.

لقد حصلنا على قطعة واحدة من الحجم الصحيح. سيكون أصغر فقط إذا لم يكن هناك عدد كاف من الخلايا. بسبب طريقة ملء الحدود ، تتكون المؤامرة دائمًا من خط يمتد شمال غرب. يغير الاتجاه فقط عندما يصل إلى حافة الخريطة.

نحن نربط الخلايا


نادرًا ما تشبه مساحات الأراضي الخطوط ، وإذا كانت كذلك ، فإنها لا تكون دائمًا موجهة بنفس الطريقة. لتغيير شكل الموقع ، نحتاج إلى تغيير أولويات الخلايا. يمكن استخدام الخلية العشوائية الأولى كمركز للرسم. ثم ستكون المسافة إلى جميع الخلايا الأخرى نسبة إلى هذه النقطة. لذلك سنعطي أولوية أعلى للخلايا الأقرب إلى المركز ، لذلك لن ينمو الموقع كخط ، ولكن حول المركز.

  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); } } } 


تراكم الخلايا.

وفي الواقع ، الآن تمتلئ خلايانا السبع بشكل جميل في منطقة سداسية مضغوطة إذا لم تظهر الخلية المركزية على حافة الخريطة. دعنا نحاول الآن استخدام قطعة أرض بحجم 30.

  RaiseTerrain(30); 


كتلة سوشي في 30 خلية.

حصلنا مرة أخرى على نفس الشكل ، على الرغم من عدم وجود خلايا كافية للحصول على السداسي الصحيح. نظرًا لأن نصف قطر المخطط أكبر ، فمن المرجح أن يكون قريبًا من حافة الخريطة ، مما سيجبرها على اتخاذ شكل مختلف.

العشوائية السوشي


لا نريد أن تبدو جميع المناطق متشابهة ، لذلك سنغير أولويات الخلية قليلاً. في كل مرة نضيف فيها خلية مجاورة إلى الحدود ، إذا كان الرقم التالي Random.valueأقل من قيمة حد معينة ، فإن التوجيه في هذه الخلية لا يصبح 0 ، ولكن 1. لنستخدم القيمة 0.5 كعتبة ، أي أنها ستؤثر على الأرجح على نصف الخلايا.

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


منطقة مشوهة.

من خلال زيادة البحث عن البحث عن الخلية ، قمنا بزيارتها في وقت متأخر عن المتوقع. في الوقت نفسه ، سيتم زيارة الخلايا الأخرى الموجودة على بعد خطوة واحدة من المركز في وقت سابق ، ما لم تزيد أيضًا من الاستدلال. هذا يعني أنه إذا قمنا بزيادة التوجيه لجميع الخلايا بقيمة واحدة ، فلن يؤثر ذلك على الخريطة. أي أن العتبة 1 لن يكون لها تأثير ، مثل العتبة 0. والعتبة 0.8 ستكون مساوية لـ 0.2. أي أن احتمالية 0.5 تجعل عملية البحث هي الأكثر "ارتجافًا".

تعتمد كمية التذبذب المناسبة على نوع التضاريس المطلوب ، لذا دعنا نخصصه. قم بإضافة حقل عائم عام jitterProbabilityمع السمة إلى المولدRangeمحدودة في نطاق 0-0.5. دعونا نعطيها قيمة افتراضية تساوي متوسط ​​هذه الفترة ، أي 0.25. سيتيح لنا ذلك تكوين المولد في نافذة مفتش الوحدة.

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


احتمال التقلبات.

هل يمكنك جعله قابلاً للتخصيص في واجهة مستخدم اللعبة؟
, . UI, . , UI. , . , .

الآن ، لاتخاذ قرار بشأن الوقت الذي يجب أن يكون فيه التوجيه يساوي 1 ، نستخدم الاحتمالية بدلاً من القيمة الثابتة.

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

نستخدم قيمًا استكشافية من 0 و 1. على الرغم من أنه يمكن استخدام القيم الأكبر ، إلا أن هذا سيزيد من تشوه الأقسام بشكل كبير ، على الأرجح تحويلها إلى مجموعة من الخطوط.

ارفعوا بعض الأرض


لن نقتصر على إنشاء مساحة أرض واحدة. على سبيل المثال ، نضع مكالمة RaiseTerrainداخل حلقة للحصول على خمسة أقسام.

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


خمس قطع أرض.

على الرغم من أننا الآن نولد خمس قطع أرض من 30 خلية لكل منها ، ولكن ليس بالضرورة أن نحصل على 150 خلية بالضبط من الأرض. نظرًا لأن كل موقع يتم إنشاؤه بشكل منفصل ، فإنهم لا يعرفون عن بعضهم البعض ، حتى يتمكنوا من التقاطع. هذا أمر طبيعي لأنه يمكن أن يخلق مناظر طبيعية أكثر إثارة للاهتمام من مجرد مجموعة من الأقسام المعزولة.

لزيادة تنوع الأرض ، يمكننا أيضًا تغيير حجم كل قطعة أرض. أضف حقلي عدد صحيحين للتحكم في الحد الأدنى والحد الأقصى لأحجام القطع. قم بتعيين فاصل زمني كبير بما فيه الكفاية ، على سبيل المثال ، 20-200. سأجعل الحد الأدنى القياسي يساوي 30 ، والحد الأقصى القياسي - 100.

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


الفاصل الزمني التحجيم.

نستخدم هذه الحقول لتحديد حجم المنطقة بشكل عشوائي عند استدعائها RaiseTerrain.

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


خمسة أقسام بحجم عشوائي على الخريطة الوسطى.

اصنع ما يكفي من السوشي


في حين أننا لا نستطيع السيطرة بشكل خاص على مساحة الأراضي المتولدة. على الرغم من أنه يمكننا إضافة خيار التكوين لعدد القطع ، إلا أن قطع الأرض نفسها عشوائية الحجم وقد تتداخل قليلاً أو بقوة. لذلك ، فإن عدد المواقع لا يضمن استلام الكمية المطلوبة من الأرض على الخريطة. دعنا نضيف خيارًا للتحكم المباشر في النسبة المئوية للأرض المعبر عنها كعدد صحيح. نظرًا لأن 100 ٪ من الأراضي أو المياه ليست مثيرة للاهتمام للغاية ، فإننا نقصرها على الفاصل الزمني 5-95 ، بقيمة 50 افتراضيًا.

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


نسبة السوشي.

لضمان إنشاء المساحة المناسبة من الأرض ، نحتاج فقط إلى الاستمرار في رفع مساحات التضاريس حتى نحصل على كمية كافية. للقيام بذلك ، نحن بحاجة للسيطرة على العملية ، الأمر الذي سيعقد تعقيد توليد الأرض. لذلك ، دعنا نستبدل الدورة الحالية لرفع المواقع عن طريق استدعاء طريقة جديدة CreateLand. أول شيء تفعله هذه الطريقة هو حساب عدد الخلايا التي يجب أن تصبح أرضًا. سيكون هذا المبلغ إجمالي مجموع خلايا السوشي لدينا.

  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); } 

CreateLandسوف يسبب RaiseTerrainحتى ننفق كامل كمية الخلايا. لكي لا يتجاوز المبلغ ، نقوم بتغييره RaiseTerrainبحيث يتلقى المبلغ كمعلمة إضافية. بعد الانتهاء من العمل عليه إعادة المبلغ المتبقي.

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

يجب أن ينخفض ​​المبلغ في كل مرة تتم إزالة الخلية من الحدود وتحويلها إلى أرض. إذا تم إنفاق المبلغ بالكامل بعد ذلك ، فيجب علينا إيقاف البحث وإكمال الموقع. بالإضافة إلى ذلك ، يجب أن يتم ذلك فقط عندما تكون الخلية الحالية لم تهبط بعد.

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

الآن CreateLandيمكنها رفع الأرض حتى تنفق كامل كمية الخلايا.

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


أصبح نصف الخريطة بالضبط أرضًا.

حزمة الوحدة

خذ بعين الاعتبار الارتفاع


الأرض ليست مجرد لوحة مسطحة ، محدودة بالساحل. لديها ارتفاع متغير ، يحتوي على التلال والجبال والوديان والبحيرات ، وما إلى ذلك. توجد اختلافات كبيرة في الارتفاع بسبب تفاعل الصفائح التكتونية التي تتحرك ببطء. على الرغم من أننا لن نحاكيها ، يجب أن تشبه مناطق أرضنا هذه اللوحات بطريقة أو بأخرى. لا تتحرك المواقع ، ولكن قد تتقاطع. ويمكننا الاستفادة من هذا.

ادفع الأرض


تمثل كل قطعة أرض جزءًا من الأرض مدفوعة من قاع المحيط. لذلك ، دعونا باستمرار زيادة ارتفاع الخلية الحالية RaiseTerrainونرى ما سيحدث.

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


أرض بارتفاعات.

وصلنا إلى المرتفعات ، لكن من الصعب رؤيتها. يمكنك جعلها أكثر وضوحًا إذا كنت تستخدم نوع التضاريس الخاص بك لكل مستوى من الارتفاع ، مثل الطبقات الجغرافية. سنفعل ذلك فقط بحيث تكون الارتفاعات أكثر وضوحًا ، بحيث يمكنك ببساطة استخدام مستوى الارتفاع كمؤشر للارتفاع.

ماذا يحدث إذا تجاوز الارتفاع عدد أنواع التضاريس؟
. , .

بدلاً من تحديث نوع تضاريس الخلية مع كل تغيير في الارتفاع ، دعنا ننشئ طريقة منفصلة SetTerrainTypeلتعيين جميع أنواع التضاريس مرة واحدة فقط.

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

سوف نطلق على هذه الطريقة بعد إنشاء السوشي.

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

الآن RaiseTerrainلا يستطيع التعامل مع نوع الراحة ، والتركيز على المرتفعات. للقيام بذلك ، تحتاج إلى تغيير منطقه. إذا كان الارتفاع الجديد للخلية الحالية هو 1 ، فقد أصبح أكثر جفافًا ، لذا فقد انخفض مجموع الخلايا ، مما قد يؤدي إلى استكمال نمو الموقع.

  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; // } // } 


التقسيم الطبقي للطبقات.

أضف الماء


دعنا نوضح بشكل صريح أي الخلايا هي الماء أو الأرض ، وتعيين مستوى المياه لجميع الخلايا إلى 1. قم بذلك GenerateMapقبل إنشاء الأرض.

  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(); … } 

الآن لتعيين طبقات الأرض يمكننا استخدام جميع أنواع التضاريس. ستبقى جميع خلايا الغواصة رملية ، وكذلك الخلايا الأرضية الأقل. يمكن القيام بذلك عن طريق طرح مستوى الماء من الارتفاع واستخدام القيمة كمؤشر لنوع الإغاثة.

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


الأرض والمياه.

ارفع مستوى الماء


نحن لا نقتصر على مستوى مياه واحد. دعنا نجعله قابلاً للتخصيص باستخدام حقل مشترك بفاصل زمني 1-5 وقيمة افتراضية 3. استخدم هذا المستوى عند تهيئة الخلايا.

  [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; } … } 



مستوى المياه 3.

عندما يكون مستوى المياه 3 ، نحصل على أرض أقل مما توقعنا. هذا لأنه RaiseTerrainلا يزال يعتقد أن مستوى المياه هو 1. فلنصلح ذلك.

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

يؤدي استخدام مستويات أعلى من المياه إلى ذلك. أن الخلايا لا تصبح أرضًا على الفور. عندما يكون مستوى الماء 2 ، سيظل القسم الأول تحت الماء. لقد ارتفع قاع المحيط ، ولكنه لا يزال تحت الماء. تتكون الأرض فقط عند تقاطع قسمين على الأقل. كلما ارتفع مستوى المياه ، يجب أن تعبر المزيد من المواقع لإنشاء الأرض. لذلك ، مع ارتفاع مستويات المياه ، تصبح الأرض أكثر فوضوية. بالإضافة إلى ذلك ، عندما تكون هناك حاجة إلى المزيد من الأراضي ، فمن المرجح أن تتقاطع على الأرض الموجودة بالفعل ، وهذا هو السبب في أن الجبال ستكون أكثر شيوعًا وأرض مسطحة أقل في كثير من الأحيان ، كما هو الحال في استخدام قطع الأرض الصغيرة.





مستويات المياه 2-5 ، السوشي دائما 50٪.

حزمة الوحدة

حركة رأسية


حتى الآن ، قمنا برفع المؤامرات إلى مستوى واحد في كل مرة ، ولكن ليس علينا أن نقصر أنفسنا على ذلك.

مواقع عالية


على الرغم من أن كل قسم يزيد من ارتفاع خلاياه بمقدار مستوى واحد ، فقد تحدث قصاصات. يحدث هذا عندما تلمس حواف قسمين. يمكن أن يؤدي ذلك إلى إنشاء جرف صخري معزول ، لكن خطوط الجرف الطويلة ستكون نادرة. يمكننا زيادة تكرار ظهورها عن طريق زيادة ارتفاع المؤامرة بأكثر من خطوة. ولكن يجب القيام بذلك فقط لنسبة معينة من المواقع. إذا كانت جميع المناطق ترتفع ، فسيكون من الصعب جدًا التحرك على طول التضاريس. لذلك دعونا نجعل هذه المعلمة قابلة للتخصيص باستخدام حقل احتمالي بقيمة افتراضية 0.25.

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


احتمالية ارتفاع قوي في الخلايا.

على الرغم من أنه يمكننا استخدام أي زيادة في الارتفاع للمناطق المرتفعة ، إلا أن ذلك يخرج عن السيطرة بسرعة. فرق الارتفاع 2 يخلق بالفعل المنحدرات ، لذلك هذا يكفي. نظرًا لأنه يمكنك تخطي ارتفاع مساوٍ لمستوى الماء ، نحتاج إلى تغيير الطريقة التي نحدد بها ما إذا كانت الخلية قد أصبحت أرضًا. إذا كان تحت مستوى الماء ، والآن هو على نفس المستوى أو أعلى ، فقد أنشأنا خلية أرضية جديدة.

  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; … } 





احتمالات زيادة قوية في الارتفاع هي 0.25 و 0.50 و 0.75 و 1.

اخفض الأرض


الأرض لا ترتفع دائمًا ، وأحيانًا تسقط. عندما تنخفض الأرض بدرجة كافية ، يملأها الماء وتضيع. حتى الآن نحن لا نفعل ذلك. نظرًا لأننا ندفع المناطق فقط للأعلى ، تبدو الأرض عادةً كمجموعة من المناطق المستديرة إلى حد ما ممزوجة معًا. إذا قمنا في بعض الأحيان بتخفيض المنطقة لأسفل ، نحصل على أشكال أكثر تنوعًا.


خريطة كبيرة بدون سوشي غارقة.

يمكننا التحكم في تكرار هبوط الأرض باستخدام مجال احتمال آخر. بما أن التخفيض يمكن أن يدمر الأرض ، يجب أن يكون احتمال الانخفاض دائمًا أقل من احتمال الرفع. خلاف ذلك ، قد يستغرق الأمر وقتا طويلا جدا للحصول على النسبة الصحيحة من الأرض. لذلك ، دعنا نستخدم احتمال تخفيض أقصى يبلغ 0.4 مع قيمة افتراضية 0.2.

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


احتمالية التخفيض.

يشبه خفض الموقع رفع ، مع بعض الاختلافات. لذلك ، نقوم بتكرار الأسلوب RaiseTerrainوتغيير اسمه إلى SinkTerrain. بدلاً من تحديد حجم الارتفاع ، نحتاج إلى قيمة أقل يمكن أن تستخدم نفس المنطق. في الوقت نفسه ، يجب إجراء مقارنات للتحقق مما إذا كنا قد مررنا عبر سطح الماء. بالإضافة إلى ذلك ، عند خفض الإغاثة ، نحن لا نقتصر على مجموع الخلايا. بدلاً من ذلك ، تقوم كل خلية سوشي مفقودة بإرجاع المبلغ الذي تم إنفاقه عليها ، لذلك نزيدها ونستمر في العمل.

  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; } 

الآن ، في كل تكرار للداخل ، CreateLandيجب إما أن نخفض الأرض أو نرفعها ، اعتمادًا على احتمال الانخفاض.

  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); } } } 





احتمال السقوط 0.1 و 0.2 و 0.3 و 0.4.

ارتفاع الحد


في المرحلة الحالية ، يمكن أن نتداخل مع العديد من الأقسام ، وأحيانًا بارتفاعات عديدة في الارتفاع ، وبعضها يمكن أن ينخفض ​​ثم يرتفع مرة أخرى. في الوقت نفسه ، يمكننا إنشاء ارتفاعات عالية جدًا وأحيانًا منخفضة جدًا ، خاصة عندما تكون هناك حاجة إلى نسبة عالية من الأراضي.


مرتفعات ضخمة بنسبة 90٪ من الأرض.

للحد من الارتفاع ، دعنا نضيف الحد الأدنى والحد الأقصى المخصص. سيكون الحد الأدنى المعقول في مكان ما بين −4 و 0 ، وقد يكون الحد الأقصى المقبول في نطاق 6-10. اجعل القيم الافتراضية هي −2 و 8. عند تعديل الخريطة يدويًا ، ستكون خارج الحد المقبول ، حتى تتمكن من تغيير شريط تمرير واجهة مستخدم المحرر ، أو تركه كما هو.

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


الارتفاعات الدنيا والقصوى.

الآن RaiseTerrainيجب أن نتأكد من أن الارتفاع لا يتجاوز الحد الأقصى المسموح به. يمكن القيام بذلك عن طريق التحقق مما إذا كانت الخلايا الحالية مرتفعة للغاية. إذا كان الأمر كذلك ، فإننا نتخطاهم دون تغيير الارتفاع وإضافة جيرانهم. سيؤدي هذا إلى حقيقة أن مناطق الأرض ستتجنب المناطق التي وصلت إلى أقصى ارتفاع ، وتنمو حولها.

  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; 

افعل الشيء نفسه SinkTerrainولكن بحد أدنى للارتفاع.

  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; 


ارتفاع محدود مع 90٪ أرض.

الحفاظ على الارتفاع السلبي


في هذه المرحلة ، لا يمكن لرمز الحفظ والتحميل التعامل مع الارتفاعات السالبة لأننا نخزن الارتفاع كبايت. يتم تحويل رقم سالب عند حفظه إلى موجب كبير. لذلك ، عند حفظ وتحميل الخريطة التي تم إنشاؤها ، قد تظهر خرائط عالية جدًا بدلاً من الخلايا الأصلية تحت الماء.

يمكننا إضافة دعم للارتفاعات السلبية عن طريق تخزينه كعدد صحيح وليس بايت. ومع ذلك ، ما زلنا لا نحتاج إلى دعم مستويات متعددة من الارتفاع. بالإضافة إلى ذلك ، يمكننا تعويض القيمة المخزنة عن طريق إضافة 127. وهذا سيسمح لنا بتخزين الارتفاعات بشكل صحيح في النطاق −127-128 في بايت واحد. HexCell.Saveحسب التغيير .

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

نظرًا لأننا قمنا بتغيير طريقة حفظ بيانات الخرائط ، فإننا نزيدها SaveLoadMenu.mapFileVersionإلى 4.

  const int mapFileVersion = 4; 

وأخيرًا ، قم بتغييره HexCell.Loadبحيث يطرح 127 من الارتفاعات المحملة من ملفات الإصدار 4.

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

حزمة الوحدة

إعادة إنشاء نفس الخريطة


يمكننا الآن إنشاء مجموعة متنوعة من الخرائط. عند إنشاء كل نتيجة جديدة ستكون عشوائية. يمكننا التحكم باستخدام خيارات التكوين فقط خصائص البطاقة ، ولكن ليس الشكل الأكثر دقة. ولكن في بعض الأحيان نحتاج إلى إعادة إنشاء نفس الخريطة مرة أخرى. على سبيل المثال ، لمشاركة خريطة جميلة مع صديق ، أو البدء مرة أخرى بعد تحريرها يدويًا. كما أنها مفيدة في عملية تطوير اللعبة ، لذا دعنا نضيف هذه الميزة.

باستخدام البذور


لجعل عملية إنشاء الخريطة غير متوقعة ، نستخدم Random.Rangeو Random.value. للحصول على نفس تسلسل الأرقام الزائفة والعشوائية مرة أخرى ، تحتاج إلى استخدام نفس القيمة الأولية. لقد اتخذنا بالفعل نهجا مماثلا من قبل ، في HexMetrics.InitializeHashGrid. يقوم أولاً بحفظ الحالة الحالية لمولد الأرقام الذي تمت تهيئته بقيمة أساسية محددة ، ثم يستعيد حالته الأصلية. يمكننا استخدام نفس النهج ل HexMapGenerator.GenerateMap. يمكننا مرة أخرى أن نتذكر الحالة القديمة ونستعيدها بعد الانتهاء ، حتى لا نتدخل في أي شيء آخر يستخدم Random.

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

بعد ذلك ، نحتاج إلى توفير البذور المستخدمة لإنشاء البطاقة الأخيرة. يتم ذلك باستخدام حقل عدد صحيح مشترك.

  public int seed; 


عرض البذور.

الآن نحن بحاجة إلى قيمة البذور للتهيئة Random. لإنشاء بطاقات عشوائية ، تحتاج إلى استخدام بذرة عشوائية. أبسط النهج هو استخدام قيمة البذور التعسفية لتوليد Random.Range. حتى لا تؤثر على الحالة العشوائية الأولية ، نحتاج إلى القيام بذلك بعد حفظها.

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

منذ الانتهاء ، نستعيد حالة عشوائية ، إذا أنشأنا على الفور بطاقة أخرى ، ونتيجة لذلك نحصل على نفس قيمة الأصل. بالإضافة إلى ذلك ، لا نعرف كيف تم تهيئة الحالة العشوائية الأولية. لذلك ، على الرغم من أنه يمكن أن يكون بمثابة نقطة انطلاق عشوائية ، فإننا بحاجة إلى شيء أكثر عشوائيًا لكل مكالمة.

هناك طرق مختلفة لتهيئة مولدات الأرقام العشوائية. في هذه الحالة ، يمكنك ببساطة الجمع بين العديد من القيم العشوائية التي تختلف على نطاق واسع ، أي أن احتمال إعادة إنشاء نفس البطاقة سيكون منخفضًا. على سبيل المثال ، نستخدم أقل 32 بتًا من وقت النظام ، معبراً عنها بالدورات ، بالإضافة إلى وقت التشغيل الحالي للتطبيق. قم بدمج هذه القيم باستخدام عملية أحادي المعامل OR حتى لا تكون النتيجة كبيرة جدًا.

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

قد يكون الرقم الناتج سلبيًا ، والذي لا يبدو لطيفًا جدًا بالنسبة لبذور القيمة العامة. يمكننا أن نجعلها إيجابية تمامًا باستخدام إخفاء البت مع الحد الأقصى من القيمة الصحيحة التي ستعيد تعيين بت الإشارة.

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

البذور القابلة لإعادة الاستخدام


ما زلنا ننشئ بطاقات عشوائية ، ولكن يمكننا الآن معرفة قيمة البذور التي تم استخدامها لكل منها. لإعادة إنشاء نفس الخريطة مرة أخرى ، يجب أن نطلب من المولد استخدام نفس القيمة الأساسية مرة أخرى ، بدلاً من إنشاء واحدة جديدة. سنفعل ذلك بإضافة مفتاح تبديل باستخدام حقل منطقي.

  public bool useFixedSeed; 


خيار استخدام بذرة ثابتة.

إذا تم اختيار بذرة ثابتة ، فإننا ببساطة نتخطى توليد البذرة الجديدة GenerateMap. إذا لم نغير حقل البذور يدويًا ، فستكون النتيجة هي نفس الخريطة مرة أخرى.

  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); 

يمكننا الآن نسخ القيمة الأولية للخريطة التي نحبها وحفظها في مكان ما ، لتوليدها مرة أخرى في المستقبل. لا تنس أننا لن نحصل على نفس البطاقة إلا إذا استخدمنا نفس معلمات المولد بالضبط ، أي نفس حجم البطاقة ، بالإضافة إلى جميع خيارات التكوين الأخرى. حتى التغيير البسيط في هذه الاحتمالات يمكن أن يخلق خريطة مختلفة تمامًا. لذلك ، بالإضافة إلى البذور ، نحتاج إلى تذكر جميع الإعدادات.



بطاقات كبيرة بقيم أساسية 0 و 929396788 ، معلمات قياسية.

حزمة الوحدة

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


All Articles