خرائط مسدس في الوحدة: الأجزاء 1-3

الصورة

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

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

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

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

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

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

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

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

الجزء 1: الربط من السداسيات


جدول المحتويات


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

هذا البرنامج التعليمي هو بداية سلسلة حول البطاقات السداسية. تُستخدم شبكات السداسي في العديد من الألعاب ، وخاصة في الاستراتيجيات ، بما في ذلك Age of Wonders 3 و Civilization 5 و Endless Legend. سنبدأ بالأساسيات ، وسنضيف ميزات جديدة تدريجيًا ، ونتيجة لذلك ، سننشئ ارتياحًا معقدًا يعتمد على السداسيات.

يفترض هذا البرنامج التعليمي أنك قد درست بالفعل سلسلة Mesh Basics ، التي تبدأ بالشبكة الإجرائية . تم إنشاؤه على الوحدة 5.3.1. تستخدم السلسلة عدة إصدارات من الوحدة. الجزء الأخير مصنوع على Unity 2017.3.0p3.


خريطة بسيطة للسداسيات.

حول السداسيات


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


الميدان وجيرانه.

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

ما هي المسافة بين مراكز خلايا الشبكة المربعة المجاورة؟ إذا كان طول الحافة 1 ، فإن الجواب الأفقي والرأسي هو 1. ولكن بالنسبة للجيران المائلة تكون الإجابة √2.

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


مسدس وجيرانه.

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

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


الشعاع الخارجي والداخلي للسداسي.

يوجد أيضًا نصف قطر داخلي ، وهو المسافة من المركز إلى كل من الحواف. هذه المعلمة مهمة لأن المسافة بين مراكز الجيران تساوي هذه القيمة ضرب اثنين. نصف القطر الداخلي  f r a c s q r t 3 2  من نصف القطر الخارجي ، أي في حالتنا 5 ق ف ص ر 3  . دعونا نضع هذه المعلمات في فئة ثابتة للراحة.

using UnityEngine; public static class HexMetrics { public const float outerRadius = 10f; public const float innerRadius = outerRadius * 0.866025404f; } 

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

لذلك ، لطول الضلع e نصف القطر الداخلي  sqrte2(e/2)2= sqrt3e2/4=e sqrt3/2 تقريبًا0.886e .

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


التوجهات الممكنة.

  public static Vector3[] corners = { new Vector3(0f, 0f, outerRadius), new Vector3(innerRadius, 0f, 0.5f * outerRadius), new Vector3(innerRadius, 0f, -0.5f * outerRadius), new Vector3(0f, 0f, -outerRadius), new Vector3(-innerRadius, 0f, -0.5f * outerRadius), new Vector3(-innerRadius, 0f, 0.5f * outerRadius) }; 

حزمة الوحدة

الربط


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

 using UnityEngine; public class HexCell : MonoBehaviour { } 

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


استخدام طائرة كمقدمة لخلية سداسية.

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

 using UnityEngine; public class HexGrid : MonoBehaviour { public int width = 6; public int height = 6; public HexCell cellPrefab; } 


كائن شبكة سداسي.

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

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

  HexCell[] cells; void Awake () { cells = new HexCell[height * width]; for (int z = 0, i = 0; z < height; z++) { for (int x = 0; x < width; x++) { CreateCell(x, z, i++); } } } void CreateCell (int x, int z, int i) { Vector3 position; position.x = x * 10f; position.y = 0f; position.z = z * 10f; HexCell cell = cells[i] = Instantiate<HexCell>(cellPrefab); cell.transform.SetParent(transform, false); cell.transform.localPosition = position; } 


شبكة مربعة من الطائرات.

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

عرض الإحداثيات


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

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

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



قماش لإحداثيات الشبكة السداسي.

لعرض الإحداثيات ، قم بإنشاء كائن نص ( GameObject / UI / Text ) وتحويله إلى الإعداد المسبق. قم بتوسيط المراسي والمحور ، اضبط الحجم على 5 × 15. يجب أيضًا محاذاة النص أفقياً ورأسياً في المركز. قم بتعيين حجم الخط على 4. وأخيرًا ، لا نريد استخدام النص الافتراضي ولن نستخدم Rich Text . أيضًا ، لا يهمنا ما إذا كان هدف Raycast قيد التشغيل ، لأنه لا يزال غير مطلوب بالنسبة إلى لوحتنا .



تسمية خلية الجاهزة.

الآن نحن بحاجة إلى إخبار الشبكة عن القماش والجاهزة. أضف إلى بداية النص using UnityEngine.UI; للوصول بسهولة إلى نوع UnityEngine.UI.Text . يحتاج الملصق الجاهز إلى متغير مشترك ، ويمكن العثور على اللوحة القماشية عن طريق استدعاء GetComponentInChildren .

  public Text cellLabelPrefab; Canvas gridCanvas; void Awake () { gridCanvas = GetComponentInChildren<Canvas>(); … } 


العلامات الجاهزة للاتصال.

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

  void CreateCell (int x, int z, int i) { … Text label = Instantiate<Text>(cellLabelPrefab); label.rectTransform.SetParent(gridCanvas.transform, false); label.rectTransform.anchoredPosition = new Vector2(position.x, position.z); label.text = x.ToString() + "\n" + z.ToString(); } 


عرض الإحداثيات.

مواقف سداسية


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


هندسة السداسيات المجاورة.

  position.x = x * (HexMetrics.innerRadius * 2f); position.y = 0f; position.z = z * (HexMetrics.outerRadius * 1.5f); 


نطبق المسافات بين السداسيات بدون تعويضات.

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

  position.x = (x + z * 0.5f) * (HexMetrics.innerRadius * 2f); 


يؤدي وضع السداسيات بشكل صحيح إلى إنشاء شبكة على شكل ماسة.

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

  position.x = (x + z * 0.5f - z / 2) * (HexMetrics.innerRadius * 2f); 


موقع السداسيات في منطقة مستطيلة.

حزمة الوحدة

التقديم السداسي


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


لا توجد طائرات أخرى.

كما في دروس Mesh Basics ، نستخدم شبكة واحدة لعرض الشبكة بالكامل. ومع ذلك ، هذه المرة لن نقوم بتحديد عدد القمم والمثلثات المطلوبة مسبقًا. بدلاً من ذلك ، سنستخدم القوائم.

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

 using UnityEngine; using System.Collections.Generic; [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public class HexMesh : MonoBehaviour { Mesh hexMesh; List<Vector3> vertices; List<int> triangles; void Awake () { GetComponent<MeshFilter>().mesh = hexMesh = new Mesh(); hexMesh.name = "Hex Mesh"; vertices = new List<Vector3>(); triangles = new List<int>(); } } 

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



كائن شبكة سداسي عشري.

HexGrid الآن من استعادة HexGrid السداسية بنفس الطريقة التي وجد بها قماش.

  HexMesh hexMesh; void Awake () { gridCanvas = GetComponentInChildren<Canvas>(); hexMesh = GetComponentInChildren<HexMesh>(); … } 

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

  void Start () { hexMesh.Triangulate(cells); } 

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

  public void Triangulate (HexCell[] cells) { hexMesh.Clear(); vertices.Clear(); triangles.Clear(); for (int i = 0; i < cells.Length; i++) { Triangulate(cells[i]); } hexMesh.vertices = vertices.ToArray(); hexMesh.triangles = triangles.ToArray(); hexMesh.RecalculateNormals(); } void Triangulate (HexCell cell) { } 

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

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

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

  void Triangulate (HexCell cell) { Vector3 center = cell.transform.localPosition; AddTriangle( center, center + HexMetrics.corners[0], center + HexMetrics.corners[1] ); } 


المثلث الأول لكل خلية.

نجح هذا ، لذا دعنا ندور حول جميع المثلثات الستة.

  Vector3 center = cell.transform.localPosition; for (int i = 0; i < 6; i++) { AddTriangle( center, center + HexMetrics.corners[i], center + HexMetrics.corners[i + 1] ); } 

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

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

  public static Vector3[] corners = { new Vector3(0f, 0f, outerRadius), new Vector3(innerRadius, 0f, 0.5f * outerRadius), new Vector3(innerRadius, 0f, -0.5f * outerRadius), new Vector3(0f, 0f, -outerRadius), new Vector3(-innerRadius, 0f, -0.5f * outerRadius), new Vector3(-innerRadius, 0f, 0.5f * outerRadius), new Vector3(0f, 0f, outerRadius) }; 


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

حزمة الوحدة

إحداثيات سداسية


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


إحداثيات الإزاحة مع خطوط صفرية مميزة.

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

 using UnityEngine; [System.Serializable] public struct HexCoordinates { public int X { get; private set; } public int Z { get; private set; } public HexCoordinates (int x, int z) { X = x; Z = z; } } 

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

  public static HexCoordinates FromOffsetCoordinates (int x, int z) { return new HexCoordinates(x, z); } } 

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

  public override string ToString () { return "(" + X.ToString() + ", " + Z.ToString() + ")"; } public string ToStringOnSeparateLines () { return X.ToString() + "\n" + Z.ToString(); } 

يمكننا الآن تمرير الكثير من الإحداثيات إلى مكون HexCell .

 public class HexCell : MonoBehaviour { public HexCoordinates coordinates; } 

قم بتغيير HexGrid.CreateCell حتى يتمكن من استخدام الإحداثيات الجديدة.

  HexCell cell = cells[i] = Instantiate<HexCell>(cellPrefab); cell.transform.SetParent(transform, false); cell.transform.localPosition = position; cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); Text label = Instantiate<Text>(cellLabelPrefab); label.rectTransform.SetParent(gridCanvas.transform, false); label.rectTransform.anchoredPosition = new Vector2(position.x, position.z); label.text = cell.coordinates.ToStringOnSeparateLines(); 

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

  public static HexCoordinates FromOffsetCoordinates (int x, int z) { return new HexCoordinates(x - z / 2, z); } 



إحداثيات محورية.

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


يظهر قياس ص.

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

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

  public int Y { get { return -X - Z; } } public override string ToString () { return "(" + X.ToString() + ", " + Y.ToString() + ", " + Z.ToString() + ")"; } public string ToStringOnSeparateLines () { return X.ToString() + "\n" + Y.ToString() + "\n" + Z.ToString(); } 


إحداثيات مكعبة.

ينسق المفتش


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


المفتش لا يعرض الإحداثيات.

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

  [SerializeField] private int x, z; public int X { get { return x; } } public int Z { get { return z; } } public HexCoordinates (int x, int z) { this.x = x; this.z = z; } 


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

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

يجب أن تقوم الفئة بتوسيع UnityEditor.PropertyDrawer وتحتاج إلى السمة UnityEditor.CustomPropertyDrawer بنوع مناسب.

 using UnityEngine; using UnityEditor; [CustomPropertyDrawer(typeof(HexCoordinates))] public class HexCoordinatesDrawer : PropertyDrawer { } 

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

  public override void OnGUI ( Rect position, SerializedProperty property, GUIContent label ) { } 

نقوم باستخراج قيم x و z من الخاصية ، ثم نستخدمهما لإنشاء مجموعة جديدة من الإحداثيات. ثم ارسم ملصق GUI في الموضع المحدد باستخدام طريقة HexCoordinates.ToString .

  public override void OnGUI ( Rect position, SerializedProperty property, GUIContent label ) { HexCoordinates coordinates = new HexCoordinates( property.FindPropertyRelative("x").intValue, property.FindPropertyRelative("z").intValue ); GUI.Label(position, coordinates.ToString()); } 


إحداثيات بدون تسمية بادئة.

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

  position = EditorGUI.PrefixLabel(position, label); GUI.Label(position, coordinates.ToString()); 


ينسق مع تسمية.

حزمة الوحدة

لمس الخلايا


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

للمس خلية ، يمكنك انبعاث الأشعة في المشهد من موضع مؤشر الماوس. يمكننا استخدام نفس النهج الموجود في البرنامج التعليمي Mesh Deformation .

  void Update () { if (Input.GetMouseButton(0)) { HandleInput(); } } void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { TouchCell(hit.point); } } void TouchCell (Vector3 position) { position = transform.InverseTransformPoint(position); Debug.Log("touched at " + position); } 

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

  MeshCollider meshCollider; void Awake () { GetComponent<MeshFilter>().mesh = hexMesh = new Mesh(); meshCollider = gameObject.AddComponent<MeshCollider>(); … } 

بعد اكتمال التثليث ، عيّن شبكة للمصادم.

  public void Triangulate (HexCell[] cells) { … meshCollider.sharedMesh = hexMesh; } 

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

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

  public void TouchCell (Vector3 position) { position = transform.InverseTransformPoint(position); HexCoordinates coordinates = HexCoordinates.FromPosition(position); Debug.Log("touched at " + coordinates.ToString()); } 

كيف ستحدد هذه الطريقة الإحداثيات التي تنتمي إلى المنصب؟ يمكننا أن نبدأ بقسمة x على العرض الأفقي للسداسي. وبما أن الإحداثيات Y هي صورة طبق الأصل للإحداثيات X ، فإن x سالب يعطينا y.

  public static HexCoordinates FromPosition (Vector3 position) { float x = position.x / (HexMetrics.innerRadius * 2f); float y = -x; } 

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

  float offset = position.z / (HexMetrics.outerRadius * 3f); x -= offset; y -= offset; 

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

  int iX = Mathf.RoundToInt(x); int iY = Mathf.RoundToInt(y); int iZ = Mathf.RoundToInt(-x -y); return new HexCoordinates(iX, iZ); 

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

  if (iX + iY + iZ != 0) { Debug.LogWarning("rounding error!"); } return new HexCoordinates(iX, iZ); 

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

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

  if (iX + iY + iZ != 0) { float dX = Mathf.Abs(x - iX); float dY = Mathf.Abs(y - iY); float dZ = Mathf.Abs(-x -y - iZ); if (dX > dY && dX > dZ) { iX = -iY - iZ; } else if (dZ > dY) { iZ = -iX - iY; } } 

صفحة تلوين السداسي


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

  public Color defaultColor = Color.white; public Color touchedColor = Color.magenta; 


اختيار لون الخلية.

أضف إلى HexCellحقل اللون العام.

 public class HexCell : MonoBehaviour { public HexCoordinates coordinates; public Color color; } 

قم بتعيينه HexGrid.CreateCellإلى اللون الافتراضي.

  void CreateCell (int x, int z, int i) { … cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); cell.color = defaultColor; … } 

نحتاج أيضًا إلى الإضافة إلى HexMeshمعلومات الألوان.

  List<Color> colors; void Awake () { … vertices = new List<Vector3>(); colors = new List<Color>(); … } public void Triangulate (HexCell[] cells) { hexMesh.Clear(); vertices.Clear(); colors.Clear(); … hexMesh.vertices = vertices.ToArray(); hexMesh.colors = colors.ToArray(); … } 

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

  void Triangulate (HexCell cell) { Vector3 center = cell.transform.localPosition; for (int i = 0; i < 6; i++) { AddTriangle( center, center + HexMetrics.corners[i], center + HexMetrics.corners[i + 1] ); AddTriangleColor(cell.color); } } void AddTriangleColor (Color color) { colors.Add(color); colors.Add(color); colors.Add(color); } 

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

هل نحن حقًا بحاجة إلى إعادة تثليث الشبكة بالكامل؟
, . . , , . .

  public void TouchCell (Vector3 position) { position = transform.InverseTransformPoint(position); HexCoordinates coordinates = HexCoordinates.FromPosition(position); int index = coordinates.X + coordinates.Z * width + coordinates.Z / 2; HexCell cell = cells[index]; cell.color = touchedColor; hexMesh.Triangulate(cells); } 

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

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

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


خلايا ملونة.

أحصل على التحف الظل الغريبة!
Unity . , , Z-. .

حزمة الوحدة

محرر الخرائط


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

 public void ColorCell (Vector3 position, Color color) { position = transform.InverseTransformPoint(position); HexCoordinates coordinates = HexCoordinates.FromPosition(position); int index = coordinates.X + coordinates.Z * width + coordinates.Z / 2; HexCell cell = cells[index]; cell.color = color; hexMesh.Triangulate(cells); } 

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

 using UnityEngine; public class HexMapEditor : MonoBehaviour { public Color[] colors; public HexGrid hexGrid; private Color activeColor; void Awake () { SelectColor(0); } void Update () { if (Input.GetMouseButton(0)) { HandleInput(); } } void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { hexGrid.ColorCell(hit.point, activeColor); } } public void SelectColor (int index) { activeColor = colors[index]; } } 

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


محرر خريطة سداسي بأربعة ألوان.

أضف لوحة إلى اللوحة لتخزين محددات الألوان ( GameObject / UI / Panel ). أضف مجموعة تبديلها ( Components / UI / Toggle Group ). اجعل اللوحة صغيرة وضعها في زاوية الشاشة.


لوحة ملونة مع مجموعة تبديل.

الآن املأ اللوحة بمفاتيح لكل لون ( GameObject / UI / Toggle ). طالما أننا لا نهتم بإنشاء واجهة مستخدم معقدة ، يكفي التكوين اليدوي البسيط.



مفتاح واحد لكل لون.

قم بتشغيل المفتاح الأول. قم أيضًا بعمل جميع مفاتيح التبديل في مجموعة التبديل بحيث يمكن تحديد واحد فقط منها في كل مرة. أخيرًا ، SelectColorاربطها بطريقة محررنا . يمكن القيام بذلك باستخدام زر "+" واجهة المستخدم الخاصة بحدث On Value Changed . حدد كائن محرر الخرائط ، ثم حدد الطريقة المطلوبة من القائمة المنسدلة.


المفتاح الأول.

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

متى يتم استدعاء طريقة حدث التبديل؟
. , , .

, , . , SelectColor . , .


تلوين بعدة ألوان.

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

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

 using UnityEngine; using UnityEngine.EventSystems; … void Update () { if ( Input.GetMouseButton(0) && !EventSystem.current.IsPointerOverGameObject() ) { HandleInput(); } } 

حزمة الوحدة

الجزء 2: مزج ألوان الخلايا


جدول المحتويات


  • ربط الجيران.
  • استيفاء الألوان بين المثلثات.
  • إنشاء مناطق مزج.
  • بسّط الهندسة.

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


انتقالات سلسة بين الخلايا.

الخلايا المجاورة


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

 public enum HexDirection { NE, E, SE, SW, W, NW } 

ما هو التعداد؟
enum , . . , . , .

enum . , integer . , - , integer.


ستة جيران ، ستة اتجاهات.

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

  [SerializeField] HexCell[] neighbors; 

هل نحن بحاجة لتخزين جميع الاتصالات مع الجيران؟
, . — , .

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


هناك مساحة لستة جيران في جاهزنا.

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

  public HexCell GetNeighbor (HexDirection direction) { return neighbors[(int)direction]; } 

أضف طريقة لتحديد جار.

  public void SetNeighbor (HexDirection direction, HexCell cell) { neighbors[(int)direction] = cell; } 

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

  public void SetNeighbor (HexDirection direction, HexCell cell) { neighbors[(int)direction] = cell; cell.neighbors[(int)direction.Opposite()] = this; } 


الجيران في اتجاهين متعاكسين.

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

 public enum HexDirection { NE, E, SE, SW, W, NW } public static class HexDirectionExtensions { public static HexDirection Opposite (this HexDirection direction) { return (int)direction < 3 ? (direction + 3) : (direction - 3); } } 

ما هي طريقة التمديد؟
— , - . — , , , . this . , .

? , , , . ? — . , , .

اتصال الجار


يمكننا تهيئة ارتباط الجيران في HexGrid.CreateCell. عند اجتياز الخلايا سطرا بسطر ، من اليسار إلى اليمين ، نعرف الخلايا التي تم إنشاؤها بالفعل. هذه هي الخلايا التي يمكننا الاتصال بها.

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


الاتصال من E إلى W أثناء تكوين الخلايا.

  void CreateCell (int x, int z, int i) { … cell.color = defaultColor; if (x > 0) { cell.SetNeighbor(HexDirection.W, cells[i - 1]); } Text label = Instantiate<Text>(cellLabelPrefab); … } 


ترتبط الجارتين الشرقية والغربية.

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

  if (x > 0) { cell.SetNeighbor(HexDirection.W, cells[i - 1]); } if (z > 0) { } 

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


اتصال من NW إلى SE للخطوط الزوجية.

  if (z > 0) { if ((z & 1) == 0) { cell.SetNeighbor(HexDirection.SE, cells[i - width]); } } 

ماذا يفعل z & 1؟
&& — , & — . , . 1, 1. , 10101010 & 00001111 00001010 .

. 0 1. 1, 2, 3, 4 1, 10, 11, 100. , 0.

, , . 0, .

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


اتصال من NE إلى SW للخطوط الزوجية.

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

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

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


جميع الجيران متصلون.

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


الجيران لكل خلية.

حزمة الوحدة

خلط اللون


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

  void Triangulate (HexCell cell) { for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { Triangulate(d, cell); } } void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.transform.localPosition; AddTriangle( center, center + HexMetrics.corners[(int)direction], center + HexMetrics.corners[(int)direction + 1] ); AddTriangleColor(cell.color); } 

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

  AddTriangle( center, center + HexMetrics.GetFirstCorner(direction), center + HexMetrics.GetSecondCorner(direction) ); 

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

  static Vector3[] corners = { new Vector3(0f, 0f, outerRadius), new Vector3(innerRadius, 0f, 0.5f * outerRadius), new Vector3(innerRadius, 0f, -0.5f * outerRadius), new Vector3(0f, 0f, -outerRadius), new Vector3(-innerRadius, 0f, -0.5f * outerRadius), new Vector3(-innerRadius, 0f, 0.5f * outerRadius), new Vector3(0f, 0f, outerRadius) }; public static Vector3 GetFirstCorner (HexDirection direction) { return corners[(int)direction]; } public static Vector3 GetSecondCorner (HexDirection direction) { return corners[(int)direction + 1]; } 

عدة ألوان على مثلث


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

  void AddTriangleColor (Color c1, Color c2, Color c3) { colors.Add(c1); colors.Add(c2); colors.Add(c3); } 

الآن يمكننا البدء في خلط الألوان! لنبدأ ببساطة باستخدام لون الجوار للقمتين الأخريين.

  void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.transform.localPosition; AddTriangle( center, center + HexMetrics.GetFirstCorner(direction), center + HexMetrics.GetSecondCorner(direction) ); HexCell neighbor = cell.GetNeighbor(direction); AddTriangleColor(cell.color, neighbor.color, neighbor.color); } 

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

  HexCell neighbor = cell.GetNeighbor(direction) ?? cell; 

ماذا يفعل عامل الهاتف ؟؟
null-coalescing operator. , a ?? ba != null ? a : b .

, - Unity . null . .


هناك مزيج من الألوان ، ولكن يتم ذلك بشكل غير صحيح.

أين ذهبت تسميات الإحداثيات؟
, UI.

متوسط ​​اللون


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

  HexCell neighbor = cell.GetNeighbor(direction) ?? cell; Color edgeColor = (cell.color + neighbor.color) * 0.5f; AddTriangleColor(cell.color, edgeColor, edgeColor); 


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


ثلاثة جيران ، أربعة ألوان.

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

دعنا نضيف HexDirectionExtensionsطريقتين إضافيتين للانتقال المريح إلى الاتجاهين السابق والتالي.

  public static HexDirection Previous (this HexDirection direction) { return direction == HexDirection.NE ? HexDirection.NW : (direction - 1); } public static HexDirection Next (this HexDirection direction) { return direction == HexDirection.NW ? HexDirection.NE : (direction + 1); } 

الآن يمكننا الحصول على جميع الجيران الثلاثة وإجراء خلط ثلاثي.

  HexCell prevNeighbor = cell.GetNeighbor(direction.Previous()) ?? cell; HexCell neighbor = cell.GetNeighbor(direction) ?? cell; HexCell nextNeighbor = cell.GetNeighbor(direction.Next()) ?? cell; AddTriangleColor( cell.color, (cell.color + prevNeighbor.color + neighbor.color) / 3f, (cell.color + neighbor.color + nextNeighbor.color) / 3f ); 


اخلط الزوايا.

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

حزمة الوحدة

مناطق الخلط


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


التظليل المستمر للسنت مع مناطق الخلط.

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

  public const float solidFactor = 0.75f; public const float blendFactor = 1f - solidFactor; 

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

  public static Vector3 GetFirstSolidCorner (HexDirection direction) { return corners[(int)direction] * solidFactor; } public static Vector3 GetSecondSolidCorner (HexDirection direction) { return corners[(int)direction + 1] * solidFactor; } 

الآن ، قم بتغييره HexMesh.Triangulateبحيث يستخدم زوايا التظليل الصلبة بدلاً من الزوايا الأصلية. نترك الألوان كما هي الآن.

  AddTriangle( center, center + HexMetrics.GetFirstSolidCorner(direction), center + HexMetrics.GetSecondSolidCorner(direction) ); 


سداسيات صلبة بدون حواف.

تثليث مناطق الخلط


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


ضلع شبه منحرف.

  void AddQuad (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); } void AddQuadColor (Color c1, Color c2, Color c3, Color c4) { colors.Add(c1); colors.Add(c2); colors.Add(c3); colors.Add(c4); } 

نعيد تشكيله HexMesh.Triangulateبحيث يتلقى المثلث لونًا واحدًا ، وتقوم الرباعية بعمل خليط بين لون صلب وألوان زاويتين.

  void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.transform.localPosition; Vector3 v1 = center + HexMetrics.GetFirstSolidCorner(direction); Vector3 v2 = center + HexMetrics.GetSecondSolidCorner(direction); AddTriangle(center, v1, v2); AddTriangleColor(cell.color); Vector3 v3 = center + HexMetrics.GetFirstCorner(direction); Vector3 v4 = center + HexMetrics.GetSecondCorner(direction); AddQuad(v1, v2, v3, v4); HexCell prevNeighbor = cell.GetNeighbor(direction.Previous()) ?? cell; HexCell neighbor = cell.GetNeighbor(direction) ?? cell; HexCell nextNeighbor = cell.GetNeighbor(direction.Next()) ?? cell; AddQuadColor( cell.color, cell.color, (cell.color + prevNeighbor.color + neighbor.color) / 3f, (cell.color + neighbor.color + nextNeighbor.color) / 3f ); } 


الاختلاط مع الأضلاع شبه المنحرفة.

الجسور بين الأضلاع


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


الجسر بين الأضلاع.

يمكن أن نجد وظائف جديدة v3و v4، بدءا من v1و v2ثم انتقل على طول الجسر مباشرة إلى حافة الخلية. ماذا سيكون إزاحة الجسر؟ يمكننا العثور عليه عن طريق أخذ نقطة الوسط بين الزوايا المقابلة ، ثم تطبيق معامل الخلط عليه. هذا سيفعل HexMetrics.

  public static Vector3 GetBridge (HexDirection direction) { return (corners[(int)direction] + corners[(int)direction + 1]) * 0.5f * blendFactor; } 

عودة إلى HexMesh، سيكون من المنطقي الآن إضافة خيار AddQuadColorيتطلب لونين فقط.

  void AddQuadColor (Color c1, Color c2) { colors.Add(c1); colors.Add(c1); colors.Add(c2); colors.Add(c2); } 

قم بتغييره Triangulateبحيث يقوم بإنشاء جسور مختلطة بشكل صحيح بين الجيران.

  Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; AddQuad(v1, v2, v3, v4); HexCell prevNeighbor = cell.GetNeighbor(direction.Previous()) ?? cell; HexCell neighbor = cell.GetNeighbor(direction) ?? cell; HexCell nextNeighbor = cell.GetNeighbor(direction.Next()) ?? cell; AddQuadColor(cell.color, (cell.color + neighbor.color) * 0.5f); 


الجسور المطلية بشكل صحيح مع مسافات الزاوية.

سد الثغرات


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

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

  Color bridgeColor = (cell.color + neighbor.color) * 0.5f; AddQuadColor(cell.color, bridgeColor); AddTriangle(v1, center + HexMetrics.GetFirstCorner(direction), v3); AddTriangleColor( cell.color, (cell.color + prevNeighbor.color + neighbor.color) / 3f, bridgeColor ); 


كل شيء جاهز تقريبًا.

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

  AddTriangle(v2, v4, center + HexMetrics.GetSecondCorner(direction)); AddTriangleColor( cell.color, bridgeColor, (cell.color + neighbor.color + nextNeighbor.color) / 3f ); 


تلوين كامل.

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

لكن التحولات بين الألوان لا تزال قبيحة
. . .

حزمة الوحدة

فيب ريب


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


ثلاثة هياكل بصرية.

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


أصعب من الضرورة.

لماذا نحتاج حتى هذا؟
, . , , . , , . , , .

الجسر المباشر


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

  public static Vector3 GetBridge (HexDirection direction) { return (corners[(int)direction] + corners[(int)direction + 1]) * blendFactor; } 


تمتد الجسور على طول كامل وتتداخل مع بعضها البعض.

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

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

  void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.transform.localPosition; Vector3 v1 = center + HexMetrics.GetFirstSolidCorner(direction); Vector3 v2 = center + HexMetrics.GetSecondSolidCorner(direction); AddTriangle(center, v1, v2); AddTriangleColor(cell.color); TriangulateConnection(direction, cell, v1, v2); } void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { HexCell neighbor = cell.GetNeighbor(direction) ?? cell; Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; AddQuad(v1, v2, v3, v4); AddQuadColor(cell.color, neighbor.color); } 

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

  if (direction == HexDirection.NE) { TriangulateConnection(direction, cell, v1, v2); } 


الجسور هي فقط في اتجاه NE.

يبدو أنه يمكننا تغطية جميع المركبات عن طريق تثليثها فقط في الاتجاهات الثلاثة الأولى: NE و E و SE.

  if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, v1, v2); } 


جميع الجسور والجسور الداخلية على الحدود.

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

  void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { HexCell neighbor = cell.GetNeighbor(direction); if (neighbor == null) { return; } … } 


الجسور الداخلية فقط.

المفاصل المثلثية


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

  void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { … HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor != null) { AddTriangle(v2, v4, v2); AddTriangleColor(cell.color, neighbor.color, nextNeighbor.color); } } 

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

  AddTriangle(v2, v4, v2 + HexMetrics.GetBridge(direction.Next())); 


نحن نقوم بالتثليث الكامل مرة أخرى.

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

  if (direction <= HexDirection.E && nextNeighbor != null) { AddTriangle(v2, v4, v2 + HexMetrics.GetBridge(direction.Next())); AddTriangleColor(cell.color, neighbor.color, nextNeighbor.color); } 

حزمة الوحدة

الجزء 3: المرتفعات


جدول المحتويات


  • أضف ارتفاع الخلايا.
  • تثليث المنحدرات.
  • أدخل الحواف.
  • الجمع بين الحواف والمنحدرات.

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


مرتفعات وحواف.

ارتفاع الخلية


قسمنا خريطتنا إلى خلايا منفصلة تغطي مساحة مسطحة. الآن سنعطي كل خلية مستوى ارتفاعها الخاص. سنستخدم مستويات ارتفاع منفصلة لتخزينها كحقل صحيح فيه HexCell.

  public int elevation; 

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

  public const float elevationStep = 5f; 

تغيير الخلايا


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

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

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

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

  public void Refresh () { hexMesh.Triangulate(cells); } 

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

  void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { EditCell(hexGrid.GetCell(hit.point)); } } void EditCell (HexCell cell) { cell.color = activeColor; hexGrid.Refresh(); } 

يمكننا تغيير الارتفاعات ببساطة عن طريق تعيين الخلية المطلوبة لمستوى الارتفاع المطلوب.

  int activeElevation; void EditCell (HexCell cell) { cell.color = activeColor; cell.elevation = activeElevation; hexGrid.Refresh(); } 

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

  public void SetElevation (float elevation) { activeElevation = (int)elevation; } 

أضف منزلقًا على اللوحة ( GameObject / Create / Slider ) وضعه تحت لوحة الألوان. نصنعه عموديًا ، من الأسفل إلى الأعلى ، بحيث يتوافق بصريًا مع مستويات المرتفعات. نحن نقصره على الأعداد الصحيحة وننشئ فاصل زمني مناسب ، على سبيل المثال ، من 0 إلى 6. ثم نرفق الحدث On Chan Changed الخاص به إلى أسلوب SetElevationكائن Hex Map Editor . يجب تحديد الطريقة من القائمة الديناميكية بحيث يتم استدعاؤها بقيمة المنزلق.



منزلق الارتفاع.

تصور الارتفاع


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

يكفي أن نغير الموضع المحلي الرأسي للخلية عند تغيير الارتفاع. للراحة ، دعنا نجعل الطريقة HexCell.elevationخاصة وإضافة خاصية عامة HexCell.Elevation.

  public int Elevation { get { return elevation; } set { elevation = value; } } int elevation; 

الآن يمكننا تغيير الموضع الرأسي للخلية عند تحرير الارتفاع.

  set { elevation = value; Vector3 position = transform.localPosition; position.y = value * HexMetrics.elevationStep; transform.localPosition = position; } 

بالطبع ، هذا يتطلب تغييرات صغيرة ل HexMapEditor.EditCell.

  void EditCell (HexCell cell) { cell.color = activeColor; cell.Elevation = activeElevation; hexGrid.Refresh(); } 


خلايا ذات ارتفاعات مختلفة.

هل يتغير مصادم الشبكة ليتناسب مع الارتفاع الجديد؟
Unity mesh collider null. , , null . . ( ) .

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

تغيير موضع تسميات الخلايا


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

  public RectTransform uiRect; 

قم بتعيينهم في النهاية HexGrid.CreateCell.

  void CreateCell (int x, int z, int i) { … cell.uiRect = label.rectTransform; } 

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

  set { elevation = value; Vector3 position = transform.localPosition; position.y = value * HexMetrics.elevationStep; transform.localPosition = position; Vector3 uiPosition = uiRect.localPosition; uiPosition.z = elevation * -HexMetrics.elevationStep; uiRect.localPosition = uiPosition; } 


الكلمات ذات الارتفاع.

إنشاء المنحدرات


الآن نحن بحاجة إلى تحويل اتصالات الخلايا المسطحة إلى المنحدرات. يتم ذلك في HexMesh.TriangulateConnection. في حالة اتصالات الحافة ، نحتاج إلى إعادة تعريف ارتفاع الطرف الآخر للجسر.

  Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; v3.y = v4.y = neighbor.Elevation * HexMetrics.elevationStep; 

في حالة مفاصل الزاوية ، نحتاج إلى القيام بنفس الشيء مع الجسر إلى الجار التالي.

  if (direction <= HexDirection.E && nextNeighbor != null) { Vector3 v5 = v2 + HexMetrics.GetBridge(direction.Next()); v5.y = nextNeighbor.Elevation * HexMetrics.elevationStep; AddTriangle(v2, v4, v5); AddTriangleColor(cell.color, neighbor.color, nextNeighbor.color); } 


اتصال مع مراعاة الارتفاع.

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

حزمة الوحدة

مفاصل ضلع مع حواف


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

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


حافتان على المنحدر.

يمكننا تحديد عدد الخطوات للمنحدر HexMetricsوحساب عدد المراحل بناءً على ذلك.

  public const int terracesPerSlope = 2; public const int terraceSteps = terracesPerSlope * 2 + 1; 

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

  public static Vector3 TerraceLerp (Vector3 a, Vector3 b, int step) { return a; } 

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

  public const float horizontalTerraceStepSize = 1f / terraceSteps; public static Vector3 TerraceLerp (Vector3 a, Vector3 b, int step) { float h = step * HexMetrics.horizontalTerraceStepSize; ax += (bx - ax) * h; az += (bz - az) * h; return a; } 

كيف يعمل الاستيفاء بين قيمتين؟
a و b t . t 0, a . 1, b . t - 0 1, a و b . : (1t)a+tb .

, (1t)a+tb=ata+tb=a+t(ba) . a b (ba) . , .

لتغيير Y فقط في المراحل الفردية ، يمكننا استخدام ( S ر ه ص + 1 ) / 2 . إذا استخدمنا القسمة الصحيحة ، فستحول السلسلة 1 ، 2 ، 3 ، 4 إلى 1 ، 1 ، 2 ، 2.

  public const float verticalTerraceStepSize = 1f / (terracesPerSlope + 1); public static Vector3 TerraceLerp (Vector3 a, Vector3 b, int step) { float h = step * HexMetrics.horizontalTerraceStepSize; ax += (bx - ax) * h; az += (bz - az) * h; float v = ((step + 1) / 2) * HexMetrics.verticalTerraceStepSize; ay += (by - ay) * v; return a; } 

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

  public static Color TerraceLerp (Color a, Color b, int step) { float h = step * HexMetrics.horizontalTerraceStepSize; return Color.Lerp(a, b, h); } 

التثليث


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

  void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { … Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; v3.y = v4.y = neighbor.Elevation * HexMetrics.elevationStep; TriangulateEdgeTerraces(v1, v2, cell, v3, v4, neighbor); // AddQuad(v1, v2, v3, v4); // AddQuadColor(cell.color, neighbor.color); … } void TriangulateEdgeTerraces ( Vector3 beginLeft, Vector3 beginRight, HexCell beginCell, Vector3 endLeft, Vector3 endRight, HexCell endCell ) { AddQuad(beginLeft, beginRight, endLeft, endRight); AddQuadColor(beginCell.color, endCell.color); } 

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

  void TriangulateEdgeTerraces ( Vector3 beginLeft, Vector3 beginRight, HexCell beginCell, Vector3 endLeft, Vector3 endRight, HexCell endCell ) { Vector3 v3 = HexMetrics.TerraceLerp(beginLeft, endLeft, 1); Vector3 v4 = HexMetrics.TerraceLerp(beginRight, endRight, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.color, endCell.color, 1); AddQuad(beginLeft, beginRight, v3, v4); AddQuadColor(beginCell.color, c2); } 


الخطوة الأولى في إنشاء الحافة.

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

  AddQuad(beginLeft, beginRight, v3, v4); AddQuadColor(beginCell.color, c2); AddQuad(v3, v4, endLeft, endRight); AddQuadColor(c2, endCell.color); 


الخطوة الأخيرة في إنشاء الحافة.

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

  AddQuad(beginLeft, beginRight, v3, v4); AddQuadColor(beginCell.color, c2); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v3; Vector3 v2 = v4; Color c1 = c2; v3 = HexMetrics.TerraceLerp(beginLeft, endLeft, i); v4 = HexMetrics.TerraceLerp(beginRight, endRight, i); c2 = HexMetrics.TerraceLerp(beginCell.color, endCell.color, i); AddQuad(v1, v2, v3, v4); AddQuadColor(c1, c2); } AddQuad(v3, v4, endLeft, endRight); AddQuadColor(c2, endCell.color); 


جميع الخطوات الوسيطة.

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


جميع مفاصل الحواف لها حواف.

حزمة الوحدة

أنواع الاتصال


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

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

 public enum HexEdgeType { Flat, Slope, Cliff } 

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

  public static HexEdgeType GetEdgeType (int elevation1, int elevation2) { } 

إذا كانت الارتفاعات متماثلة ، فسيكون لدينا ضلع مسطح.

  public static HexEdgeType GetEdgeType (int elevation1, int elevation2) { if (elevation1 == elevation2) { return HexEdgeType.Flat; } } 

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

  public static HexEdgeType GetEdgeType (int elevation1, int elevation2) { if (elevation1 == elevation2) { return HexEdgeType.Flat; } int delta = elevation2 - elevation1; if (delta == 1 || delta == -1) { return HexEdgeType.Slope; } return HexEdgeType.Cliff; } 

دعنا نضيف أيضًا طريقة ملائمة HexCell.GetEdgeTypeللحصول على نوع حافة الخلية في اتجاه معين.

  public HexEdgeType GetEdgeType (HexDirection direction) { return HexMetrics.GetEdgeType( elevation, neighbors[(int)direction].elevation ); } 

ألا نحتاج إلى التحقق من وجود جار في هذا الاتجاه؟
, , . , NullReferenceException . , , - . , . .

, , , . - , NullReferenceException .

إنشاء حواف للمنحدرات فقط


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

  if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(v1, v2, cell, v3, v4, neighbor); } // AddQuad(v1, v2, v3, v4); // AddQuadColor(cell.color, neighbor.color); 

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

  if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(v1, v2, cell, v3, v4, neighbor); } else { AddQuad(v1, v2, v3, v4); AddQuadColor(cell.color, neighbor.color); } 


يتم إنشاء الخطوات فقط على المنحدرات.

حزمة الوحدة

الحواف مع الحواف


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

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


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

  void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { AddTriangle(bottom, left, right); AddTriangleColor(bottomCell.color, leftCell.color, rightCell.color); } 

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

  void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { … HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (direction <= HexDirection.E && nextNeighbor != null) { Vector3 v5 = v2 + HexMetrics.GetBridge(direction.Next()); v5.y = nextNeighbor.Elevation * HexMetrics.elevationStep; if (cell.Elevation <= neighbor.Elevation) { if (cell.Elevation <= nextNeighbor.Elevation) { TriangulateCorner(v2, cell, v4, neighbor, v5, nextNeighbor); } } } } 

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

  if (cell.Elevation <= neighbor.Elevation) { if (cell.Elevation <= nextNeighbor.Elevation) { TriangulateCorner(v2, cell, v4, neighbor, v5, nextNeighbor); } else { TriangulateCorner(v5, nextNeighbor, v2, cell, v4, neighbor); } } 

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

  if (cell.Elevation <= neighbor.Elevation) { if (cell.Elevation <= nextNeighbor.Elevation) { TriangulateCorner(v2, cell, v4, neighbor, v5, nextNeighbor); } else { TriangulateCorner(v5, nextNeighbor, v2, cell, v4, neighbor); } } else if (neighbor.Elevation <= nextNeighbor.Elevation) { TriangulateCorner(v4, neighbor, v5, nextNeighbor, v2, cell); } else { TriangulateCorner(v5, nextNeighbor, v2, cell, v4, neighbor); } 


أدر عكس عقارب الساعة ، لا دوران ، دوران عقارب الساعة.

تثليث المنحدر


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

  public HexEdgeType GetEdgeType (HexCell otherCell) { return HexMetrics.GetEdgeType( elevation, otherCell.elevation ); } 

نستخدم هذه الطريقة الجديدة HexMesh.TriangulateCornerلتحديد أنواع الحواف اليسرى واليمنى.

  void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { HexEdgeType leftEdgeType = bottomCell.GetEdgeType(leftCell); HexEdgeType rightEdgeType = bottomCell.GetEdgeType(rightCell); AddTriangle(bottom, left, right); AddTriangleColor(bottomCell.color, leftCell.color, rightCell.color); } 

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


منحدرين وطائرة ، SSP.

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

  void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { HexEdgeType leftEdgeType = bottomCell.GetEdgeType(leftCell); HexEdgeType rightEdgeType = bottomCell.GetEdgeType(rightCell); if (leftEdgeType == HexEdgeType.Slope) { if (rightEdgeType == HexEdgeType.Slope) { TriangulateCornerTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); return; } } AddTriangle(bottom, left, right); AddTriangleColor(bottomCell.color, leftCell.color, rightCell.color); } void TriangulateCornerTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { } 

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


هناك فراغ.

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

  void TriangulateCornerTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { Vector3 v3 = HexMetrics.TerraceLerp(begin, left, 1); Vector3 v4 = HexMetrics.TerraceLerp(begin, right, 1); Color c3 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1); Color c4 = HexMetrics.TerraceLerp(beginCell.color, rightCell.color, 1); AddTriangle(begin, v3, v4); AddTriangleColor(beginCell.color, c3, c4); } 


المرحلة الأولى من المثلث.

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

  AddTriangle(begin, v3, v4); AddTriangleColor(beginCell.color, c3, c4); AddQuad(v3, v4, left, right); AddQuadColor(c3, c4, leftCell.color, rightCell.color); 


المرحلة الأخيرة من الرباعي.

جميع المراحل بينهما هي أيضًا أربعة أجزاء.

  AddTriangle(begin, v3, v4); AddTriangleColor(beginCell.color, c3, c4); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v3; Vector3 v2 = v4; Color c1 = c3; Color c2 = c4; v3 = HexMetrics.TerraceLerp(begin, left, i); v4 = HexMetrics.TerraceLerp(begin, right, i); c3 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, i); c4 = HexMetrics.TerraceLerp(beginCell.color, rightCell.color, i); AddQuad(v1, v2, v3, v4); AddQuadColor(c1, c2, c3, c4); } AddQuad(v3, v4, left, right); AddQuadColor(c3, c4, leftCell.color, rightCell.color); 


جميع المراحل.

اثنين من اختلافات المنحدر


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


ATP و MSS.

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

  if (leftEdgeType == HexEdgeType.Slope) { if (rightEdgeType == HexEdgeType.Slope) { TriangulateCornerTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); return; } if (rightEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( left, leftCell, right, rightCell, bottom, bottomCell ); return; } } if (rightEdgeType == HexEdgeType.Slope) { if (leftEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); return; } } 

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


الحواف الصلبة.

حزمة الوحدة

دمج المنحدرات والمنحدرات


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




SOS و COO.

دعنا نضيف طريقة جديدة للتعامل مع جميع حالات المنحدر المنحدر.

  void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { } 

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

  if (leftEdgeType == HexEdgeType.Slope) { if (rightEdgeType == HexEdgeType.Slope) { TriangulateCornerTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); return; } if (rightEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( left, leftCell, right, rightCell, bottom, bottomCell ); return; } TriangulateCornerTerracesCliff( bottom, bottomCell, left, leftCell, right, rightCell ); return; } if (rightEdgeType == HexEdgeType.Slope) { if (leftEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); return; } } 

كيف نثلث هذا؟ يمكن تقسيم هذه المهمة إلى قسمين: السفلي والعلوي.

الجزء السفلي


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


ضغط الحواف.

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


ضغط على الحدود.

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

  void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, right, b); Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b); } 

للتأكد من أننا حصلنا عليها بشكل صحيح ، نغطي الجزء السفلي بالكامل بمثلث واحد.

  float b = 1f / (rightCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, right, b); Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b); AddTriangle(begin, left, boundary); AddTriangleColor(beginCell.color, leftCell.color, boundaryColor); 


مثلث سفلي.

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

  float b = 1f / (rightCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, right, b); Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b); Vector3 v2 = HexMetrics.TerraceLerp(begin, left, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1); AddTriangle(begin, v2, boundary); AddTriangleColor(beginCell.color, c2, boundaryColor); 


المرحلة الأولى من الضغط.

هذه المرة ، ستكون المرحلة الأخيرة أيضًا مثلثًا.

  AddTriangle(begin, v2, boundary); AddTriangleColor(beginCell.color, c2, boundaryColor); AddTriangle(v2, left, boundary); AddTriangleColor(c2, leftCell.color, boundaryColor); 


المرحلة الأخيرة من الضغط.

وجميع الخطوات الوسيطة أيضًا مثلثات.

  AddTriangle(begin, v2, boundary); AddTriangleColor(beginCell.color, c2, boundaryColor); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = HexMetrics.TerraceLerp(begin, left, i); c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, i); AddTriangle(v1, v2, boundary); AddTriangleColor(c1, c2, boundaryColor); } AddTriangle(v2, left, boundary); AddTriangleColor(c2, leftCell.color, boundaryColor); 


حواف مضغوطة.

ألا يمكننا الحفاظ على مستوى الحافة؟
, , , . . , . .

اكتمال الزاوية


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

  void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, right, b); Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b); TriangulateBoundaryTriangle( begin, beginCell, left, leftCell, boundary, boundaryColor ); } void TriangulateBoundaryTriangle ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 boundary, Color boundaryColor ) { Vector3 v2 = HexMetrics.TerraceLerp(begin, left, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1); AddTriangle(begin, v2, boundary); AddTriangleColor(beginCell.color, c2, boundaryColor); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = HexMetrics.TerraceLerp(begin, left, i); c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, i); AddTriangle(v1, v2, boundary); AddTriangleColor(c1, c2, boundaryColor); } AddTriangle(v2, left, boundary); AddTriangleColor(c2, leftCell.color, boundaryColor); } 

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

  void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, right, b); Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b); TriangulateBoundaryTriangle( begin, beginCell, left, leftCell, boundary, boundaryColor ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, leftCell, right, rightCell, boundary, boundaryColor ); } else { AddTriangle(left, right, boundary); AddTriangleColor(leftCell.color, rightCell.color, boundaryColor); } } 



التثليث الكامل للجزئين.

حالات معكوسة


درسنا حالات "المنحدر منحدر". هناك أيضًا حالتا مرآة ، كل منهما يحتوي على منحدر على اليسار.


OSS و CCA.

سنستخدم النهج السابق ، مع وجود اختلافات طفيفة بسبب تغيير الاتجاه. نقوم بنسخها TriangulateCornerTerracesCliffوتغييرها وفقًا لذلك.

  void TriangulateCornerCliffTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (leftCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, left, b); Color boundaryColor = Color.Lerp(beginCell.color, leftCell.color, b); TriangulateBoundaryTriangle( right, rightCell, begin, beginCell, boundary, boundaryColor ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, leftCell, right, rightCell, boundary, boundaryColor ); } else { AddTriangle(left, right, boundary); AddTriangleColor(leftCell.color, rightCell.color, boundaryColor); } } 

أضف هذه الحالات إلى TriangulateCorner.

  if (leftEdgeType == HexEdgeType.Slope) { … } if (rightEdgeType == HexEdgeType.Slope) { if (leftEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); return; } TriangulateCornerCliffTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); return; } 



مثلثات OSS و CCA.

منحدرات مزدوجة


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

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




OOSP و OOSL.

نحن يمكن أن تغطي كلتا الحالتين في TriangulateCornerأساليب الدعوة TriangulateCornerCliffTerracesو TriangulateCornerTerracesCliffمع خلايا التقلبات المختلفة.

  if (leftEdgeType == HexEdgeType.Slope) { … } if (rightEdgeType == HexEdgeType.Slope) { … } if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { if (leftCell.Elevation < rightCell.Elevation) { TriangulateCornerCliffTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); } else { TriangulateCornerTerracesCliff( left, leftCell, right, rightCell, bottom, bottomCell ); } return; } 

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

  void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } … } void TriangulateCornerCliffTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (leftCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } … } 



OOSP المثلث و OOSL.

اكتساح


قمنا بفحص جميع الحالات التي تتطلب معالجة خاصة لضمان التثليث الصحيح للأطراف.


التثليث الكامل مع الحواف.

يمكننا التنظيف قليلاً عن طريق TriangulateCornerالتخلص من عوامل التشغيل returnواستخدام الكتل بدلاً من ذلك else.

  void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { HexEdgeType leftEdgeType = bottomCell.GetEdgeType(leftCell); HexEdgeType rightEdgeType = bottomCell.GetEdgeType(rightCell); if (leftEdgeType == HexEdgeType.Slope) { if (rightEdgeType == HexEdgeType.Slope) { TriangulateCornerTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); } else if (rightEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( left, leftCell, right, rightCell, bottom, bottomCell ); } else { TriangulateCornerTerracesCliff( bottom, bottomCell, left, leftCell, right, rightCell ); } } else if (rightEdgeType == HexEdgeType.Slope) { if (leftEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); } else { TriangulateCornerCliffTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); } } else if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { if (leftCell.Elevation < rightCell.Elevation) { TriangulateCornerCliffTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); } else { TriangulateCornerTerracesCliff( left, leftCell, right, rightCell, bottom, bottomCell ); } } else { AddTriangle(bottom, left, right); AddTriangleColor(bottomCell.color, leftCell.color, rightCell.color); } } 

elseيغطي الكتلة الأخيرة جميع الحالات المتبقية التي لم يتم تغطيتها بعد. هذه الحالات هي RFP (الطائرة الطائرة الطائرة) ، OOP ، LLC و LLC. كلها مغطاة بمثلث واحد.


.

unitypackage

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


All Articles