خرائط سداسية في الوحدة: مكتشف المسار وفرق اللاعبين والرسوم المتحركة

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

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

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

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

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

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

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

الجزء 16: إيجاد الطريق


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

بعد حساب المسافات بين الخلايا ، شرعنا في العثور على المسارات بينها.

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


التخطيط لرحلة

الخلايا البارزة


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

نسيج مخطط


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


مخطط الخلية على خلفية سوداء

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


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

نقش واحد لكل خلية


أسرع طريقة هي إضافة كفاف محتمل للخلايا ، وإضافة كل نقش متحرك. قم بإنشاء كائن لعبة جديد ، وأضف مكون الصورة ( Component / UI / Image ) إليه وقم بتعيينه العفريت التفصيلي. ثم قم بإدراج مثيل الإعداد المسبق Hex Cell Label في المشهد ، واجعل كائن العفريت تابعًا له ، وقم بتطبيق التغييرات على البادئة ، ثم تخلص من البادئة.



عنصر اختيار الطفل الجاهزة

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


الاختيار العفاريت مخبأة جزئيا من قبل الإغاثة

الرسم فوق كل شيء


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

Shader "Custom/Highlight" { Properties { [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {} _Color ("Tint", Color) = (1,1,1,1) [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0 [HideInInspector] _RendererColor ("RendererColor", Color) = (1,1,1,1) [HideInInspector] _Flip ("Flip", Vector) = (1,1,1,1) [PerRendererData] _AlphaTex ("External Alpha", 2D) = "white" {} [PerRendererData] _EnableExternalAlpha ("Enable External Alpha", Float) = 0 } SubShader { Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" "CanUseSpriteAtlas"="True" } Cull Off ZWrite Off Blend One OneMinusSrcAlpha Pass { CGPROGRAM #pragma vertex SpriteVert #pragma fragment SpriteFrag #pragma target 2.0 #pragma multi_compile_instancing #pragma multi_compile _ PIXELSNAP_ON #pragma multi_compile _ ETC1_EXTERNAL_ALPHA #include "UnitySprites.cginc" ENDCG } } } 

التغيير الأول هو أننا نتجاهل المخزن المؤقت للعمق ، مما يجعل اختبار Z ينجح دائمًا.

  ZWrite Off ZTest Always 

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

  "Queue"="Transparent+10" 

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



نحن نستخدم مواد العفريت الخاصة بنا

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


تجاهل المخزن المؤقت للعمق

التحكم في الاختيار


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


مكون صورة معطل

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

  public void DisableHighlight () { Image highlight = uiRect.GetChild(0).GetComponent<Image>(); highlight.enabled = false; } public void EnableHighlight () { Image highlight = uiRect.GetChild(0).GetComponent<Image>(); highlight.enabled = true; } 

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

  public void EnableHighlight (Color color) { Image highlight = uiRect.GetChild(0).GetComponent<Image>(); highlight.color = color; highlight.enabled = true; } 

حزمة الوحدة

إيجاد الطريق


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

بدء البحث


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

  HexCell previousCell, searchFromCell; 

داخل HandleInput يمكننا استخدام Input.GetKey(KeyCode.LeftShift) لاختبار مفتاح Shift مع Input.GetKey(KeyCode.LeftShift) .

  if (editMode) { EditCells(currentCell); } else if (Input.GetKey(KeyCode.LeftShift)) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); } else { hexGrid.FindDistancesTo(currentCell); } 


أين ننظر

نقطة نهاية البحث


بدلاً من البحث عن جميع المسافات إلى الخلية ، نبحث الآن عن مسار بين خليتين محددتين. لذلك ، HexGrid.FindDistancesTo تسمية HexGrid.FindDistancesTo إلى HexGrid.FindPath HexCell المعلمة HexCell الثانية. أيضًا ، قم بتغيير طريقة Search .

  public void FindPath (HexCell fromCell, HexCell toCell) { StopAllCoroutines(); StartCoroutine(Search(fromCell, toCell)); } IEnumerator Search (HexCell fromCell, HexCell toCell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; } WaitForSeconds delay = new WaitForSeconds(1 / 60f); List<HexCell> frontier = new List<HexCell>(); fromCell.Distance = 0; frontier.Add(fromCell); … } 

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

  if (editMode) { EditCells(currentCell); } else if (Input.GetKey(KeyCode.LeftShift)) { … } else if (searchFromCell && searchFromCell != currentCell) { hexGrid.FindPath(searchFromCell, currentCell); } 

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

  IEnumerator Search (HexCell fromCell, HexCell toCell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; cells[i].DisableHighlight(); } fromCell.EnableHighlight(Color.blue); toCell.EnableHighlight(Color.red); … } 


نقاط النهاية لمسار محتمل

تقييد البحث


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

  while (frontier.Count > 0) { yield return delay; HexCell current = frontier[0]; frontier.RemoveAt(0); if (current == toCell) { break; } for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … } } 


توقف عند نقطة النهاية

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

عرض المسار


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

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


شبكة شجرة تصف مسارات المركز

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

  public HexCell PathFrom { get; set; } 

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

  if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; neighbor.PathFrom = current; frontier.Add(neighbor); } else if (distance < neighbor.Distance) { neighbor.Distance = distance; neighbor.PathFrom = current; } 

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

  if (current == toCell) { current = current.PathFrom; while (current != fromCell) { current.EnableHighlight(Color.white); current = current.PathFrom; } break; } 


تم العثور على المسار

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

تغيير بداية البحث


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

  HexCell previousCell, searchFromCell, searchToCell; 

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

  else if (Input.GetKey(KeyCode.LeftShift)) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); if (searchToCell) { hexGrid.FindPath(searchFromCell, searchToCell); } } else if (searchFromCell && searchFromCell != currentCell) { searchToCell = currentCell; hexGrid.FindPath(searchFromCell, searchToCell); } 

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

  if (editMode) { EditCells(currentCell); } else if ( Input.GetKey(KeyCode.LeftShift) && searchToCell != currentCell ) { … } 

حزمة الوحدة

بحث أذكى


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

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

البحث عن الاستدلال


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

  public int SearchHeuristic { get; set; } 

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

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

  if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); frontier.Add(neighbor); } 

أولوية البحث


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

  public int SearchPriority { get { return distance + SearchHeuristic; } } 

لكي يعمل هذا ، قم HexGrid.Search بحيث يستخدم هذه الخاصية لفرز الحدود.

  frontier.Sort( (x, y) => x.SearchPriority.CompareTo(y.SearchPriority) ); 



البحث بدون استدلال وباستخدام الاستدلال

ارشادي صالح


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

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


استخدام الاستدلال × 5

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



استدلال مفرط وصالح

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

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

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

حزمة الوحدة

قائمة انتظار الأولوية


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

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

إنشاء قائمة الانتظار الخاصة بك


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

 using System.Collections.Generic; public class HexCellPriorityQueue { List<HexCell> list = new List<HexCell>(); public void Enqueue (HexCell cell) { } public HexCell Dequeue () { return null; } public void Change (HexCell cell) { } public void Clear () { list.Clear(); } } 

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

  public void Change (HexCell cell, int oldPriority) { } 

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

  int count = 0; public int Count { get { return count; } } public void Enqueue (HexCell cell) { count += 1; } public HexCell Dequeue () { count -= 1; return null; } … public void Clear () { list.Clear(); count = 0; } 

أضف إلى قائمة الانتظار


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

  public void Enqueue (HexCell cell) { count += 1; int priority = cell.SearchPriority; list[priority] = cell; } 

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

  int priority = cell.SearchPriority; while (priority >= list.Count) { list.Add(null); } list[priority] = cell; 


قائمة بالثقوب

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

  public HexCell NextWithSamePriority { get; set; } 

لإنشاء سلسلة ، اسمح لـ HexCellPriorityQueue.Enqueue الخلية المضافة حديثًا للإشارة إلى القيمة الحالية بالأولوية نفسها ، قبل حذفها.

  cell.NextWithSamePriority = list[priority]; list[priority] = cell; 


قائمة القوائم المرتبطة

إزالة من قائمة الانتظار


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

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

  public HexCell Dequeue () { count -= 1; for (int i = 0; i < list.Count; i++) { HexCell cell = list[i]; if (cell != null) { return cell; } } return null; } 

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

  if (cell != null) { list[i] = cell.NextWithSamePriority; return cell; } 

تتبع الحد الأدنى


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

  int minimum = int.MaxValue; … public void Clear () { list.Clear(); count = 0; minimum = int.MaxValue; } 

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

  public void Enqueue (HexCell cell) { count += 1; int priority = cell.SearchPriority; if (priority < minimum) { minimum = priority; } … } 

وعند الانسحاب من قائمة الانتظار ، نستخدم القائمة على الأقل للتكرار ، ولا نبدأ من نقطة الصفر.

  public HexCell Dequeue () { count -= 1; for (; minimum < list.Count; minimum++) { HexCell cell = list[minimum]; if (cell != null) { list[minimum] = cell.NextWithSamePriority; return cell; } } return null; } 

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

تغيير الأولويات


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

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

  public void Change (HexCell cell, int oldPriority) { HexCell current = list[oldPriority]; HexCell next = current.NextWithSamePriority; } 

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

  HexCell current = list[oldPriority]; HexCell next = current.NextWithSamePriority; if (current == cell) { list[oldPriority] = next; } 

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

  if (current == cell) { list[oldPriority] = next; } else { while (next != cell) { current = next; next = current.NextWithSamePriority; } } 

في هذه المرحلة ، يمكننا إزالة الخلية التي تم تغييرها من القائمة المرتبطة ، وتخطيها.

  while (next != cell) { current = next; next = current.NextWithSamePriority; } current.NextWithSamePriority = cell.NextWithSamePriority; 

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

  public void Change (HexCell cell, int oldPriority) { … Enqueue(cell); } 

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

  Enqueue(cell); count -= 1; 

استخدام قائمة الانتظار


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

  HexCellPriorityQueue searchFrontier; … IEnumerator Search (HexCell fromCell, HexCell toCell) { if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } else { searchFrontier.Clear(); } … } 

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

  WaitForSeconds delay = new WaitForSeconds(1 / 60f); // List<HexCell> frontier = new List<HexCell>(); fromCell.Distance = 0; // frontier.Add(fromCell); searchFrontier.Enqueue(fromCell); while (searchFrontier.Count > 0) { yield return delay; HexCell current = searchFrontier.Dequeue(); // frontier.RemoveAt(0); … } 

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

  if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); // frontier.Add(neighbor); searchFrontier.Enqueue(neighbor); } else if (distance < neighbor.Distance) { int oldPriority = neighbor.SearchPriority; neighbor.Distance = distance; neighbor.PathFrom = current; searchFrontier.Change(neighbor, oldPriority); } 

بالإضافة إلى ذلك ، لم نعد بحاجة إلى فرز الحدود.

 // frontier.Sort( // (x, y) => x.SearchPriority.CompareTo(y.SearchPriority) // ); 


البحث باستخدام قائمة انتظار الأولوية

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



قائمة مرتبة وقائمة انتظار مع أولوية

حزمة الوحدة

الجزء 17: حركة محدودة


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

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


السفر من عدة تحركات

حركة خطوة بخطوة


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

السرعة


لتقديم دعم للحركة المحدودة ، نضيف HexGrid.FindPathإلى HexGrid.Searchالمعلمة الصحيحة وندخلها speed. يحدد نطاق الحركة لحركة واحدة.

  public void FindPath (HexCell fromCell, HexCell toCell, int speed) { StopAllCoroutines(); StartCoroutine(Search(fromCell, toCell, speed)); } IEnumerator Search (HexCell fromCell, HexCell toCell, int speed) { … } 

أنواع مختلفة من الوحدات في اللعبة تستخدم سرعات مختلفة. الفرسان سريع ، المشاة بطيئة ، وهكذا. ليس لدينا وحدات حتى الآن ، لذلك سنستخدم الآن سرعة ثابتة. لنأخذ قيمة 24. هذه قيمة كبيرة إلى حد ما ، ولا يمكن تقسيمها على 5 (التكلفة الافتراضية للانتقال). إضافة حجة ل FindPathفي HexMapEditor.HandleInputسرعة ثابتة.

  if (editMode) { EditCells(currentCell); } else if ( Input.GetKey(KeyCode.LeftShift) && searchToCell != currentCell ) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); if (searchToCell) { hexGrid.FindPath(searchFromCell, searchToCell, 24); } } else if (searchFromCell && searchFromCell != currentCell) { searchToCell = currentCell; hexGrid.FindPath(searchFromCell, searchToCell, 24); } 

يتحرك


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

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

  int currentTurn = current.Distance / speed; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … int distance = current.Distance; 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; } int turn = distance / speed; … } 

الحركة الضائعة


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

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

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

يعتمد اختيار الخيار على اللعبة. بشكل عام ، يعتبر الأسلوب الأول أكثر ملاءمة للألعاب التي يمكن للوحدات فيها التحرك بضع خطوات فقط لكل دور ، على سبيل المثال ، للألعاب في سلسلة Civilization. وهذا يضمن أن الوحدات يمكنها دائمًا تحريك خلية واحدة على الأقل لكل دور. إذا كانت الوحدات يمكنها تحريك العديد من الخلايا في كل دور ، كما هو الحال في Age of Wonders أو Battle for Wesnoth ، فإن الخيار الثاني هو الأفضل.

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

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

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

  int distance = current.Distance + moveCost; int turn = distance / speed; if (turn > currentTurn) { distance = turn * speed + moveCost; } 

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


أطول من المتوقع

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

عرض التحركات بدلاً من المسافات


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

أولاً ، تخلص من UpdateDistanceLabelمكالمته HexCell.

  public int Distance { get { return distance; } set { distance = value; // UpdateDistanceLabel(); } } … // void UpdateDistanceLabel () { // UnityEngine.UI.Text label = uiRect.GetComponent<Text>(); // label.text = distance == int.MaxValue ? "" : distance.ToString(); // } 

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

  public void SetLabel (string text) { UnityEngine.UI.Text label = uiRect.GetComponent<Text>(); label.text = text; } 

نستخدم هذه الطريقة الجديدة في HexGrid.Searchتنظيف الخلايا. لإخفاء الخلايا ، ما عليك سوى تعيينها null.

  for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; cells[i].SetLabel(null); cells[i].DisableHighlight(); } 

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

  if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; neighbor.SetLabel(turn.ToString()); neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); searchFrontier.Enqueue(neighbor); } else if (distance < neighbor.Distance) { int oldPriority = neighbor.SearchPriority; neighbor.Distance = distance; neighbor.SetLabel(turn.ToString()); neighbor.PathFrom = current; searchFrontier.Change(neighbor, oldPriority); } 


عدد الحركات المطلوبة للتحرك على طول مسار

حزمة الوحدة

المسارات الفورية


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

بدون كوروتين


لمرور بطيء من خلال الخوارزمية ، استخدمنا كوروتين. لسنا بحاجة للقيام بذلك بعد الآن ، لذلك سنتخلص من المكالمات StartCoroutineو StopAllCoroutinesc HexGrid. بدلاً من ذلك ، نستدعيها ببساطة Searchكطريقة منتظمة.

  public void Load (BinaryReader reader, int header) { // StopAllCoroutines(); … } public void FindPath (HexCell fromCell, HexCell toCell, int speed) { // StopAllCoroutines(); // StartCoroutine(Search(fromCell, toCell, speed)); Search(fromCell, toCell, speed); } 

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

  void Search (HexCell fromCell, HexCell toCell, int speed) { … // WaitForSeconds delay = new WaitForSeconds(1 / 60f); fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); while (searchFrontier.Count > 0) { // yield return delay; HexCell current = searchFrontier.Dequeue(); … } } 


نتائج فورية

تعريف وقت البحث


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

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

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

  public void FindPath (HexCell fromCell, HexCell toCell, int speed) { System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); sw.Start(); Search(fromCell, toCell, speed); sw.Stop(); Debug.Log(sw.ElapsedMilliseconds); } 

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


البحث في أسوأ الحالات

يمكن أن يكون الوقت المستغرق في البحث عنه مختلفًا ، لأن محرر الوحدة ليس العملية الوحيدة التي تعمل على جهازك. لذا اختبره عدة مرات لفهم متوسط ​​المدة. في حالتي ، يستغرق البحث حوالي 45 مللي ثانية. هذا ليس كثيرًا ويتوافق مع 22.22 مسارًا في الثانية ؛ تشير إلى ذلك بـ 22 نقطة (مسارات في الثانية). هذا يعني أن معدل إطارات اللعبة سينخفض ​​أيضًا بحد أقصى 22 إطارًا في الثانية في هذا الإطار عند حساب هذا المسار. وهذا دون مراعاة جميع الأعمال الأخرى ، على سبيل المثال ، عرض الإطار نفسه. أي نحصل على انخفاض كبير إلى حد ما في معدل الإطارات ، وسوف ينخفض ​​إلى 20 إطارًا في الثانية.

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

أين يمكنني رؤية سجل التصحيح للتجميع؟
Unity , . . , , Unity Log Files .

ابحث فقط عند الضرورة.


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

  if (editMode) { EditCells(currentCell); } else if ( Input.GetKey(KeyCode.LeftShift) && searchToCell != currentCell ) { if (searchFromCell != currentCell) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); if (searchToCell) { hexGrid.FindPath(searchFromCell, searchToCell, 24); } } } else if (searchFromCell && searchFromCell != currentCell) { if (searchToCell != currentCell) { searchToCell = currentCell; hexGrid.FindPath(searchFromCell, searchToCell, 24); } } 

إظهار التسميات للمسار فقط


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

  if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; // neighbor.SetLabel(turn.ToString()); neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); searchFrontier.Enqueue(neighbor); } else if (distance < neighbor.Distance) { int oldPriority = neighbor.SearchPriority; neighbor.Distance = distance; // neighbor.SetLabel(turn.ToString()); neighbor.PathFrom = current; searchFrontier.Change(neighbor, oldPriority); } 

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

  if (current == toCell) { current = current.PathFrom; while (current != fromCell) { int turn = current.Distance / speed; current.SetLabel(turn.ToString()); current.EnableHighlight(Color.white); current = current.PathFrom; } break; } 


عرض التسميات لخلايا المسار فقط

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

  fromCell.EnableHighlight(Color.blue); // toCell.EnableHighlight(Color.red); fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); while (searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); if (current == toCell) { // current = current.PathFrom; while (current != fromCell) { int turn = current.Distance / speed; current.SetLabel(turn.ToString()); current.EnableHighlight(Color.white); current = current.PathFrom; } toCell.EnableHighlight(Color.red); break; } … } 


معلومات التقدم هي الأهم بالنسبة لنقطة النهاية.

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

حزمة الوحدة

البحث الأذكى


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

مرحلة بحث الخلية


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

  public int SearchPhase { get; set; } 

على سبيل المثال ، يعني 0 أن الخلايا لم تصل بعد ، 1 - أن الخلية موجودة في الحد الآن ، و 2 - التي تمت إزالتها بالفعل من الحد.

ضرب الحدود


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

  int searchFrontierPhase; … void Search (HexCell fromCell, HexCell toCell, int speed) { searchFrontierPhase += 2; … } 

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

  fromCell.SearchPhase = searchFrontierPhase; fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); 

وفي كل مرة نضيف جارا إلى الحدود.

  if (neighbor.Distance == int.MaxValue) { neighbor.SearchPhase = searchFrontierPhase; neighbor.Distance = distance; neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); searchFrontier.Enqueue(neighbor); } 

فحص الحدود


حتى الآن ، للتحقق من أن الخلية لم تتم إضافتها بعد إلى الحدود ، استخدمنا مسافة مساوية لـ int.MaxValue. يمكننا الآن مقارنة مرحلة البحث عن الخلية بالحدود الحالية.

 // if (neighbor.Distance == int.MaxValue) { if (neighbor.SearchPhase < searchFrontierPhase) { neighbor.SearchPhase = searchFrontierPhase; neighbor.Distance = distance; neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); searchFrontier.Enqueue(neighbor); } 

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

  for (int i = 0; i < cells.Length; i++) { // cells[i].Distance = int.MaxValue; cells[i].SetLabel(null); cells[i].DisableHighlight(); } 

ترك الحدود


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

  while (searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.SearchPhase += 1; … } 

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

  for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if ( neighbor == null || neighbor.SearchPhase > searchFrontierPhase ) { continue; } … } 

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

يمكننا أيضًا حساب عدد المرات التي تمت فيها معالجة الخلية بواسطة الخوارزمية ، وزيادة العداد عند حساب المسافة إلى الخلية. في السابق ، كانت خوارزميتنا في أسوأ الحالات تحسب مسافات 28،239. في خوارزمية A * الجاهزة ، نحسب مسافاتها 14،120. انخفض المبلغ بنسبة 50٪. درجة تأثير هذه المؤشرات على الإنتاجية تعتمد على التكاليف في حساب تكلفة الانتقال. في حالتنا ، لا يوجد الكثير من العمل هنا ، لذا فإن التحسين في التجميع ليس كبيرًا جدًا ، ولكنه ملحوظ جدًا في المحرر.

حزمة الوحدة

تطهير الطريق


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

البحث فقط


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

  void Search (HexCell fromCell, HexCell toCell, int speed) { searchFrontierPhase += 2; if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } else { searchFrontier.Clear(); } // for (int i = 0; i < cells.Length; i++) { // cells[i].SetLabel(null); // cells[i].DisableHighlight(); // } // fromCell.EnableHighlight(Color.blue); fromCell.SearchPhase = searchFrontierPhase; fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); while (searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.SearchPhase += 1; if (current == toCell) { // while (current != fromCell) { // int turn = current.Distance / speed; // current.SetLabel(turn.ToString()); // current.EnableHighlight(Color.white); // current = current.PathFrom; // } // toCell.EnableHighlight(Color.red); // break; } … } } 

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

  bool Search (HexCell fromCell, HexCell toCell, int speed) { searchFrontierPhase += 2; if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } else { searchFrontier.Clear(); } fromCell.SearchPhase = searchFrontierPhase; fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); while (searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.SearchPhase += 1; if (current == toCell) { return true; } … } return false; } 

تذكر الطريق


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

  HexCell currentPathFrom, currentPathTo; bool currentPathExists; … public void FindPath (HexCell fromCell, HexCell toCell, int speed) { System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); sw.Start(); currentPathFrom = fromCell; currentPathTo = toCell; currentPathExists = Search(fromCell, toCell, speed); sw.Stop(); Debug.Log(sw.ElapsedMilliseconds); } 

اعرض المسار مرة أخرى


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

  void ShowPath (int speed) { if (currentPathExists) { HexCell current = currentPathTo; while (current != currentPathFrom) { int turn = current.Distance / speed; current.SetLabel(turn.ToString()); current.EnableHighlight(Color.white); current = current.PathFrom; } } currentPathFrom.EnableHighlight(Color.blue); currentPathTo.EnableHighlight(Color.red); } 

اتصل بهذه الطريقة FindPathبعد البحث.

  currentPathExists = Search(fromCell, toCell, speed); ShowPath(speed); 

اكتساح


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

  void ClearPath () { if (currentPathExists) { HexCell current = currentPathTo; while (current != currentPathFrom) { current.SetLabel(null); current.DisableHighlight(); current = current.PathFrom; } current.DisableHighlight(); currentPathExists = false; } currentPathFrom = currentPathTo = null; } 

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

  sw.Start(); ClearPath(); currentPathFrom = fromCell; currentPathTo = toCell; currentPathExists = Search(fromCell, toCell, speed); if (currentPathExists) { ShowPath(speed); } sw.Stop(); 

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

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

وأيضا قبل تحميل بطاقة أخرى.

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

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

الآن بعد أن قمنا بإجراء بحث سريع عن المسارات ، يمكننا إزالة رمز التصحيح المؤقت.

  public void FindPath (HexCell fromCell, HexCell toCell, int speed) { // System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); // sw.Start(); ClearPath(); currentPathFrom = fromCell; currentPathTo = toCell; currentPathExists = Search(fromCell, toCell, speed); ShowPath(speed); // sw.Stop(); // Debug.Log(sw.ElapsedMilliseconds); } 

حزمة الوحدة

الجزء 18: الوحدات


  • نضع الفرق على الخريطة.
  • حفظ وتحميل فرق.
  • نجد طرق للقوات.
  • ننتقل الوحدات.

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


وصلت التعزيزات

تشكيل فرق


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

فرقة جاهزة


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

 using UnityEngine; public class HexUnit : MonoBehaviour { } 

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


فرقة جاهزة.

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



عنصر المكعب الفرعي قم

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

إنشاء مثيلات فرقة


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

  public HexUnit unitPrefab; 


توصيل المباني الجاهزة

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

  HexCell GetCellUnderCursor () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { return hexGrid.GetCell(hit.point); } return null; } 

الآن يمكننا استخدام هذه الطريقة في HandleInputتبسيطها.

  void HandleInput () { // Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); // RaycastHit hit; // if (Physics.Raycast(inputRay, out hit)) { // HexCell currentCell = hexGrid.GetCell(hit.point); HexCell currentCell = GetCellUnderCursor(); if (currentCell) { … } else { previousCell = null; } } 

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

  void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell) { Instantiate(unitPrefab); } } 

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

  void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell) { HexUnit unit = Instantiate(unitPrefab); unit.transform.SetParent(hexGrid.transform, false); } } 

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

  void Update () { // if ( // Input.GetMouseButton(0) && // !EventSystem.current.IsPointerOverGameObject() // ) { // HandleInput(); // } // else { // previousCell = null; // } if (!EventSystem.current.IsPointerOverGameObject()) { if (Input.GetMouseButton(0)) { HandleInput(); return; } if (Input.GetKeyDown(KeyCode.U)) { CreateUnit(); return; } } previousCell = null; } 


تم إنشاء مثيل للفرقة

تنسيب القوات


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

  public HexCell Location { get { return location; } set { location = value; transform.localPosition = value.Position; } } HexCell location; 

الآن HexMapEditor.CreateUnitيجب أن أقوم بتعيين موضع خلية الفريق تحت المؤشر. ثم ستكون الوحدات حيث يجب.

  void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell) { HexUnit unit = Instantiate(unitPrefab); unit.transform.SetParent(hexGrid.transform, false); unit.Location = cell; } } 


فرق على الخريطة

اتجاه الوحدة


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

  public float Orientation { get { return orientation; } set { orientation = value; transform.localRotation = Quaternion.Euler(0f, value, 0f); } } float orientation; 

في HexMapEditor.CreateUnitتعيين دوران عشوائي من 0 إلى 360 درجة.

  void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell) { HexUnit unit = Instantiate(unitPrefab); unit.transform.SetParent(hexGrid.transform, false); unit.Location = cell; unit.Orientation = Random.Range(0f, 360f); } } 


اتجاهات الوحدة المختلفة

فرقة واحدة لكل خلية


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


وحدات متراكبة

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

  public HexUnit Unit { get; set; } 

نستخدم هذه الخاصية HexUnit.Locationلإعلام الخلية إذا كانت الوحدة موجودة عليها.

  public HexCell Location { get { return location; } set { location = value; value.Unit = this; transform.localPosition = value.Position; } } 

الآن HexMapEditor.CreateUnitيمكن التحقق مما إذا كانت الخلية الحالية مجانية.

  void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell && !cell.Unit) { HexUnit unit = Instantiate(unitPrefab); unit.Location = cell; unit.Orientation = Random.Range(0f, 360f); } } 

تحرير الخلايا المشغولة


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


الفرق المعلقة والغارقة

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

  public void ValidateLocation () { transform.localPosition = location.Position; } 

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

  void Refresh () { if (chunk) { chunk.Refresh(); … if (Unit) { Unit.ValidateLocation(); } } } void RefreshSelfOnly () { chunk.Refresh(); if (Unit) { Unit.ValidateLocation(); } } 

إزالة الفرق


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

  void DestroyUnit () { HexCell cell = GetCellUnderCursor(); if (cell && cell.Unit) { Destroy(cell.Unit.gameObject); } } 

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

دعونا Updateنستخدم مزيجًا من Shift + U الأيسر لتدمير الفرقة .

  if (Input.GetKeyDown(KeyCode.U)) { if (Input.GetKey(KeyCode.LeftShift)) { DestroyUnit(); } else { CreateUnit(); } return; } 

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

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

سنستدعي هذه الطريقة HexMapEditor.DestroyUnit، ولن ندمر الفرقة مباشرة.

  void DestroyUnit () { HexCell cell = GetCellUnderCursor(); if (cell && cell.Unit) { // Destroy(cell.Unit.gameObject); cell.Unit.Die(); } } 

حزمة الوحدة

إنقاذ وتحميل فرق


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

تتبع الوحدة


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

  List<HexUnit> units = new List<HexUnit>(); 

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

  void ClearUnits () { for (int i = 0; i < units.Count; i++) { units[i].Die(); } units.Clear(); } 

نحن نسمي هذا الأسلوب CreateMapو Load. لنقم بذلك بعد تنظيف الطريق.

  public bool CreateMap (int x, int z) { … ClearPath(); ClearUnits(); … } … public void Load (BinaryReader reader, int header) { ClearPath(); ClearUnits(); … } 

إضافة فرق إلى الشبكة


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

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

الآن HexMapEditor.CreatUnitسيكون كافيًا الاتصال AddUnitبمثيل جديد للانفصال وموقعه واتجاهه العشوائي.

  void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell && !cell.Unit) { // HexUnit unit = Instantiate(unitPrefab); // unit.transform.SetParent(hexGrid.transform, false); // unit.Location = cell; // unit.Orientation = Random.Range(0f, 360f); hexGrid.AddUnit( Instantiate(unitPrefab), cell, Random.Range(0f, 360f) ); } } 

إزالة الفرق من الشبكة


أضف طريقة لإزالة الفريق و ج HexGrid. فقط قم بإخراج الفريق من القائمة واطلب منه أن يموت.

  public void RemoveUnit (HexUnit unit) { units.Remove(unit); unit.Die(); } 

نسمي هذه الطريقة HexMapEditor.DestroyUnit، بدلاً من تدمير الفرقة مباشرة.

  void DestroyUnit () { HexCell cell = GetCellUnderCursor(); if (cell && cell.Unit) { // cell.Unit.Die(); hexGrid.RemoveUnit(cell.Unit); } } 

وحدات التوفير


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

 using UnityEngine; using System.IO; [System.Serializable] public struct HexCoordinates { … public void Save (BinaryWriter writer) { writer.Write(x); writer.Write(z); } } 

طريقة Saveل HexUnitيمكن الآن تسجيل الإحداثيات والتوجه للوحدة. هذه هي جميع بيانات الوحدات التي لدينا في الوقت الحالي.

 using UnityEngine; using System.IO; public class HexUnit : MonoBehaviour { … public void Save (BinaryWriter writer) { location.coordinates.Save(writer); writer.Write(orientation); } } 

نظرًا لأنه HexGridيتتبع الوحدات ، فإن طريقته Saveستسجل بيانات الوحدات. أولاً ، اكتب العدد الإجمالي للوحدات ، ثم انتقل حولها جميعًا في حلقة.

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

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

  void Save (string path) { using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(2); hexGrid.Save(writer); } } 

تحميل الفرق


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

  public static HexCoordinates Load (BinaryReader reader) { HexCoordinates c; cx = reader.ReadInt32(); cz = reader.ReadInt32(); return c; } 

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

لماذا أعزب؟
float , . , double , . Unity .

  public static void Load (BinaryReader reader) { HexCoordinates coordinates = HexCoordinates.Load(reader); float orientation = reader.ReadSingle(); } 

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

  public static HexUnit unitPrefab; 

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

  public HexUnit unitPrefab; … void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; CreateMap(cellCountX, cellCountZ); } … void OnEnable () { if (!HexMetrics.noiseSource) { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; } } 


نجتاز الوحدات الجاهزة.

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

 // public HexUnit unitPrefab; … void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell && !cell.Unit) { hexGrid.AddUnit( Instantiate(HexUnit.unitPrefab), cell, Random.Range(0f, 360f) ); } } 

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

  public static void Load (BinaryReader reader, HexGrid grid) { HexCoordinates coordinates = HexCoordinates.Load(reader); float orientation = reader.ReadSingle(); grid.AddUnit( Instantiate(unitPrefab), grid.GetCell(coordinates), orientation ); } 

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

  public void Load (BinaryReader reader, int header) { … int unitCount = reader.ReadInt32(); for (int i = 0; i < unitCount; i++) { HexUnit.Load(reader, this); } } 

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

  if (header >= 2) { int unitCount = reader.ReadInt32(); for (int i = 0; i < unitCount; i++) { HexUnit.Load(reader, this); } } 

يمكننا الآن تحميل ملفات الإصدار 2 بشكل صحيح ، لذلك SaveLoadMenu.Loadزيادة عدد النسخة المدعومة إلى 2.

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

حزمة الوحدة

حركة القوات


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

تنظيف محرر الخرائط


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

 // HexCell previousCell, searchFromCell, searchToCell; HexCell previousCell; … void HandleInput () { HexCell currentCell = GetCellUnderCursor(); if (currentCell) { if (previousCell && previousCell != currentCell) { ValidateDrag(currentCell); } else { isDrag = false; } if (editMode) { EditCells(currentCell); } // else if ( // Input.GetKey(KeyCode.LeftShift) && searchToCell != currentCell // ) { // if (searchFromCell != currentCell) { // if (searchFromCell) { // searchFromCell.DisableHighlight(); // } // searchFromCell = currentCell; // searchFromCell.EnableHighlight(Color.blue); // if (searchToCell) { // hexGrid.FindPath(searchFromCell, searchToCell, 24); // } // } // } // else if (searchFromCell && searchFromCell != currentCell) { // if (searchToCell != currentCell) { // searchToCell = currentCell; // hexGrid.FindPath(searchFromCell, searchToCell, 24); // } // } previousCell = currentCell; } else { previousCell = null; } } 

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

 // bool editMode; … public void SetEditMode (bool toggle) { // editMode = toggle; // hexGrid.ShowUI(!toggle); enabled = toggle; } … void HandleInput () { HexCell currentCell = GetCellUnderCursor(); if (currentCell) { if (previousCell && previousCell != currentCell) { ValidateDrag(currentCell); } else { isDrag = false; } // if (editMode) { EditCells(currentCell); // } previousCell = currentCell; } else { previousCell = null; } } 

نظرًا لأننا افتراضيًا لسنا في وضع تحرير الخريطة ، فإننا في Awake سنقوم بتعطيل المحرر.

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

يعد استخدام raycast للبحث عن الخلية الحالية تحت المؤشر أمرًا ضروريًا عند تحرير الخريطة وإدارة الوحدات. ربما في المستقبل سيكون مفيدًا لنا لشيء آخر. دعنا ننقل منطق البث الإشعاعي من HexGridطريقة جديدة GetCellمع معلمة شعاع.

  public HexCell GetCell (Ray ray) { RaycastHit hit; if (Physics.Raycast(ray, out hit)) { return GetCell(hit.point); } return null; } 

HexMapEditor.GetCellUniderCursor يمكن فقط استدعاء هذه الطريقة مع شعاع المؤشر.

  HexCell GetCellUnderCursor () { return hexGrid.GetCell(Camera.main.ScreenPointToRay(Input.mousePosition)); } 

لعبة واجهة المستخدم


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

 using UnityEngine; using UnityEngine.EventSystems; public class HexGameUI : MonoBehaviour { public HexGrid grid; } 

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



كائن واجهة مستخدم اللعبة

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

  public void SetEditMode (bool toggle) { enabled = !toggle; grid.ShowUI(!toggle); } 

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


العديد من طرق الحدث.

تتبع الخلية الحالية


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

  HexCell currentCell; 

قم بإنشاء طريقة UpdateCurrentCellتستخدم HexGrid.GetCellشعاع المؤشر لتحديث هذا الحقل.

  void UpdateCurrentCell () { currentCell = grid.GetCell(Camera.main.ScreenPointToRay(Input.mousePosition)); } 

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

  bool UpdateCurrentCell () { HexCell cell = grid.GetCell(Camera.main.ScreenPointToRay(Input.mousePosition)); if (cell != currentCell) { currentCell = cell; return true; } return false; } 

اختيار الوحدة


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

  HexUnit selectedUnit; 

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

  void DoSelection () { UpdateCurrentCell(); if (currentCell) { selectedUnit = currentCell.Unit; } } 

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

  void Update () { if (!EventSystem.current.IsPointerOverGameObject()) { if (Input.GetMouseButtonDown(0)) { DoSelection(); } } } 

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

بحث فرقة


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

  void Update () { if (!EventSystem.current.IsPointerOverGameObject()) { if (Input.GetMouseButtonDown(0)) { DoSelection(); } else if (selectedUnit) { DoPathfinding(); } } } 

DoPathfindingمجرد تحديث الخلية الحالية والمكالمات HexGrid.FindPathإذا كان هناك نقطة نهاية. نستخدم مرة أخرى سرعة ثابتة تبلغ 24.

  void DoPathfinding () { UpdateCurrentCell(); grid.FindPath(selectedUnit.Location, currentCell, 24); } 

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

  void DoPathfinding () { if (UpdateCurrentCell()) { grid.FindPath(selectedUnit.Location, currentCell, 24); } } 


البحث عن مسار فرقة

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

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

بالطبع ، هذا يتطلب أن HexGrid.ClearPathيكون الأمر شائعًا ، لذلك نجري مثل هذا التغيير.

  public void ClearPath () { … } 

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

  void DoSelection () { grid.ClearPath(); UpdateCurrentCell(); if (currentCell) { selectedUnit = currentCell.Unit; } } 

أخيرًا ، سنقوم بمسح المسار عند تغيير وضع التحرير.

  public void SetEditMode (bool toggle) { enabled = !toggle; grid.ShowUI(!toggle); grid.ClearPath(); } 

ابحث عن نقاط النهاية الصالحة فقط


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

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

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

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

نستخدم هذه الطريقة HexGameUI.DoPathfindingلتجاهل نقاط النهاية غير الصالحة.

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

الانتقال إلى نقطة النهاية


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

  public bool HasPath { get { return currentPathExists; } } 

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

  void DoMove () { if (grid.HasPath) { selectedUnit.Location = currentCell; grid.ClearPath(); } } 

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

  void Update () { if (!EventSystem.current.IsPointerOverGameObject()) { if (Input.GetMouseButtonDown(0)) { DoSelection(); } else if (selectedUnit) { if (Input.GetMouseButtonDown(1)) { DoMove(); } else { DoPathfinding(); } } } } 

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

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

تجنب الفرق


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


يتم تجاهل الوحدات الموجودة على الطريق ،

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

  if ( neighbor == null || neighbor.SearchPhase > searchFrontierPhase ) { continue; } if (neighbor.IsUnderwater || neighbor.Unit) { continue; } 


مفارز تجنب

unitypackage

الجزء 19: الحركة المتحركة


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

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


فرق على الطريق

الحركة على طول الطريق


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

خطأ في الأدوار


بسبب الإشراف ، نحسب بشكل غير صحيح المسار الذي سيتم الوصول إلى الخلية فيه. الآن نحدد المسار بتقسيم المسافة الإجمالية على سرعة الفريقt = d / s ونبذ الباقي. يحدث الخطأ عند الدخول إلى الخلية تحتاج إلى إنفاق جميع نقاط الحركة المتبقية لكل حركة. على سبيل المثال ، عندما تكلف كل خطوة 1 ، والسرعة 3 ، يمكننا تحريك ثلاث خلايا في كل دور. ومع ذلك ، مع الحسابات الحالية ، لا يمكننا اتخاذ سوى خطوتين في الخطوة الأولى ، لأن الخطوة الثالثة

ر = د / ق = 3 / 3 = 1 .


التكاليف المجمعة للتحرك بحركات محددة بشكل غير صحيح ، السرعة 3 من

أجل الحساب الصحيح للحركات ، نحتاج إلى تحريك الحدود خطوة واحدة من الخلية الأولية. يمكننا القيام بذلك عن طريق تقليل المسافة بمقدار 1 قبل حساب الحركة. ثم ستكون الخطوة للخطوة الثالثةر = 2 / 3 = 0


التحركات الصحيحة

يمكننا القيام بذلك عن طريق تغيير صيغة الحساب إلىر = ( د - 1 ) / ثانية .سنقوم بهذا التغيير إلى HexGrid.Search.

  bool Search (HexCell fromCell, HexCell toCell, int speed) { … while (searchFrontier.Count > 0) { … int currentTurn = (current.Distance - 1) / speed; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … int distance = current.Distance + moveCost; int turn = (distance - 1) / speed; if (turn > currentTurn) { distance = turn * speed + moveCost; } … } } return false; } 

نغير أيضًا علامات التحركات.

  void ShowPath (int speed) { if (currentPathExists) { HexCell current = currentPathTo; while (current != currentPathFrom) { int turn = (current.Distance - 1) / speed; … } } … } 

لاحظ أنه باستخدام هذا النهج ، يكون مسار الخلية الأولي −1. هذا أمر طبيعي ، لأننا لا نعرضه ، وتظل خوارزمية البحث عاملة.

الطريق


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

  public List<HexCell> GetPath () { if (!currentPathExists) { return null; } List<HexCell> path = ListPool<HexCell>.Get(); return path; } 

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

  List<HexCell> path = ListPool<HexCell>.Get(); for (HexCell c = currentPathTo; c != currentPathFrom; c = c.PathFrom) { path.Add(c); } return path; 

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

  for (HexCell c = currentPathTo; c != currentPathFrom; c = c.PathFrom) { path.Add(c); } path.Add(currentPathFrom); return path; 

الآن لدينا المسار بالترتيب العكسي. يمكننا العمل معه ، لكنه لن يكون بديهيًا للغاية. دعونا نقلب القائمة بحيث تنتقل من البداية إلى النهاية.

  path.Add(currentPathFrom); path.Reverse(); return path; 

طلب الحركة


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

 using UnityEngine; using System.Collections.Generic; using System.IO; public class HexUnit : MonoBehaviour { … public void Travel (List<HexCell> path) { Location = path[path.Count - 1]; } … } 

لطلب الحركة ، نقوم بتغييرها HexGameUI.DoMoveبحيث تستدعي طريقة جديدة مع المسار الحالي ، ولا تقوم فقط بتعيين موقع الوحدة.

  void DoMove () { if (grid.HasPath) { // selectedUnit.Location = currentCell; selectedUnit.Travel(grid.GetPath()); grid.ClearPath(); } } 

تصور المسار


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

  List<HexCell> pathToTravel; … public void Travel (List<HexCell> path) { Location = path[path.Count - 1]; pathToTravel = path; } 

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

  void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } } 

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

  void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } for (int i = 0; i < pathToTravel.Count; i++) { Gizmos.DrawSphere(pathToTravel[i].Position, 2f); } } 

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


تعرض Gizmos آخر المسارات التي تم قطعها. من

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

  for (int i = 1; i < pathToTravel.Count; i++) { Vector3 a = pathToTravel[i - 1].Position; Vector3 b = pathToTravel[i].Position; for (float t = 0f; t < 1f; t += 0.1f) { Gizmos.DrawSphere(Vector3.Lerp(a, b, t), 2f); } } 


طرق أكثر وضوحا

انسل على طول الطريق


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

 using UnityEngine; using System.Collections; using System.Collections.Generic; using System.IO; public class HexUnit : MonoBehaviour { … IEnumerator TravelPath () { for (int i = 1; i < pathToTravel.Count; i++) { Vector3 a = pathToTravel[i - 1].Position; Vector3 b = pathToTravel[i].Position; for (float t = 0f; t < 1f; t += Time.deltaTime) { transform.localPosition = Vector3.Lerp(a, b, t); yield return null; } } } … } 

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

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

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

  const float travelSpeed = 4f; … IEnumerator TravelPath () { for (int i = 1; i < pathToTravel.Count; i++) { Vector3 a = pathToTravel[i - 1].Position; Vector3 b = pathToTravel[i].Position; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Vector3.Lerp(a, b, t); yield return null; } } } 

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


الوحدات المتحركة.

ماذا عن الفرق في الارتفاع؟
, . , . , . , . , Endless Legend, , . , .

الموقف بعد التجميع


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

  void OnEnable () { if (location) { transform.localPosition = location.Position; } } 

حزمة الوحدة

حركة سلسة


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

الانتقال من الضلع إلى الضلع


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


ثلاث طرق للانتقال من الحافة إلى الحافة

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

  void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } Vector3 a, b = pathToTravel[0].Position; for (int i = 1; i < pathToTravel.Count; i++) { // Vector3 a = pathToTravel[i - 1].Position; // Vector3 b = pathToTravel[i].Position; a = b; b = (pathToTravel[i - 1].Position + pathToTravel[i].Position) * 0.5f; for (float t = 0f; t < 1f; t += 0.1f) { Gizmos.DrawSphere(Vector3.Lerp(a, b, t), 2f); } } } 

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

  void OnDrawGizmos () { … for (int i = 1; i < pathToTravel.Count; i++) { … } a = b; b = pathToTravel[pathToTravel.Count - 1].Position; for (float t = 0f; t < 1f; t += 0.1f) { Gizmos.DrawSphere(Vector3.Lerp(a, b, t), 2f); } } 




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

  IEnumerator TravelPath () { Vector3 a, b = pathToTravel[0].Position; for (int i = 1; i < pathToTravel.Count; i++) { // Vector3 a = pathToTravel[i - 1].Position; // Vector3 b = pathToTravel[i].Position; a = b; b = (pathToTravel[i - 1].Position + pathToTravel[i].Position) * 0.5f; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Vector3.Lerp(a, b, t); yield return null; } } a = b; b = pathToTravel[pathToTravel.Count - 1].Position; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Vector3.Lerp(a, b, t); yield return null; } } 


التحرك بسرعة متغيرة

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

تتبع المنحنيات


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


المنحنيات من الحافة إلى الحافة

قم بإنشاء فئة مساعدة Bezierبطريقة للحصول على نقاط على منحنى Bezier التربيعي. كما هو موضح في البرنامج التعليمي Curves and Splines ، يتم استخدام الصيغة لهذا الغرض( 1 - ر ) 2 أ + 2 ( 1 - ر ) ر ب + ر 2 ج أين أ ، ب و C هي نقاط التحكم ، و t هي interpolator.

 using UnityEngine; public static class Bezier { public static Vector3 GetPoint (Vector3 a, Vector3 b, Vector3 c, float t) { float r = 1f - t; return r * r * a + 2f * r * t * b + t * t * c; } } 

ألا يجب أن يقتصر GetPoint على 0-1؟
0-1, . . , GetPointClamped , t . , GetPointUnclamped .

لإظهار مسار المنحنى OnDrawGizmos، نحتاج إلى تتبع ليس نقطتين ، ولكن ثلاث نقاط. نقطة إضافية هي مركز الخلية التي نعمل معها في التكرار الحالي ، والذي يحتوي على فهرس i - 1، لأن الدورة تبدأ بـ 1. بعد استلام جميع النقاط ، يمكننا استبدالها Vector3.Lerpبـ Bezier.GetPoint.

في خلايا البداية والنهاية ، بدلاً من النهاية ونقاط المنتصف ، يمكننا ببساطة استخدام مركز الخلية.

  void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } Vector3 a, b, c = pathToTravel[0].Position; for (int i = 1; i < pathToTravel.Count; i++) { a = c; b = pathToTravel[i - 1].Position; c = (b + pathToTravel[i].Position) * 0.5f; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { Gizmos.DrawSphere(Bezier.GetPoint(a, b, c, t), 2f); } } a = c; b = pathToTravel[pathToTravel.Count - 1].Position; c = b; for (float t = 0f; t < 1f; t += 0.1f) { Gizmos.DrawSphere(Bezier.GetPoint(a, b, c, t), 2f); } } 


المسارات التي تم إنشاؤها باستخدام منحنيات Bezier

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

  IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; for (int i = 1; i < pathToTravel.Count; i++) { a = c; b = pathToTravel[i - 1].Position; c = (b + pathToTravel[i].Position) * 0.5f; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); yield return null; } } a = c; b = pathToTravel[pathToTravel.Count - 1].Position; c = b; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); yield return null; } } 


نتحرك على طول المنحنيات ، وأصبحت

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

تتبع الوقت


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

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

  IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; float t = 0f; for (int i = 1; i < pathToTravel.Count; i++) { a = c; b = pathToTravel[i - 1].Position; c = (b + pathToTravel[i].Position) * 0.5f; for (; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); yield return null; } t -= 1f; } a = c; b = pathToTravel[pathToTravel.Count - 1].Position; c = b; for (; t < 1f; t += Time.deltaTime * traveSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); yield return null; } } 

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

  float t = Time.deltaTime * travelSpeed; 

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

  IEnumerator TravelPath () { … transform.localPosition = location.Position; } 

حزمة الوحدة

الرسوم المتحركة التوجيهية


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

نتطلع


كما في البرنامج التعليمي Curves and Splines ، يمكننا استخدام مشتق المنحنى لتحديد اتجاه الوحدة. صيغة مشتق منحنى بيزي التربيعي:2 ( ( 1 - t ) ( B - A ) + t ( C - B ) ) .أضف إلى Bezierالطريقة لحسابها.

  public static Vector3 GetDerivative ( Vector3 a, Vector3 b, Vector3 c, float t ) { return 2f * ((1f - t) * (b - a) + t * (c - b)); } 

يقع ناقل المشتق على خط مستقيم واحد مع اتجاه الحركة. يمكننا استخدام الطريقة Quaternion.LookRotationلتحويلها إلى فرقة. سنقوم بتنفيذها في كل خطوة HexUnit.TravelPath.

  transform.localPosition = Bezier.GetPoint(a, b, c, t); Vector3 d = Bezier.GetDerivative(a, b, c, t); transform.localRotation = Quaternion.LookRotation(d); yield return null; … transform.localPosition = Bezier.GetPoint(a, b, c, t); Vector3 d = Bezier.GetDerivative(a, b, c, t); transform.localRotation = Quaternion.LookRotation(d); yield return null; 

ألا يوجد خطأ في بداية المسار؟
, . A و B , . , t=0 , , Quaternion.LookRotation . , , t=0 . . , t>0 .
, t<1 .

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

  transform.localPosition = location.Position; orientation = transform.localRotation.eulerAngles.y; 

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

  Vector3 d = Bezier.GetDerivative(a, b, c, t); dy = 0f; transform.localRotation = Quaternion.LookRotation(d); … Vector3 d = Bezier.GetDerivative(a, b, c, t); dy = 0f; transform.localRotation = Quaternion.LookRotation(d); 


نتطلع إلى الأمام أثناء التحرك

نحن ننظر إلى النقطة


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

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

  void LookAt (Vector3 point) { point.y = transform.localPosition.y; transform.LookAt(point); orientation = transform.localRotation.eulerAngles.y; } 

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

  const float rotationSpeed = 180f; … IEnumerator LookAt (Vector3 point) { … } 

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

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

  IEnumerator LookAt (Vector3 point) { point.y = transform.localPosition.y; Quaternion fromRotation = transform.localRotation; Quaternion toRotation = Quaternion.LookRotation(point - transform.localPosition); for (float t = Time.deltaTime; t < 1f; t += Time.deltaTime) { transform.localRotation = Quaternion.Slerp(fromRotation, toRotation, t); yield return null; } transform.LookAt(point); orientation = transform.localRotation.eulerAngles.y; } 

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

  Quaternion fromRotation = transform.localRotation; Quaternion toRotation = Quaternion.LookRotation(point - transform.localPosition); float angle = Quaternion.Angle(fromRotation, toRotation); float speed = rotationSpeed / angle; for ( float t = Time.deltaTime * speed; t < 1f; t += Time.deltaTime * speed ) { transform.localRotation = Quaternion.Slerp(fromRotation, toRotation, t); yield return null; } 

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

  float angle = Quaternion.Angle(fromRotation, toRotation); if (angle > 0f) { float speed = rotationSpeed / angle; for ( … ) { … } } 

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

  IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; yield return LookAt(pathToTravel[1].Position); float t = Time.deltaTime * travelSpeed; … } 

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

  Vector3 a, b, c = pathToTravel[0].Position; transform.localPosition = c; yield return LookAt(pathToTravel[1].Position); 


استدر قبل التحرك

اكتساح


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

 // void OnDrawGizmos () { // … // } 

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

  IEnumerator TravelPath () { … ListPool<HexCell>.Add(pathToTravel); pathToTravel = null; } 

ماذا عن الرسوم المتحركة الحقيقية للفرق؟
, . 3D- . . , . Mecanim, TravelPath .

حزمة الوحدة

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


All Articles