خرائط Unity Hexagon: المياه والمعالم وجدران القلعة

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

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

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

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

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

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

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

الجزء الثامن: الماء


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

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


الماء قادم.

مستوى الماء


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

  public int WaterLevel { get { return waterLevel; } set { if (waterLevel == value) { return; } waterLevel = value; Refresh(); } } int waterLevel; 

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

خلايا الفيضان


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

  public bool IsUnderwater { get { return waterLevel > elevation; } } 

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

 // public const float riverSurfaceElevationOffset = -0.5f; public const float waterElevationOffset = -0.5f; 

قم بتغيير HexCell.RiverSurfaceY بحيث يستخدم الاسم الجديد. ثم نضيف خاصية مشابهة لسطح الماء للخلية المغمورة.

  public float RiverSurfaceY { get { return (elevation + HexMetrics.waterElevationOffset) * HexMetrics.elevationStep; } } public float WaterSurfaceY { get { return (waterLevel + HexMetrics.waterElevationOffset) * HexMetrics.elevationStep; } } 

تحرير الماء


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

  int activeElevation; int activeWaterLevel; … bool applyElevation = true; bool applyWaterLevel = true; 

أضف طرقًا لربط هذه المعلمات بواجهة المستخدم.

  public void SetApplyWaterLevel (bool toggle) { applyWaterLevel = toggle; } public void SetWaterLevel (float level) { activeWaterLevel = (int)level; } 

وأضف مستوى الماء إلى EditCell .

  void EditCell (HexCell cell) { if (cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } if (applyWaterLevel) { cell.WaterLevel = activeWaterLevel; } … } } 

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


منزلق مستوى المياه.

حزمة الوحدة

تثليث الماء


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

 Shader "Custom/Water" { Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 } SubShader { Tags { "RenderType"="Transparent" "Queue"="Transparent" } LOD 200 CGPROGRAM #pragma surface surf Standard alpha #pragma target 3.0 sampler2D _MainTex; struct Input { float2 uv_MainTex; }; half _Glossiness; half _Metallic; fixed4 _Color; void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = _Color; o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } ENDCG } FallBack "Diffuse" } 

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


مواد مائية.

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



كائن كائن مائي.

بعد ذلك ، أضف دعم شبكة المياه إلى HexGridChunk .

  public HexMesh terrain, rivers, roads, water; public void Triangulate () { terrain.Clear(); rivers.Clear(); roads.Clear(); water.Clear(); for (int i = 0; i < cells.Length; i++) { Triangulate(cells[i]); } terrain.Apply(); rivers.Apply(); roads.Apply(); water.Apply(); } 

وربطها بالطفل الجاهز.


تم توصيل كائن الماء.

مسدس الماء


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

  void Triangulate (HexDirection direction, HexCell cell) { … if (cell.IsUnderwater) { TriangulateWater(direction, cell, center); } } void TriangulateWater ( HexDirection direction, HexCell cell, Vector3 center ) { } 

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

  void TriangulateWater ( HexDirection direction, HexCell cell, Vector3 center ) { center.y = cell.WaterSurfaceY; Vector3 c1 = center + HexMetrics.GetFirstSolidCorner(direction); Vector3 c2 = center + HexMetrics.GetSecondSolidCorner(direction); water.AddTriangle(center, c1, c2); } 


مسدس الماء.

مركبات الماء


يمكننا ربط الخلايا المجاورة بالماء بربع واحد.

  water.AddTriangle(center, c1, c2); if (direction <= HexDirection.SE) { HexCell neighbor = cell.GetNeighbor(direction); if (neighbor == null || !neighbor.IsUnderwater) { return; } Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 e1 = c1 + bridge; Vector3 e2 = c2 + bridge; water.AddQuad(c1, c2, e1, e2); } 


وصلات حواف الماء.

واملأ الزوايا بمثلث واحد.

  if (direction <= HexDirection.SE) { … water.AddQuad(c1, c2, e1, e2); if (direction <= HexDirection.E) { HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor == null || !nextNeighbor.IsUnderwater) { return; } water.AddTriangle( c2, e2, c2 + HexMetrics.GetBridge(direction.Next()) ); } } 


مفاصل زوايا المياه.

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

منسق مستويات المياه


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


مستويات مياه غير متناسقة.

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

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

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

حزمة الوحدة

الرسوم المتحركة المائية


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


الماء المسطح تماما.

دعونا نفعل ما فعلناه مع الأنهار. نحن نختبر الضوضاء مع موقع العالم ونضيفه إلى لون موحد. لتحريك السطح ، أضف الوقت إلى إحداثيات V.

  struct Input { float2 uv_MainTex; float3 worldPos; }; … void surf (Input IN, inout SurfaceOutputStandard o) { float2 uv = IN.worldPos.xz; uv.y += _Time.y; float4 noise = tex2D(_MainTex, uv * 0.025); float waves = noise.z; fixed4 c = saturate(_Color + waves); o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } 


التمرير المياه ، الوقت × 10.

اتجاهان


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

  float2 uv1 = IN.worldPos.xz; uv1.y += _Time.y; float4 noise1 = tex2D(_MainTex, uv1 * 0.025); float2 uv2 = IN.worldPos.xz; uv2.x += _Time.y; float4 noise2 = tex2D(_MainTex, uv2 * 0.025); float waves = noise1.z + noise2.x; 

عند جمع كلتا العيّنتين ، نحصل على نتائج في الفترة من 0 إلى 2 ، لذا نحتاج إلى تغيير حجمها إلى 0-1. بدلاً من تقسيم الأمواج إلى نصفين ببساطة ، يمكننا استخدام وظيفة smoothstep للحصول على نتيجة أكثر إثارة للاهتمام. نضع ¾ - 2 على 0–1 حتى لا تكون هناك موجات مرئية على سطح الماء.

  float waves = noise1.z + noise2.x; waves = smoothstep(0.75, 2, waves); 


اتجاهين ، الوقت × 10.

موجات الخلط


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

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

  float blendWave = sin((IN.worldPos.x + IN.worldPos.z) * 0.1 + _Time.y); 

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

  sin((IN.worldPos.x + IN.worldPos.z) * 0.1 + _Time.y); blendWave *= blendWave; float waves = noise1.z + noise2.x; waves = smoothstep(0.75, 2, waves); fixed4 c = blendWave; //saturate(_Color + waves); 


موجات الخلط.

لجعل موجات الخلط أقل وضوحًا ، أضف بعض الضوضاء من كلتا العيّنتين إليها.

  float blendWave = sin( (IN.worldPos.x + IN.worldPos.z) * 0.1 + (noise1.y + noise2.z) + _Time.y ); blendWave *= blendWave; 


موجات الخلط المشوهة.

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

  float waves = lerp(noise1.z, noise1.w, blendWave) + lerp(noise2.x, noise2.y, blendWave); waves = smoothstep(0.75, 2, waves); fixed4 c = saturate(_Color + waves); 


أمواج الخلط ، الزمن × 2.

حزمة الوحدة

الساحل


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

  void TriangulateWater ( HexDirection direction, HexCell cell, Vector3 center ) { center.y = cell.WaterSurfaceY; HexCell neighbor = cell.GetNeighbor(direction); if (neighbor != null && !neighbor.IsUnderwater) { TriangulateWaterShore(direction, cell, neighbor, center); } else { TriangulateOpenWater(direction, cell, neighbor, center); } } void TriangulateOpenWater ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { Vector3 c1 = center + HexMetrics.GetFirstSolidCorner(direction); Vector3 c2 = center + HexMetrics.GetSecondSolidCorner(direction); water.AddTriangle(center, c1, c2); if (direction <= HexDirection.SE && neighbor != null) { // HexCell neighbor = cell.GetNeighbor(direction); // if (neighbor == null || !neighbor.IsUnderwater) { // return; // } Vector3 bridge = HexMetrics.GetBridge(direction); … } } void TriangulateWaterShore ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { } 


لا يوجد تثليث على طول الساحل.

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

  void TriangulateWaterShore ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { EdgeVertices e1 = new EdgeVertices( center + HexMetrics.GetFirstSolidCorner(direction), center + HexMetrics.GetSecondSolidCorner(direction) ); 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); } 


مراوح مثلثات على طول الساحل.

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

  water.AddTriangle(center, e1.v4, e1.v5); Vector3 bridge = HexMetrics.GetBridge(direction); EdgeVertices e2 = new EdgeVertices( e1.v1 + bridge, e1.v5 + bridge ); water.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); water.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); water.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); water.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); 


خطوط الأضلاع على طول الساحل.

وبالمثل ، يجب أيضًا إضافة مثلث زاوي في كل مرة.

  water.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor != null) { water.AddTriangle( e1.v5, e2.v5, e1.v5 + HexMetrics.GetBridge(direction.Next()) ); } 


زوايا الأضلاع على طول الساحل.

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

ساحل فوق البنفسجية


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

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

  public HexMesh terrain, rivers, roads, water, waterShore; public void Triangulate () { terrain.Clear(); rivers.Clear(); roads.Clear(); water.Clear(); waterShore.Clear(); for (int i = 0; i < cells.Length; i++) { Triangulate(cells[i]); } terrain.Apply(); rivers.Apply(); roads.Apply(); water.Apply(); waterShore.Apply(); } 

ستستخدم هذه الشبكة الجديدة TriangulateWaterShore .

  void TriangulateWaterShore ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { … waterShore.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); waterShore.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); waterShore.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); waterShore.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor != null) { waterShore.AddTriangle( e1.v5, e2.v5, e1.v5 + HexMetrics.GetBridge(direction.Next()) ); } } 

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


مرفق الشاطئ المائي ومواد الأشعة فوق البنفسجية.

قم بتغيير تظليل Water Shore بحيث يعرض إحداثيات UV بدلاً من الماء.

  fixed4 c = fixed4(IN.uv_MainTex, 1, 1); 

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


شبكة منفصلة للساحل.

دعنا نضع معلومات الساحل في الإحداثيات V. على جانب الماء ، قم بتعيينها قيمة 0 ، على جانب الأرض - القيمة 1. نظرًا لأننا لا نحتاج إلى إرسال أي شيء آخر ، فإن جميع إحداثيات U ستكون ببساطة 0.

  waterShore.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); waterShore.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); waterShore.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); waterShore.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); waterShore.AddQuadUV(0f, 0f, 0f, 1f); waterShore.AddQuadUV(0f, 0f, 0f, 1f); waterShore.AddQuadUV(0f, 0f, 0f, 1f); waterShore.AddQuadUV(0f, 0f, 0f, 1f); HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor != null) { waterShore.AddTriangle( e1.v5, e2.v5, e1.v5 + HexMetrics.GetBridge(direction.Next()) ); waterShore.AddTriangleUV( new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(0f, 0f) ); } 


الانتقال إلى السواحل خاطئ.

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

  waterShore.AddTriangleUV( new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(0f, nextNeighbor.IsUnderwater ? 0f : 1f) ); 


التحولات إلى السواحل صحيحة.

رغوة على الساحل


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

  void surf (Input IN, inout SurfaceOutputStandard o) { float shore = IN.uv_MainTex.y; float foam = shore; fixed4 c = saturate(_Color + foam); o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } 


رغوة خطية.

لجعل الرغوة أكثر إثارة للاهتمام ، اضربها في مربع الجيب.

  float foam = sin(shore * 10); foam *= foam * shore; 


يتلاشى الرغوة الجيبية المربعة.

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

  float shore = IN.uv_MainTex.y; shore = sqrt(shore); 


تصبح الرغوة أكثر سمكًا بالقرب من الشاطئ.

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

  float2 noiseUV = IN.worldPos.xz; float4 noise = tex2D(_MainTex, noiseUV * 0.015); float distortion = noise.x * (1 - shore); float foam = sin((shore + distortion) * 10); foam *= foam * shore; 


رغوة مع تشويه.

وبالطبع ، نحن نحيي كل هذا: كل من الجيب والتشوهات.

  float2 noiseUV = IN.worldPos.xz + _Time.y * 0.25; float4 noise = tex2D(_MainTex, noiseUV * 0.015); float distortion = noise.x * (1 - shore); float foam = sin((shore + distortion) * 10 - _Time.y); foam *= foam * shore; 


رغوة متحركة.

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

  float distortion1 = noise.x * (1 - shore); float foam1 = sin((shore + distortion1) * 10 - _Time.y); foam1 *= foam1; float distortion2 = noise.y * (1 - shore); float foam2 = sin((shore + distortion2) * 10 + _Time.y + 2); foam2 *= foam2 * 0.7; float foam = max(foam1, foam2) * shore; 


الرغوة الواردة والتراجع.

مزيج من الأمواج والرغوة


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

بدلاً من نسخ رمز الموجة ، دعنا نلصقه في ملف تضمين Water.cginc . في الواقع ، نقوم بإدراج رمز فيه لكل من الرغوة والموجات ، كل كوظيفة منفصلة.

كيف يعمل تظليل الملفات؟
يتم تغطية إنشاء ملفات التظليل الخاصة بك في البرنامج التعليمي Rendering 5 ، مصابيح متعددة .

 float Foam (float shore, float2 worldXZ, sampler2D noiseTex) { // float shore = IN.uv_MainTex.y; shore = sqrt(shore); float2 noiseUV = worldXZ + _Time.y * 0.25; float4 noise = tex2D(noiseTex, noiseUV * 0.015); float distortion1 = noise.x * (1 - shore); float foam1 = sin((shore + distortion1) * 10 - _Time.y); foam1 *= foam1; float distortion2 = noise.y * (1 - shore); float foam2 = sin((shore + distortion2) * 10 + _Time.y + 2); foam2 *= foam2 * 0.7; return max(foam1, foam2) * shore; } float Waves (float2 worldXZ, sampler2D noiseTex) { float2 uv1 = worldXZ; uv1.y += _Time.y; float4 noise1 = tex2D(noiseTex, uv1 * 0.025); float2 uv2 = worldXZ; uv2.x += _Time.y; float4 noise2 = tex2D(noiseTex, uv2 * 0.025); float blendWave = sin( (worldXZ.x + worldXZ.y) * 0.1 + (noise1.y + noise2.z) + _Time.y ); blendWave *= blendWave; float waves = lerp(noise1.z, noise1.w, blendWave) + lerp(noise2.x, noise2.y, blendWave); return smoothstep(0.75, 2, waves); } 

قم بتغيير تظليل الماء بحيث يستخدم ملف التضمين الجديد.

  #include "Water.cginc" sampler2D _MainTex; … void surf (Input IN, inout SurfaceOutputStandard o) { float waves = Waves(IN.worldPos.xz, _MainTex); fixed4 c = saturate(_Color + waves); o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } 

في تظليل Water Shore ، يتم حساب القيم لكل من الرغوة والموجات. ثم نقوم بكتم الأمواج ونحن نقترب من الشاطئ. ستكون النتيجة النهائية هي الحد الأقصى للرغوة والموجات.

  #include "Water.cginc" sampler2D _MainTex; … void surf (Input IN, inout SurfaceOutputStandard o) { float shore = IN.uv_MainTex.y; float foam = Foam(shore, IN.worldPos.xz, _MainTex); float waves = Waves(IN.worldPos.xz, _MainTex); waves *= 1 - shore; fixed4 c = saturate(_Color + max(foam, waves)); o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } 


مزيج من الرغوة والأمواج.

حزمة الوحدة

مرة أخرى عن المياه الساحلية


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


المياه الساحلية الخفية تقريبا.

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

معامل النزاهة 0.8. لمضاعفة حجم مركبات المياه ، نحتاج إلى ضبط معامل الماء على 0.6.

  public const float waterFactor = 0.6f; public static Vector3 GetFirstWaterCorner (HexDirection direction) { return corners[(int)direction] * waterFactor; } public static Vector3 GetSecondWaterCorner (HexDirection direction) { return corners[(int)direction + 1] * waterFactor; } 

سنستخدم هذه الطرق الجديدة HexGridChunkلإيجاد زوايا الماء.

  void TriangulateOpenWater ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { Vector3 c1 = center + HexMetrics.GetFirstWaterCorner(direction); Vector3 c2 = center + HexMetrics.GetSecondWaterCorner(direction); … } void TriangulateWaterShore ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { EdgeVertices e1 = new EdgeVertices( center + HexMetrics.GetFirstWaterCorner(direction), center + HexMetrics.GetSecondWaterCorner(direction) ); … } 


استخدام زوايا الماء.

تضاعفت المسافة بين سداسيات الماء في الواقع. الآن HexMetricsيجب أن يكون لديها أيضًا طريقة لإنشاء جسور في الماء.

  public const float waterBlendFactor = 1f - waterFactor; public static Vector3 GetWaterBridge (HexDirection direction) { return (corners[(int)direction] + corners[(int)direction + 1]) * waterBlendFactor; } 

التغيير HexGridChunkبحيث يستخدم الطريقة الجديدة.

  void TriangulateOpenWater ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { … if (direction <= HexDirection.SE && neighbor != null) { Vector3 bridge = HexMetrics.GetWaterBridge(direction); … if (direction <= HexDirection.E) { … water.AddTriangle( c2, e2, c2 + HexMetrics.GetWaterBridge(direction.Next()) ); } } } void TriangulateWaterShore ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { … Vector3 bridge = HexMetrics.GetWaterBridge(direction); … HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor != null) { waterShore.AddTriangle( e1.v5, e2.v5, e1.v5 + HexMetrics.GetWaterBridge(direction.Next()) ); … } } 


جسور طويلة في الماء.

بين ضلوع الماء والأرض


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

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

 // Vector3 bridge = HexMetrics.GetWaterBridge(direction); Vector3 center2 = neighbor.Position; center2.y = center.y; EdgeVertices e2 = new EdgeVertices( center2 + HexMetrics.GetSecondSolidCorner(direction.Opposite()), center2 + HexMetrics.GetFirstSolidCorner(direction.Opposite()) ); … HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor != null) { Vector3 center3 = nextNeighbor.Position; center3.y = center.y; waterShore.AddTriangle( e1.v5, e2.v5, center3 + HexMetrics.GetFirstSolidCorner(direction.Previous()) ); … } 


زوايا حواف خاطئة.

نجح ذلك ، الآن فقط نحتاج مرة أخرى إلى النظر في حالتين للمثلثات الزاوية.

  HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor != null) { // Vector3 center3 = nextNeighbor.Position; // center3.y = center.y; Vector3 v3 = nextNeighbor.Position + (nextNeighbor.IsUnderwater ? HexMetrics.GetFirstWaterCorner(direction.Previous()) : HexMetrics.GetFirstSolidCorner(direction.Previous())); v3.y = center.y; waterShore.AddTriangle(e1.v5, e2.v5, v3); waterShore.AddTriangleUV( new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(0f, nextNeighbor.IsUnderwater ? 0f : 1f) ); } 


الزوايا الصحيحة للحواف.

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

  shore = sqrt(shore) * 0.9; 


رغوة جاهزة.

حزمة الوحدة

أنهار تحت الماء


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


الأنهار تتدفق في الماء.

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

  Tags { "RenderType"="Transparent" "Queue"="Transparent+1" } 


نرسم الأنهار الأخيرة.

يختبئ نهر تحت الماء


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

  void TriangulateWithRiverBeginOrEnd ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … if (!cell.IsUnderwater) { bool reversed = cell.HasIncomingRiver; … } } void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … if (!cell.IsUnderwater) { bool reversed = cell.IncomingRiver == direction; … } } 

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

  if (cell.HasRiverThroughEdge(direction)) { e2.v3.y = neighbor.StreamBedY; if (!cell.IsUnderwater && !neighbor.IsUnderwater) { TriangulateRiverQuad( e1.v2, e1.v4, e2.v2, e2.v4, cell.RiverSurfaceY, neighbor.RiverSurfaceY, 0.8f, cell.HasIncomingRiver && cell.IncomingRiver == direction ); } } 


لا مزيد من الأنهار تحت الماء.

الشلالات


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

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

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

  void TriangulateWaterfallInWater ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y1, float y2, float waterY ) { v1.y = v2.y = y1; v3.y = v4.y = y2; rivers.AddQuad(v1, v2, v3, v4); rivers.AddQuadUV(0f, 1f, 0.8f, 1f); } 

سنطلق على هذه الطريقة TriangulateConnectionعندما يكون الجار تحت الماء وننشئ شلالًا.

  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 ); } else if (cell.Elevation > neighbor.WaterLevel) { TriangulateWaterfallInWater( e1.v2, e1.v4, e2.v2, e2.v4, cell.RiverSurfaceY, neighbor.RiverSurfaceY, neighbor.WaterSurfaceY ); } } 

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

  if (!cell.IsUnderwater) { … } else if ( !neighbor.IsUnderwater && neighbor.Elevation > cell.WaterLevel ) { TriangulateWaterfallInWater( e2.v4, e2.v2, e1.v4, e1.v2, neighbor.RiverSurfaceY, cell.RiverSurfaceY, cell.WaterSurfaceY ); } 

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


استيفاء.

لتحريك القمم السفلية لأعلى ، قم بتقسيم المسافة تحت سطح الماء على ارتفاع الشلال. هذا سيعطينا قيمة interpolator.

  v1.y = v2.y = y1; v3.y = v4.y = y2; float t = (waterY - y2) / (y1 - y2); v3 = Vector3.Lerp(v3, v1, t); v4 = Vector3.Lerp(v4, v2, t); rivers.AddQuad(v1, v2, v3, v4); rivers.AddQuadUV(0f, 1f, 0.8f, 1f); 

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

  v1.y = v2.y = y1; v3.y = v4.y = y2; v1 = HexMetrics.Perturb(v1); v2 = HexMetrics.Perturb(v2); v3 = HexMetrics.Perturb(v3); v4 = HexMetrics.Perturb(v4); float t = (waterY - y2) / (y1 - y2); v3 = Vector3.Lerp(v3, v1, t); v4 = Vector3.Lerp(v4, v2, t); rivers.AddQuadUnperturbed(v1, v2, v3, v4); rivers.AddQuadUV(0f, 1f, 0.8f, 1f); 

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

  public void AddQuadUnperturbed ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4 ) { int vertexIndex = vertices.Count; vertices.Add(v1); vertices.Add(v2); vertices.Add(v3); vertices.Add(v4); triangles.Add(vertexIndex); triangles.Add(vertexIndex + 2); triangles.Add(vertexIndex + 1); triangles.Add(vertexIndex + 1); triangles.Add(vertexIndex + 2); triangles.Add(vertexIndex + 3); } 


الشلالات تنتهي على سطح الماء.

حزمة الوحدة

مصبات الأنهار


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


يلتقي النهر مع الساحل دون تشويه القمم.

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

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

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

  void TriangulateWaterShore ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { … if (cell.HasRiverThroughEdge(direction)) { TriangulateEstuary(e1, e2); } else { waterShore.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); waterShore.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); waterShore.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); waterShore.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); waterShore.AddQuadUV(0f, 0f, 0f, 1f); waterShore.AddQuadUV(0f, 0f, 0f, 1f); waterShore.AddQuadUV(0f, 0f, 0f, 1f); waterShore.AddQuadUV(0f, 0f, 0f, 1f); } … } void TriangulateEstuary (EdgeVertices e1, EdgeVertices e2) { } 

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

  void TriangulateEstuary (EdgeVertices e1, EdgeVertices e2) { 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) ); } 


ثقب شبه منحرف لمنطقة الخلط.

إحداثيات UV2


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

  public bool useCollider, useColors, useUVCoordinates, useUV2Coordinates; [NonSerialized] List<Vector2> uvs, uv2s; public void Clear () { … if (useUVCoordinates) { uvs = ListPool<Vector2>.Get(); } if (useUV2Coordinates) { uv2s = ListPool<Vector2>.Get(); } triangles = ListPool<int>.Get(); } public void Apply () { … if (useUVCoordinates) { hexMesh.SetUVs(0, uvs); ListPool<Vector2>.Add(uvs); } if (useUV2Coordinates) { hexMesh.SetUVs(1, uv2s); ListPool<Vector2>.Add(uv2s); } … } 

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

  public void AddTriangleUV2 (Vector2 uv1, Vector2 uv2, Vector3 uv3) { uv2s.Add(uv1); uv2s.Add(uv2); uv2s.Add(uv3); } public void AddQuadUV2 (Vector2 uv1, Vector2 uv2, Vector3 uv3, Vector3 uv4) { uv2s.Add(uv1); uv2s.Add(uv2); uv2s.Add(uv3); uv2s.Add(uv4); } public void AddQuadUV2 (float uMin, float uMax, float vMin, float vMax) { uv2s.Add(new Vector2(uMin, vMin)); uv2s.Add(new Vector2(uMax, vMin)); uv2s.Add(new Vector2(uMin, vMax)); uv2s.Add(new Vector2(uMax, vMax)); } 

وظيفة نهر شادر


لأننا سوف يتم استخدام تأثير النهر في اثنين من تظليل، نقل رمز للتظليل نهر في الجديدة وظيفة تشمل ملف المياه المياه .

 float River (float2 riverUV, sampler2D noiseTex) { float2 uv = riverUV; uv.x = uv.x * 0.0625 + _Time.y * 0.005; uv.y -= _Time.y * 0.25; float4 noise = tex2D(noiseTex, uv); float2 uv2 = riverUV; uv2.x = uv2.x * 0.0625 - _Time.y * 0.0052; uv2.y -= _Time.y * 0.23; float4 noise2 = tex2D(noiseTex, uv2); return noise.x * noise2.w; } 

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

  #include "Water.cginc" sampler2D _MainTex; … void surf (Input IN, inout SurfaceOutputStandard o) { float river = River(IN.uv_MainTex, _MainTex); fixed4 c = saturate(_Color + river); … } 

كائنات الفم


أضف HexGridChunkفمًا لدعم كائن الشبكة.

  public HexMesh terrain, rivers, roads, water, waterShore, estuaries; public void Triangulate () { terrain.Clear(); rivers.Clear(); roads.Clear(); water.Clear(); waterShore.Clear(); estuaries.Clear(); for (int i = 0; i < cells.Length; i++) { Triangulate(cells[i]); } terrain.Apply(); rivers.Apply(); roads.Apply(); water.Apply(); waterShore.Apply(); estuaries.Apply(); } 

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


Estuarties الكائن.

تثليث الفم


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

  void TriangulateEstuary (EdgeVertices e1, EdgeVertices e2) { … estuaries.AddTriangle(e1.v3, e2.v2, e2.v4); estuaries.AddTriangleUV( new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(0f, 1f) ); } 


المثلث الأوسط.

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

  estuaries.AddQuad(e1.v2, e1.v3, e2.v1, e2.v2); estuaries.AddTriangle(e1.v3, e2.v2, e2.v4); estuaries.AddQuad(e1.v3, e1.v4, e2.v4, e2.v5); estuaries.AddQuadUV(0f, 0f, 0f, 1f); estuaries.AddTriangleUV( new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(0f, 1f) ); estuaries.AddQuadUV(0f, 0f, 0f, 1f); 


شبه منحرف جاهز.

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

  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(0f, 1f), new Vector2(0f, 0f) ); // estuaries.AddQuadUV(0f, 0f, 0f, 1f); 


رباعي مستدير ، هندسة متناظرة

تدفق النهر


لدعم تأثير النهر ، نحتاج إلى إضافة إحداثيات UV2. يقع الجزء السفلي من المثلث الأوسط في منتصف النهر ، لذا يجب أن يكون إحداثياته ​​U مساوية لـ 0.5. نظرًا لأن النهر يتدفق في اتجاه الماء ، تتلقى النقطة اليسرى إحداثيات U تساوي 1 ، وتتلقى النقطة اليمنى إحداثيات U بقيمة 0. نقوم بتعيين الإحداثيات Y إلى 0 و 1 ، المقابلة لاتجاه التيار.

  estuaries.AddTriangleUV2( new Vector2(0.5f, 1f), new Vector2(1f, 0f), new Vector2(0f, 0f) ); 

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

  estuaries.AddQuadUV2( new Vector2(1f, 0f), new Vector2(1f, 1f), new Vector2(1f, 0f), new Vector2(0.5f, 1f) ); estuaries.AddTriangleUV2( new Vector2(0.5f, 1f), new Vector2(1f, 0f), new Vector2(0f, 0f) ); estuaries.AddQuadUV2( new Vector2(0.5f, 1f), new Vector2(0f, 1f), new Vector2(0f, 0f), new Vector2(0f, 0f) ); 


شبه منحرف UV2.

للتأكد من تعيين إحداثيات UV2 بشكل صحيح ، اجعل أداة تظليل Estuary تجعلها. يمكننا الوصول إلى هذه الإحداثيات عن طريق الإضافة إلى هيكل الإدخال float2 uv2_MainTex.

  struct Input { float2 uv_MainTex; float2 uv2_MainTex; float3 worldPos; }; … void surf (Input IN, inout SurfaceOutputStandard o) { float shore = IN.uv_MainTex.y; float foam = Foam(shore, IN.worldPos.xz, _MainTex); float waves = Waves(IN.worldPos.xz, _MainTex); waves *= 1 - shore; fixed4 c = fixed4(IN.uv2_MainTex, 1, 1); … } 


إحداثيات UV2.

كل شيء يبدو جيدًا ، يمكنك استخدام تظليل لخلق تأثير النهر.

  void surf (Input IN, inout SurfaceOutputStandard o) { … float river = River(IN.uv2_MainTex, _MainTex); fixed4 c = saturate(_Color + river); … } 


استخدم UV2 لإنشاء تأثير نهر.

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

  estuaries.AddQuadUV2( new Vector2(1f, 0.8f), new Vector2(1f, 1.1f), new Vector2(1f, 0.8f), new Vector2(0.5f, 1.1f) ); estuaries.AddTriangleUV2( new Vector2(0.5f, 1.1f), new Vector2(1f, 0.8f), new Vector2(0f, 0.8f) ); estuaries.AddQuadUV2( new Vector2(0.5f, 1.1f), new Vector2(0f, 1.1f), new Vector2(0f, 0.8f), new Vector2(0f, 0.8f) ); 



التدفق المتزامن للنهر ومصب النهر.

إعداد التدفق


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

بدلًا من الحفاظ على إحداثيات U العليا ثابتة خارج عرض النهر ، قم بتحريكها بمقدار 0.5. أقصى اليسار 1.5 ، أقصى اليسار −0.5.

في الوقت نفسه ، نقوم بتوسيع التدفق عن طريق تحريك إحداثيات U للنقاط السفلية اليسرى واليمنى. قم بتغيير اليسار من 1 إلى 0.7 ، واليمين من 0 إلى 0.3.

  estuaries.AddQuadUV2( new Vector2(1.5f, 0.8f), new Vector2(0.7f, 1.1f), new Vector2(1f, 0.8f), new Vector2(0.5f, 1.1f) ); … estuaries.AddQuadUV2( new Vector2(0.5f, 1.1f), new Vector2(0.3f, 1.1f), new Vector2(0f, 0.8f), new Vector2(-0.5f, 0.8f) ); 



توسيع النهر.

لإكمال تأثير الانحناء ، قم بتغيير إحداثيات V لنفس النقاط الأربع. نظرًا لأن المياه تتدفق بعيدًا عن نهاية النهر ، فسوف نزيد إحداثيات النقاط العليا V إلى 1. ولإنشاء منحنى أفضل ، سنزيد إحداثيات V للنقطتين السفليتين إلى 1.15.

  estuaries.AddQuadUV2( new Vector2(1.5f, 1f), new Vector2(0.7f, 1.15f), new Vector2(1f, 0.8f), new Vector2(0.5f, 1.1f) ); estuaries.AddTriangleUV2( new Vector2(0.5f, 1.1f), new Vector2(1f, 0.8f), new Vector2(0f, 0.8f) ); estuaries.AddQuadUV2( new Vector2(0.5f, 1.1f), new Vector2(0.3f, 1.15f), new Vector2(0f, 0.8f), new Vector2(-0.5f, 1f) ); 



مجرى النهر المنحني.

مزيج النهر والساحل


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

  float shoreWater = max(foam, waves); float river = River(IN.uv2_MainTex, _MainTex); float water = lerp(shoreWater, river, IN.uv_MainTex.x); fixed4 c = saturate(_Color + water); 

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

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

  #pragma surface surf Standard alpha vertex:vert … struct Input { float2 uv_MainTex; float2 riverUV; float3 worldPos; }; … void vert (inout appdata_full v, out Input o) { UNITY_INITIALIZE_OUTPUT(Input, o); o.riverUV = v.texcoord1.xy; } void surf (Input IN, inout SurfaceOutputStandard o) { … float river = River(IN.riverUV, _MainTex); … } 


الاستيفاء على أساس قيمة الساحل.

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

  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.AddQuadUV(0f, 0f, 0f, 1f); 


المزيج الصحيح.

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


مصبات الأنهار في وحدة العمل



الأنهار المتدفقة من المسطحات المائية


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

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

  bool IsValidRiverDestination (HexCell neighbor) { return neighbor && ( elevation >= neighbor.elevation || waterLevel == neighbor.elevation ); } 

سنستخدم طريقتنا الجديدة لتحديد ما إذا كان من الممكن إنشاء نهر صادر.

  public void SetOutgoingRiver (HexDirection direction) { if (hasOutgoingRiver && outgoingRiver == direction) { return; } HexCell neighbor = GetNeighbor(direction); // if (!neighbor || elevation < neighbor.elevation) { if (!IsValidRiverDestination(neighbor)) { return; } RemoveOutgoingRiver(); … } 

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

  void ValidateRivers () { if ( hasOutgoingRiver && !IsValidRiverDestination(GetNeighbor(outgoingRiver)) ) { RemoveOutgoingRiver(); } if ( hasIncomingRiver && !GetNeighbor(incomingRiver).IsValidRiverDestination(this) ) { RemoveIncomingRiver(); } } 

دعونا نستخدم هذا الأسلوب الجديد في خصائص Elevationو WaterLevel.

  public int Elevation { … set { … // if ( // hasOutgoingRiver && // elevation < GetNeighbor(outgoingRiver).elevation // ) { // RemoveOutgoingRiver(); // } // if ( // hasIncomingRiver && // elevation > GetNeighbor(incomingRiver).elevation // ) { // RemoveIncomingRiver(); // } ValidateRivers(); … } } public int WaterLevel { … set { if (waterLevel == value) { return; } waterLevel = value; ValidateRivers(); Refresh(); } } 


بحيرات النهر المنتهية ولايته.

اقلب المد


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

  void TriangulateEstuary ( EdgeVertices e1, EdgeVertices e2, bool incomingRiver ) { … } 

سنقوم بتمرير هذه المعلومات عند استدعاء هذه الطريقة من TriangulateWaterShore.

  if (cell.HasRiverThroughEdge(direction)) { TriangulateEstuary(e1, e2, cell.IncomingRiver == direction); } 

الآن نحن بحاجة إلى توسيع تدفق النهر عن طريق تغيير إحداثيات UV2. تحتاج الإحداثيات U للأنهار الصادرة إلى عكسها: becomes0.5 يصبح 1.5 ، 0 يصبح 1 ، 1 يصبح 0 ، 1.5 يصبح −0.5.

مع إحداثيات V ، تصبح الأمور أكثر تعقيدًا. إذا نظرت إلى كيفية عملنا مع الوصلات النهرية المعكوسة ، فيجب أن يكون 0.8 0 ، و 1 يجب أن يكون −0.2. هذا يعني أن 1.1 تصبح −0.3 ، و 1.15 تصبح −0.35.

نظرًا لأن إحداثيات UV2 مختلفة تمامًا في كل حالة ، فلنكتب رمزًا منفصلاً لهم.

  void TriangulateEstuary ( EdgeVertices e1, EdgeVertices e2, bool incomingRiver ) { … if (incomingRiver) { estuaries.AddQuadUV2( new Vector2(1.5f, 1f), new Vector2(0.7f, 1.15f), new Vector2(1f, 0.8f), new Vector2(0.5f, 1.1f) ); estuaries.AddTriangleUV2( new Vector2(0.5f, 1.1f), new Vector2(1f, 0.8f), new Vector2(0f, 0.8f) ); estuaries.AddQuadUV2( new Vector2(0.5f, 1.1f), new Vector2(0.3f, 1.15f), new Vector2(0f, 0.8f), new Vector2(-0.5f, 1f) ); } else { estuaries.AddQuadUV2( new Vector2(-0.5f, -0.2f), new Vector2(0.3f, -0.35f), new Vector2(0f, 0f), new Vector2(0.5f, -0.3f) ); estuaries.AddTriangleUV2( new Vector2(0.5f, -0.3f), new Vector2(0f, 0f), new Vector2(1f, 0f) ); estuaries.AddQuadUV2( new Vector2(0.5f, -0.3f), new Vector2(0.7f, -0.35f), new Vector2(1f, 0f), new Vector2(1.5f, -0.2f) ); } } 


المسار الصحيح للأنهار.

حزمة الوحدة

الجزء 9: ميزات الإغاثة


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

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


الصراع بين الغابات والأراضي الزراعية والتحضر.

إضافة دعم للكائنات


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

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

إدارة الكائنات


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

سنبدأ بتطبيق فارغ لن يفعل شيئًا في الوقت الحالي.

 using UnityEngine; public class HexFeatureManager : MonoBehaviour { public void Clear () {} public void Apply () {} public void AddFeature (Vector3 position) {} } 

الآن يمكننا إضافة رابط لمثل هذا المكون في HexGridChunk. ثم يمكنك تضمينها في عملية التثليث ، مثل جميع العناصر الفرعية HexMesh.

  public HexFeatureManager features; public void Triangulate () { terrain.Clear(); rivers.Clear(); roads.Clear(); water.Clear(); waterShore.Clear(); estuaries.Clear(); features.Clear(); for (int i = 0; i < cells.Length; i++) { Triangulate(cells[i]); } terrain.Apply(); rivers.Apply(); roads.Apply(); water.Apply(); waterShore.Apply(); estuaries.Apply(); features.Apply(); } 

لنبدأ بوضع كائن واحد في وسط كل خلية

  void Triangulate (HexCell cell) { for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { Triangulate(d, cell); } features.AddFeature(cell.Position); } 

الآن نحن بحاجة إلى مدير كائن حقيقي. إضافة طفل آخر إلى الجاهزة Hex Grid Chunk وإعطائه مكون HexFeatureManager. ثم يمكنك توصيل جزء به.




تمت إضافة مدير كائن إلى الإعداد المسبق للجزء.

كائنات الجاهزة


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


مكعب الجاهزة.

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

  public Transform featurePrefab; 


مدير الكائن مع الجاهزة.

إنشاء مثيلات الكائن


الهيكل جاهز ، ويمكننا البدء في إضافة ميزات التضاريس! ما عليك سوى إنشاء مثيل من HexFeatureManager.AddFeatureالإعداد المسبق وتعيين موضعه.

  public void AddFeature (Vector3 position) { Transform instance = Instantiate(featurePrefab); instance.localPosition = position; } 


مثيلات ميزات التضاريس.

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

  public void AddFeature (Vector3 position) { Transform instance = Instantiate(featurePrefab); position.y += instance.localScale.y * 0.5f; instance.localPosition = position; } 


مكعبات على سطح الإغاثة.

ماذا لو استخدمنا شبكة أخرى؟
. , , . .

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

  instance.localPosition = HexMetrics.Perturb(position); 


مواقف الأشياء المشوهة.

تدمير مواد الإغاثة


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

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

  Transform container; public void Clear () { if (container) { Destroy(container.gameObject); } container = new GameObject("Features Container").transform; container.SetParent(transform, false); } … public void AddFeature (Vector3 position) { Transform instance = Instantiate(featurePrefab); position.y += instance.localScale.y * 0.5f; instance.localPosition = HexMetrics.Perturb(position); instance.SetParent(container, false); } 

ربما ، من غير الفعال إنشاء وتدمير أجسام الإغاثة في كل مرة.
, , . . . , , , . HexFeatureManager.Apply . . , , .

حزمة الوحدة

وضع أشياء الإغاثة


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


الكائنات في كل مكان.

لذلك ، دعنا نتحقق قبل وضع الكائن HexGridChunk.Triangulateما إذا كانت الخلية فارغة.

  if (!cell.IsUnderwater && !cell.HasRiver && !cell.HasRoads) { features.AddFeature(cell.Position); } 


سكن محدود.

كائن واحد لكل اتجاه


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

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

  void Triangulate (HexDirection direction, HexCell cell) { … if (cell.HasRiver) { … } else { TriangulateWithoutRiver(direction, cell, center, e); if (!cell.IsUnderwater && !cell.HasRoadThroughEdge(direction)) { features.AddFeature((center + e.v1 + e.v5) * (1f / 3f)); } } … } 


العديد من المرافق ، ولكن ليس بالقرب من الأنهار.

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

  void TriangulateAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … if (!cell.IsUnderwater && !cell.HasRoadThroughEdge(direction)) { features.AddFeature((center + e.v1 + e.v5) * (1f / 3f)); } } 


ظهرت الأشياء بجانب الأنهار.

هل من الممكن تقديم أشياء كثيرة؟
, dynamic batching Unity. , . batch. « », . instancing, dynamic batching.

حزمة الوحدة

مجموعة متنوعة من الأشياء


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

  public void AddFeature (Vector3 position) { Transform instance = Instantiate(featurePrefab); position.y += instance.localScale.y * 0.5f; instance.localPosition = HexMetrics.Perturb(position); instance.localRotation = Quaternion.Euler(0f, 360f * Random.value, 0f); instance.SetParent(container, false); } 


يتحول عشوائي.

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

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

إنشاء جدول تجزئة


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

  public const int hashGridSize = 256; static float[] hashGrid; public static void InitializeHashGrid () { hashGrid = new float[hashGridSize * hashGridSize]; for (int i = 0; i < hashGrid.Length; i++) { hashGrid[i] = Random.value; } } 

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

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

  public static void InitializeHashGrid (int seed) { hashGrid = new float[hashGridSize * hashGridSize]; Random.InitState(seed); for (int i = 0; i < hashGrid.Length; i++) { hashGrid[i] = Random.value; } } 

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

  Random.State currentState = Random.state; Random.InitState(seed); for (int i = 0; i < hashGrid.Length; i++) { hashGrid[i] = Random.value; } Random.state = currentState; 

تتم تهيئة جدول التجزئة HexGridفي نفس الوقت الذي يعين فيه نسيج الضوضاء. هذا هو ، في الأساليب HexGrid.Startو HexGrid.Awake. نصنعها بحيث لا يتم توليد القيم أكثر من اللازم.

  public int seed; void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); … } void OnEnable () { if (!HexMetrics.noiseSource) { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); } } 

يسمح لنا متغير البذور العام بتحديد قيمة البذور للخريطة. ستفعل أي قيمة. اخترت 1234.


اختيار البذور.

باستخدام جدول التجزئة


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

  public static float SampleHashGrid (Vector3 position) { int x = (int)position.x % hashGridSize; int z = (int)position.z % hashGridSize; return hashGrid[x + z * hashGridSize]; } 

ماذا تفعل٪؟
, , — . , −4, −3, −2, −1, 0, 1, 2, 3, 4 modulo 3 −1, 0, −2, −1, 0, 1, 2, 0, 1.

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

  int x = (int)position.x % hashGridSize; if (x < 0) { x += hashGridSize; } int z = (int)position.z % hashGridSize; if (z < 0) { z += hashGridSize; } 

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

  public const float hashGridScale = 0.25f; public static float SampleHashGrid (Vector3 position) { int x = (int)(position.x * hashGridScale) % hashGridSize; if (x < 0) { x += hashGridSize; } int z = (int)(position.z * hashGridScale) % hashGridSize; if (z < 0) { z += hashGridSize; } return hashGrid[x + z * hashGridSize]; } 

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

  public void AddFeature (Vector3 position) { float hash = HexMetrics.SampleHashGrid(position); Transform instance = Instantiate(featurePrefab); position.y += instance.localScale.y * 0.5f; instance.localPosition = HexMetrics.Perturb(position); instance.localRotation = Quaternion.Euler(0f, 360f * hash, 0f); instance.SetParent(container, false); } 

عتبة التنسيب


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

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

 using UnityEngine; public struct HexHash { public float a, b; public static HexHash Create () { HexHash hash; hash.a = Random.value; hash.b = Random.value; return hash; } } 

ألا تحتاج إلى إجراء تسلسل؟
, , Unity. , .

قم بتغييره HexMetricsبحيث يستخدم الهيكل الجديد.

  static HexHash[] hashGrid; public static void InitializeHashGrid (int seed) { hashGrid = new HexHash[hashGridSize * hashGridSize]; Random.State currentState = Random.state; Random.InitState(seed); for (int i = 0; i < hashGrid.Length; i++) { hashGrid[i] = HexHash.Create(); } Random.state = currentState; } public static HexHash SampleHashGrid (Vector3 position) { … } 

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

  public void AddFeature (Vector3 position) { HexHash hash = HexMetrics.SampleHashGrid(position); if (hash.a >= 0.5f) { return; } Transform instance = Instantiate(featurePrefab); position.y += instance.localScale.y * 0.5f; instance.localPosition = HexMetrics.Perturb(position); instance.localRotation = Quaternion.Euler(0f, 360f * hash.b, 0f); instance.SetParent(container, false); } 


يتم تقليل كثافة الأشياء بنسبة 50٪.

حزمة الوحدة

رسم الكائنات


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

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

  public int UrbanLevel { get { return urbanLevel; } set { if (urbanLevel != value) { urbanLevel = value; RefreshSelfOnly(); } } } int urbanLevel; 

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

منزلق الكثافة


لتغيير مستوى التحضر ، نضيف HexMapEditorشريط تمرير إضافي للدعم.

  int activeUrbanLevel; … bool applyUrbanLevel; … public void SetApplyUrbanLevel (bool toggle) { applyUrbanLevel = toggle; } public void SetUrbanLevel (float level) { activeUrbanLevel = (int)level; } void EditCell (HexCell cell) { if (cell) { … if (applyWaterLevel) { cell.WaterLevel = activeWaterLevel; } if (applyUrbanLevel) { cell.UrbanLevel = activeUrbanLevel; } if (riverMode == OptionalToggle.No) { cell.RemoveRiver(); } … } } 

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

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



منزلق التحضر.

تغيير العتبة


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

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

  public void AddFeature (HexCell cell, Vector3 position) { HexHash hash = HexMetrics.SampleHashGrid(position); if (hash.a >= cell.UrbanLevel * 0.25f) { return; } … } 

لكي ينجح هذا ، دعنا نمرر الخلايا إلى HexGridChunk.

  void Triangulate (HexCell cell) { … if (!cell.IsUnderwater && !cell.HasRiver && !cell.HasRoads) { features.AddFeature(cell, cell.Position); } } void Triangulate (HexDirection direction, HexCell cell) { … if (!cell.IsUnderwater && !cell.HasRoadThroughEdge(direction)) { features.AddFeature(cell, (center + e.v1 + e.v5) * (1f / 3f)); } … } … void TriangulateAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … if (!cell.IsUnderwater && !cell.HasRoadThroughEdge(direction)) { features.AddFeature(cell, (center + e.v1 + e.v5) * (1f / 3f)); } } 


رسم مستويات كثافة التحضر.

حزمة الوحدة

العديد من المباني الجاهزة لأشياء الإغاثة


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

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

 <del>// public Transform featurePrefab;</del> public Transform[] urbanPrefabs; public void AddFeature (HexCell cell, Vector3 position) { … Transform instance = Instantiate(urbanPrefabs[cell.UrbanLevel - 1]); … } 

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



استخدام المباني الجاهزة المختلفة لكل مستوى من التحضر.

مزيج الجاهزة


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

في المستوى 1 نستخدم وضع الأكواخ في 40٪ من الحالات. لن يكون هناك مباني أخرى هنا على الإطلاق. بالنسبة للمستوى ، نستخدم القيم الثلاث (0.4 ، 0 ، 0).

في المستوى 2 ، استبدل الأكواخ بمباني أكبر ، وأضف فرصة 20٪ للأكواخ الإضافية. لن نقوم بالمباني العالية. أي أننا نستخدم القيم الثلاث العتبة (0.2 ، 0.4 ، 0).

في المستوى 3 ، نستبدل المباني المتوسطة بأخرى طويلة ، ونستبدل الأكواخ مرة أخرى ، ونضيف فرصة 20٪ أخرى للأكواخ. ستكون قيم العتبة تساوي (0.2 ، 0.2 ، 0.4).

أي ، الفكرة هي أنه مع زيادة مستوى التحضر ، سنقوم بترقية المباني القائمة وإضافة مباني جديدة إلى الأماكن الفارغة. لإزالة مبنى موجود ، نحتاج إلى استخدام نفس فواصل قيمة التجزئة. إذا كانت التجزئة بين 0 و 0.4 في المستوى 1 عبارة عن أكواخ ، فعند المستوى 3 سيخلق نفس الفاصل المباني الشاهقة. في المستوى 3 ، يجب إنشاء المباني الشاهقة باستخدام التجزئة في النطاق من 0 إلى 0.4 ، والمباني المكونة من طابقين في النطاق من 0.4 إلى 0.6 ، والأكواخ في النطاق من 0.6 إلى 0.8. إذا قمت بتدقيقها من الأكبر إلى الأصغر ، فيمكن القيام بذلك باستخدام ثلاثية العتبات (0.4 ، 0.6 ، 0.8). عندئذٍ تصبح عتبات المستوى 2 (0 ، 0.4 ، 0.6) ، وستصبح عتبات المستوى 1 (0 ، 0 ، 0.4).

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

  static float[][] featureThresholds = { new float[] {0.0f, 0.0f, 0.4f}, new float[] {0.0f, 0.4f, 0.6f}, new float[] {0.4f, 0.6f, 0.8f} }; public static float[] GetFeatureThresholds (int level) { return featureThresholds[level]; } 

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

  Transform PickPrefab (int level, float hash) { if (level > 0) { float[] thresholds = HexMetrics.GetFeatureThresholds(level - 1); for (int i = 0; i < thresholds.Length; i++) { if (hash < thresholds[i]) { return urbanPrefabs[i]; } } } return null; } 

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


ترتيب مسبق مقلوب.

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

  public void AddFeature (HexCell cell, Vector3 position) { HexHash hash = HexMetrics.SampleHashGrid(position); // if (hash.a >= cell.UrbanLevel * 0.25f) { // return; // } // Transform instance = Instantiate(urbanPrefabs[cell.UrbanLevel - 1]); Transform prefab = PickPrefab(cell.UrbanLevel, hash.a); if (!prefab) { return; } Transform instance = Instantiate(prefab); position.y += instance.localScale.y * 0.5f; instance.localPosition = HexMetrics.Perturb(position); instance.localRotation = Quaternion.Euler(0f, 360f * hash.b, 0f); instance.SetParent(container, false); } 


امزج الجاهزة.

اختلافات المستوى


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

  public float a, b, c; public static HexHash Create () { HexHash hash; hash.a = Random.value; hash.b = Random.value; hash.c = Random.value; return hash; } 

دعنا نحولها HexFeatureManager.urbanPrefabsإلى مصفوفة من الصفائف ، ونضيف PickPrefabمعلمة إلى الطريقة choice. نستخدمه لاختيار فهرس المصفوفة المدمجة ، وضربه في طول المصفوفة وتحويله إلى عدد صحيح.

  public Transform[][] urbanPrefabs; … Transform PickPrefab (int level, float hash, float choice) { if (level > 0) { float[] thresholds = HexMetrics.GetFeatureThresholds(level - 1); for (int i = 0; i < thresholds.Length; i++) { if (hash < thresholds[i]) { return urbanPrefabs[i][(int)(choice * urbanPrefabs[i].Length)]; } } } return null; } 

دعنا نبرر اختيارنا على قيمة التجزئة الثانية (B). ثم تحتاج إلى التحول من B إلى C.

  public void AddFeature (HexCell cell, Vector3 position) { HexHash hash = HexMetrics.SampleHashGrid(position); Transform prefab = PickPrefab(cell.UrbanLevel, hash.a, hash.b); if (!prefab) { return; } Transform instance = Instantiate(prefab); position.y += instance.localScale.y * 0.5f; instance.localPosition = HexMetrics.Perturb(position); instance.localRotation = Quaternion.Euler(0f, 360f * hash.c, 0f); instance.SetParent(container, false); } 

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

  public static HexHash Create () { HexHash hash; hash.a = Random.value * 0.999f; hash.b = Random.value * 0.999f; hash.c = Random.value * 0.999f; return hash; } 

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

 using UnityEngine; [System.Serializable] public struct HexFeatureCollection { public Transform[] prefabs; public Transform Pick (float choice) { return prefabs[(int)(choice * prefabs.Length)]; } } 

نستخدم HexFeatureManagerبدلاً من المصفوفات المدمجة مجموعة من هذه المجموعات.

 // public Transform[][] urbanPrefabs; public HexFeatureCollection[] urbanCollections; … Transform PickPrefab (int level, float hash, float choice) { if (level > 0) { float[] thresholds = HexMetrics.GetFeatureThresholds(level - 1); for (int i = 0; i < thresholds.Length; i++) { if (hash < thresholds[i]) { return urbanCollections[i].Pick(choice); } } } return null; } 

الآن يمكننا تعيين العديد من المباني لكل مستوى كثافة. نظرًا لأنها مستقلة ، لا يتعين علينا استخدام نفس المبلغ لكل مستوى. لقد استخدمت للتو خيارين لكل مستوى ، مع إضافة خيار أقل لكل منهما. اخترت المقاييس لهم (3.5 ، 3 ، 2) ، (2.75 ، 1.5 ، 1.5) و (1.75 ، 1 ، 1).



نوعان من المباني لكل مستوى كثافة.

حزمة الوحدة

عدة أنواع من الأشياء


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

  public int FarmLevel { get { return farmLevel; } set { if (farmLevel != value) { farmLevel = value; RefreshSelfOnly(); } } } public int PlantLevel { get { return plantLevel; } set { if (plantLevel != value) { plantLevel = value; RefreshSelfOnly(); } } } int urbanLevel, farmLevel, plantLevel; 

بالطبع ، هذا يتطلب الدعم في HexMapEditorمنزلقين إضافيين.

  int activeUrbanLevel, activeFarmLevel, activePlantLevel; bool applyUrbanLevel, applyFarmLevel, applyPlantLevel; … public void SetApplyFarmLevel (bool toggle) { applyFarmLevel = toggle; } public void SetFarmLevel (float level) { activeFarmLevel = (int)level; } public void SetApplyPlantLevel (bool toggle) { applyPlantLevel = toggle; } public void SetPlantLevel (float level) { activePlantLevel = (int)level; } … void EditCell (HexCell cell) { if (cell) { … if (applyUrbanLevel) { cell.UrbanLevel = activeUrbanLevel; } if (applyFarmLevel) { cell.FarmLevel = activeFarmLevel; } if (applyPlantLevel) { cell.PlantLevel = activePlantLevel; } … } } 

أضفها إلى واجهة المستخدم.


ثلاث سلايدر.

أيضا ، ستكون هناك حاجة إلى مجموعات إضافية HexFeatureManager.

  public HexFeatureCollection[] urbanCollections, farmCollections, plantCollections; 


ثلاث مجموعات من مواد الإغاثة.

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

لقد صنعت مكعبات المزرعة بارتفاع 0.1 وحدة للإشارة إلى تخصيصات مربعة من الأراضي الزراعية. كمقاييس عالية الكثافة ، اخترت (2.5 ، 0.1 ، 2.5) و (3.5 ، 0.1 ، 2). في المتوسط ​​، تبلغ مساحة المواقع 1.75 وحجم 2.5 × 1.25. تم الحصول على مستوى منخفض من الكثافة في المنطقة 1 وحجم 1.5 بنسبة 0.75.

تشير النباتات الجاهزة إلى الأشجار العالية والشجيرات الكبيرة. تعتبر المباني الجاهزة عالية الكثافة هي الأكبر (1.25 ، 4.5 ، 1.25) و (1.5 ، 3 ، 1.5). المقاييس المتوسطة هي (0.75 ، 3 ، 0.75) و (1 ، 1.5 ، 1). أصغر النباتات لها أحجام (0.5 ، 1.5 ، 0.5) و (0.75 ، 1 ، 0.75).

اختيار ميزات الإغاثة


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

  public float a, b, c, d, e; public static HexHash Create () { HexHash hash; hash.a = Random.value * 0.999f; hash.b = Random.value * 0.999f; hash.c = Random.value * 0.999f; hash.d = Random.value * 0.999f; hash.e = Random.value * 0.999f; return hash; } 

الآن عليك HexFeatureManager.PickPrefabالعمل مع مجموعات مختلفة. أضف معلمة لتبسيط العملية. أيضا ، قم بتغيير التجزئة التي استخدمها متغير الإعداد المسبق المحدد إلى D ، والتجزئة للتدوير إلى E.

  Transform PickPrefab ( HexFeatureCollection[] collection, int level, float hash, float choice ) { if (level > 0) { float[] thresholds = HexMetrics.GetFeatureThresholds(level - 1); for (int i = 0; i < thresholds.Length; i++) { if (hash < thresholds[i]) { return collection[i].Pick(choice); } } } return null; } public void AddFeature (HexCell cell, Vector3 position) { HexHash hash = HexMetrics.SampleHashGrid(position); Transform prefab = PickPrefab( urbanCollections, cell.UrbanLevel, hash.a, hash.d ); … instance.localRotation = Quaternion.Euler(0f, 360f * hash.e, 0f); instance.SetParent(container, false); } 

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

  Transform prefab = PickPrefab( urbanCollections, cell.UrbanLevel, hash.a, hash.d ); Transform otherPrefab = PickPrefab( farmCollections, cell.FarmLevel, hash.b, hash.d ); if (!prefab) { return; } 

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

  Transform otherPrefab = PickPrefab( farmCollections, cell.FarmLevel, hash.b, hash.d ); if (prefab) { if (otherPrefab && hash.b < hash.a) { prefab = otherPrefab; } } else if (otherPrefab) { prefab = otherPrefab; } else { return; } 


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

  if (prefab) { if (otherPrefab && hash.b < hash.a) { prefab = otherPrefab; } } else if (otherPrefab) { prefab = otherPrefab; } otherPrefab = PickPrefab( plantCollections, cell.PlantLevel, hash.c, hash.d ); if (prefab) { if (otherPrefab && hash.c < hash.a) { prefab = otherPrefab; } } else if (otherPrefab) { prefab = otherPrefab; } else { return; } 

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

  float usedHash = hash.a; if (prefab) { if (otherPrefab && hash.b < hash.a) { prefab = otherPrefab; usedHash = hash.b; } } else if (otherPrefab) { prefab = otherPrefab; usedHash = hash.b; } otherPrefab = PickPrefab( plantCollections, cell.PlantLevel, hash.c, hash.d ); if (prefab) { if (otherPrefab && hash.c < usedHash) { prefab = otherPrefab; } } else if (otherPrefab) { prefab = otherPrefab; } else { return; } 


مزيج من الأشياء الحضرية والريفية والنباتية.

حزمة الوحدة

الجزء 10: الجدران


  • نحيط الخلايا.
  • نحن نبني الجدران على طول حواف الخلايا.
  • دعنا نذهب عبر الأنهار والطرق.
  • تجنب الماء واتصل بالمنحدرات.

في هذا الجزء سوف نضيف بين خلايا الجدار.


لا يوجد شيء أكثر جاذبية من الجدار العالي.

تحرير الجدار


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


الجدران على طول الحواف.

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

الملكية المسورة


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

  public bool Walled { get { return walled; } set { if (walled != value) { walled = value; Refresh(); } } } bool walled; 

مفتاح المحرر


لتبديل الحالة "المسيجة" للخلايا ، نحتاج إلى إضافة HexMapEditorدعم للمحول. لذلك ، نقوم بإضافة حقل آخر OptionalToggleوطريقة لضبطه.

  OptionalToggle riverMode, roadMode, walledMode; … public void SetWalledMode (int mode) { walledMode = (OptionalToggle)mode; } 

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

  void EditCell (HexCell cell) { if (cell) { … if (roadMode == OptionalToggle.No) { cell.RemoveRoads(); } if (walledMode != OptionalToggle.Ignore) { cell.Walled = walledMode == OptionalToggle.Yes; } if (isDrag) { … } } } 

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


مفتاح "المبارزة".

حزمة الوحدة

إنشاء الجدران


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



الجدران الجاهزة التابعة.

سيكون من المنطقي أن الجدران هي كائن حضري ، لذلك استخدمت لهم المواد الحمراء للمباني.

إدارة الجدار


بما أن الجدران هي أشياء راحة ، يجب أن تتعامل معها HexFeatureManager. ولذلك، فإن مدير تقديم الإغاثة مرجع كائن إلى كائن الجدران ، والتأكد من استدعاء الأساليب Clearو Apply.

  public HexMesh walls; … public void Clear () { … walls.Clear(); } public void Apply () { walls.Apply(); } 


الجدران متصلة بمدير الطبوغرافيا.

ألا يجب أن تكون الجدران طفلًا من الميزات؟
, . , Walls Hex Grid Chunk .

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

  public void AddWall ( EdgeVertices near, HexCell nearCell, EdgeVertices far, HexCell farCell ) { } 

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

  void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { … if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { … } else { … } features.AddWall(e1, cell, e2, neighbor); HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (direction <= HexDirection.E && nextNeighbor != null) { … } } 

بناء جزء الجدار


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

  void AddWallSegment ( Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight ) { } 


الجوانب القريبة والبعيدة.

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

  public void AddWall ( EdgeVertices near, HexCell nearCell, EdgeVertices far, HexCell farCell ) { if (nearCell.Walled != farCell.Walled) { AddWallSegment(near.v1, far.v1, near.v5, far.v5); } } 

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

  void AddWallSegment ( Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight ) { Vector3 left = Vector3.Lerp(nearLeft, farLeft, 0.5f); Vector3 right = Vector3.Lerp(nearRight, farRight, 0.5f); } 

كم يبلغ طول الجدار؟ دعونا نضع ارتفاعه على HexMetrics. لقد صنعتها بحجم مستوى ارتفاع خلية واحدة.

  public const float wallHeight = 3f; 

HexFeatureManager.AddWallSegmentيمكن استخدام هذا الارتفاع لوضع الذروتين الثالثة والرابعة للرباعية ، وإضافته أيضًا إلى الشبكة walls.

  Vector3 left = Vector3.Lerp(nearLeft, farLeft, 0.5f); Vector3 right = Vector3.Lerp(nearRight, farRight, 0.5f); Vector3 v1, v2, v3, v4; v1 = v3 = left; v2 = v4 = right; v3.y = v4.y = left.y + HexMetrics.wallHeight; walls.AddQuad(v1, v2, v3, v4); 

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


الجدران الرباعية أحادية الجانب.

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

  walls.AddQuad(v1, v2, v3, v4); walls.AddQuad(v2, v1, v4, v3); 


الجدران الثنائية.

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

جدران سميكة


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

  public const float wallThickness = 0.75f; 

لجعل جدارين سميكين ، تحتاج إلى جزء من كواد إلى الجانبين. يجب أن يتحركوا في اتجاهين متعاكسين. يجب أن يتحرك أحد الجانبين نحو الحافة القريبة ، والآخر نحو الحافة البعيدة. متجه الإزاحة لهذا متساوٍ far - near، ولكن لترك الجزء العلوي من الجدار مسطحًا ، نحتاج إلى تعيين المكون Y إلى 0.

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

  public static Vector3 WallThicknessOffset (Vector3 near, Vector3 far) { Vector3 offset; offset.x = far.x - near.x; offset.y = 0f; offset.z = far.z - near.z; return offset; } 

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

  return offset.normalized * (wallThickness * 0.5f); 

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

  Vector3 left = Vector3.Lerp(nearLeft, farLeft, 0.5f); Vector3 right = Vector3.Lerp(nearRight, farRight, 0.5f); Vector3 leftThicknessOffset = HexMetrics.WallThicknessOffset(nearLeft, farLeft); Vector3 rightThicknessOffset = HexMetrics.WallThicknessOffset(nearRight, farRight); Vector3 v1, v2, v3, v4; v1 = v3 = left - leftThicknessOffset; v2 = v4 = right - rightThicknessOffset; v3.y = v4.y = left.y + HexMetrics.wallHeight; walls.AddQuad(v1, v2, v3, v4); v1 = v3 = left + leftThicknessOffset; v2 = v4 = right + rightThicknessOffset; v3.y = v4.y = left.y + HexMetrics.wallHeight; walls.AddQuad(v2, v1, v4, v3); 


جدران مع تعويضات.

الكواد الآن منحازة ، على الرغم من أن هذا ليس ملحوظًا تمامًا.

هل سمك الجدار هو نفسه؟
, «-» . , . . , . , . , - , . .

قمم الجدران


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

  Vector3 v1, v2, v3, v4; v1 = v3 = left - leftThicknessOffset; v2 = v4 = right - rightThicknessOffset; v3.y = v4.y = left.y + HexMetrics.wallHeight; walls.AddQuad(v1, v2, v3, v4); Vector3 t1 = v3, t2 = v4; v1 = v3 = left + leftThicknessOffset; v2 = v4 = right + rightThicknessOffset; v3.y = v4.y = left.y + HexMetrics.wallHeight; walls.AddQuad(v2, v1, v4, v3); walls.AddQuad(t1, t2, v3, v4); 


الجدران ذات القمم.

المنعطفات


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


تكوينات الزاوية.

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


أدوار الخلية.

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

  void AddWallSegment ( Vector3 pivot, HexCell pivotCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { AddWallSegment(pivot, left, pivot, right); } 

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

  public void AddWall ( Vector3 c1, HexCell cell1, Vector3 c2, HexCell cell2, Vector3 c3, HexCell cell3 ) { if (cell1.Walled) { if (cell2.Walled) { if (!cell3.Walled) { AddWallSegment(c3, cell3, c1, cell1, c2, cell2); } } else if (cell3.Walled) { AddWallSegment(c2, cell2, c3, cell3, c1, cell1); } else { AddWallSegment(c1, cell1, c2, cell2, c3, cell3); } } else if (cell2.Walled) { if (cell3.Walled) { AddWallSegment(c1, cell1, c2, cell2, c3, cell3); } else { AddWallSegment(c2, cell2, c3, cell3, c1, cell1); } } else if (cell3.Walled) { AddWallSegment(c3, cell3, c1, cell1, c2, cell2); } } 

لإضافة شرائح الزاوية ، اتصل بهذه الطريقة في النهاية HexGridChunk.TriangulateCorner.

  void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … features.AddWall(bottom, bottomCell, left, leftCell, right, rightCell); } 


الجدران ذات الزوايا ، ولكن لا تزال هناك ثقوب.

أغلق الثقوب


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

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

  float leftTop = left.y + HexMetrics.wallHeight; float rightTop = right.y + HexMetrics.wallHeight; Vector3 v1, v2, v3, v4; v1 = v3 = left - leftThicknessOffset; v2 = v4 = right - rightThicknessOffset; v3.y = leftTop; v4.y = rightTop; walls.AddQuad(v1, v2, v3, v4); Vector3 t1 = v3, t2 = v4; v1 = v3 = left + leftThicknessOffset; v2 = v4 = right + rightThicknessOffset; v3.y = leftTop; v4.y = rightTop; walls.AddQuad(v2, v1, v4, v3); 


جدران مغلقة.

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

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


لم تعد هناك ثقوب في الظلال.

حزمة الوحدة

جدار يدج


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


جدران مستقيمة على الحواف.

اتبع الحافة


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

  public void AddWall ( EdgeVertices near, HexCell nearCell, EdgeVertices far, HexCell farCell ) { if (nearCell.Walled != farCell.Walled) { AddWallSegment(near.v1, far.v1, near.v2, far.v2); AddWallSegment(near.v2, far.v2, near.v3, far.v3); AddWallSegment(near.v3, far.v3, near.v4, far.v4); AddWallSegment(near.v4, far.v4, near.v5, far.v5); } } 


تقويس الجدران.

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

وضع الجدران على الأرض


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


جدران معلقة في الهواء.

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

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


الجدار السفلي.

دعنا نضيف إلى HexMetricsالطريقة WallLerpالتي تتعامل مع هذا الاستيفاء ، بالإضافة إلى حساب متوسط ​​إحداثيات X و Z للقمتين القريبة والبعيدة. يقوم على طريقة TerraceLerp.

  public const float wallElevationOffset = verticalTerraceStepSize; … public static Vector3 WallLerp (Vector3 near, Vector3 far) { near.x += (far.x - near.x) * 0.5f; near.z += (far.z - near.z) * 0.5f; float v = near.y < far.y ? wallElevationOffset : (1f - wallElevationOffset); near.y += (far.y - near.y) * v; return near; } 

فرض HexFeatureManagerهذه الطريقة لتحديد الذروات اليسرى واليمنى.

  void AddWallSegment ( Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight ) { Vector3 left = HexMetrics.WallLerp(nearLeft, farLeft); Vector3 right = HexMetrics.WallLerp(nearRight, farRight); … } 


الجدران واقفة على الأرض.

تغيير تشويه الجدار


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

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

  void AddWallSegment ( Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight ) { nearLeft = HexMetrics.Perturb(nearLeft); farLeft = HexMetrics.Perturb(farLeft); nearRight = HexMetrics.Perturb(nearRight); farRight = HexMetrics.Perturb(farRight); … walls.AddQuadUnperturbed(v1, v2, v3, v4); … walls.AddQuadUnperturbed(v2, v1, v4, v3); walls.AddQuadUnperturbed(t1, t2, v3, v4); } 


القمم غير المشوهة للجدران.

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


سمك الجدار أكثر اتساقا.

حزمة الوحدة

ثقوب في الجدران


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

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

  public void AddWall ( EdgeVertices near, HexCell nearCell, EdgeVertices far, HexCell farCell, bool hasRiver, bool hasRoad ) { if (nearCell.Walled != farCell.Walled) { AddWallSegment(near.v1, far.v1, near.v2, far.v2); if (hasRiver || hasRoad) { // Leave a gap. } else { AddWallSegment(near.v2, far.v2, near.v3, far.v3); AddWallSegment(near.v3, far.v3, near.v4, far.v4); } AddWallSegment(near.v4, far.v4, near.v5, far.v5); } } 

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

  void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { … bool hasRiver = cell.HasRiverThroughEdge(direction); bool hasRoad = cell.HasRoadThroughEdge(direction); if (hasRiver) { … } if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(e1, cell, e2, neighbor, hasRoad); } else { TriangulateEdgeStrip(e1, cell.Color, e2, neighbor.Color, hasRoad); } features.AddWall(e1, cell, e2, neighbor, hasRiver, hasRoad); … } 


ثقوب في الجدران لمرور الأنهار والطرق.

نحن نغطي الجدران


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

  void AddWallCap (Vector3 near, Vector3 far) { near = HexMetrics.Perturb(near); far = HexMetrics.Perturb(far); Vector3 center = HexMetrics.WallLerp(near, far); Vector3 thickness = HexMetrics.WallThicknessOffset(near, far); Vector3 v1, v2, v3, v4; v1 = v3 = center - thickness; v2 = v4 = center + thickness; v3.y = v4.y = center.y + HexMetrics.wallHeight; walls.AddQuadUnperturbed(v1, v2, v3, v4); } 

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

  public void AddWall ( EdgeVertices near, HexCell nearCell, EdgeVertices far, HexCell farCell, bool hasRiver, bool hasRoad ) { if (nearCell.Walled != farCell.Walled) { AddWallSegment(near.v1, far.v1, near.v2, far.v2); if (hasRiver || hasRoad) { AddWallCap(near.v2, far.v2); AddWallCap(far.v4, near.v4); } … } } 


ثقوب مغلقة في الحوائط.

ماذا عن الثقوب حول حواف الخريطة؟
, . . , .

حزمة الوحدة

تجنب المنحدرات والمياه


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


الجدران على المنحدرات وفي الماء.

يمكننا إزالة الجدران من هذه الحواف غير الضرورية مع تسجيلات إضافية AddWall. لا يمكن أن يكون الجدار تحت الماء ، ولا يمكن أن يكون الضلع المشترك معه منحدرًا.

  public void AddWall ( EdgeVertices near, HexCell nearCell, EdgeVertices far, HexCell farCell, bool hasRiver, bool hasRoad ) { if ( nearCell.Walled != farCell.Walled && !nearCell.IsUnderwater && !farCell.IsUnderwater && nearCell.GetEdgeType(farCell) != HexEdgeType.Cliff ) { … } } 


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

إزالة زوايا الجدار


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

  void AddWallSegment ( Vector3 pivot, HexCell pivotCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { if (pivotCell.IsUnderwater) { return; } AddWallSegment(pivot, left, pivot, right); } 


لم تعد هناك خلايا دعم تحت الماء.

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

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

  if (pivotCell.IsUnderwater) { return; } bool hasLeftWall = !leftCell.IsUnderwater && pivotCell.GetEdgeType(leftCell) != HexEdgeType.Cliff; bool hasRighWall = !rightCell.IsUnderwater && pivotCell.GetEdgeType(rightCell) != HexEdgeType.Cliff; if (hasLeftWall && hasRighWall) { AddWallSegment(pivot, left, pivot, right); } 


تمت إزالة جميع الزوايا المسببة للتداخل.

أغلق الزوايا


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

  if (hasLeftWall) { if (hasRighWall) { AddWallSegment(pivot, left, pivot, right); } else { AddWallCap(pivot, left); } } else if (hasRighWall) { AddWallCap(right, pivot); } 


نحن نغلق الجدران.

ربط الجدران بالمنحدرات


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


ثقوب بين الجدران ووجوه المنحدرات.

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

  void AddWallWedge (Vector3 near, Vector3 far, Vector3 point) { near = HexMetrics.Perturb(near); far = HexMetrics.Perturb(far); point = HexMetrics.Perturb(point); Vector3 center = HexMetrics.WallLerp(near, far); Vector3 thickness = HexMetrics.WallThicknessOffset(near, far); Vector3 v1, v2, v3, v4; Vector3 pointTop = point; point.y = center.y; v1 = v3 = center - thickness; v2 = v4 = center + thickness; v3.y = v4.y = pointTop.y = center.y + HexMetrics.wallHeight; // walls.AddQuadUnperturbed(v1, v2, v3, v4); walls.AddQuadUnperturbed(v1, point, v3, pointTop); walls.AddQuadUnperturbed(point, v2, pointTop, v4); walls.AddTriangleUnperturbed(pointTop, v3, v4); } 

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

  if (hasLeftWall) { if (hasRighWall) { AddWallSegment(pivot, left, pivot, right); } else if (leftCell.Elevation < rightCell.Elevation) { AddWallWedge(pivot, left, right); } else { AddWallCap(pivot, left); } } else if (hasRighWall) { if (rightCell.Elevation < leftCell.Elevation) { AddWallWedge(right, pivot, left); } else { AddWallCap(right, pivot); } } 


, .

unitypackage

11:


  • .
  • .
  • .


.


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

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

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

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


برج الجاهزة.

أضف رابطًا إلى هذا الإعداد المسبق HexFeatureManagerوقم بتوصيله.

  public Transform wallTower; 


تصل إلى برج الجاهزة.

أبراج البناء


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

  void AddWallSegment ( Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight ) { … Transform towerInstance = Instantiate(wallTower); towerInstance.transform.localPosition = (left + right) * 0.5f; towerInstance.SetParent(container, false); } 


برج واحد لكل قطعة حائط.

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

بدلاً من حساب الدوران بأنفسنا ، نقوم ببساطة بتعيين Transform.rightمتجه للملكية . سيغير رمز الوحدة دوران الكائن بحيث يتوافق حق اتجاهه المحلي مع الناقل المرسَل.

  Transform towerInstance = Instantiate(wallTower); towerInstance.transform.localPosition = (left + right) * 0.5f; Vector3 rightDirection = right - left; rightDirection.y = 0f; towerInstance.transform.right = rightDirection; towerInstance.SetParent(container, false); 


يتم محاذاة الأبراج مع الجدار.

كيف تعمل المهمة Transform.right؟
Quaternion.FromToRotation . .

 public Vector3 right { get { return rotation * Vector3.right; } set { rotation = Quaternion.FromToRotation(Vector3.right, value); } } 

قلل عدد الأبراج


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

  void AddWallSegment ( Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight, bool addTower = false ) { … if (addTower) { Transform towerInstance = Instantiate(wallTower); towerInstance.transform.localPosition = (left + right) * 0.5f; Vector3 rightDirection = right - left; rightDirection.y = 0f; towerInstance.transform.right = rightDirection; towerInstance.SetParent(container, false); } } 

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

  void AddWallSegment ( Vector3 pivot, HexCell pivotCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … AddWallSegment(pivot, left, pivot, right, true); … } 


الأبراج فقط في الزوايا.

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

  HexHash hash = HexMetrics.SampleHashGrid( (pivot + left + right) * (1f / 3f) ); bool hasTower = hash.e < HexMetrics.wallTowerThreshold; AddWallSegment(pivot, left, pivot, right, hasTower); 

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

  public const float wallTowerThreshold = 0.5f; 


أبراج عشوائية.

نزيل الأبراج من المنحدرات


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


أبراج على المنحدرات.

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

  bool hasTower = false; if (leftCell.Elevation == rightCell.Elevation) { HexHash hash = HexMetrics.SampleHashGrid( (pivot + left + right) * (1f / 3f) ); hasTower = hash.e < HexMetrics.wallTowerThreshold; } AddWallSegment(pivot, left, pivot, right, hasTower); 


لم تعد هناك أبراج على جدران المنحدرات.

نضع الجدران والأبراج على الأرض


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


أبراج في الهواء.

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


الجدران في الهواء.

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

  public const float wallHeight = 4f; public const float wallYOffset = -1f; 

نقوم بتغييره HexMetrics.WallLerpبحيث يأخذ الإزاحة الجديدة في الاعتبار عند تحديد إحداثيات ص.

  public static Vector3 WallLerp (Vector3 near, Vector3 far) { near.x += (far.x - near.x) * 0.5f; near.z += (far.z - near.z) * 0.5f; float v = near.y < far.y ? wallElevationOffset : (1f - wallElevationOffset); near.y += (far.y - near.y) * v + wallYOffset; return near; } 

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



الجدران والأبراج على الأرض.

حزمة الوحدة

الجسور


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

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

أضف رابطًا إلى الجسر الجاهز HexFeatureManagerوقم بتعيينه.

  public Transform wallTower, bridge; 


الجسر المسبق المعين.

وضع الجسور


لوضع الجسر ، نحتاج إلى طريقة HexFeatureManager.AddBridge. يجب أن يقع الجسر بين مركز النهر وأحد جانبي النهر.

  public void AddBridge (Vector3 roadCenter1, Vector3 roadCenter2) { Transform instance = Instantiate(bridge); instance.localPosition = (roadCenter1 + roadCenter2) * 0.5f; instance.SetParent(container, false); } 

سننقل مراكز الطرق غير المشوهة ، لذا سيتعين علينا تشويهها قبل وضع الجسر.

  roadCenter1 = HexMetrics.Perturb(roadCenter1); roadCenter2 = HexMetrics.Perturb(roadCenter2); Transform instance = Instantiate(bridge); 

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

  Transform instance = Instantiate(bridge); instance.localPosition = (roadCenter1 + roadCenter2) * 0.5f; instance.forward = roadCenter2 - roadCenter1; instance.SetParent(container, false); 

نحن نبني الجسور عبر الأنهار المستقيمة


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

للبدء ، دعنا نكتشف أنهارًا مستقيمة. في الداخل ، يقوم HexGridChunk.TriangulateRoadAdjacentToRiverالمشغل الأول else ifبترتيب الطرق بالقرب من هذه الأنهار. لذلك ، سنضيف هنا الجسور.

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

  void TriangulateRoadAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { … roadCenter += corner * 0.5f; features.AddBridge(roadCenter, center - corner * 0.5f); center += corner * 0.25f; } … } 


الجسور فوق الأنهار المستقيمة.

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

  roadCenter += corner * 0.5f; if (cell.IncomingRiver == direction.Next()) { features.AddBridge(roadCenter, center - corner * 0.5f); } center += corner * 0.25f; 

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

  if (cell.IncomingRiver == direction.Next() && ( cell.HasRoadThroughEdge(direction.Next2()) || cell.HasRoadThroughEdge(direction.Opposite()) )) { features.AddBridge(roadCenter, center - corner * 0.5f); } 


جسور بين الطرق على الجانبين.

الجسور فوق الأنهار المنحنية


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

  void TriangulateRoadAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … else { HexDirection middle; if (previousHasRiver) { middle = direction.Next(); } else if (nextHasRiver) { middle = direction.Previous(); } else { middle = direction; } if ( !cell.HasRoadThroughEdge(middle) && !cell.HasRoadThroughEdge(middle.Previous()) && !cell.HasRoadThroughEdge(middle.Next()) ) { return; } Vector3 offset = HexMetrics.GetSolidEdgeMiddle(middle); roadCenter += offset * 0.25f; } … } 

مقياس الإزاحة من خارج المنحنى هو 0.25 ، وداخله HexMetrics.innerToOuter * 0.7f. نستخدمه لوضع الجسر.

  Vector3 offset = HexMetrics.GetSolidEdgeMiddle(middle); roadCenter += offset * 0.25f; features.AddBridge( roadCenter, center - offset * (HexMetrics.innerToOuter * 0.7f) ); 


الجسور فوق الأنهار المنحنية.

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

  Vector3 offset = HexMetrics.GetSolidEdgeMiddle(middle); roadCenter += offset * 0.25f; if (direction == middle) { features.AddBridge( roadCenter, center - offset * (HexMetrics.innerToOuter * 0.7f) ); } 

ومرة أخرى ، عليك التأكد من أن الطريق على الجانب الآخر.

  if ( direction == middle && cell.HasRoadThroughEdge(direction.Opposite()) ) { features.AddBridge( roadCenter, center - offset * (HexMetrics.innerToOuter * 0.7f) ); } 


جسور بين الطرق على الجانبين.

تحجيم الجسر


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


تختلف المسافات ولكن أطوال الجسر ثابتة.

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

لإجراء القياس المناسب نحتاج إلى معرفة الطول الأولي للجسر الجاهز. سنقوم بتخزين هذا الطول في HexMetrics.

  public const float bridgeDesignLength = 7f; 

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

  public void AddBridge (Vector3 roadCenter1, Vector3 roadCenter2) { roadCenter1 = HexMetrics.Perturb(roadCenter1); roadCenter2 = HexMetrics.Perturb(roadCenter2); Transform instance = Instantiate(bridge); instance.localPosition = (roadCenter1 + roadCenter2) * 0.5f; instance.forward = roadCenter2 - roadCenter1; float length = Vector3.Distance(roadCenter1, roadCenter2); instance.localScale = new Vector3( 1f, 1f, length * (1f / HexMetrics.bridgeDesignLength) ); instance.SetParent(container, false); } 


تغيير طول الجسور.

بناء الجسر


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



جسور مقوسة ذات أطوال مختلفة.

حزمة الوحدة

كائنات خاصة


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

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


الجاهزة القلعة.

يمكن أن يكون الكائن الخاص الآخر زقورة ، على سبيل المثال ، مبنية من ثلاث مكعبات موضوعة فوق بعضها البعض. بالنسبة للمكعب السفلي ، المقياس (8 ، 2.5 ، 8) مناسب.


زقورة بريفاب.

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


ميغا فلورا الجاهزة.

أضف إلى HexFeatureManagerالصفيف لتتبع هذه المباني الجاهزة.

  public Transform[] special; 

أولاً ، أضف قلعة إلى الصفيف ، ثم ziggurat ، ثم megaflora.


تخصيص كائنات خاصة.

جعل الخلايا خاصة


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

  int specialIndex; 

مثل أشياء الإغاثة الأخرى ، دعونا نعطيها القدرة على تلقي وتعيين هذه القيمة.

  public int SpecialIndex { get { return specialIndex; } set { if (specialIndex != value) { specialIndex = value; RefreshSelfOnly(); } } } 

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

  public bool IsSpecial { get { return specialIndex > 0; } } 

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

  int activeUrbanLevel, activeFarmLevel, activePlantLevel, activeSpecialIndex; … bool applyUrbanLevel, applyFarmLevel, applyPlantLevel, applySpecialIndex; … public void SetApplySpecialIndex (bool toggle) { applySpecialIndex = toggle; } public void SetSpecialIndex (float index) { activeSpecialIndex = (int)index; } … void EditCell (HexCell cell) { if (cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } if (applyWaterLevel) { cell.WaterLevel = activeWaterLevel; } if (applySpecialIndex) { cell.SpecialIndex = activeSpecialIndex; } if (applyUrbanLevel) { cell.UrbanLevel = activeUrbanLevel; } … } } 

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


منزلق للكائنات الخاصة.

إضافة كائنات خاصة


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

  public void AddSpecialFeature (HexCell cell, Vector3 position) { Transform instance = Instantiate(special[cell.SpecialIndex - 1]); instance.localPosition = HexMetrics.Perturb(position); instance.SetParent(container, false); } 

دعونا نعطي الكائن دورانًا تعسفيًا باستخدام جدول التجزئة.

  public void AddSpecialFeature (HexCell cell, Vector3 position) { Transform instance = Instantiate(special[cell.SpecialIndex - 1]); instance.localPosition = HexMetrics.Perturb(position); HexHash hash = HexMetrics.SampleHashGrid(position); instance.localRotation = Quaternion.Euler(0f, 360f * hash.e, 0f); instance.SetParent(container, false); } 

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

  void Triangulate (HexCell cell) { for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { Triangulate(d, cell); } if (!cell.IsUnderwater && !cell.HasRiver && !cell.HasRoads) { features.AddFeature(cell, cell.Position); } if (cell.IsSpecial) { features.AddSpecialFeature(cell, cell.Position); } } 


كائنات خاصة. هم أكبر بكثير من المعتاد.

تجنب الأنهار


نظرًا لوجود أجسام خاصة في وسط الخلايا ، فإنها لا تتحد مع الأنهار ، لأنها ستعلق فوقها.


كائنات على الأنهار.

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

  public int SpecialIndex { … set { if (specialIndex != value && !HasRiver) { specialIndex = value; RefreshSelfOnly(); } } } 

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

  public void SetOutgoingRiver (HexDirection direction) { … hasOutgoingRiver = true; outgoingRiver = direction; specialIndex = 0; neighbor.RemoveIncomingRiver(); neighbor.hasIncomingRiver = true; neighbor.incomingRiver = direction.Opposite(); neighbor.specialIndex = 0; SetRoad((int)direction, false); } 

نتجنب الطرق


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


كائنات على الطريق.

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

  public int SpecialIndex { … set { if (specialIndex != value && !HasRiver) { specialIndex = value; RemoveRoads(); RefreshSelfOnly(); } } } 

ماذا لو قمنا بحذف كائن معين؟
0, , . .

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

  public void AddRoad (HexDirection direction) { if ( !roads[(int)direction] && !HasRiverThroughEdge(direction) && !IsSpecial && !GetNeighbor(direction).IsSpecial && GetElevationDifference(direction) <= 1 ) { SetRoad((int)direction, true); } } 

تجنب الأشياء الأخرى


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


كائن يتقاطع مع كائنات أخرى.

في هذه الحالة ، سنقمع الأشياء الصغيرة ، كما لو كانت تحت الماء. هذه المرة سوف نتحقق HexFeatureManager.AddFeature.

  public void AddFeature (HexCell cell, Vector3 position) { if (cell.IsSpecial) { return; } … } 

تجنب الماء


لدينا أيضًا مشكلة في الماء. هل ستستمر الميزات الخاصة أثناء الفيضانات؟ نظرًا لأننا ندمر الأشياء الصغيرة في الخلايا المغمورة ، فلنفعل الشيء نفسه مع الكائنات الخاصة.


كائنات في الماء.

في HexGridChunk.Triangulateسنقوم بنفس فحص الفيضان لكل من الكائنات الخاصة والعادية.

  void Triangulate (HexCell cell) { for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { Triangulate(d, cell); } if (!cell.IsUnderwater && !cell.HasRiver && !cell.HasRoads) { features.AddFeature(cell, cell.Position); } if (!cell.IsUnderwater && cell.IsSpecial) { features.AddSpecialFeature(cell, cell.Position); } } 

نظرًا لأن كلا المشغلين يتحققان ifالآن مما إذا كانت الخلية تحت الماء ، فيمكننا نقل الاختبار وإجراءه مرة واحدة فقط.

  void Triangulate (HexCell cell) { for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { Triangulate(d, cell); } if (!cell.IsUnderwater) { if (!cell.HasRiver && !cell.HasRoads) { features.AddFeature(cell, cell.Position); } if (cell.IsSpecial) { features.AddSpecialFeature(cell, cell.Position); } } } 

للتجارب ، سيكون مثل هذا العدد من الأشياء كافياً بالنسبة لنا.

حزمة الوحدة

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


All Articles