خرائط مسدس في الوحدة: الحفظ والتحميل ، القوام ، المسافات

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

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

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

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

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

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

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

الجزء 12: حفظ وتحميل


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

نحن نعرف بالفعل كيفية إنشاء خرائط مثيرة للاهتمام. الآن أنت بحاجة إلى تعلم كيفية حفظها.


تم تحميله من ملف test.map .

نوع التضاريس


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

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

تحريك مصفوفة من الألوان


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

  public static Color[] colors; 

مثل جميع البيانات العالمية الأخرى ، مثل الضوضاء ، يمكننا تهيئة هذه الألوان باستخدام HexGrid .

  public Color[] colors; … void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexMetrics.colors = colors; … } … void OnEnable () { if (!HexMetrics.noiseSource) { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexMetrics.colors = colors; } } 

وبما أننا لا نقوم الآن بتعيين الألوان مباشرة للخلايا ، فسوف نتخلص من اللون الافتراضي.

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

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


تمت إضافة الألوان إلى الشبكة.

إعادة بناء الخلية


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

 // Color color; int terrainTypeIndex; 

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

  public Color Color { get { return HexMetrics.colors[terrainTypeIndex]; } // set { // … // } } 

أضف خاصية جديدة للحصول على مؤشر نوع ارتفاع جديد وتعيينه.

  public int TerrainTypeIndex { get { return terrainTypeIndex; } set { if (terrainTypeIndex != value) { terrainTypeIndex = value; Refresh(); } } } 

إعادة هيكلة محرر


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

 // public Color[] colors; … // Color activeColor; … // bool applyColor; … // public void SelectColor (int index) { // applyColor = index >= 0; // if (applyColor) { // activeColor = colors[index]; // } // } … // void Awake () { // SelectColor(0); // } … void EditCell (HexCell cell) { if (cell) { // if (applyColor) { // cell.Color = activeColor; // } … } } 

أضف الآن حقل وطريقة للتحكم في فهرس نوع الارتفاع النشط.

  int activeTerrainTypeIndex; … public void SetTerrainTypeIndex (int index) { activeTerrainTypeIndex = index; } 

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

قم بتغيير EditCell بحيث يتم تعيين فهرس نوع الارتفاع للخلية التي يتم تحريرها.

  void EditCell (HexCell cell) { if (cell) { if (activeTerrainTypeIndex >= 0) { cell.TerrainTypeIndex = activeTerrainTypeIndex; } … } } 

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


الأصفر هو اللون الافتراضي الجديد.

حزمة الوحدة

حفظ البيانات في ملف


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

  public void Save () { } public void Load () { } 

أضف زرين إلى واجهة المستخدم ( GameObject / UI / Button ). قم بتوصيلها بالأزرار وإعطاء الملصقات المناسبة. لقد وضعتهم في أسفل اللوحة اليمنى.


أزرار الحفظ والتحميل.

موقع الملف


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

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

  public void Save () { Debug.Log(Application.persistentDataPath); } 

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


اسم الشركة والمنتج.

لماذا لا يمكنني العثور على مجلد Library على جهاز Mac؟
غالبًا ما يكون مجلد المكتبة مخفيًا. تعتمد طريقة عرضه على إصدار OS X. إذا لم يكن لديك إصدار قديم ، فحدد المجلد الرئيسي في Finder وانتقل إلى Show View Options . يوجد مربع اختيار لمجلد المكتبة .

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

إنشاء ملف


لإنشاء ملف ، نحتاج إلى استخدام فئات من مساحة الاسم System.IO . لذلك ، نضيف عبارة using لها عبر فئة HexMapEditor .

 using UnityEngine; using UnityEngine.EventSystems; using System.IO; public class HexMapEditor : MonoBehaviour { … } 

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

  public void Save () { string path = Path.Combine(Application.persistentDataPath, "test.map"); } 

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

  string path = Path.Combine(Application.persistentDataPath, "test.map"); File.Open(path, FileMode.Create); 

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

  string path = Path.Combine(Application.persistentDataPath, "test.map"); Stream fileStream = File.Open(path, FileMode.Create); fileStream.Close(); 

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

اكتب إلى الملف


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

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

  string path = Path.Combine(Application.persistentDataPath, "test.map"); BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)); writer.Close(); 

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

  BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)); writer.Write(123); writer.Close(); 

انقر فوق الزر حفظ وفحص خريطة الاختبار مرة أخرى. الآن حجمها 4 بايت ، لأن الحجم الصحيح هو 4 بايت.

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

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

يمكنك فتح الملف في محرر ست عشري. في هذه الحالة ، سنرى 7b 00 00 00 . هذه أربعة بايتات من عددنا الصحيح ، تم تعيينها بترميز سداسي عشري. في الأرقام العشرية العادية ، تكون هذه القيمة 123 0 0 0 . في ثنائي ، البايت الأول يشبه 01111011 .

رمز ASCII لـ { هو 123 ، لذا يمكن عرض هذا الحرف في محرر نصوص. ASCII 0 هو حرف فارغ لا يطابق أي أحرف مرئية.

البايتات الثلاثة المتبقية تساوي صفر ، لأننا كتبنا رقمًا أقل من 256. إذا كتبنا 256 ، فسنرى 00 01 00 00 في محرر السداسي.

ألا يجب تخزين 123 كـ 00 00 00 7b؟
يستخدم BinaryWriter تنسيق النهاية الصغيرة لحفظ الأرقام. هذا يعني أنه يتم كتابة البايت الأقل أهمية أولاً. تم استخدام هذا التنسيق بواسطة Microsoft في تطوير إطار عمل .NET. ربما تم اختياره لأن وحدة المعالجة المركزية Intel تستخدم تنسيق النهاية الصغيرة.

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

نحن نجعل الموارد مجانية


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

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

  using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(123); } // writer.Close(); 

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

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

استرجاع البيانات


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

  public void Load () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using ( BinaryReader reader = new BinaryReader(File.Open(path, FileMode.Open)) ) { } } 

في هذه الحالة ، يمكننا استخدام طريقة File.OpenRead لفتح الملف للقراءة.

  using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { } 

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

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

  using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { Debug.Log(reader.ReadInt32()); } 

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

حزمة الوحدة

كتابة بيانات الخرائط وقراءتها


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

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

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

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

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

 using UnityEngine; using System.IO; public class HexCell : MonoBehaviour { … public void Save (BinaryWriter writer) { } public void Load (BinaryReader reader) { } } 

أضف طرق Save Load إلى HexGrid . تتخطى هذه الطرق جميع الخلايا ببساطة عن طريق استدعاء طرق Load الخاصة بها.

 using UnityEngine; using UnityEngine.UI; using System.IO; public class HexGrid : MonoBehaviour { … public void Save (BinaryWriter writer) { for (int i = 0; i < cells.Length; i++) { cells[i].Save(writer); } } public void Load (BinaryReader reader) { for (int i = 0; i < cells.Length; i++) { cells[i].Load(reader); } } } 

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

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

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

  public void Save () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { hexGrid.Save(writer); } } public void Load () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { hexGrid.Load(reader); } } 

حفظ نوع الإغاثة


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

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

  public void Save (BinaryWriter writer) { writer.Write(terrainTypeIndex); } public void Load (BinaryReader reader) { terrainTypeIndex = reader.ReadInt32(); } 

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

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

حفظ كل عدد صحيح


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

  public void Save (BinaryWriter writer) { writer.Write(terrainTypeIndex); writer.Write(elevation); writer.Write(waterLevel); writer.Write(urbanLevel); writer.Write(farmLevel); writer.Write(plantLevel); writer.Write(specialIndex); } public void Load (BinaryReader reader) { terrainTypeIndex = reader.ReadInt32(); elevation = reader.ReadInt32(); waterLevel = reader.ReadInt32(); urbanLevel = reader.ReadInt32(); farmLevel = reader.ReadInt32(); plantLevel = reader.ReadInt32(); specialIndex = reader.ReadInt32(); } 

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

  void RefreshPosition () { Vector3 position = transform.localPosition; position.y = elevation * HexMetrics.elevationStep; position.y += (HexMetrics.SampleNoise(position).y * 2f - 1f) * HexMetrics.elevationPerturbStrength; transform.localPosition = position; Vector3 uiPosition = uiRect.localPosition; uiPosition.z = -position.y; uiRect.localPosition = uiPosition; } 

الآن يمكننا استدعاء الأسلوب عند تعيين الخاصية ، وكذلك بعد تحميل بيانات الارتفاع.

  public int Elevation { … set { if (elevation == value) { return; } elevation = value; RefreshPosition(); ValidateRivers(); … } } … public void Load (BinaryReader reader) { terrainTypeIndex = reader.ReadInt32(); elevation = reader.ReadInt32(); RefreshPosition(); … } 

بعد هذا التغيير ، ستغير الخلايا ارتفاعها الظاهري بشكل صحيح عند التحميل.

حفظ جميع البيانات


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

  public void Save (BinaryWriter writer) { writer.Write(terrainTypeIndex); writer.Write(elevation); writer.Write(waterLevel); writer.Write(urbanLevel); writer.Write(farmLevel); writer.Write(plantLevel); writer.Write(specialIndex); writer.Write(walled); writer.Write(hasIncomingRiver); writer.Write(hasOutgoingRiver); for (int i = 0; i < roads.Length; i++) { writer.Write(roads[i]); } } 

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

  writer.Write(hasIncomingRiver); writer.Write((int)incomingRiver); writer.Write(hasOutgoingRiver); writer.Write((int)outgoingRiver); 

تتم قراءة القيم المنطقية باستخدام طريقة BinaryReader.ReadBoolean . اتجاهات الأنهار صحيحة ، ويجب علينا تحويلها إلى HexDirection .

  public void Load (BinaryReader reader) { terrainTypeIndex = reader.ReadInt32(); elevation = reader.ReadInt32(); RefreshPosition(); waterLevel = reader.ReadInt32(); urbanLevel = reader.ReadInt32(); farmLevel = reader.ReadInt32(); plantLevel = reader.ReadInt32(); specialIndex = reader.ReadInt32(); walled = reader.ReadBoolean(); hasIncomingRiver = reader.ReadBoolean(); incomingRiver = (HexDirection)reader.ReadInt32(); hasOutgoingRiver = reader.ReadBoolean(); outgoingRiver = (HexDirection)reader.ReadInt32(); for (int i = 0; i < roads.Length; i++) { roads[i] = reader.ReadBoolean(); } } 

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

حزمة الوحدة

تقليل حجم الملف


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

اختزال الفاصل الرقمي


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

  writer.Write((byte)terrainTypeIndex); writer.Write((byte)elevation); writer.Write((byte)waterLevel); writer.Write((byte)urbanLevel); writer.Write((byte)farmLevel); writer.Write((byte)plantLevel); writer.Write((byte)specialIndex); writer.Write(walled); writer.Write(hasIncomingRiver); writer.Write((byte)incomingRiver); writer.Write(hasOutgoingRiver); writer.Write((byte)outgoingRiver); 

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

  terrainTypeIndex = reader.ReadByte(); elevation = reader.ReadByte(); RefreshPosition(); waterLevel = reader.ReadByte(); urbanLevel = reader.ReadByte(); farmLevel = reader.ReadByte(); plantLevel = reader.ReadByte(); specialIndex = reader.ReadByte(); walled = reader.ReadBoolean(); hasIncomingRiver = reader.ReadBoolean(); incomingRiver = (HexDirection)reader.ReadByte(); hasOutgoingRiver = reader.ReadBoolean(); outgoingRiver = (HexDirection)reader.ReadByte(); 

لذلك نتخلص من ثلاثة بايت لكل عدد صحيح ، مما يوفر 27 بايت لكل خلية. الآن ننفق 18 بايت لكل خلية ، و 5400 بايت فقط لكل 300 خلية.

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

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

اتحاد نهر بايت


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

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

لدينا ستة اتجاهات محتملة ، يتم تخزينها كأرقام في الفاصل الزمني 0-5. يكفي ثلاث بتات لهذا ، لأنه في أرقام النماذج الثنائية من 0 إلى 5 تبدو مثل 000 ، 001 ، 010 ، 011 ، 100 ، 101 و 110. وهذا يعني أن بايتة أخرى لا تزال غير مستخدمة وخمس بتات أخرى. يمكننا استخدام واحد منهم للإشارة إلى وجود نهر. على سبيل المثال ، يمكنك استخدام البت الثامن ، المقابل للرقم 128.

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

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

 // writer.Write(hasIncomingRiver); // writer.Write((byte)incomingRiver); if (hasIncomingRiver) { writer.Write((byte)(incomingRiver + 128)); } else { writer.Write((byte)0); } // writer.Write(hasOutgoingRiver); // writer.Write((byte)outgoingRiver); if (hasOutgoingRiver) { writer.Write((byte)(outgoingRiver + 128)); } else { writer.Write((byte)0); } 

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

 // hasIncomingRiver = reader.ReadBoolean(); // incomingRiver = (HexDirection)reader.ReadByte(); byte riverData = reader.ReadByte(); if (riverData >= 128) { hasIncomingRiver = true; incomingRiver = (HexDirection)(riverData - 128); } else { hasIncomingRiver = false; } // hasOutgoingRiver = reader.ReadBoolean(); // outgoingRiver = (HexDirection)reader.ReadByte(); riverData = reader.ReadByte(); if (riverData >= 128) { hasOutgoingRiver = true; outgoingRiver = (HexDirection)(riverData - 128); } else { hasOutgoingRiver = false; } 

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

حفظ الطرق في بايت واحد


يمكننا استخدام حيلة مماثلة لضغط بيانات الطريق. لدينا ست قيم منطقية يمكن تخزينها في البتات الستة الأولى من البايت. أي أن كل اتجاه للطريق يتم تمثيله برقم بقوة اثنين. هذه هي 1 و 2 و 4 و 8 و 16 و 32 أو في شكل ثنائي 1 و 10 و 100 و 1000 و 10000 و 100000.

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

  int roadFlags = 0; for (int i = 0; i < roads.Length; i++) { // writer.Write(roads[i]); if (roads[i]) { roadFlags |= 1 << i; } } writer.Write((byte)roadFlags); 

كيف يعمل <<؟
. integer . . integer . , . 1 << n 2 n , .

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

  int roadFlags = reader.ReadByte(); for (int i = 0; i < roads.Length; i++) { roads[i] = (roadFlags & (1 << i)) != 0; } 

بعد أن ضغطنا 6 بايت في واحدة ، تلقينا 11 بايت لكل خلية. مع 300 خلية ، هذا هو فقط 3300 بايت. أي ، بعد أن عملنا قليلاً مع البايت ، قللنا حجم الملف بنسبة 75٪.

الاستعداد للمستقبل


قبل الإعلان عن اكتمال تنسيق الحفظ ، نضيف تفاصيل أخرى. قبل حفظ بيانات الخريطة ، سنضطر HexMapEditorلكتابة عدد صحيح صفر.

  public void Save () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(0); hexGrid.Save(writer); } } 

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

  public void Load () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { reader.ReadInt32(); hexGrid.Load(reader); } } 

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

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

  using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { int header = reader.ReadInt32(); if (header == 0) { hexGrid.Load(reader); } else { Debug.LogWarning("Unknown map format " + header); } } 


حزمة الوحدة

الجزء 13: إدارة البطاقة


  • نقوم بإنشاء بطاقات جديدة في وضع التشغيل.
  • أضف الدعم لمختلف أحجام البطاقات.
  • أضف حجم الخريطة إلى البيانات المحفوظة.
  • حفظ وتحميل الخرائط العشوائية.
  • عرض قائمة البطاقات.

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

بدءًا من هذا الجزء ، سيتم إنشاء البرامج التعليمية في Unity 5.5.0.


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

إنشاء خرائط جديدة


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

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

  void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexMetrics.colors = colors; CreateMap(); } public void CreateMap () { cellCountX = chunkCountX * HexMetrics.chunkSizeX; cellCountZ = chunkCountZ * HexMetrics.chunkSizeZ; CreateChunks(); CreateCells(); } 

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


زر خريطة جديد.

دعنا نربط حدث On Click لهذا الزر بطريقة CreateMapكائننا HexGrid. بمعنى ، لن نتصفح محرر خرائط Hex ، ولكننا ندعو مباشرة طريقة كائن Hex Grid .


قم بإنشاء خريطة بالنقر.

مسح البيانات القديمة


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

  public void CreateMap () { if (chunks != null) { for (int i = 0; i < chunks.Length; i++) { Destroy(chunks[i].gameObject); } } … } 

هل يمكننا إعادة استخدام الأشياء الموجودة؟
, . , . , — , .

هل من الممكن تدمير عناصر تابعة مثل هذه في حلقة؟
بالطبع. .

حدد الحجم في الخلايا بدلاً من الأجزاء


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

 // public int chunkCountX = 4, chunkCountZ = 3; public int cellCountX = 20, cellCountZ = 15; … // int cellCountX, cellCountZ; int chunkCountX, chunkCountZ; … public void CreateMap () { … // cellCountX = chunkCountX * HexMetrics.chunkSizeX; // cellCountZ = chunkCountZ * HexMetrics.chunkSizeZ; chunkCountX = cellCountX / HexMetrics.chunkSizeX; chunkCountZ = cellCountZ / HexMetrics.chunkSizeZ; CreateChunks(); CreateCells(); } 

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

  Vector3 ClampPosition (Vector3 position) { float xMax = (grid.cellCountX - 0.5f) * (2f * HexMetrics.innerRadius); position.x = Mathf.Clamp(position.x, 0f, xMax); float zMax = (grid.cellCountZ - 1) * (1.5f * HexMetrics.outerRadius); position.z = Mathf.Clamp(position.z, 0f, zMax); return position; } 

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


بشكل افتراضي ، يبلغ حجم البطاقة 20 × 15.

أحجام بطاقات مخصصة


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

  void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexMetrics.colors = colors; CreateMap(cellCountX, cellCountZ); } public void CreateMap (int x, int z) { … cellCountX = x; cellCountZ = z; chunkCountX = cellCountX / HexMetrics.chunkSizeX; chunkCountZ = cellCountZ / HexMetrics.chunkSizeZ; CreateChunks(); CreateCells(); } 

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

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

  public void CreateMap (int x, int z) { if ( x <= 0 || x % HexMetrics.chunkSizeX != 0 || z <= 0 || z % HexMetrics.chunkSizeZ != 0 ) { Debug.LogError("Unsupported map size."); return; } … } 

قائمة بطاقة جديدة


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

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



قائمة لوحة خريطة جديدة.

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


إعدادات صورة الخلفية.

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



قائمة خريطة جديدة.

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

 using UnityEngine; public class NewMapMenu : MonoBehaviour { public HexGrid hexGrid; } 


مكون قائمة الخريطة الجديدة.

الافتتاح والختام


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

  public void Open () { gameObject.SetActive(true); } public void Close () { gameObject.SetActive(false); } 

الآن قم بتوصيل زر New Map UI الخاص بالمحرر بالطريقة Openفي كائن قائمة الخريطة الجديدة .


فتح القائمة بالضغط.

قم أيضًا بتوصيل زر " إلغاء الأمر" بالطريقة Close. سيتيح لنا ذلك فتح القائمة المنبثقة وإغلاقها.

إنشاء خرائط جديدة


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

  void CreateMap (int x, int z) { hexGrid.CreateMap(x, z); Close(); } 

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

  public void CreateSmallMap () { CreateMap(20, 15); } public void CreateMediumMap () { CreateMap(40, 30); } public void CreateLargeMap () { CreateMap(80, 60); } 

قفل الكاميرا


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

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

  static HexMapCamera instance; … void Awake () { instance = this; swivel = transform.GetChild(0); stick = swivel.GetChild(0); } 

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

  public static bool Locked { set { instance.enabled = !value; } } 

الآن NewMapMenu.Openيمكنها حجب الكاميرا NewMapMenu.Closeو- فتحها.

  public void Open () { gameObject.SetActive(true); HexMapCamera.Locked = true; } public void Close () { gameObject.SetActive(false); HexMapCamera.Locked = false; } 

الحفاظ على وضع الكاميرا الصحيح


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

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

  public static void ValidatePosition () { instance.AdjustPosition(0f, 0f); } 

استدعاء الطريقة بالداخل NewMapMenu.CreateMapبعد إنشاء خريطة جديدة.

  void CreateMap (int x, int z) { hexGrid.CreateMap(x, z); HexMapCamera.ValidatePosition(); Close(); } 

حزمة الوحدة

حفظ حجم الخريطة


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

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

  public void Load (BinaryReader reader) { CreateMap(20, 15); for (int i = 0; i < cells.Length; i++) { cells[i].Load(reader); } for (int i = 0; i < chunks.Length; i++) { chunks[i].Refresh(); } } 

تخزين بحجم البطاقة


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

  public void Save (BinaryWriter writer) { writer.Write(cellCountX); writer.Write(cellCountZ); for (int i = 0; i < cells.Length; i++) { cells[i].Save(writer); } } 

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

  public void Load (BinaryReader reader) { CreateMap(reader.ReadInt32(), reader.ReadInt32()); … } 

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

  public void Load () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { int header = reader.ReadInt32(); if (header == 0) { hexGrid.Load(reader, header); HexMapCamera.ValidatePosition(); } else { Debug.LogWarning("Unknown map format " + header); } } } 

تنسيق ملف جديد


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

  public void Save () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(1); hexGrid.Save(writer); } } 

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

  public void Load () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { int header = reader.ReadInt32(); if (header == 1) { hexGrid.Load(reader); HexMapCamera.ValidatePosition(); } else { Debug.LogWarning("Unknown map format " + header); } } } 

التوافق مع الإصدارات السابقة


في الواقع ، إذا أردنا ، لا يزال بإمكاننا تنزيل خرائط الإصدار 0 ، بافتراض أن جميعها لها نفس الحجم 20 × 15. أي أنه لا يجب أن يكون العنوان 1 ، يمكن أن يكون صفرًا أيضًا. نظرًا لأن كل إصدار يتطلب منهجًا خاصًا به ، HexMapEditor.Loadيجب أن يمرر الرأس إلى الطريقة HexGrid.Load.

  if (header <= 1) { hexGrid.Load(reader, header); HexMapCamera.ValidatePosition(); } 

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

  public void Load (BinaryReader reader, int header) { int x = 20, z = 15; if (header >= 1) { x = reader.ReadInt32(); z = reader.ReadInt32(); } CreateMap(x, z); … } 

إصدار ملف الخريطة 0

فحص حجم البطاقة


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

  public bool CreateMap (int x, int z) { if ( x <= 0 || x % HexMetrics.chunkSizeX != 0 || z <= 0 || z % HexMetrics.chunkSizeZ != 0 ) { Debug.LogError("Unsupported map size."); return false; } … return true; } 

الآن ، HexGrid.Loadيمكنه أيضًا إيقاف التنفيذ عند فشل إنشاء الخريطة.

  public void Load (BinaryReader reader, int header) { int x = 20, z = 15; if (header >= 1) { x = reader.ReadInt32(); z = reader.ReadInt32(); } if (!CreateMap(x, z)) { return; } … } 

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

  if (x != cellCountX || z != cellCountZ) { if (!CreateMap(x, z)) { return; } } 

حزمة الوحدة

إدارة الملفات


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

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

سنقوم بإنشاء تصميم Save Load Menu .كما لو كانت قائمة حفظ. في وقت لاحق ، سنحولها ديناميكيًا إلى قائمة تمهيد. مثل قائمة أخرى ، يجب أن يكون لها خلفية وشريط قائمة ، تسمية قائمة ، وزر إلغاء. ثم أضف عرض تمرير ( GameObject / UI / Scroll View ) إلى القائمة لعرض قائمة بالملفات. أدناه نقوم بإدراج حقل الإدخال ( GameObject / UI / Input Field ) للإشارة إلى أسماء البطاقات الجديدة. نحتاج أيضًا إلى زر إجراء لحفظ الخريطة. وأخيرًا. إضافة زر حذف لإزالة بطاقات غير المرغوب فيها.



تصميم قائمة تحميل حفظ.

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


خيارات قائمة الملفات.

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

نص العنصر النائب لكائن إدخال الاسم في العنصر النائب الخاص به . لقد استخدمت نصًا وصفيًا ، ولكن يمكنك تركه فارغًا والتخلص من العنصر النائب.


تم تغيير تصميم القائمة.

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

إدارة القائمة


لكي تعمل القائمة ، نحتاج إلى برنامج نصي آخر ، في هذه الحالة - SaveLoadMenu. مثل NewMapMenu، يحتاج إلى ارتباط بالشبكة ، وكذلك الأساليب Openو Close.

 using UnityEngine; public class SaveLoadMenu : MonoBehaviour { public HexGrid hexGrid; public void Open () { gameObject.SetActive(true); HexMapCamera.Locked = true; } public void Close () { gameObject.SetActive(false); HexMapCamera.Locked = false; } } 

أضف هذا المكون إلى SaveLoadMenu وأعطه ارتباطًا لكائن الشبكة.


مكون SaveLoadMenu.

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

  bool saveMode; public void Open (bool saveMode) { this.saveMode = saveMode; gameObject.SetActive(true); HexMapCamera.Locked = true; } 

الآن الجمع بين أزرار حفظ و تحميل كائن عرافة خريطة محرر مع أسلوب Openالكائن حفظ تحميل القائمة . تحقق من المعلمة المنطقية لزر الحفظ فقط .


فتح القائمة في وضع الحفظ.

إذا لم تكن قد فعلت، ربط الحدث أزرار إلغاء الطريقة Close. الآن حفظ تحميل القائمة يمكن أن تفتح وتغلق.

تغير في المظهر


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

 using UnityEngine; using UnityEngine.UI; public class SaveLoadMenu : MonoBehaviour { public Text menuLabel, actionButtonLabel; … } 


الاتصال بالعلامات.

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

  public void Open (bool saveMode) { this.saveMode = saveMode; if (saveMode) { menuLabel.text = "Save Map"; actionButtonLabel.text = "Save"; } else { menuLabel.text = "Load Map"; actionButtonLabel.text = "Load"; } gameObject.SetActive(true); HexMapCamera.Locked = true; } 

أدخل اسم البطاقة


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

  public InputField nameInput; 


الاتصال بمجال الإدخال.

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

 using UnityEngine; using UnityEngine.UI; using System.IO; public class SaveLoadMenu : MonoBehaviour { … string GetSelectedPath () { string mapName = nameInput.text; if (mapName.Length == 0) { return null; } return Path.Combine(Application.persistentDataPath, mapName + ".map"); } } 

ماذا يحدث إذا أدخل المستخدم أحرفًا غير صالحة؟
, . , , .

Content Type . , - , . , , .

حفظ وتحميل


الآن سوف تشارك في الحفظ والتحميل SaveLoadMenu. لذلك، ونحن نتحرك الطرق Saveو Loadلل HexMapEditorفي SaveLoadMenu. لم تعد هناك حاجة إلى مشاركتها ، وستعمل مع معلمة المسار بدلاً من المسار الثابت.

  void Save (string path) { // string path = Path.Combine(Application.persistentDataPath, "test.map"); using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(1); hexGrid.Save(writer); } } void Load (string path) { // string path = Path.Combine(Application.persistentDataPath, "test.map"); using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { int header = reader.ReadInt32(); if (header <= 1) { hexGrid.Load(reader, header); HexMapCamera.ValidatePosition(); } else { Debug.LogWarning("Unknown map format " + header); } } } 

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

  void Load (string path) { if (!File.Exists(path)) { Debug.LogError("File does not exist " + path); return; } … } 

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

  public void Action () { string path = GetSelectedPath(); if (path == null) { return; } if (saveMode) { Save(path); } else { Load(path); } Close(); } 

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

عناصر قائمة الخريطة


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

  public void SelectItem (string name) { nameInput.text = name; } 

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



الزر عنصر قائمة.

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

 using UnityEngine; using UnityEngine.UI; public class SaveLoadItem : MonoBehaviour { public SaveLoadMenu menu; public string MapName { get { return mapName; } set { mapName = value; transform.GetChild(0).GetComponent<Text>().text = value; } } string mapName; public void Select () { menu.SelectItem(mapName); } } 

أضف مكونًا إلى عنصر القائمة واجعل الزر يستدعي طريقته Select.


مكون العنصر.

قائمة التعبئة


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

  public RectTransform listContent; public SaveLoadItem itemPrefab; 


امزج محتويات القائمة والتجهيز المسبق.

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

  void FillList () { string[] paths = Directory.GetFiles(Application.persistentDataPath, "*.map"); } 

للأسف ، لا يتم ضمان ترتيب الملف. لعرضها بالترتيب الأبجدي ، نحتاج إلى فرز الصفيف باستخدام System.Array.Sort.

 using UnityEngine; using UnityEngine.UI; using System; using System.IO; public class SaveLoadMenu : MonoBehaviour { … void FillList () { string[] paths = Directory.GetFiles(Application.persistentDataPath, "*.map"); Array.Sort(paths); } … } 

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

  Array.Sort(paths); for (int i = 0; i < paths.Length; i++) { SaveLoadItem item = Instantiate(itemPrefab); item.menu = this; item.MapName = paths[i]; item.transform.SetParent(listContent, false); } 

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

  item.MapName = Path.GetFileNameWithoutExtension(paths[i]); 

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

  public void Open (bool saveMode) { … FillList(); gameObject.SetActive(true); HexMapCamera.Locked = true; } 

عند إعادة ملء القائمة ، نحتاج إلى حذف جميع العناصر القديمة قبل إضافة عناصر جديدة.

  void FillList () { for (int i = 0; i < listContent.childCount; i++) { Destroy(listContent.GetChild(i).gameObject); } … } 


عناصر بدون ترتيب.

ترتيب النقاط


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





باستخدام مجموعة التخطيط الرأسي.

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



باستخدام مجرب حجم المحتوى.

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


يظهر شريط التمرير.

حذف البطاقة


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

  public void Delete () { string path = GetSelectedPath(); if (path == null) { return; } File.Delete(path); } 

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

  if (File.Exists(path)) { File.Delete(path); } 

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

  if (File.Exists(path)) { File.Delete(path); } nameInput.text = ""; FillList(); 

حزمة الوحدة

الجزء 14: مواد الإغاثة


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

حتى هذه اللحظة ، استخدمنا الألوان الصلبة لبطاقات التلوين. الآن سنقوم بتطبيق الملمس.


رسم القوام.

مزيج من ثلاثة أنواع


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

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

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

استخدام ألوان الذروة كخرائط Splat


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


خريطة المثلث تنبيه.

هل مجموع خريطة دائرة المثلث يساوي دائمًا واحد؟
نعم . . , (1, 0, 0) , (½, ½, 0) (&frac13;, &frac13;, &frac13;) .

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


ثلاثة تكوينات خريطة تنبيه.

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

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

  static Color color1 = new Color(1f, 0f, 0f); static Color color2 = new Color(0f, 1f, 0f); static Color color3 = new Color(0f, 0f, 1f); 

مراكز الخلايا


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

  void TriangulateWithoutRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { TriangulateEdgeFan(center, e, color1); … } 


المراكز الحمراء للخلايا.

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

حي النهر


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

  void TriangulateAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … TriangulateEdgeStrip(m, color1, e, color1); TriangulateEdgeFan(center, m, color1); … } 


المقاطع الحمراء المجاورة للأنهار.

الأنهار


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

  void TriangulateWithRiverBeginOrEnd ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … TriangulateEdgeStrip(m, color1, e, color1); TriangulateEdgeFan(center, m, color1); … } 

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

  void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … TriangulateEdgeStrip(m, color1, e, color1); terrain.AddTriangle(centerL, m.v1, m.v2); // terrain.AddTriangleColor(cell.Color); terrain.AddQuad(centerL, center, m.v2, m.v3); // terrain.AddQuadColor(cell.Color); terrain.AddQuad(center, centerR, m.v3, m.v4); // terrain.AddQuadColor(cell.Color); terrain.AddTriangle(centerR, m.v4, m.v5); // terrain.AddTriangleColor(cell.Color); terrain.AddTriangleColor(color1); terrain.AddQuadColor(color1); terrain.AddQuadColor(color1); terrain.AddTriangleColor(color1); … } 


الأنهار الحمراء على طول الخلايا.

الأضلاع


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

  void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { … if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(e1, cell, e2, neighbor, hasRoad); } else { TriangulateEdgeStrip(e1, color1, e2, color2, hasRoad); } … } 


ضلوع حمراء وخضراء باستثناء الحواف.

ألن الانتقال الحاد بين الأحمر والأخضر يسبب مشاكل؟
, , . . splat map, . .

, .

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

  void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell, bool hasRoad ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color c2 = HexMetrics.TerraceLerp(color1, color2, 1); TriangulateEdgeStrip(begin, color1, e2, c2, hasRoad); for (int i = 2; i < HexMetrics.terraceSteps; i++) { EdgeVertices e1 = e2; Color c1 = c2; e2 = EdgeVertices.TerraceLerp(begin, end, i); c2 = HexMetrics.TerraceLerp(color1, color2, i); TriangulateEdgeStrip(e1, c1, e2, c2, hasRoad); } TriangulateEdgeStrip(e2, c2, end, color2, hasRoad); } 


حواف أضلاع حمراء حمراء.

الزوايا


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

  void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … else { terrain.AddTriangle(bottom, left, right); terrain.AddTriangleColor(color1, color2, color3); } features.AddWall(bottom, bottomCell, left, leftCell, right, rightCell); } 


زوايا حمراء وخضراء وزرقاء باستثناء الحواف.

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

  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(color1, color2, 1); Color c4 = HexMetrics.TerraceLerp(color1, color3, 1); terrain.AddTriangle(begin, v3, v4); terrain.AddTriangleColor(color1, 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(color1, color2, i); c4 = HexMetrics.TerraceLerp(color1, color3, i); terrain.AddQuad(v1, v2, v3, v4); terrain.AddQuadColor(c1, c2, c3, c4); } terrain.AddQuad(v3, v4, left, right); terrain.AddQuadColor(c3, c4, color2, color3); } 


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

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

  void TriangulateBoundaryTriangle ( Vector3 begin, Color beginColor, Vector3 left, Color leftColor, Vector3 boundary, Color boundaryColor ) { Vector3 v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, 1)); Color c2 = HexMetrics.TerraceLerp(beginColor, leftColor, 1); terrain.AddTriangleUnperturbed(HexMetrics.Perturb(begin), v2, boundary); terrain.AddTriangleColor(beginColor, c2, boundaryColor); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, i)); c2 = HexMetrics.TerraceLerp(beginColor, leftColor, i); terrain.AddTriangleUnperturbed(v1, v2, boundary); terrain.AddTriangleColor(c1, c2, boundaryColor); } terrain.AddTriangleUnperturbed(v2, HexMetrics.Perturb(left), boundary); terrain.AddTriangleColor(c2, leftColor, boundaryColor); } 

قم بتغييره TriangulateCornerTerracesCliffبحيث يستخدم الألوان الصحيحة.

  void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … Color boundaryColor = Color.Lerp(color1, color3, b); TriangulateBoundaryTriangle( begin, color1, left, color2, boundary, boundaryColor ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, color2, right, color3, boundary, boundaryColor ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleColor(color2, color3, boundaryColor); } } 

ونفعل نفس الشيء من أجل TriangulateCornerCliffTerraces.

  void TriangulateCornerCliffTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … Color boundaryColor = Color.Lerp(color1, color2, b); TriangulateBoundaryTriangle( right, color3, begin, color1, boundary, boundaryColor ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, color2, right, color3, boundary, boundaryColor ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleColor(color2, color3, boundaryColor); } } 


خريطة إغاثة كاملة.

حزمة الوحدة

صفائف الملمس


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

هل تدعم جميع GPUs المصفوفات النسيجية؟
GPU , . Unity .
  • Direct3D 11/12 (Windows, Xbox One)
  • OpenGL Core (Mac OS X, Linux)
  • Metal (iOS, Mac OS X)
  • OpenGL ES 3.0 (Android, iOS, WebGL 2.0)
  • PlayStation 4


السيد


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

لماذا إنشاء أصل؟
, Play . , .

, . Unity . , . , .

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

 using UnityEditor; using UnityEngine; public class TextureArrayWizard : ScriptableWizard { } 

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

  static void CreateWizard () { ScriptableWizard.DisplayWizard<TextureArrayWizard>( "Create Texture Array", "Create" ); } 

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

  [MenuItem("Assets/Create/Texture Array")] static void CreateWizard () { … } 


معالجنا المخصص.

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

  public Texture2D[] textures; 


ماجستير مع مواد.

لنقم بإنشاء شيء


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

  void OnWizardCreate () { } 

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

  void OnWizardCreate () { if (textures.Length == 0) { return; } } 

الخطوة التالية هي طلب الموقع لحفظ أصل صفيف النسيج. يمكن فتح لوحة حفظ الملف باستخدام الطريقة EditorUtility.SaveFilePanelInProject. تحدد معلماته اسم اللوحة واسم الملف الافتراضي وملحق الملف والوصف. تستخدم صفائف الزخرفة امتداد ملف الأصل العام .

  if (textures.Length == 0) { return; } EditorUtility.SaveFilePanelInProject( "Save Texture Array", "Texture Array", "asset", "Save Texture Array" ); 

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

  string path = EditorUtility.SaveFilePanelInProject( "Save Texture Array", "Texture Array", "asset", "Save Texture Array" ); if (path.Length == 0) { return; } 

إنشاء مجموعة من القوام


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

  if (path.Length == 0) { return; } Texture2D t = textures[0]; Texture2DArray textureArray = new Texture2DArray( t.width, t.height, textures.Length, t.format, t.mipmapCount > 1 ); 

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

  Texture2DArray textureArray = new Texture2DArray( t.width, t.height, textures.Length, t.format, t.mipmapCount > 1 ); textureArray.anisoLevel = t.anisoLevel; textureArray.filterMode = t.filterMode; textureArray.wrapMode = t.wrapMode; 

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

  textureArray.wrapMode = t.wrapMode; for (int i = 0; i < textures.Length; i++) { for (int m = 0; m < t.mipmapCount; m++) { Graphics.CopyTexture(textures[i], 0, m, textureArray, i, m); } } 

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

  for (int i = 0; i < textures.Length; i++) { … } AssetDatabase.CreateAsset(textureArray, path); 


القوام


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






قوام الرمل والعشب والأرض والحجر والثلج.

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

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



إنشاء مجموعة من القوام.

بعد إنشاء أصل صفيف النسيج ، حدده وافحصه في المفتش.


مفتش مجموعة الملمس.

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

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

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

شادر


الآن بعد أن أصبح لدينا مجموعة من القوام ، نحتاج إلى تعليم shader كيفية العمل معها. في الوقت الحالي ، نستخدم تظليل VertexColors لتقديم التضاريس . نظرًا لأننا سنستخدم الآن القوام بدلاً من الألوان ، نعيد تسميته إلى Terrain . ثم نحول معلمة _MainTex إلى مصفوفة من الأنسجة ونعينها مادة عرض.

 Shader "Custom/Terrain" { Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Terrain Texture Array", 2DArray) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 } … } 


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

لتمكين صفائف النسيج على جميع الأنظمة الأساسية التي تدعمها ، تحتاج إلى زيادة المستوى المستهدف للتظليل من 3.0 إلى 3.5.

  #pragma target 3.5 

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

 // sampler2D _MainTex; UNITY_DECLARE_TEX2DARRAY(_MainTex); 

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

  struct Input { // float2 uv_MainTex; float4 color : COLOR; float3 worldPos; }; 

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

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

  void surf (Input IN, inout SurfaceOutputStandard o) { float2 uv = IN.worldPos.xz * 0.02; fixed4 c = UNITY_SAMPLE_TEX2DARRAY(_MainTex, float3(uv, 0)); Albedo = c.rgb * _Color; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } 


كل شيء أصبح رمال.

حزمة الوحدة

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


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

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

بيانات الشبكات


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

  public bool useCollider, useColors, useUVCoordinates, useUV2Coordinates; public bool useTerrainTypes; [NonSerialized] List<Vector3> vertices, terrainTypes; 

تمكين أنواع التضاريس لطفل التضاريس من Hex Grid Chunk الجاهزة .


نستخدم أنواع الإغاثة.

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

  public void Clear () { … if (useTerrainTypes) { terrainTypes = ListPool<Vector3>.Get(); } triangles = ListPool<int>.Get(); } 

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

  public void Apply () { … if (useTerrainTypes) { hexMesh.SetUVs(2, terrainTypes); ListPool<Vector3>.Add(terrainTypes); } hexMesh.SetTriangles(triangles, 0); … } 

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

  public void AddTriangleTerrainTypes (Vector3 types) { terrainTypes.Add(types); terrainTypes.Add(types); terrainTypes.Add(types); } 

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

  public void AddQuadTerrainTypes (Vector3 types) { terrainTypes.Add(types); terrainTypes.Add(types); terrainTypes.Add(types); terrainTypes.Add(types); } 

مراوح مثلثات الأضلاع


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

  void TriangulateEdgeFan (Vector3 center, EdgeVertices edge, Color color) { terrain.AddTriangle(center, edge.v1, edge.v2); // terrain.AddTriangleColor(color); terrain.AddTriangle(center, edge.v2, edge.v3); // terrain.AddTriangleColor(color); terrain.AddTriangle(center, edge.v3, edge.v4); // terrain.AddTriangleColor(color); terrain.AddTriangle(center, edge.v4, edge.v5); // terrain.AddTriangleColor(color); terrain.AddTriangleColor(color1); terrain.AddTriangleColor(color1); terrain.AddTriangleColor(color1); terrain.AddTriangleColor(color1); } 

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

  void TriangulateEdgeFan (Vector3 center, EdgeVertices edge, float type) { … Vector3 types; types.x = types.y = types.z = type; terrain.AddTriangleTerrainTypes(types); terrain.AddTriangleTerrainTypes(types); terrain.AddTriangleTerrainTypes(types); terrain.AddTriangleTerrainTypes(types); } 

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

 // TriangulateEdgeFan(center, e, color1); TriangulateEdgeFan(center, e, cell.TerrainTypeIndex); 

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

خطوط الضلع


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

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

الآن نحن بحاجة إلى تغيير التحديات TriangulateEdgeStrip. أولا TriangulateAdjacentToRiver، TriangulateWithRiverBeginOrEndو TriangulateWithRiverيجب استخدام نوع من الخلايا لكلا الجانبين من قطاع الزعانف.

 // TriangulateEdgeStrip(m, color1, e, color1); TriangulateEdgeStrip( m, color1, cell.TerrainTypeIndex, e, color1, cell.TerrainTypeIndex ); 

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

  void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { … if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(e1, cell, e2, neighbor, hasRoad); } else { // TriangulateEdgeStrip(e1, color1, e2, color2, hasRoad); TriangulateEdgeStrip( e1, color1, cell.TerrainTypeIndex, e2, color2, neighbor.TerrainTypeIndex, hasRoad ); } … } 

الأمر نفسه ينطبق على TriangulateEdgeTerracesما يحفز ثلاث مرات TriangulateEdgeStrip. أنواع الحواف هي نفسها.

  void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell, bool hasRoad ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color c2 = HexMetrics.TerraceLerp(color1, color2, 1); float t1 = beginCell.TerrainTypeIndex; float t2 = endCell.TerrainTypeIndex; TriangulateEdgeStrip(begin, color1, t1, e2, c2, t2, hasRoad); for (int i = 2; i < HexMetrics.terraceSteps; i++) { EdgeVertices e1 = e2; Color c1 = c2; e2 = EdgeVertices.TerraceLerp(begin, end, i); c2 = HexMetrics.TerraceLerp(color1, color2, i); TriangulateEdgeStrip(e1, c1, t1, e2, c2, t2, hasRoad); } TriangulateEdgeStrip(e2, c2, t1, end, color2, t2, hasRoad); } 

الزوايا


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

  void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … else { terrain.AddTriangle(bottom, left, right); terrain.AddTriangleColor(color1, color2, color3); Vector3 types; types.x = bottomCell.TerrainTypeIndex; types.y = leftCell.TerrainTypeIndex; types.z = rightCell.TerrainTypeIndex; terrain.AddTriangleTerrainTypes(types); } features.AddWall(bottom, bottomCell, left, leftCell, right, rightCell); } 

نحن نستخدم نفس النهج TriangulateCornerTerraces، هنا فقط نقوم بإنشاء مجموعة من quad-s.

  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(color1, color2, 1); Color c4 = HexMetrics.TerraceLerp(color1, color3, 1); Vector3 types; types.x = beginCell.TerrainTypeIndex; types.y = leftCell.TerrainTypeIndex; types.z = rightCell.TerrainTypeIndex; terrain.AddTriangle(begin, v3, v4); terrain.AddTriangleColor(color1, c3, c4); terrain.AddTriangleTerrainTypes(types); 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(color1, color2, i); c4 = HexMetrics.TerraceLerp(color1, color3, i); terrain.AddQuad(v1, v2, v3, v4); terrain.AddQuadColor(c1, c2, c3, c4); terrain.AddQuadTerrainTypes(types); } terrain.AddQuad(v3, v4, left, right); terrain.AddQuadColor(c3, c4, color2, color3); terrain.AddQuadTerrainTypes(types); } 

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

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

في TriangulateCornerTerracesCliffإنشاء ناقلات الأنواع على أساس الخلايا المنقولة. ثم أضفه إلى مثلث واحد وأدخله TriangulateBoundaryTriangle.

  void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } Vector3 boundary = Vector3.Lerp( HexMetrics.Perturb(begin), HexMetrics.Perturb(right), b ); Color boundaryColor = Color.Lerp(color1, color3, b); Vector3 types; types.x = beginCell.TerrainTypeIndex; types.y = leftCell.TerrainTypeIndex; types.z = rightCell.TerrainTypeIndex; TriangulateBoundaryTriangle( begin, color1, left, color2, boundary, boundaryColor, types ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, color2, right, color3, boundary, boundaryColor, types ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleColor(color2, color3, boundaryColor); terrain.AddTriangleTerrainTypes(types); } } 

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

  void TriangulateCornerCliffTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (leftCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } Vector3 boundary = Vector3.Lerp( HexMetrics.Perturb(begin), HexMetrics.Perturb(left), b ); Color boundaryColor = Color.Lerp(color1, color2, b); Vector3 types; types.x = beginCell.TerrainTypeIndex; types.y = leftCell.TerrainTypeIndex; types.z = rightCell.TerrainTypeIndex; TriangulateBoundaryTriangle( right, color3, begin, color1, boundary, boundaryColor, types ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, color2, right, color3, boundary, boundaryColor, types ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleColor(color2, color3, boundaryColor); terrain.AddTriangleTerrainTypes(types); } } 

الأنهار


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

  void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … terrain.AddTriangleColor(color1); terrain.AddQuadColor(color1); terrain.AddQuadColor(color1); terrain.AddTriangleColor(color1); Vector3 types; types.x = types.y = types.z = cell.TerrainTypeIndex; terrain.AddTriangleTerrainTypes(types); terrain.AddQuadTerrainTypes(types); terrain.AddQuadTerrainTypes(types); terrain.AddTriangleTerrainTypes(types); … } 

اكتب مزيج


في هذه المرحلة ، تحتوي الشبكات على مؤشرات الارتفاع اللازمة. كل ما تبقى لنا هو إجبار تظليل التضاريس على استخدامها. من أجل أن تقع المؤشرات في جهاز تظليل الجزء ، نحتاج أولاً إلى تمريرها عبر جهاز تظليل القمة. يمكننا القيام بذلك في وظيفة الذروة الخاصة بنا ، كما فعلنا في Shader Estuary . في هذه الحالة ، نقوم بإضافة حقل إلى بنية الإدخال float3 terrainونسخه إليه v.texcoord2.xyz.

  #pragma surface surf Standard fullforwardshadows vertex:vert #pragma target 3.5 … struct Input { float4 color : COLOR; float3 worldPos; float3 terrain; }; void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); data.terrain = v.texcoord2.xyz; } 

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

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

هل يمكننا العمل مع المتجه كمصفوفة؟
نعم - color[0] color.r . color[1] color.g , .

باستخدام هذه الوظيفة ، يمكننا ببساطة تجربة مجموعة الملمس ثلاث مرات ودمج النتائج.

  void surf (Input IN, inout SurfaceOutputStandard o) { // float2 uv = IN.worldPos.xz * 0.02; fixed4 c = GetTerrainColor(IN, 0) + GetTerrainColor(IN, 1) + GetTerrainColor(IN, 2); o.Albedo = c.rgb * _Color; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } 


راحة مزخرفة.

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

اكتساح


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


خيارات الإغاثة.

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

 // public Color Color { // get { // return HexMetrics.colors[terrainTypeIndex]; // } // } 

يمكنك HexGridأيضًا إزالة مجموعة من الألوان والشفرة المرتبطة بها.

 // public Color[] colors; … void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); // HexMetrics.colors = colors; CreateMap(cellCountX, cellCountZ); } … … void OnEnable () { if (!HexMetrics.noiseSource) { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); // HexMetrics.colors = colors; } } 

وأخيرًا ، لا حاجة أيضًا إلى مجموعة من الألوان HexMetrics.

 // public static Color[] colors; 

حزمة الوحدة

الجزء 15: المسافات


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

بعد إنشاء خرائط عالية الجودة ، سنبدأ التنقل.


أقصر مسار ليس دائما مستقيما.

عرض الشبكة


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

نسيج شبكي


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


تكرار نسيج شبكي.

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

إسقاط الشبكة


لإسقاط نقش شبكي ، نحتاج إلى إضافة خاصية نسيج إلى Shader Terrain .

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


مواد الإغاثة بنسيج شبكي.

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

  sampler2D _GridTex; … void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = GetTerrainColor(IN, 0) + GetTerrainColor(IN, 1) + GetTerrainColor(IN, 2); fixed4 grid = tex2D(_GridTex, IN.worldPos.xz); o.Albedo = c.rgb * grid * _Color; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } 


ضرب البيدو بشبكة دقيقة.

نحتاج إلى قياس النمط بحيث يتطابق مع الخلايا في الخريطة. المسافة بين مراكز الخلايا المجاورة هي 15 ، ويجب مضاعفتها لتحريك خليتين. أي أننا بحاجة إلى تقسيم إحداثيات الشبكة V على 30. يبلغ نصف القطر الداخلي للخلايا 5√3 ، ولتحريك خليتين إلى اليمين ، نحتاج إلى أربعة أضعاف ذلك. لذلك ، من الضروري تقسيم إحداثيات شبكة U على 20√3.

  float2 gridUV = IN.worldPos.xz; gridUV.x *= 1 / (4 * 8.66025404); gridUV.y *= 1 / (2 * 15.0); fixed4 grid = tex2D(_GridTex, gridUV); 


حجم الشبكة الصحيح.

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


إسقاط على الخلايا ذات الارتفاع.

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


شبكة في المسافة.

إدراج الشبكة


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

  #pragma surface surf Standard fullforwardshadows vertex:vert #pragma target 3.5 #pragma multi_compile _ GRID_ON 

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

  fixed4 grid = 1; #if defined(GRID_ON) float2 gridUV = IN.worldPos.xz; gridUV.x *= 1 / (4 * 8.66025404); gridUV.y *= 1 / (2 * 15.0); grid = tex2D(_GridTex, gridUV); #endif o.Albedo = c.rgb * grid * _Color; 

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

  public Material terrainMaterial; … public void ShowGrid (bool visible) { if (visible) { terrainMaterial.EnableKeyword("GRID_ON"); } else { terrainMaterial.DisableKeyword("GRID_ON"); } } 


محرر مسدسات مسيرة بالإشارة إلى المادة.

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


تبديل الشبكة.

حفظ الدولة


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

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

لبدء اللعبة دائمًا بدون شبكة ، سنعطل الكلمة الرئيسية GRID_ONفي Awake HexMapEditor.

  void Awake () { terrainMaterial.DisableKeyword("GRID_ON"); } 

حزمة الوحدة

وضع التحرير


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

تحرير التبديل


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

  bool editMode; … public void SetEditMode (bool toggle) { editMode = toggle; } 


مفتاح وضع التحرير.

لتعطيل التعديل حقًا ، اجعل المكالمة EditCellsتعتمد على editMode.

  void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { HexCell currentCell = hexGrid.GetCell(hit.point); if (previousCell && previousCell != currentCell) { ValidateDrag(currentCell); } else { isDrag = false; } if (editMode) { EditCells(currentCell); } previousCell = currentCell; } else { previousCell = null; } } 

تسميات التصحيح


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

  public void SetEditMode (bool toggle) { editMode = toggle; hexGrid.ShowUI(!toggle); } 

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

  void Awake () { gridCanvas = GetComponentInChildren<Canvas>(); cells = new HexCell[HexMetrics.chunkSizeX * HexMetrics.chunkSizeZ]; // ShowUI(false); } 


تنسيق التسميات.

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


العلامات ذات حجم الخط الغامق 8.

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


العلامات الكبيرة.

نظرًا لأننا لم نعد بحاجة إلى الإحداثيات ، فسنحذف HexGrid.CreateCellالقيمة في المهمة label.text.

  void CreateCell (int x, int z, int i) { … Text label = Instantiate<Text>(cellLabelPrefab); label.rectTransform.anchoredPosition = new Vector2(position.x, position.z); // label.text = cell.coordinates.ToStringOnSeparateLines(); cell.uiRect = label.rectTransform; … } 

يمكنك أيضًا إزالة تبديل التصنيفات والطريقة المرتبطة به من واجهة المستخدم HexMapEditor.ShowUI.

 // public void ShowUI (bool visible) { // hexGrid.ShowUI(visible); // } 


لم يعد مفتاح الأسلوب أكثر.

حزمة الوحدة

إيجاد المسافات


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

عرض المسافة


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

  int distance; 

عندما يتم تعيين المسافة ، يجب علينا تحديث تسمية الخلية لعرض قيمتها. HexCellلديه إشارة إلى RectTransformكائن واجهة المستخدم. سنحتاج إلى الاتصال به GetComponent<Text>للوصول إلى الزنزانة. ضع في اعتبارك ما Textيوجد في مساحة الاسم UnityEngine.UI، لذا استخدمه في بداية البرنامج النصي.

  void UpdateDistanceLabel () { Text label = uiRect.GetComponent<Text>(); label.text = distance.ToString(); } 

ألا يجب أن نحتفظ برابط مباشر إلى مكون النص؟
, . , , , . , .

لنقم بتعيين الخاصية العامة لاستلام وتعيين المسافة إلى الخلية ، بالإضافة إلى تحديث التسمية الخاصة بها.

  public int Distance { get { return distance; } set { distance = value; UpdateDistanceLabel(); } } 

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

  public void FindDistancesTo (HexCell cell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = 0; } } 

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

  if (editMode) { EditCells(currentCell); } else { hexGrid.FindDistancesTo(currentCell); } 

المسافات بين الإحداثيات


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

  public void FindDistancesTo (HexCell cell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = cell.coordinates.DistanceTo(cells[i].coordinates); } } 

أضف الآن إلى HexCoordinatesالطريقة DistanceTo. يجب عليه مقارنة إحداثياته ​​مع إحداثيات مجموعة أخرى. لنبدأ فقط بقياس X ، وسنطرح إحداثيات X من بعضها البعض.

  public int DistanceTo (HexCoordinates other) { return x - other.x; } 

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

  return x < other.x ? other.x - x : x - other.x; 


المسافات على طول X.

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

  return (x < other.x ? other.x - x : x - other.x) + (Y < other.Y ? other.Y - Y : Y - other.Y) + (z < other.z ? other.z - z : z - other.z); 


مجموع مسافات XYZ.

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

  return ((x < other.x ? other.x - x : x - other.x) + (Y < other.Y ? other.Y - Y : Y - other.Y) + (z < other.z ? other.z - z : z - other.z)) / 2; 


مسافات حقيقية.

لماذا يساوي المجموع ضعف المسافة؟
, . , (1, −3, 2). . , . . , . .


.

حزمة الوحدة

العمل مع العقبات


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

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

تصوّر البحث


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

  public void FindDistancesTo (HexCell cell) { StartCoroutine(Search(cell)); } IEnumerator Search (HexCell cell) { WaitForSeconds delay = new WaitForSeconds(1 / 60f); for (int i = 0; i < cells.Length; i++) { yield return delay; cells[i].Distance = cell.coordinates.DistanceTo(cells[i].coordinates); } } 

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

  public void FindDistancesTo (HexCell cell) { StopAllCoroutines(); StartCoroutine(Search(cell)); } 

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

  public void Load (BinaryReader reader, int header) { StopAllCoroutines(); … } 

اتساع نطاق البحث الأول


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

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

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

  IEnumerator Search (HexCell cell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; } … } 

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

  void UpdateDistanceLabel () { Text label = uiRect.GetComponent<Text>(); label.text = distance == int.MaxValue ? "" : distance.ToString(); } 

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

  IEnumerator Search (HexCell cell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; } WaitForSeconds delay = new WaitForSeconds(1 / 60f); Queue<HexCell> frontier = new Queue<HexCell>(); cell.Distance = 0; frontier.Enqueue(cell); // for (int i = 0; i < cells.Length; i++) { // yield return delay; // cells[i].Distance = // cell.coordinates.DistanceTo(cells[i].coordinates); // } } 

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

  frontier.Enqueue(cell); while (frontier.Count > 0) { yield return delay; HexCell current = frontier.Dequeue(); } 

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

  while (frontier.Count > 0) { yield return delay; HexCell current = frontier.Dequeue(); for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if (neighbor != null) { neighbor.Distance = current.Distance + 1; frontier.Enqueue(neighbor); } } } 

لكن يجب أن نضيف فقط تلك الخلايا التي لم تعط بعد مسافة.

  if (neighbor != null && neighbor.Distance == int.MaxValue) { neighbor.Distance = current.Distance + 1; frontier.Enqueue(neighbor); } 


بحث واسع.

تجنب الماء


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

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

  for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if (neighbor == null || neighbor.Distance != int.MaxValue) { continue; } neighbor.Distance = current.Distance + 1; frontier.Enqueue(neighbor); } 

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

  if (neighbor == null || neighbor.Distance != int.MaxValue) { continue; } if (neighbor.IsUnderwater) { continue; } 


المسافات دون التحرك عبر الماء.

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

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


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

  if (neighbor.IsUnderwater) { continue; } if (current.GetEdgeType(neighbor) == HexEdgeType.Cliff) { continue; } 


المسافات دون عبور المنحدرات.

حزمة الوحدة

تكاليف السفر


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

الطرق السريعة


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

  int distance = current.Distance; if (current.HasRoadThroughEdge(d)) { distance += 1; } else { distance += 10; } neighbor.Distance = distance; 


الطرق ذات المسافات الخاطئة.

فرز الحدود


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

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

  List<HexCell> frontier = new List<HexCell>(); cell.Distance = 0; frontier.Add(cell); while (frontier.Count > 0) { yield return delay; HexCell current = frontier[0]; frontier.RemoveAt(0); for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … neighbor.Distance = distance; frontier.Add(neighbor); } } 

ألا يمكنني استخدام ListPool <HexCell>؟
, , . , , .

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

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

  frontier.Add(neighbor); frontier.Sort((x, y) => x.Distance.CompareTo(y.Distance)); 

كيف تعمل طريقة الفرز هذه؟
. , . .

  frontier.Sort(CompareDistances); … static int CompareDistances (HexCell x, HexCell y) { return x.Distance.CompareTo(y.Distance); } 


الحدود التي تم فرزها لا تزال غير صحيحة.

تحديث الحدود


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

  HexCell neighbor = current.GetNeighbor(d); if (neighbor == null) { continue; } if (neighbor.IsUnderwater) { continue; } if (current.GetEdgeType(neighbor) == HexEdgeType.Cliff) { continue; } int distance = current.Distance; if (current.HasRoadThroughEdge(d)) { distance += 1; } else { distance += 10; } if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; frontier.Add(neighbor); } else if (distance < neighbor.Distance) { neighbor.Distance = distance; } frontier.Sort((x, y) => x.Distance.CompareTo(y.Distance)); 


المسافات الصحيحة.

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

المنحدرات


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

  HexEdgeType edgeType = current.GetEdgeType(neighbor); if (edgeType == HexEdgeType.Cliff) { continue; } int distance = current.Distance; if (current.HasRoadThroughEdge(d)) { distance += 1; } else { distance += edgeType == HexEdgeType.Flat ? 5 : 10; } 


للتغلب على المنحدرات ، تحتاج إلى مزيد من العمل ، والطرق سريعة دائمًا.

كائنات الإغاثة


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

  if (current.HasRoadThroughEdge(d)) { distance += 1; } else { distance += edgeType == HexEdgeType.Flat ? 5 : 10; distance += neighbor.UrbanLevel + neighbor.FarmLevel + neighbor.PlantLevel; } 


الأشياء تتباطأ إذا لم يكن هناك طريق.

الجدران


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

  if (current.HasRoadThroughEdge(d)) { distance += 1; } else if (current.Walled != neighbor.Walled) { continue; } else { distance += edgeType == HexEdgeType.Flat ? 5 : 10; distance += neighbor.UrbanLevel + neighbor.FarmLevel + neighbor.PlantLevel; } 


الجدران لا تسمح لنا بالمرور ، تحتاج إلى البحث عن البوابة.

حزمة الوحدة

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


All Articles