المجال
- إنشاء حقل البلاط.
- ابحث عن مسارات باستخدام البحث المتسع أولاً.
- تطبيق دعم البلاط الفارغ ونهاية ، وكذلك بلاط الجدران.
- تحرير المحتوى في وضع اللعبة.
- عرض اختياري لحقول الشبكة والمسارات.
هذا هو الجزء الأول من سلسلة من الدروس حول إنشاء لعبة بسيطة
للدفاع عن البرج . في هذا الجزء ، سننظر في إنشاء ملعب ، وإيجاد مسار ، ووضع البلاط والجدران النهائية.
تم إنشاء البرنامج التعليمي في الوحدة 2018.3.0f2.
حقل جاهز للاستخدام في لعبة بلاط برج الدفاع النوع.لعبة برج الدفاع
برج الدفاع هو نوع هدف اللاعب فيه هو تدمير حشود الأعداء حتى يصلوا إلى نقطتهم النهائية. يحقق اللاعب هدفه من خلال بناء الأبراج التي تهاجم الأعداء. هذا النوع لديه الكثير من الاختلافات. سنقوم بإنشاء لعبة مع حقل البلاط. سينتقل الأعداء عبر الحقل نحو نقطة النهاية ، وسيخلق اللاعب عقبات أمامهم.
سأفترض أنك درست بالفعل سلسلة من الدروس حول
إدارة الأشياء .
المجال
تعد ساحة اللعب الجزء الأكثر أهمية في اللعبة ، لذلك سننشئها أولاً. سيكون هذا كائنًا لعبة مع
GameBoard
الخاص به ، والذي يمكن تهيئته عن طريق تعيين الحجم في بعدين ، والذي يمكننا من خلاله استخدام قيمة
Vector2Int
. يجب أن يعمل الحقل بأي حجم ، لكننا سنختار الحجم في مكان آخر ، لذلك
Initialize
طريقة
Initialize
مشتركة لهذا الغرض.
بالإضافة إلى ذلك ، نحن نتصور الحقل بواسطة رباعي الزوايا ، والذي يدل على الأرض. لن نجعل كائن الحقل نفسه رباعي الأطراف ، لكننا سنضيف كائن رباعي تابع إليه. عند التهيئة ، سنجعل مقياس XY للأرض مساوياً لحجم الحقل. وهذا يعني ، سيكون لكل البلاط حجم وحدة واحدة مربعة للقياس للمحرك.
using UnityEngine; public class GameBoard : MonoBehaviour { [SerializeField] Transform ground = default; Vector2Int size; public void Initialize (Vector2Int size) { this.size = size; ground.localScale = new Vector3(size.x, size.y, 1f); } }
لماذا وضع صراحة على القيمة الافتراضية؟الفكرة هي أن كل شيء قابل للتخصيص من خلال محرر Unity يمكن الوصول إليه من خلال الحقول المخفية المتسلسلة. من الضروري أن يتم تغيير هذه الحقول فقط في المفتش. لسوء الحظ ، سيعرض محرر Unity باستمرار تحذيرًا مترجمًا بعدم تعيين القيمة أبدًا. يمكننا كبح هذا التحذير من خلال تحديد القيمة الافتراضية للحقل بشكل صريح. يمكنك أيضًا تعيين قيمة null
، لكنني قمت بذلك لإظهار أننا نستخدم القيمة الافتراضية ببساطة ، وهي ليست إشارة حقيقية إلى الأرض ، لذلك نستخدم القيمة default
.
قم بإنشاء كائن حقل في مشهد جديد وإضافة رباعي تابع بمواد تشبه الأرض. نظرًا لأننا نصمم لعبة نموذج أولي بسيطة ، فستكون مادة خضراء موحدة كافية. تدوير رباعية 90 درجة على طول المحور X بحيث تقع على متن الطائرة XZ.
ميدان اللعب.لماذا لا تضع اللعبة على الطائرة XY؟على الرغم من أن اللعبة ستتم في مساحة ثنائية الأبعاد ، إلا أننا سنعرضها ثلاثية الأبعاد ، مع أعداء ثلاثي الأبعاد وكاميرا يمكن تحريكها بالنسبة إلى نقطة معينة. تعد طائرة XZ أكثر ملاءمة لهذا وتتوافق مع اتجاه skybox القياسي المستخدم للإضاءة المحيطة.
اللعبة
بعد ذلك ، قم بإنشاء مكون
Game
الذي سيكون مسؤولاً عن اللعبة بأكملها. في هذه المرحلة ، سيعني هذا أنه يتم تهيئة الحقل. نحن فقط نجعل الحجم قابل للتخصيص من خلال المفتش ونجبر المكون على تهيئة الحقل عندما يستيقظ. دعنا نستخدم الحجم الافتراضي 11 × 11.
using UnityEngine; public class Game : MonoBehaviour { [SerializeField] Vector2Int boardSize = new Vector2Int(11, 11); [SerializeField] GameBoard board = default; void Awake () { board.Initialize(boardSize); } }
يمكن أن تكون أحجام الحقول موجبة فقط وليس من المنطقي إنشاء حقل ببلاط واحد. لذلك دعونا نحدد الحد الأدنى إلى 2 × 2. يمكن القيام بذلك عن طريق إضافة أسلوب
OnValidate
، والحد من القيم الدنيا بالقوة.
void OnValidate () { if (boardSize.x < 2) { boardSize.x = 2; } if (boardSize.y < 2) { boardSize.y = 2; } }
عندما يسمى Onvalidate؟إذا كان موجودًا ، فإن محرر الوحدة يدعوها للمكونات بعد تغييرها. بما في ذلك عند إضافتها إلى كائن اللعبة وبعد تحميل المشهد وبعد إعادة التحويل وبعد التغيير في المحرر وبعد الإلغاء / إعادة المحاولة وبعد إعادة ضبط المكون.
OnValidate
هو المكان الوحيد في الكود حيث يمكنك تعيين قيم لحقول تكوين المكون.
كائن اللعبة.الآن ، عند بدء تشغيل وضع اللعبة ، سوف نتلقى حقلًا بالحجم الصحيح. أثناء اللعبة ، ضع الكاميرا بحيث تكون اللوحة بأكملها مرئية ، وانسخ مكون التحويل الخاص بها ، واخرج من وضع التشغيل والصق قيم المكون. في حالة وجود حقل 11 × 11 في الأصل ، للحصول على منظر مناسب من الأعلى ، يمكنك وضع الكاميرا في موضعها (0.10.0) وتدويرها 90 درجة على طول محور X. سنترك الكاميرا في هذا الموضع الثابت ، ولكن من الممكن تغييره في المستقبل.
الكاميرا فوق الميدان.كيفية نسخ ولصق قيم المكونات؟من خلال القائمة المنسدلة التي تظهر عند النقر فوق الزر مع الترس في الزاوية اليمنى العليا للمكون.
البلاط الجاهزة
يتكون الحقل من مربعات مربعة. سيكون الأعداء قادرين على الانتقال من البلاط إلى البلاط ، معبر الحواف ، ولكن ليس بشكل قطري. ستحدث الحركة دائمًا نحو أقرب نقطة نهاية. دعنا نشير بيانيا إلى اتجاه الحركة على طول البلاط مع سهم. يمكنك تنزيل نسيج السهم
هنا .
سهم على خلفية سوداء.ضع نسيج السهم في مشروعك وقم بتمكين خيار
Alpha As Transparency . ثم قم بإنشاء مادة للسهم ، والتي يمكن أن تكون المادة الافتراضية التي تم تحديد وضع القطع لها ، وحدد السهم كملمس رئيسي.
مادة السهم.لماذا استخدام وضع تجسيد انقطاع؟يسمح لك بإخفاء السهم باستخدام خط أنابيب تقديم الوحدة القياسي.
للإشارة إلى كل بلاطة في اللعبة ، سنستخدم كائن اللعبة. سيكون لكل منهم رباعته الخاصة مع مادة السهم ، تمامًا كما يحتوي الحقل على رباعي من الأرض. سنضيف أيضًا مربعات إلى مكون GameTile مع رابط لسهمهم.
using UnityEngine; public class GameTile : MonoBehaviour { [SerializeField] Transform arrow = default; }
قم بإنشاء كائن مربع وتحويله إلى الجاهزة. سيتم غمر البلاط بالأرض ، لذا ارفع السهم لأعلى قليلاً لتجنب مشاكل العمق عند العرض. يمكنك أيضًا التصغير قليلاً ، بحيث يكون هناك مساحة صغيرة بين الأسهم المجاورة. A Y إزاحة 0.001 ومقياس 0.8 الذي هو نفسه بالنسبة لجميع المحاور سوف تفعل.
البلاط الجاهزة.أين هو التسلسل الهرمي للبلاط الجاهزة؟يمكنك فتح وضع التعديل المسبق من خلال النقر المزدوج على مادة العرض الجاهزة ، أو عن طريق تحديد الجاهزة والضغط على زر فتح الجاهزة في المفتش. يمكنك الخروج من وضع التحرير المسبق عن طريق النقر على الزر مع وجود سهم في الركن الأيسر العلوي من رأس التسلسل الهرمي.
لاحظ أن البلاط نفسها لا يجب أن تكون كائنات لعبة. هناك حاجة فقط من أجل تتبع حالة المجال. يمكن أن نستخدم نفس الطريقة المتبعة في السلوك في سلسلة دروس
إدارة الكائنات . ولكن في المراحل المبكرة من الألعاب البسيطة أو النماذج الأولية من كائنات اللعبة ، نحن سعداء للغاية. هذا يمكن تغييره في المستقبل.
لدينا البلاط
لإنشاء مربعات ، يجب أن يكون لدى
GameBoard
رابط إلى
GameBoard
الجاهزة.
[SerializeField] GameTile tilePrefab = default;
رابط للبلاط الجاهزة.يمكنه بعد ذلك إنشاء حالاته باستخدام حلقة مزدوجة عبر بعدين من الشبكة. على الرغم من التعبير عن الحجم كـ X و Y ، إلا أننا سنرتب البلاط على مستوى XZ ، وكذلك الحقل نفسه. نظرًا لأن الحقل يتركز بالنسبة إلى الأصل ، نحتاج إلى طرح الحجم المقابل مطروحًا منه مقسومًا على اثنين من مكونات موضع التجانب. يرجى ملاحظة أن هذا يجب أن يكون تقسيم الفاصلة العائمة ، وإلا فلن يعمل حتى الأحجام.
public void Initialize (Vector2Int size) { this.size = size; ground.localScale = new Vector3(size.x, size.y, 1f); Vector2 offset = new Vector2( (size.x - 1) * 0.5f, (size.y - 1) * 0.5f ); for (int y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++) { GameTile tile = Instantiate(tilePrefab); tile.transform.SetParent(transform, false); tile.transform.localPosition = new Vector3( x - offset.x, 0f, y - offset.y ); } } }
مثيلات إنشاء البلاط.سنحتاج لاحقًا إلى الوصول إلى هذه المربعات ، لذلك سنتعقبها في صفيف. لا نحتاج إلى قائمة ، لأنه بعد التهيئة ، لن يتغير حجم الحقل.
GameTile[] tiles; public void Initialize (Vector2Int size) { … tiles = new GameTile[size.x * size.y]; for (int i = 0, y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++, i++) { GameTile tile = tiles[i] = Instantiate(tilePrefab); … } } }
كيف تعمل هذه المهمة؟هذه مهمة مرتبطة. في هذه الحالة ، هذا يعني أننا نقوم بتعيين ارتباط لمثيل التجانب لكل من عنصر الصفيف والمتغير المحلي. تؤدي هذه العمليات نفس الكود الموضح أدناه.
GameTile t = Instantiate(tilePrefab); tiles[i] = t; GameTile tile = t;
البحث عن وسيلة
في هذه المرحلة ، يكون لكل بلاطة سهم ، لكنهم يشيرون جميعًا إلى الاتجاه الإيجابي للمحور Z ، والذي سنفسره على أنه الشمال. الخطوة التالية هي تحديد الاتجاه الصحيح للبلاط. نحن نفعل ذلك من خلال إيجاد المسار الذي يجب على الأعداء اتباعه حتى نقطة النهاية.
بلاط الجيران
تنتقل المسارات من البلاط إلى البلاط ، في الشمال أو الشرق أو الجنوب أو الغرب. لتبسيط البحث ، قم
GameTile
ارتباطات مسار
GameTile
إلى جيرانها الأربعة.
GameTile north, east, south, west;
العلاقات بين الجيران متماثلة. إذا كان البلاط هو الجار الشرقي للبلاط الثاني ، فإن الثاني هو الجار الغربي للبلاط الثاني. إضافة طريقة ثابتة عامة إلى
GameTile
لتحديد هذه العلاقة بين اثنين من البلاط.
public static void MakeEastWestNeighbors (GameTile east, GameTile west) { west.east = east; east.west = west; }
لماذا استخدام طريقة ثابتة؟يمكننا أن نجعلها طريقة مثيل مع معلمة واحدة ، وفي هذه الحالة سوف نسميها كـ eastTile.MakeEastWestNeighbors(westTile)
أو شيء من هذا القبيل. لكن في الحالات التي يكون فيها من غير الواضح أي من المربعات التي يجب استدعاء الطريقة بها ، من الأفضل استخدام الأساليب الثابتة. ومن الأمثلة على ذلك Distance
وطرق Dot
للفئة Vector3
.
بمجرد الاتصال ، يجب ألا يتغير أبدًا. إذا حدث هذا ، فقد ارتكبنا خطأ في الكود. يمكنك التحقق من ذلك عن طريق مقارنة كلا الارتباطين قبل تعيين القيم إلى قيمة
null
، وعرض خطأ إذا كان غير صحيح. يمكنك استخدام الأسلوب
Debug.Assert
لهذا
Debug.Assert
.
public static void MakeEastWestNeighbors (GameTile east, GameTile west) { Debug.Assert( west.east == null && east.west == null, "Redefined neighbors!" ); west.east = east; east.west = west; }
ماذا Debug.Assert تفعل؟إذا كانت الوسيطة الأولى false
، فسوف تعرض خطأً في الشرط ، باستخدام الوسيطة الثانية إذا كانت محددة. يتم تضمين مثل هذه الدعوة فقط في بنيات الاختبار ، ولكن ليس في بنيات الإصدار. لذلك ، فهذه طريقة جيدة لإضافة الشيكات أثناء عملية التطوير والتي لن تؤثر على الإصدار النهائي.
أضف طريقة مماثلة لإنشاء علاقات بين الجارتين الشمالية والجنوبية.
public static void MakeNorthSouthNeighbors (GameTile north, GameTile south) { Debug.Assert( south.north == null && north.south == null, "Redefined neighbors!" ); south.north = north; north.south = south; }
يمكننا إقامة هذه العلاقة عند إنشاء مربعات في
GameBoard.Initialize
. إذا كان الإحداثي X أكبر من الصفر ، فيمكننا إنشاء علاقة بين الشرق والغرب بين المربعات الحالية والسابقة. إذا كان الإحداثي Y أكبر من الصفر ، فيمكننا إنشاء علاقة بين الشمال والجنوب بين البلاطة الحالية والبلاطة من السطر السابق.
for (int i = 0, y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++, i++) { … if (x > 0) { GameTile.MakeEastWestNeighbors(tile, tiles[i - 1]); } if (y > 0) { GameTile.MakeNorthSouthNeighbors(tile, tiles[i - size.x]); } } }
لاحظ أن التجانبات الموجودة على حواف الحقل لا تحتوي على أربعة جوار. سيبقى مرجع جار واحد أو اثنين
null
.
المسافة والاتجاه
لن نجبر جميع الأعداء على البحث باستمرار عن الطريق. هذا يجب القيام به مرة واحدة فقط لكل بلاطة. عندها سيكون الأعداء قادرين على الطلب من البلاط الذي يوجدون فيه للمضي قدما. سنقوم بتخزين هذه المعلومات في
GameTile
عن طريق إضافة رابط إلى تجانب المسار التالي. بالإضافة إلى ذلك ، سنوفر أيضًا المسافة إلى نقطة النهاية ، معبراً عنها بعدد البلاط الذي يجب زيارته قبل أن يصل العدو إلى نقطة النهاية. بالنسبة للأعداء ، هذه المعلومات عديمة الفائدة ، لكننا سنستخدمها للعثور على أقصر الطرق.
GameTile north, east, south, west, nextOnPath; int distance;
في كل مرة نقرر فيها أننا بحاجة إلى البحث عن المسارات ، سنحتاج إلى تهيئة بيانات المسار. حتى يتم العثور على المسار ، لا يوجد البلاط التالي ويمكن اعتبار المسافة لانهائية. يمكننا أن نتخيل هذا كقيمة عدد صحيح ممكن من
int.MaxValue
. إضافة طريقة
ClearPath
عامة لإعادة تعيين
GameTile
إلى هذه الحالة.
public void ClearPath () { distance = int.MaxValue; nextOnPath = null; }
لا يمكن البحث عن المسارات إلا إذا كانت لدينا نقطة نهاية. هذا يعني أن البلاط يجب أن يصبح نقطة النهاية. تبلغ مساحة هذا التجانب صفرًا ، ولا يحتوي على التجانب الأخير ، لأن المسار ينتهي عليه. أضف طريقة عامة تحول البلاط إلى نقطة نهاية.
public void BecomeDestination () { distance = 0; nextOnPath = null; }
في نهاية المطاف ، يجب أن تتحول جميع المربعات إلى مسار ، وبالتالي فإن مسافتها لن تساوي
int.MaxValue
. إضافة خاصية getter مريحة للتحقق مما إذا كان البلاط يحتوي حاليًا على مسار.
public bool HasPath => distance != int.MaxValue;
كيف تعمل هذه الخاصية؟هذا هو إدخال مختصرة لخاصية getter تحتوي على تعبير واحد فقط. يفعل نفس الكود الموضح أدناه.
public bool HasPath { get { return distance != int.MaxValue; } }
يمكن أيضًا استخدام عامل التشغيل
=>
بشكل فردي للجمع وضبط الخصائص ، ولهيئات الطرق ، والبنائين ، وفي بعض الأماكن الأخرى.
نحن ننمو الطريق
إذا كان لدينا بلاط به مسار ، فيمكننا أن ندعه ينمو في اتجاه أحد جيرانه. في البداية ، فإن نقطة النهاية الوحيدة هي المسار ، لذلك نبدأ من مسافة الصفر ونزيدها من هنا ، ونتحرك في الاتجاه المعاكس لحركة الأعداء. أي ، سيكون لكل الجيران المباشرين لنقطة النهاية مسافة 1 ، وسيكون لكل الجيران في هذه البلاطات مسافة 2 وما إلى ذلك.
أضف طريقة مخفية لـ
GameTile
لتنمية المسار إلى أحد جيرانها ، المحدد من خلال المعلمة. المسافة إلى الجار هي أكثر من التجانب الحالي ، ويشير مسار الجار إلى التجانب الحالي. يجب استدعاء هذه الطريقة فقط للبلاط الذي له مسار بالفعل ، لذلك دعونا نتحقق من ذلك بتأكيد.
void GrowPathTo (GameTile neighbor) { Debug.Assert(HasPath, "No path!"); neighbor.distance = distance + 1; neighbor.nextOnPath = this; }
الفكرة هي أن نسمي هذه الطريقة مرة واحدة لكل من الجيران الأربعة للبلاط. نظرًا لأن بعض هذه الروابط ستكون
null
، فسوف نتحقق من هذا الأمر ونوقف التنفيذ ، إذا كان الأمر كذلك. بالإضافة إلى ذلك ، إذا كان لدى أحد الجيران بالفعل طريق ، فلا ينبغي لنا فعل أي شيء والتوقف عن فعل ذلك.
void GrowPathTo (GameTile neighbor) { Debug.Assert(HasPath, "No path!"); if (neighbor == null || neighbor.HasPath) { return; } neighbor.distance = distance + 1; neighbor.nextOnPath = this; }
الطريقة التي
GameTile
بها
GameTile
جيرانها غير معروفة لبقية الكود. لذلك ، يتم إخفاء
GrowPathTo
. سنضيف طرقًا عامة تخبر التجانب أن ينمو طريقه في اتجاه معين ، مع استدعاء
GrowPathTo
بشكل غير مباشر. لكن الكود الذي يبحث في الحقل يجب أن يتتبع أي البلاط تم زيارته. لذلك ، سنجعله يعيد جارًا أو
null
إذا تم إنهاء التنفيذ.
GameTile GrowPathTo (GameTile neighbor) { if (!HasPath || neighbor == null || neighbor.HasPath) { return null; } neighbor.distance = distance + 1; neighbor.nextOnPath = this; return neighbor; }
أضف الآن طرقًا لتنمية المسارات في اتجاهات محددة.
public GameTile GrowPathNorth () => GrowPathTo(north); public GameTile GrowPathEast () => GrowPathTo(east); public GameTile GrowPathSouth () => GrowPathTo(south); public GameTile GrowPathWest () => GrowPathTo(west);
بحث واسع
يجب أن
GameBoard
أن تحتوي جميع
GameBoard
على بيانات المسار الصحيحة. نحن نفعل ذلك من خلال إجراء بحث أولي. لنبدأ بتجانب نقطة النهاية ، ثم ننمو الطريق إلى جيرانه ، ثم إلى جيران هذه التجانبات ، وهكذا. مع كل خطوة ، تزداد المسافة خطوة بخطوة ، ولن تنمو المسارات أبدًا في اتجاه البلاط الذي يحتوي بالفعل على مسارات. هذا يضمن أن جميع البلاط نتيجة لذلك سوف تشير على طول أقصر الطرق لنقطة النهاية.
ماذا عن العثور على مسار باستخدام A *؟خوارزمية A
* هي التطور التطوري للبحث المتسع الأول. إنه مفيد عندما نبحث عن أقصر الطرق. لكننا نحتاج إلى جميع المسارات الأقصر ، لذلك A
* لا يعطي أي مزايا. للحصول على أمثلة للبحث المتسع و A
* على شبكة من السداسي مع الرسوم المتحركة ، راجع سلسلة البرامج التعليمية حول
خرائط السداسي .
لإجراء البحث ، نحتاج إلى تتبع المربعات التي أضفناها إلى المسار ، ولكننا لم نقم بعد بتطويرها. غالبًا ما تسمى هذه المجموعة من البلاط حدود البحث. من المهم أن تتم معالجة البلاط في نفس الترتيب الذي تضاف به إلى الحدود ، لذلك دعونا نستخدم
Queue
. في وقت لاحق ، سيتعين علينا إجراء البحث عدة مرات ، لذلك دعونا
GameBoard
في
GameBoard
.
using UnityEngine; using System.Collections.Generic; public class GameBoard : MonoBehaviour { … Queue<GameTile> searchFrontier = new Queue<GameTile>(); … }
لكي تكون حالة الملعب صحيحة دائمًا ، يجب أن نجد المسارات في نهاية
Initialize
، لكن نضع الكود في طريقة
FindPaths
منفصلة. بادئ ذي بدء ، تحتاج إلى مسح مسار جميع البلاط ، ثم جعل البلاط واحد نقطة النهاية وإضافته إلى الحدود. دعونا أولا تحديد البلاط الأول. منذ
tiles
هو مجموعة ، يمكننا استخدام
foreach
دون خوف من تلوث الذاكرة. إذا انتقلنا لاحقًا من صفيف إلى قائمة ، فسوف نحتاج أيضًا إلى استبدال حلقات
foreach
بحلقات.
public void Initialize (Vector2Int size) { … FindPaths(); } void FindPaths () { foreach (GameTile tile in tiles) { tile.ClearPath(); } tiles[0].BecomeDestination(); searchFrontier.Enqueue(tiles[0]); }
بعد ذلك ، يجب أن نأخذ بلاطة واحدة من الحدود وننشئ طريقًا إلى جميع جيرانه ، ونضيفهم جميعًا إلى الحدود. أولاً ، سنتحرك شمالًا ، ثم شرقًا وجنوبًا وأخيرًا غربًا.
public void FindPaths () { foreach (GameTile tile in tiles) { tile.ClearPath(); } tiles[0].BecomeDestination(); searchFrontier.Enqueue(tiles[0]); GameTile tile = searchFrontier.Dequeue(); searchFrontier.Enqueue(tile.GrowPathNorth()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathWest()); }
نكرر هذه المرحلة ، في حين أن هناك البلاط في الحدود.
while (searchFrontier.Count > 0) { GameTile tile = searchFrontier.Dequeue(); searchFrontier.Enqueue(tile.GrowPathNorth()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathWest()); }
تزايد المسار لا يقودنا دائمًا إلى بلاط جديد. قبل الإضافة إلى قائمة الانتظار ، نحتاج إلى التحقق من قيمة القيمة
null
، ولكن يمكننا تأجيل التحقق من القيمة
null
إلى ما بعد الإخراج من قائمة الانتظار.
GameTile tile = searchFrontier.Dequeue(); if (tile != null) { searchFrontier.Enqueue(tile.GrowPathNorth()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathWest()); }
عرض المسارات
الآن لدينا حقل يحتوي على المسارات الصحيحة ، ولكن حتى الآن لا نرى هذا. تحتاج إلى تكوين الأسهم بحيث تشير على طول المسار من خلال البلاط. ويمكن القيام بذلك عن طريق تحويلها. نظرًا لأن هذه المنعطفات هي نفسها دائمًا ، فإننا نضيف إلى
GameTile
ثابت واحد في
GameTile
لكل اتجاه من الاتجاهات.
static Quaternion northRotation = Quaternion.Euler(90f, 0f, 0f), eastRotation = Quaternion.Euler(90f, 90f, 0f), southRotation = Quaternion.Euler(90f, 180f, 0f), westRotation = Quaternion.Euler(90f, 270f, 0f);
أضف أيضًا طريقة
ShowPath
العامة. إذا كانت المسافة صفراً ، فإن البلاطة هي نقطة النهاية ولا يوجد شيء يمكن الإشارة إليه ، لذلك قم بإلغاء تنشيط سهمها. بخلاف ذلك ، قم بتفعيل السهم وضبط دورانه. يمكن تحديد الاتجاه المطلوب من خلال مقارنة
nextOnPath
مع جيرانها.
public void ShowPath () { if (distance == 0) { arrow.gameObject.SetActive(false); return; } arrow.gameObject.SetActive(true); arrow.localRotation = nextOnPath == north ? northRotation : nextOnPath == east ? eastRotation : nextOnPath == south ? southRotation : westRotation; }
استدعاء هذه الطريقة لجميع البلاط في النهاية GameBoard.FindPaths
. public void FindPaths () { … foreach (GameTile tile in tiles) { tile.ShowPath(); } }
طرق وجدت.لماذا لا نحول السهم مباشرة إلى GrowPathTo؟لفصل منطق وتصور البحث. في وقت لاحق سوف نجعل التصور تعطيل. إذا لم تظهر الأسهم ، فلن نحتاج إلى تدويرها في كل مرة نتصل بها FindPaths
.
تغيير أولوية البحث
اتضح أنه عندما تكون نقطة النهاية هي الركن الجنوبي الغربي ، فإن جميع المسارات تتجه غربًا تمامًا حتى تصل إلى حافة الحقل ، وبعد ذلك تتجه جنوبًا. كل شيء صحيح هنا ، لأنه لا يوجد بالفعل طرق أقصر لنقطة النهاية ، لأن الحركات القطرية مستحيلة. ومع ذلك ، هناك العديد من الطرق الأقصر الأخرى التي قد تبدو أجمل.لفهم سبب العثور على مثل هذه المسارات بشكل أفضل ، انقل نقطة النهاية إلى مركز الخريطة. بحجم حقل فردي ، إنه مجرد بلاطة في منتصف الصفيف. tiles[tiles.Length / 2].BecomeDestination(); searchFrontier.Enqueue(tiles[tiles.Length / 2]);
نقطة النهاية في المركز.تبدو النتيجة منطقية إذا كنت تتذكر كيفية عمل البحث. نظرًا لأننا أضفنا جيرانًا في الترتيب بين الشمال الشرقي والجنوب الغربي ، فإن الشمال له الأولوية القصوى. بما أننا نقوم بالبحث بترتيب عكسي ، فهذا يعني أن الاتجاه الأخير الذي سلكناه هو الجنوب. هذا هو السبب في عدد قليل من الأسهم تشير إلى الجنوب والعديد من نقطة إلى الشرق.يمكنك تغيير النتيجة عن طريق تحديد أولويات الاتجاهات. دعونا مبادلة الشرق والجنوب. لذلك علينا أن نحصل على التماثل بين الشمال والجنوب والشرق والغرب. searchFrontier.Enqueue(tile.GrowPathNorth()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathWest())
ترتيب البحث هو الشمال والجنوب الشرقي الغربي.يبدو أجمل ، ولكن من الأفضل للمسارات تغيير الاتجاه ، مع اقتراب الحركة القطرية حيث ستبدو طبيعية. يمكننا القيام بذلك عن طريق عكس أولويات البحث للبلاط المجاور في نموذج رقعة الشطرنج.بدلاً من معرفة نوع البلاط الذي نعالجه أثناء البحث ، نضيف إلى GameTile
الخاصية العامة التي تشير إلى ما إذا كان البلاط الحالي بديلًا أم لا. public bool IsAlternative { get; set; }
سنقوم بتعيين هذه الخاصية في GameBoard.Initialize
. أولاً ، قم بتمييز البلاطات كبديل إذا كانت إحداثياتها X متساوية. for (int i = 0, y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++, i++) { … tile.IsAlternative = (x & 1) == 0; } }
ماذا العملية (س & 1) == 0 تفعل؟— (AND). . 1, 1. 10101010 00001111 00001010.
. 0 1. 1, 2, 3, 4 1, 10, 11, 100. , .
AND , , . , .
ثانياً ، نغير علامة النتيجة إذا كان إحداثي Y متساوي. لذلك سوف نخلق نمط الشطرنج. tile.IsAlternative = (x & 1) == 0; if ((y & 1) == 0) { tile.IsAlternative = !tile.IsAlternative; }
كما FindPaths
علينا أن نحافظ على نفس النظام والبحث عن بديل البلاط، ولكن لتجعل من العودة لجميع البلاط وغيرها. هذا سوف يجبر الطريق إلى حركة قطرية وإنشاء متعرج. if (tile != null) { if (tile.IsAlternative) { searchFrontier.Enqueue(tile.GrowPathNorth()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathWest()); } else { searchFrontier.Enqueue(tile.GrowPathWest()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathNorth()); } }
ترتيب بحث متغير.تغيير البلاط
في هذه المرحلة ، جميع البلاط فارغة. يتم استخدام بلاطة واحدة كنقطة نهاية ، ولكن بالإضافة إلى عدم وجود سهم مرئي ، يبدو مثل أي شخص آخر. سنضيف القدرة على تغيير البلاط من خلال وضع الأشياء عليها.بلاط المحتوى
كائنات البلاط نفسها هي ببساطة وسيلة لتتبع معلومات البلاط. نحن لا نقوم بتعديل هذه الكائنات مباشرة. بدلاً من ذلك ، قم بإضافة محتوى منفصل ووضعه في الحقل. في الوقت الحالي ، يمكننا التمييز بين البلاط الفارغ وبلاطات نقطة النهاية. للإشارة إلى هذه الحالات ، قم بإنشاء تعداد GameTileContentType
. public enum GameTileContentType { Empty, Destination }
بعد ذلك ، قم بإنشاء نوع مكون GameTileContent
يسمح لك بتعيين نوع محتوياته من خلال المفتش ، وسيكون الوصول إليه من خلال خاصية getter مشتركة. using UnityEngine; public class GameTileContent : MonoBehaviour { [SerializeField] GameTileContentType type = default; public GameTileContentType Type => type; }
بعد ذلك ، سنقوم بإنشاء أبنية جاهزة لنوعين من المحتوى ، يحتوي كل منهما على مكون GameTileContent
بالنوع المحدد المقابل. دعنا نستخدم مكعب مسطح أزرق لتعيين بلاط نقطة النهاية. لأنه مسطح تقريبًا ، فهو لا يحتاج إلى مصادم. لإعداد محتوى فارغ مسبقًا ، استخدم كائن لعبة فارغ.الجاهزة من نقطة النهاية والمحتوى الفارغ.سنعطي كائن المحتوى للبلاطات الفارغة ، لأنه بعد ذلك سيكون لكل المحتوى دائمًا المحتوى ، مما يعني أننا لن نحتاج إلى التحقق من الروابط إلى المحتويات من أجل المساواة null
.مصنع المحتوى
لجعل المحتوى قابلاً للتحرير ، سنقوم أيضًا بإنشاء مصنع لذلك ، باستخدام نفس الطريقة كما في البرنامج التعليمي لإدارة الكائنات . هذا يعني أنه GameTileContent
يجب عليك تتبع المصنع الأصلي الخاص بك ، والذي يجب ضبطه مرة واحدة فقط ، وإعادة نفسك إلى المصنع بالطريقة نفسها Recycle
. GameTileContentFactory originFactory; … public GameTileContentFactory OriginFactory { get => originFactory; set { Debug.Assert(originFactory == null, "Redefined origin factory!"); originFactory = value; } } public void Recycle () { originFactory.Reclaim(this); }
هذا يفترض وجود GameTileContentFactory
، وبالتالي ، فإننا سوف ننشئ نوع كائن نصي لهذا الغرض بالطريقة المطلوبة Recycle
. في هذه المرحلة ، لن نهتم بإنشاء مصنع يعمل بكامل طاقته ويستخدم المحتويات ، لذلك سنجعله يدمر المحتويات. في وقت لاحق سيكون من الممكن إضافة إعادة استخدام الكائنات إلى المصنع دون تغيير بقية الكود. using UnityEngine; using UnityEngine.SceneManagement; [CreateAssetMenu] public class GameTileContentFactory : ScriptableObject { public void Reclaim (GameTileContent content) { Debug.Assert(content.OriginFactory == this, "Wrong factory reclaimed!"); Destroy(content.gameObject); } }
إضافة طريقة مخفية Get
إلى المصنع مع الجاهزة كمعلمة. نحن هنا مرة أخرى تخطي إعادة استخدام الأشياء. يقوم بإنشاء مثيل للكائن ، ويقوم بتعيين المصنع الأصلي ، وينقله إلى مشهد المصنع ويعيده. GameTileContent Get (GameTileContent prefab) { GameTileContent instance = Instantiate(prefab); instance.OriginFactory = this; MoveToFactoryScene(instance.gameObject); return instance; }
تم نقل المثيل إلى مشهد محتوى المصنع ، والذي يمكن إنشاؤه حسب الحاجة. إذا كنا في المحرر ، ثم قبل إنشاء مشهد ، نحتاج إلى التحقق من وجوده ، في حالة فقدنا ذلك أثناء إعادة التشغيل الساخنة. Scene contentScene; … void MoveToFactoryScene (GameObject o) { if (!contentScene.isLoaded) { if (Application.isEditor) { contentScene = SceneManager.GetSceneByName(name); if (!contentScene.isLoaded) { contentScene = SceneManager.CreateScene(name); } } else { contentScene = SceneManager.CreateScene(name); } } SceneManager.MoveGameObjectToScene(o, contentScene); }
لدينا فقط نوعان من المحتوى ، لذلك فقط قم بإضافة حقلين للتكوين المسبق لهما. [SerializeField] GameTileContent destinationPrefab = default; [SerializeField] GameTileContent emptyPrefab = default;
آخر شيء يجب القيام به حتى يعمل المصنع هو إنشاء طريقة عامة Get
باستخدام معلمة GameTileContentType
تستقبل مثيلًا للهيئة الجاهزة المقابلة. public GameTileContent Get (GameTileContentType type) { switch (type) { case GameTileContentType.Destination: return Get(destinationPrefab); case GameTileContentType.Empty: return Get(emptyPrefab); } Debug.Assert(false, "Unsupported type: " + type); return null; }
هل يلزم إضافة مثيل منفصل للمحتوى الفارغ لكل بلاطة؟, . . , - , , , , . , . , , .
لنقم بإنشاء أصل مصنع وتكوين روابطه للأبنية الجاهزة.مصنع المحتوىثم قم بتمرير Game
الرابط إلى المصنع. [SerializeField] GameTileContentFactory tileContentFactory = default;
لعبة مع مصنع.التنصت على البلاط
لتغيير الحقل ، نحتاج إلى أن نكون قادرين على تحديد البلاط. سوف نجعلها ممكنة في وضع اللعبة. سنبعث شعاعًا إلى المشهد في المكان الذي نقر فيه اللاعب على نافذة اللعبة. إذا كان شعاع يتقاطع مع البلاط ، ثم لمسها اللاعب ، وهذا هو ، يجب تغييره. Game
سوف يتعامل مع مدخلات اللاعب ، ولكنه سيكون مسؤولاً عن تحديد البلاط الذي لمسته اللاعب GameBoard
.لا تتقاطع جميع الأشعة مع البلاط ، لذلك في بعض الأحيان لن نتلقى أي شيء. لذلك ، نضيف إلى GameBoard
الطريقة GetTile
التي دائمًا ما ترجع دائمًا مبدئيًا null
(وهذا يعني أنه لم يتم العثور على التجانب). public GameTile GetTile (Ray ray) { return null; }
لتحديد ما إذا كانت الأشعة قد عبرت بلاطًا ، نحتاج إلى الاتصال Physics.Raycast
بتحديد الشعاع كوسيطة. تقوم بإرجاع معلومات حول ما إذا كان هناك تقاطع. إذا كان الأمر كذلك ، فبإمكاننا إرجاع البلاط ، على الرغم من أننا لا نعرف أيهما بعد ، لذلك سنقوم بإعادته في الوقت الحالي null
. public GameTile TryGetTile (Ray ray) { if (Physics.Raycast(ray) { return null; } return null; }
لمعرفة ما إذا كان هناك تقاطع مع البلاط ، نحتاج إلى مزيد من المعلومات حول التقاطع. Physics.Raycast
يمكن أن توفر هذه المعلومات باستخدام المعلمة الثانية RaycastHit
. هذه هي معلمة الإخراج ، والتي يشار إليها بالكلمة out
الموجودة أمامها. هذا يعني أن استدعاء الأسلوب يمكن أن يعين قيمة للمتغير الذي ننقله إليه. RaycastHit hit; if (Physics.Raycast(ray, out hit) { return null; }
يمكننا تضمين إعلان المتغيرات المستخدمة لمعلمات الإخراج ، لذلك دعونا نفعل ذلك. if (Physics.Raycast(ray, out RaycastHit hit) { return null; }
نحن لا نهتم بأي تصادم حدث التقاطع ، نحن فقط نستخدم موضع تقاطع XZ لتحديد التجانب. نحصل على إحداثيات التجانب بإضافة نصف حجم الحقل إلى إحداثيات نقطة التقاطع ، ثم نقوم بتحويل النتائج إلى قيم عدد صحيح. سيكون مؤشر التجانب النهائي كنتيجة إحداثيته X بالإضافة إلى الإحداثي Y مضروبة في عرض الحقل. if (Physics.Raycast(ray, out RaycastHit hit)) { int x = (int)(hit.point.x + size.x * 0.5f); int y = (int)(hit.point.z + size.y * 0.5f); return tiles[x + y * size.x]; }
لكن هذا ممكن فقط عندما تكون إحداثيات البلاط داخل الحقل ، لذلك سوف نتحقق من ذلك. إذا لم يكن الأمر كذلك ، فلن يتم إرجاع البلاط. int x = (int)(hit.point.x + size.x * 0.5f); int y = (int)(hit.point.z + size.y * 0.5f); if (x >= 0 && x < size.x && y >= 0 && y < size.y) { return tiles[x + y * size.x]; }
تغيير المحتوى
بحيث يمكنك تغيير محتويات البلاط ، إضافة إلى GameTile
الملكية العامة Content
. تقوم getter ببساطة بإرجاع المحتويات ، ويتجاهل setter المحتويات السابقة ، إن وجدت ، ويضع المحتويات الجديدة. GameTileContent content; public GameTileContent Content { get => content; set { if (content != null) { content.Recycle(); } content = value; content.transform.localPosition = transform.localPosition; } }
هذا هو المكان الوحيد الذي تحتاجه للتحقق من المحتوى null
، لأنه في البداية ليس لدينا محتوى. لضمان ، ننفذ تأكيد بحيث لا يتم استدعاء واضع مع null
. set { Debug.Assert(value != null, "Null assigned to content!"); … }
وأخيرا ، نحن بحاجة إلى إدخال لاعب. يمكن تحويل نقرة بالماوس إلى شعاع عن طريق الاتصال ScreenPointToRay
باستخدام Input.mousePosition
كوسيطة. يجب إجراء المكالمة من أجل الكاميرا الرئيسية ، والتي يمكن الوصول إليها من خلال Camera.main
. إضافة خاصية ج لهذا الغرض Game
. Ray TouchRay => Camera.main.ScreenPointToRay(Input.mousePosition);
ثم نضيف طريقة Update
للتحقق مما إذا كان زر الماوس الرئيسي قد تم الضغط عليه أثناء الترقية. للقيام بذلك ، اتصل بالرقم Input.GetMouseButtonDown
صفر كوسيطة. إذا تم الضغط على المفتاح ، فإننا نعالج لمسة اللاعب ، أي أننا نأخذ البلاط من الحقل ونضع نقطة النهاية كمحتوياته ، ونأخذها من المصنع. void Update () { if (Input.GetMouseButtonDown(0)) { HandleTouch(); } } void HandleTouch () { GameTile tile = GetTile(TouchRay); if (tile != null) { tile.Content = tileContentFactory.Get(GameTileContentType.Destination); } }
الآن يمكننا تحويل أي بلاطة إلى نقطة نهاية عن طريق الضغط على المؤشر.عدة نقاط النهاية.جعل المجال الصحيح
على الرغم من أننا يمكن أن نحول المربعات إلى نقاط نهاية ، فإن هذا لا يؤثر على المسارات حتى الآن. بالإضافة إلى ذلك ، لم نحدد بعد محتوى فارغًا للبلاط. تعد المحافظة على صحة وسلامة الحقل مهمة GameBoard
، لذلك دعونا نمنحه مسؤولية تحديد محتويات البلاط. لتنفيذ ذلك ، سنعطيه رابطًا إلى مصنع المحتوى من خلال طريقته Intialize
، ونستخدمه لمنح كل المحتوى نسخة من محتوى فارغ. GameTileContentFactory contentFactory; public void Initialize ( Vector2Int size, GameTileContentFactory contentFactory ) { this.size = size; this.contentFactory = contentFactory; ground.localScale = new Vector3(size.x, size.y, 1f); tiles = new GameTile[size.x * size.y]; for (int i = 0, y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++, i++) { … tile.Content = contentFactory.Get(GameTileContentType.Empty); } } FindPaths(); }
الآن Game
يجب أن أنقل مصنعي إلى الحقل. void Awake () { board.Initialize(boardSize, tileContentFactory); }
لماذا لا تضيف حقل تكوين المصنع إلى GameBoard؟, , . , .
نظرًا لأن لدينا العديد من نقاط النهاية ، فإننا نقوم بتغييرها GameBoard.FindPaths
بحيث تستدعي BecomeDestination
كل نقطة ونضيفها جميعًا إلى الحدود. وهذا كل ما يتطلبه الأمر لدعم نقاط النهاية المتعددة. يتم مسح جميع البلاط الأخرى كالمعتاد. ثم نقوم بحذف نقطة النهاية الثابتة في الوسط. void FindPaths () { foreach (GameTile tile in tiles) { if (tile.Content.Type == GameTileContentType.Destination) { tile.BecomeDestination(); searchFrontier.Enqueue(tile); } else { tile.ClearPath(); } }
ولكن إذا استطعنا تحويل المربعات إلى نقاط نهاية ، فسنكون قادرين على تنفيذ العملية العكسية ، وتحويل نقاط النهاية إلى مربعات فارغة. ولكن بعد ذلك يمكننا الحصول على حقل بدون نقاط نهاية على الإطلاق. في هذه الحالة ، FindPaths
لن تكون قادرة على أداء مهمتها. يحدث هذا عندما تكون الحدود فارغة بعد تهيئة المسار لجميع الخلايا. نشير إلى هذا على أنه حالة غير صالحة للحقل ، مع إعادة false
التنفيذ واستكماله ؛ العودة خلاف ذلك في النهاية true
. bool FindPaths () { foreach (GameTile tile in tiles) { … } if (searchFrontier.Count == 0) { return false; } … return true; }
أسهل طريقة لتنفيذ الدعم لإزالة نقاط النهاية ، مما يجعلها عملية التبديل. بالنقر فوق البلاط الفارغ ، سنحولها إلى نقاط نهاية ، وبالضغط على نقاط النهاية ، سنحذفها. لكن الآن تعمل على تغيير المحتوى GameBoard
، لذلك سنمنحه طريقة عامة ToggleDestination
، والمعلمة منها هي التجانب. إذا كان البلاط هو نقطة النهاية ، فاجعله فارغًا واتصل به FindPaths
. خلاف ذلك ، فإننا نجعلها نقطة النهاية ونطلق عليها أيضًا FindPaths
. public void ToggleDestination (GameTile tile) { if (tile.Content.Type == GameTileContentType.Destination) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } else { tile.Content = contentFactory.Get(GameTileContentType.Destination); FindPaths(); } }
لا تؤدي إضافة نقطة نهاية أبدًا إلى إنشاء حالة حقل غير صالحة ، ويمكن حذف نقطة النهاية. لذلك ، سوف نتحقق مما إذا كان قد نجح في التنفيذ بنجاح FindPaths
بعد أن جعلنا البلاط فارغًا. إذا لم يكن الأمر كذلك ، فقم بإلغاء التغيير ، وأعد التجانب مرة أخرى إلى نقطة النهاية ، واتصل مرة أخرى FindPaths
للعودة إلى الحالة الصحيحة السابقة. if (tile.Content.Type == GameTileContentType.Destination) { tile.Content = contentFactory.Get(GameTileContentType.Empty); if (!FindPaths()) { tile.Content = contentFactory.Get(GameTileContentType.Destination); FindPaths(); } }
هل يمكن جعل التحقق من الصحة أكثر كفاءة؟, . , . , . FindPaths
, .
الآن في النهاية Initialize
يمكننا الاتصال ToggleDestination
بالبلاط المركزي كوسيطة ، بدلاً من الاتصال بشكل صريح FindPaths
. هذه هي المرة الوحيدة التي نبدأ فيها بحالة حقل غير صالحة ، لكننا نضمن أن ننتهي بالحالة الصحيحة. public void Initialize ( Vector2Int size, GameTileContentFactory contentFactory ) { …
أخيرًا ، نجبر على Game
الاتصال ToggleDestination
بدلاً من تعيين محتويات التجانب نفسه. void HandleTouch () { GameTile tile = board.GetTile(TouchRay); if (tile != null) {
نقاط النهاية متعددة مع المسارات الصحيحة.ألا ينبغي لنا حظر اللعبة من تحديد محتويات البلاط مباشرة؟. . , Game
. , .
الجدران
الهدف من برج الدفاع هو منع الأعداء من الوصول إلى النقطة النهائية. يتحقق هذا الهدف بطريقتين. أولاً ، نحن نقتلهم ، وثانياً ، نبطئهم حتى يتوفر المزيد من الوقت لقتلهم. في حقل التجانب ، يمكن تمديد الوقت ، مما يزيد من المسافة التي يحتاج إليها الأعداء. ويمكن تحقيق ذلك عن طريق وضع العقبات في هذا المجال. عادة ما تكون هذه الأبراج تقتل الأعداء أيضًا ، لكن في هذا البرنامج التعليمي سنقتصر على الجدران فقط.محتويات
الجدران هي نوع آخر من المحتوى ، لذلك دعونا نضيف GameTileContentType
عنصرًا إليها. public enum GameTileContentType { Empty, Destination, Wall }
ثم إنشاء الجاهزة الجدار. هذه المرة ، سنقوم بإنشاء كائن لعبة من محتويات التجانب وإضافة مكعب تابع إليه ، والذي سيكون أعلى الحقل وملء التجانب بالكامل. اجعلها نصف وحدة عالية واحفظ المصادم ، لأن الجدران يمكن أن تتداخل بصريًا مع جزء من البلاط الموجود خلفه. لذلك ، عندما يلمس اللاعب الحائط ، فسوف يؤثر على البلاط المقابل.الجاهزة الجدار.أضف الجدار الجاهز للمصنع ، سواء في الكود أو في المفتش. [SerializeField] GameTileContent wallPrefab = default; … public GameTileContent Get (GameTileContentType type) { switch (type) { case GameTileContentType.Destination: return Get(destinationPrefab); case GameTileContentType.Empty: return Get(emptyPrefab); case GameTileContentType.Wall: return Get(wallPrefab); } Debug.Assert(false, "Unsupported type: " + type); return null; }
مصنع مع جدار الجاهزة.بدوره على الجدران وخارجها
إضافة إلى GameBoard
طريقة تشغيل / إيقاف الجدران ، كما فعلنا لنقطة النهاية. في البداية ، لن نتحقق من الحالة غير الصحيحة للحقل. public void ToggleWall (GameTile tile) { if (tile.Content.Type == GameTileContentType.Wall) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } else { tile.Content = contentFactory.Get(GameTileContentType.Wall); FindPaths(); } }
سوف نقدم الدعم للتبديل فقط بين البلاط الفارغ وبلاط الجدران ، دون السماح للجدران باستبدال نقاط النهاية مباشرة. لذلك ، سنقوم بإنشاء جدار فقط عندما يكون البلاط فارغًا. بالإضافة إلى ذلك ، يجب أن تمنع الجدران البحث عن المسار. ولكن يجب أن يكون لكل بلاطة طريق إلى نقطة النهاية ، وإلا فإن الأعداء سيتعثرون. للقيام بذلك ، نحتاج مرة أخرى إلى استخدام التحقق من الصحة FindPaths
وتجاهل التغييرات إذا قاموا بإنشاء حالة حقل غير صحيحة. else if (tile.Content.Type == GameTileContentType.Empty) { tile.Content = contentFactory.Get(GameTileContentType.Wall); if (!FindPaths()) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } }
سيتم تشغيل وإيقاف تشغيل الجدران في كثير من الأحيان أكثر من تشغيل وإيقاف نقاط النهاية ، لذلك سنجعل تبديل الجدران في Game
لمسة رئيسية. يمكن تبديل نقاط النهاية بلمسة إضافية (عادةً ما يكون زر الماوس الأيمن) ، والذي يمكن التعرف عليه بالانتقال إلى Input.GetMouseButtonDown
قيمة 1. void Update () { if (Input.GetMouseButtonDown(0)) { HandleTouch(); } else if (Input.GetMouseButtonDown(1)) { HandleAlternativeTouch(); } } void HandleAlternativeTouch () { GameTile tile = board.GetTile(TouchRay); if (tile != null) { board.ToggleDestination(tile); } } void HandleTouch () { GameTile tile = board.GetTile(TouchRay); if (tile != null) { board.ToggleWall(tile); } }
الآن لدينا الجدران.لماذا أحصل على فجوات كبيرة بين ظلال الجدران المجاورة قطريًا؟, , , . , , far clipping plane . , far plane 20 . , MSAA, .
دعنا نتأكد أيضًا من أن نقاط النهاية لا يمكن أن تحل محل الجدران مباشرةً. public void ToggleDestination (GameTile tile) { if (tile.Content.Type == GameTileContentType.Destination) { … } else if (tile.Content.Type == GameTileContentType.Empty) { tile.Content = contentFactory.Get(GameTileContentType.Destination); FindPaths(); } }
مسار البحث قفل
لكي تمنع الجدران البحث عن المسار ، يكفي أن نضيف بلاطات بجدران إلى حدود البحث. يمكن القيام بذلك عن طريق إجبارهم على GameTile.GrowPathTo
عدم إعادة بلاط الجدران. ولكن يجب أن يظل المسار ينمو في اتجاه الجدار ، بحيث يكون لكل البلاط الموجود في الحقل مسار. هذا ضروري لأنه من الممكن أن تتحول بلاطة مع أعداء فجأة إلى جدار. GameTile GrowPathTo (GameTile neighbor) { if (!HasPath || neighbor == null || neighbor.HasPath) { return null; } neighbor.distance = distance + 1; neighbor.nextOnPath = this; return neighbor.Content.Type != GameTileContentType.Wall ? neighbor : null; }
للتأكد من أن جميع التجانبات لها مسار ، GameBoard.FindPaths
يجب أن تتحقق من ذلك بعد اكتمال البحث. إذا لم تكن هذه هي الحالة ، فإن حالة الحقل غير صالحة وتحتاج إلى إرجاعها false
. ليس من الضروري تحديث تصور المسار للحالات غير الصالحة ، لأن الحقل سيعود إلى الحالة السابقة. bool FindPaths () { … foreach (GameTile tile in tiles) { if (!tile.HasPath) { return false; } } foreach (GameTile tile in tiles) { tile.ShowPath(); } return true; }
الجدران تؤثر على الطريق.للتأكد من أن الجدران لها بالفعل المسارات الصحيحة ، تحتاج إلى جعل المكعبات شفافة.جدران شفافة.لاحظ أن شرط صحة جميع المسارات لا يسمح للجدران بإحاطة جزء من الحقل الذي لا توجد فيه نقطة نهاية. يمكننا تقسيم الخريطة ، ولكن فقط في حالة وجود نقطة نهاية واحدة على الأقل في كل جزء. بالإضافة إلى ذلك ، يجب أن يكون كل جدار مجاورًا لبلاط فارغ أو نقطة نهاية ، وإلا فلن يكون بإمكانه الحصول على مسار. على سبيل المثال ، من المستحيل عمل كتلة صلبة من 3 × 3 جدران.إخفاء الطريق
يتيح لنا تصور المسارات معرفة كيفية عمل بحث المسار والتأكد من صحته بالفعل. لكن لا يلزم إظهاره للاعب ، أو على الأقل ليس بالضرورة. لذلك ، دعونا نوفر القدرة على إيقاف الأسهم. يمكن القيام بذلك عن طريق إضافة إلى GameTile
الطريقة العامة HidePath
، والتي ببساطة تعطيل سهمها. public void HidePath () { arrow.gameObject.SetActive(false); }
حالة تعيين المسار جزء من حالة الحقل. أضف GameBoard
حقلًا منطقيًا إلى يساوي الافتراضي false
لتتبع حالته ، بالإضافة إلى خاصية شائعة كمؤسس وضبط. يجب أن يعرض واضع المسارات أو يخفيها على كل المربعات. bool showPaths; public bool ShowPaths { get => showPaths; set { showPaths = value; if (showPaths) { foreach (GameTile tile in tiles) { tile.ShowPath(); } } else { foreach (GameTile tile in tiles) { tile.HidePath(); } } } }
الآن FindPaths
يجب أن تعرض الطريقة المسارات المحدّثة فقط في حالة تمكين التقديم. bool FindPaths () { … if (showPaths) { foreach (GameTile tile in tiles) { tile.ShowPath(); } } return true; }
بشكل افتراضي ، يتم تعطيل تصور المسار. قم بإيقاف تشغيل السهم في الواجهة الجاهزة.السهم الجاهزة غير نشط بشكل افتراضي.نجعله بحيث Game
يغير حالة التصور عند الضغط على المفتاح. سيكون من المنطقي استخدام مفتاح P ، ولكنه أيضًا مفتاح تشغيل سريع لتمكين / تعطيل وضع اللعبة في محرر Unity. نتيجة لذلك ، سيتم التبديل عند استخدام مفتاح الاختصار للخروج من وضع اللعبة ، والذي لا يبدو لطيفًا للغاية. لذلك دعونا نستخدم مفتاح V (اختصار للتصور).لا السهام.عرض الشبكة
عندما تكون الأسهم مخفية ، يصبح من الصعب تمييز موقع كل مربع. دعنا نضيف خطوط الشبكة. قم بتنزيل نسيج شبكة حدود مربعة من هنا يمكن استخدامه كخلفية تجانب منفصلة.نسيج شبكة.لن نضيف هذا النسيج بشكل فردي إلى كل بلاطة ، ولكننا نطبقه على الأرض. لكننا سنجعل هذه الشبكة اختيارية ، وكذلك تصور المسارات. لذلك ، سوف نضيف إلى GameBoard
حقل التكوين Texture2D
وحدد نسيج شبكة لذلك. [SerializeField] Texture2D gridTexture = default;
الحقل مع نسيج شبكة.أضف حقل Boolean آخر وخاصية للتحكم في حالة التصور الشبكة. في هذه الحالة ، يجب على المضبط تغيير مادة الأرض ، والتي يمكن تنفيذها عن طريق استدعاء GetComponent<MeshRenderer>
الأرض والوصول إلى خاصية material
النتيجة. إذا كانت الشبكة تحتاج إلى عرضها ، فسنقوم بتعيين mainTexture
نسيج الشبكة إلى خاصية المواد. خلاف ذلك ، إسنادها إليه null
. لاحظ أنه عند تغيير نسيج المادة ، سيتم إنشاء نسخ مكررة من المادة ، بحيث تصبح مستقلة عن مادة العرض. bool showGrid, showPaths; public bool ShowGrid { get => showGrid; set { showGrid = value; Material m = ground.GetComponent<MeshRenderer>().material; if (showGrid) { m.mainTexture = gridTexture; } else { m.mainTexture = null; } } }
دعنا Game
نتحول عن تصور الشبكة باستخدام المفتاح G. void Update () { … if (Input.GetKeyDown(KeyCode.G)) { board.ShowGrid = !board.ShowGrid; } }
أيضا ، إضافة التصور شبكة الافتراضية ل Awake
. void Awake () { board.Initialize(boardSize, tileContentFactory); board.ShowGrid = true; }
شبكة غير مقياس.لدينا حتى الآن حدود حول الحقل بأكمله. يطابق الملمس ، لكن هذا ليس ما نحتاجه. نحتاج إلى توسيع نطاق الملمس الرئيسي للمادة بحيث تتناسب مع حجم الشبكة. يمكنك القيام بذلك عن طريق استدعاء طريقة SetTextureScale
المواد باسم خاصية النسيج ( _MainTex ) والحجم ثنائي الأبعاد. يمكننا استخدام حجم الحقل مباشرةً ، والذي يتم تحويله بشكل غير مباشر إلى قيمة Vector2
. if (showGrid) { m.mainTexture = gridTexture; m.SetTextureScale("_MainTex", size); }
شبكة تحجيمية مع تشغيل مسار الرؤية وإيقاف تشغيله.لذلك ، في هذه المرحلة ، حصلنا على مجال عمل لعبة البلاط من النوع الدفاع البرج. في البرنامج التعليمي التالي سنضيف الأعداء.مستودعPDF