[الجزءان
الأول والثاني من البرنامج التعليمي]
- نحن نضع في مجال البرج.
- نحن نهدف إلى أعداء بمساعدة الفيزياء.
- نحن تتبع لهم بينما كان ذلك ممكنا.
- نحن اطلاق النار عليهم مع شعاع الليزر.
هذا هو الجزء الثالث من سلسلة من الدروس حول إنشاء نوع بسيط من برج الدفاع. ويصف إنشاء الأبراج ، وتهدف إلى إطلاق النار على الأعداء.
تم إنشاء البرنامج التعليمي في الوحدة 2018.3.0f2.
دعونا تسخين الأعداء.إنشاء برج
الجدران تبطئ الأعداء فقط ، وتزيد من طول المسار الذي يحتاجون إليه. لكن الهدف من اللعبة هو تدمير الأعداء قبل بلوغهم النقطة الأخيرة. يتم حل هذه المشكلة عن طريق وضع الأبراج في الحقل الذي سيطلق عليهم النار.
بلاط المحتوى
الأبراج هي نوع آخر من محتوى التجانب ، لذلك
GameTileContent
نضيف إدخالًا إليهم في
GameTileContent
.
public enum GameTileContentType { Empty, Destination, Wall, SpawnPoint, Tower€ }
في هذا البرنامج التعليمي ، سندعم نوعًا واحدًا فقط من البرج ، والذي يمكن تنفيذه من خلال تزويد
GameTileContentFactory
واحد إلى البرج الجاهز ، والذي يمكن أيضًا إنشاء مثيل له عبر
Get
.
[SerializeField] GameTileContent towerPrefab = default; public GameTileContent Get (GameTileContentType type) { switch (type) { … case GameTileContentType.Tower€: return Get(towerPrefab); } … }
ولكن يجب إطلاق النار على الأبراج ، لذلك ستحتاج حالتهم إلى التحديث ويحتاجون إلى كودهم الخاص. إنشاء فئة
Tower
لهذا الغرض الذي يمتد فئة
GameTileContent
.
using UnityEngine; public class Tower : GameTileContent {}
يمكنك جعل البرج الجاهز مكونًا خاصًا به عن طريق تغيير نوع حقل المصنع إلى
Tower
. نظرًا لأن الفصل ما زال يعتبر
GameTileContent
، فلا يجب تغيير أي شيء آخر.
Tower towerPrefab = default;
منزل جاهز
إنشاء الجاهزة للبرج. يمكنك البدء من خلال تكرار الجاهزة للجدار واستبدال مكون
GameTileContent
الخاص به
GameTileContent
Tower
، ثم تغيير نوعه إلى
Tower . لجعل البرج مناسبًا للجدران ، احفظ مكعب الحائط كقاعدة للبرج. ثم ضع مكعب آخر فوقه. أعطيته مقياس 0.5. ضع مكعبًا آخر عليه ، مشيرًا إلى وجود برج ، فإن هذا الجزء يهدف إلى إطلاق النار على الأعداء.
ثلاثة مكعبات تشكيل برج.سيتم تدوير البرج ، ولأنه يحتوي على مصادم ، سيتم تتبعه بواسطة محرك فعلي. لكننا لسنا بحاجة إلى أن نكون دقيقين للغاية ، لأننا نستخدم مصادمات الأبراج فقط لتحديد الخلايا. ويمكن القيام بذلك تقريبا. قم بإزالة مصادم مكعب البرج وقم بتغيير مصادم مكعب البرج بحيث يغطي كلا مكعبين.
مصادم برج المكعب.سوف برج اطلاق النار شعاع الليزر. يمكن تصورها بعدة طرق ، لكننا نستخدم مكعبًا شفافًا ، وسنتوسع لتشكيل حزمة. يجب أن يكون لكل برج شعاع خاص به ، لذا أضفه إلى البرج الجاهز. ضعه داخل البرج بحيث يتم إخفاؤه افتراضيًا ومنحه مقياسًا أصغر ، على سبيل المثال 0.2. لنجعلها طفلًا من الجذر الجاهز ، وليس مكعب البرج.
مكعب خفي من شعاع الليزر.إنشاء مادة مناسبة لشعاع الليزر. لقد استخدمت المادة السوداء الشفافة القياسية وأطفأت جميع الانعكاسات ، وأعطيتها أيضًا لونًا أحمر ينبعث منها.
المواد من شعاع الليزر.تأكد من أن شعاع الليزر لا يحتوي على مصادم ، وأوقف تشغيله أيضًا.
شعاع الليزر لا يتفاعل مع الظلال.بعد الانتهاء من إنشاء البرج الجاهز ، سنضيفه إلى المصنع.
مصنع مع برج.وضع برج
سنضيف ونزيل الأبراج باستخدام طريقة تحويل أخرى. يمكنك ببساطة تكرار
GameBoard.ToggleWall
عن طريق تغيير اسم الأسلوب ونوع المحتوى.
public void ToggleTower (GameTile tile) { if (tile.Content.Type == GameTileContentType.Tower€) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } else if (tile.Content.Type == GameTileContentType.Empty) { tile.Content = contentFactory.Get(GameTileContentType.Tower€); if (!FindPaths()) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } } }
في
Game.HandleTouch
، يؤدي الضغط باستمرار على مفتاح shift إلى تبديل الأبراج بدلاً من الجدران.
void HandleTouch () { GameTile tile = board.GetTile(TouchRay); if (tile != null) { if (Input.GetKey(KeyCode.LeftShift)) { board.ToggleTower(tile); } else { board.ToggleWall(tile); } } }
أبراج على الميدان.حجب المسار
حتى الآن ، يمكن للجدران فقط أن تمنع البحث عن المسار ، وبالتالي يتحرك الأعداء عبر الأبراج. دعنا نضيف خاصية مساعدة إلى
GameTileContent
تشير إلى ما إذا كان المحتوى يحجب المسار أم لا. يتم حظر المسار إذا كان جدارًا أو برجًا.
public bool BlocksPath => Type == GameTileContentType.Wall || Type == GameTileContentType.Tower€;
استخدم هذه الخاصية في
GameTile.GrowPathTo
بدلاً من التحقق من نوع المحتوى.
GameTile GrowPathTo (GameTile neighbor, Direction direction) { … return
الآن تم حظر المسار من خلال الجدران والأبراج.استبدال الجدران
على الأرجح ، غالباً ما يستبدل اللاعب الجدران بأبراج. سيكون من غير المريح بالنسبة له إزالة الجدار أولاً ، وإلى جانب ذلك ، يمكن للأعداء اختراق هذه الفجوة التي ظهرت بشكل مؤقت. يمكنك تطبيق بديل مباشر عن طريق إجبار
GameBoard.ToggleTower
للتحقق مما إذا كان الجدار حاليًا على البلاط. إذا كان الأمر كذلك ، فاستبدله على الفور ببرج. في هذه الحالة ، لا يتعين علينا البحث عن طرق أخرى ، لأن البلاطة ما زالت تمنعها.
public void ToggleTower (GameTile tile) { if (tile.Content.Type == GameTileContentType.Tower) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } else if (tile.Content.Type == GameTileContentType.Empty) { … } else if (tile.Content.Type == GameTileContentType.Wall) { tile.Content = contentFactory.Get(GameTileContentType.Tower); } }
نحن نهدف الى الاعداء
يمكن للبرج أن ينجز مهمته فقط عندما يجد عدوًا. بعد العثور على العدو ، يجب أن تقرر أي جزء منه لهدف.
تهدف نقطة
للكشف عن الأهداف ، سوف نستخدم محرك الفيزياء. كما هو الحال في مصادم البرج ، لا نحتاج إلى مصادم العدو ليتزامن بالضرورة مع شكله. يمكنك اختيار مصادم أبسط ، أي كرة. بعد اكتشاف العدو ، سوف نستخدم موضع كائن اللعبة مع المصادم المرتبط به كنقطة للتوجيه.
لا يمكننا ربط المصادم بالكائن الجذري للعدو ، لأنه لا يتطابق دائمًا مع موضع النموذج وسيجعل البرج موجهًا على الأرض. هذا هو ، تحتاج إلى وضع المصادم في مكان ما على النموذج. سيوفر لنا محرك الفيزياء رابطًا لهذا الكائن ، والذي يمكننا استخدامه للتوجيه ، لكننا سنحتاج أيضًا إلى الوصول إلى مكون
Enemy
للكائن الجذر. لتبسيط المهمة ، دعنا ننشئ مكون
TargetPoint
. دعنا نعطيه خاصية للتخصيص الخاص والاستلام العام لمكون
Enemy
، وممتلكات أخرى للحصول على موقعه في العالم.
using UnityEngine; public class TargetPoint : MonoBehaviour { public Enemy Enemy€ { get; private set; } public Vector3 Position => transform.position; }
دعنا نعطيه طريقة
Awake
تقوم بإعداد رابط لمكون
Enemy
الخاص به. انتقل مباشرة إلى الكائن الجذر باستخدام
transform.root
. إذا كان مكون
Enemy
غير موجود ، فقد ارتكبنا خطأ عند إنشاء العدو ، لذلك دعونا نضيف بيانًا لهذا الغرض.
void Awake () { Enemy€ = transform.root.GetComponent<Enemy>(); Debug.Assert(Enemy€ != null, "Target point without Enemy root!", this); }
بالإضافة إلى ذلك ، يجب إرفاق المصادم مع نفس كائن اللعبة الذي
TargetPoint
إرفاق
TargetPoint
به.
Debug.Assert(Enemy€ != null, "Target point without Enemy root!", this); Debug.Assert( GetComponent<SphereCollider>() != null, "Target point without sphere collider!", this );
أضف مكونًا ومصادمًا إلى المكعب الجاهز للعدو. هذا سيجعل الأبراج تهدف إلى وسط المكعب. نستخدم مصادم كروي نصف قطره 0.25. يبلغ حجم المكعب 0.5 ، لذلك سيكون نصف قطر المصادم الحقيقي هو 0.125. بفضل هذا ، سيتعين على العدو عبور دائرة نطاق البرج بشكل مرئي ، وبعد مرور بعض الوقت يصبح الهدف الحقيقي. يتأثر حجم المصادم أيضًا بالمقياس العشوائي للعدو ، لذلك سيختلف حجمه أيضًا في اللعبة قليلاً.
عدو مع نقطة تهدف ومصادم على مكعب.طبقة العدو
تهتم الأبراج بالأعداء فقط ، ولا تستهدف أي شيء آخر ، لذلك سنضع كل الأعداء في طبقة منفصلة. سنستخدم الطبقة 9. قم بتغيير اسمه إلى
العدو في نافذة
الطبقات والعلامات ، والذي يمكن فتحه عبر خيار
تحرير الطبقات في قائمة
الطبقات المنسدلة في الركن الأيمن العلوي من المحرر.
سيتم استخدام الطبقة 9 للأعداء.هذه الطبقة مطلوبة فقط للاعتراف بالأعداء ، وليس للتفاعلات الجسدية. دعنا نشير إليها من خلال تعطيلها في
Layer Collision Matrix ، الموجود في لوحة
Physics لمعلمات المشروع.
مصفوفة اصطدام الطبقة.تأكد من أن كائن اللعبة الخاص بنقطة الهدف على الطبقة المطلوبة. قد تكون بقية الجاهزة للعدو على طبقات أخرى ، ولكن سيكون من الأسهل تنسيق كل شيء ووضع الجاهزة بالكامل في طبقة
العدو . إذا قمت بتغيير طبقة الكائن الجذر ، فستتم مطالبتك بتغيير الطبقة لجميع الكائنات التابعة لها.
العدو على الطبقة الصحيحة.لنقم بإضافة العبارة التي تشير إلى أن
TargetPoint
بالفعل على الطبقة الصحيحة.
void Awake () { … Debug.Assert(gameObject.layer == 9, "Target point on wrong layer!", this); }
بالإضافة إلى ذلك ، يجب تجاهل تصرفات العدو من قبل مصادمات العدو. يمكن تحقيق ذلك عن طريق إضافة وسيطة قناع طبقة إلى
Physics.Raycast
في
GameBoard.GetTile
. تحتوي هذه الطريقة على نموذج يأخذ المسافة إلى الحزمة وقناع الطبقة كوسائط إضافية. سنمنحه الحد الأقصى للمسافة وقناع الطبقة افتراضيًا ، أي 1.
public GameTile GetTile (Ray ray) { if (Physics.Raycast(ray, out RaycastHit hit, float.MaxValue, 1)) { … } return null; }
لا ينبغي أن يكون قناع الطبقة 0؟مؤشر الطبقة الافتراضي هو صفر ، لكننا نمر قناع الطبقة. يغير القناع البتات الفردية للأعداد الصحيحة إلى 1 إذا كانت الطبقة في حاجة إلى التشغيل. في هذه الحالة ، تحتاج إلى تعيين البت الأول فقط ، أي الأقل أهمية ، مما يعني 2 0 ، أي ما يعادل 1.
تحديث محتوى البلاط
لا يمكن لأبراج أداء مهامها إلا عند تحديث حالتها. وينطبق الشيء نفسه على محتويات البلاط كله ، على الرغم من أن بقية المحتويات لا تفعل شيئًا حتى الآن. لذلك ، قم بإضافة طريقة افتراضية
GameTileContent
إلى
GameUpdate
، والتي لا تفعل شيئًا بشكل افتراضي.
public virtual void GameUpdate () {}
لنجعل
Tower
يعيد تعريفه ، حتى لو أنه يعرض الآن في وحدة التحكم أنه يبحث عن هدف.
public override void GameUpdate () { Debug.Log("Searching for target..."); }
يتعامل
GameBoard
مع
GameBoard
ومحتوياتها ، لذلك سيتابع أيضًا المحتوى المطلوب تحديثه. للقيام بذلك ، قم بإضافة القائمة إليه وطريقة
GameUpdate
العامة ، والتي تقوم بتحديث كل شيء في القائمة.
List<GameTileContent> updatingContent = new List<GameTileContent>(); … public void GameUpdate () { for (int i = 0; i < updatingContent.Count; i++) { updatingContent[i].GameUpdate(); } }
في البرنامج التعليمي لدينا تحتاج فقط إلى تحديث الأبراج. تغيير
ToggleTower
بحيث يضيف ويزيل المحتوى إذا لزم الأمر. إذا كانت هناك حاجة إلى محتوى آخر أيضًا ، فسنحتاج إلى مقاربة أكثر عمومية ، لكن في الوقت الحالي ، هذا يكفي.
public void ToggleTower (GameTile tile) { if (tile.Content.Type == GameTileContentType.Tower) { updatingContent.Remove(tile.Content); tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } else if (tile.Content.Type == GameTileContentType.Empty) { tile.Content = contentFactory.Get(GameTileContentType.Tower);
لإنجاز هذا العمل ، يكفي الآن تحديث الحقل في
Game.Update
. سنقوم بتحديث الحقل بعد الأعداء. بفضل هذا ، سوف تكون الأبراج قادرة على الهدف بالضبط أين هم الأعداء. إذا فعلنا خلاف ذلك ، فإن الأبراج تهدف حيث كان الأعداء في الإطار الأخير.
void Update () { … enemies.GameUpdate(); board.GameUpdate(); }
تهدف المدى
الأبراج لها دائرة نصف قطرها تهدف محدودة. لنجعلها مخصصة بإضافة حقل إلى فئة
Tower
. يتم قياس المسافة من وسط بلاطة البرج ، لذلك في حدود 0.5 ستغطي فقط البلاط الخاص بها. لذلك ، سيكون الحد الأدنى المعقول والنطاق القياسي هو 1.5 ، والذي يغطي معظم البلاط المجاور.
[SerializeField, Range(1.5f, 10.5f)] float targetingRange = 1.5f;
تهدف المدى 2.5.دعونا تصور مجموعة مع الأداة. لا نحتاج إلى رؤيته باستمرار ، لذلك
OnDrawGizmosSelected
أسلوب
OnDrawGizmosSelected
يدعى فقط للكائنات المحددة. نرسم الإطار الأصفر للكرة بنصف قطر يساوي المسافة ويتركز حول البرج. ضعه فوق الأرض قليلاً بحيث يكون واضحًا دائمًا.
void OnDrawGizmosSelected () { Gizmos.color = Color.yellow; Vector3 position = transform.localPosition; position.y += 0.01f; Gizmos.DrawWireSphere(position, targetingRange); }
الأداة تهدف المدى.الآن يمكننا أن نرى أي من الأعداء هو هدف ميسور التكلفة لكل برج. لكن اختيار الأبراج في نافذة المشهد غير مريح ، لأنه يتعين علينا اختيار أحد المكعبات الفرعية ، ثم التبديل إلى الكائن الجذر للبرج. أنواع أخرى من محتوى البلاط تعاني أيضا من نفس المشكلة. يمكننا فرض تحديد جذر محتوى
GameTileContent
في نافذة المشهد عن طريق إضافة سمة
SelectionBase
إلى
GameTileContent
.
[SelectionBase] public class GameTileContent : MonoBehaviour { … }
القبض على الهدف
أضف حقل
TargetPoint
إلى فئة
Tower
بحيث يمكنه تتبع الهدف الذي تم التقاطه. ثم نقوم
GameUpdate
للاتصال
AquireTarget
الجديد ، والذي يعرض معلومات حول ما إذا كان قد وجد الهدف. عند الكشف ، سيتم عرض رسالة في وحدة التحكم.
TargetPoint target; public override void GameUpdate () { if (AcquireTarget()) { Debug.Log("Acquired target!"); } }
في
AcquireTarget
نحصل على جميع الأهداف المتاحة عن طريق الاتصال بـ
Physics.OverlapSphere
مع وضع البرج ونطاق الوسيطات. ستكون النتيجة مصفوفة
Collider
تحتوي على كل المصادمات الملامسة للكرة. إذا كان طول المصفوفة موجبًا ، فهناك نقطة هدف واحدة على الأقل ، وببساطة نختار النقطة الأولى. خذ مكون
TargetPoint
، والذي يجب أن يكون موجودًا دائمًا ، قم بتعيينه إلى الحقل الهدف ثم أبلغ عن نجاح. خلاف ذلك ، نحن مسح الهدف والإبلاغ عن الفشل.
bool AcquireTarget () { Collider[] targets = Physics.OverlapSphere( transform.localPosition, targetingRange ); if (targets.Length > 0) { target = targets[0].GetComponent<TargetPoint>(); Debug.Assert(target != null, "Targeted non-enemy!", targets[0]); return true; } target = null; return false; }
نحن مضمونون للحصول على نقاط الهدف الصحيحة ، إذا أخذنا في الاعتبار المصادمات فقط على طبقة الأعداء. هذه هي الطبقة 9 ، لذلك سنقوم بتمرير قناع الطبقة المقابل.
const int enemyLayerMask = 1 << 9; … bool AcquireTarget () { Collider[] targets = Physics.OverlapSphere( transform.localPosition, targetingRange, enemyLayerMask ); … }
كيف تعمل هذه القناع؟بما أن طبقة العدو تحتوي على فهرس 9 ، فيجب أن يكون للقيمة العاشرة من قناع bitm القيمة 1. وهذا يتوافق مع عدد صحيح 2 9 ، وهو 512. لكن سجل قناع البت هذا غير سهل. يمكننا أيضًا كتابة حرفي ثنائي ، على سبيل المثال 0b10_0000_0000
، ولكن بعد ذلك علينا حساب الأصفار. في هذه الحالة ، سيكون الإدخال الأكثر ملاءمة هو استخدام عامل التشغيل لليسار الأيسر <<
، الذي ينقل البتات إلى اليسار. وهو ما يتوافق مع عدد في قوة اثنين.
يمكنك تصور الهدف الذي تم التقاطه عن طريق رسم خط gizmo بين مواقع البرج والهدف.
void OnDrawGizmosSelected () { … if (target != null) { Gizmos.DrawLine(position, target.Position); } }
تصور الأهداف.لماذا لا تستخدم طرقًا مثل OnTriggerEnter؟ميزة التحقق يدويًا من الأهداف الشاملة هي أنه لا يمكننا القيام بذلك إلا عند الضرورة. لا يوجد سبب للتحقق من الأهداف إذا كان البرج بالفعل واحد. بالإضافة إلى ذلك ، من خلال الحصول على جميع الأهداف المحتملة في وقت واحد ، لا يتعين علينا معالجة قائمة بالأهداف المحتملة لكل برج ، والذي يتغير باستمرار.
قفل الهدف
يعتمد الهدف الذي تم اختياره لالتقاطه على الترتيب الذي يمثله به المحرك الفعلي ، أي أنه في الحقيقة تعسفي. لذلك ، سوف يبدو أن الهدف الذي تم التقاطه يتغير دون سبب. بعد أن يستقبل البرج الهدف ، يكون من المنطقي لها تتبع مسارها ، وليس التحول إلى الآخر. أضف طريقة
TrackTarget
التي تنفذ مثل هذا التتبع وتعرض معلومات حول نجاحه. أولاً ، سنخبرك فقط إذا كان الهدف قد تم التقاطه.
bool TrackTarget () { if (target == null) { return false; } return true; }
سوف ندعو هذه الطريقة في
GameUpdate
وفقط عند الرجوع إلى false ، سوف نسمي
AcquireTarget
. إذا عادت الطريقة صحيحة ، فلدينا هدف. يمكن القيام بذلك عن طريق إجراء مكالمات من كلا الأسلوبين في
if
تحقق مع عامل التشغيل OR ، لأنه إذا تم إرجاع المعامل الأول
true
، فلن يتم التحقق من المكالمة ، وسيتم فقد المكالمة. يعمل عامل التشغيل AND بطريقة مماثلة.
public override void GameUpdate () { if (TrackTarget() || AcquireTarget()) { Debug.Log("Locked on target!"); } }
تتبع الأهداف.نتيجة لذلك ، يتم تثبيت الأبراج على الهدف حتى تصل إلى نقطة النهاية ويتم تدميرها. إذا كنت تستخدم الأعداء مرارًا وتكرارًا ، فأنت تحتاج بدلاً من ذلك إلى التحقق من صحة الرابط ، كما هو الحال مع روابط الأشكال التي تمت معالجتها في سلسلة من البرامج التعليمية
لإدارة الكائنات .
لتتبع الأهداف فقط عندما تكون ضمن النطاق ، يجب أن يتعقب
TrackTarget
المسافة بين البرج والهدف. إذا تجاوزت قيمة النطاق ، فيجب إعادة تعيين الهدف وإرجاع خطأ. يمكنك استخدام طريقة
Vector3.Distance
لهذا الاختيار.
bool TrackTarget () { if (target == null) { return false; } Vector3 a = transform.localPosition; Vector3 b = target.Position; if (Vector3.Distance(a, b) > targetingRange) { target = null; return false; } return true; }
ومع ذلك ، لا يأخذ هذا الرمز في الاعتبار نصف قطر المصادم. لذلك ، نتيجة لذلك ، قد يفقد البرج الهدف ، ثم يلتقطه مرة أخرى ، فقط لإيقاف تتبعه في الإطار التالي ، وهكذا. يمكننا تجنب ذلك عن طريق إضافة دائرة نصف قطرها المصادم إلى النطاق.
if (Vector3.Distance(a, b) > targetingRange + 0.125f) { … }
هذا يعطينا النتائج الصحيحة ، ولكن فقط إذا لم يتم تغيير حجم العدو. نظرًا لأننا نعطي كل عدو مقياسًا عشوائيًا ، يجب أن نأخذه في الاعتبار عند تغيير النطاق. للقيام بذلك ، نحتاج إلى تذكر النطاق الذي قدمه
Enemy
وفتحه باستخدام خاصية getter.
public float Scale { get; private set; } … public void Initialize (float scale, float speed, float pathOffset) { Scale = scale; … }
الآن يمكننا التحقق من النطاق الصحيح في
Tower.TrackTarget
.
if (Vector3.Distance(a, b) > targetingRange + 0.125f * target.Enemy€.Scale) { … }
نحن مزامنة الفيزياء
يبدو أن كل شيء يعمل بشكل جيد ، ولكن الأبراج التي يمكن أن تستهدف وسط الملعب قادرة على التقاط الأهداف التي يجب أن تكون خارج النطاق. لن يكونوا قادرين على تتبع هذه الأهداف ، بحيث يتم تثبيتها عليها فقط لإطار واحد.
تهدف غير صحيح.يحدث هذا بسبب تزامن حالة المحرك الفعلي مع حالة اللعبة. يتم إنشاء مثيلات جميع الأعداء في أصل العالم ، والذي يتزامن مع مركز الميدان. ثم ننقلهم إلى نقطة الخلق ، لكن محرك الفيزياء لا يعرف عنها على الفور.
يمكنك تمكين المزامنة الفورية التي تحدث عند تغيير تحويلات الكائنات عن طريق تعيين
Physics.autoSyncTransforms
إلى
true
. ولكن بشكل افتراضي ، يتم تعطيله ، لأنه أكثر فعالية بكثير لمزامنة كل شيء معًا وإذا لزم الأمر. في حالتنا ، لا يلزم التزامن إلا عند تحديث حالة الأبراج. يمكننا تنفيذه عن طريق استدعاء
Physics.SyncTransforms
بين تحديثات العدو والحقل في
Game.Update
.
void Update () { … enemies.GameUpdate(); Physics.SyncTransforms(); board.GameUpdate(); }
تجاهل الارتفاع
في الواقع ، لدينا اللعب يحدث في 2D. لذلك دعونا نغير
Tower
بحيث يأخذ الإحداثيون X و Z في الحسبان عند إحداث الهدف والتتبع. يعمل المحرك الفعلي في الفضاء ثلاثي الأبعاد ، ولكن في جوهره يمكننا تنفيذ
AcquireTarget
في 2D:
AcquireTarget
الكرة إلى أعلى بحيث يغطي كل المصادمات ، بغض النظر من موقعها الرأسي. يمكن القيام بذلك باستخدام كبسولة بدلاً من كرة ، النقطة الثانية ستكون عدة وحدات فوق سطح الأرض (على سبيل المثال ، ثلاث).
bool AcquireTarget () { Vector3 a = transform.localPosition; Vector3 b = a; by += 3f; Collider[] targets = Physics.OverlapCapsule( a, b, targetingRange, enemyLayerMask ); … }
أليس من الممكن استخدام محرك ثنائي الأبعاد؟, XZ, 2D- XY. , , 2D- . 3D-.
من الضروري أيضا أن تتغير TrackTarget
. بالطبع ، يمكننا استخدام نواقل ثنائية الأبعاد Vector2.Distance
، ولكن دعونا نفعل الحسابات بأنفسنا وبدلاً من ذلك سنقارن مربعات المسافات ، سيكون هذا كافياً. لذلك نتخلص من عملية حساب الجذر التربيعي. bool TrackTarget () { if (target == null) { return false; } Vector3 a = transform.localPosition; Vector3 b = target.Position; float x = ax - bx; float z = az - bz; float r = targetingRange + 0.125f * target.Enemy€.Scale; if (x * x + z * z > r * r) { target = null; return false; } return true; }
كيف تعمل حسابات الرياضيات هذه؟2D- , . , . , , .
تجنب تخصيص الذاكرة
Physics.OverlapCapsule
, . ,
OverlapCapsuleNonAlloc
. . . , 1.
OverlapCapsuleNonAlloc
, , .
static Collider[] targetsBuffer = new Collider[1]; … bool AcquireTarget () { Vector3 a = transform.localPosition; Vector3 b = a; by += 2f; int hits = Physics.OverlapCapsuleNonAlloc( a, b, targetingRange, targetsBuffer, enemyLayerMask ); if (hits > 0) { target = targetsBuffer[0].GetComponent<TargetPoint>(); Debug.Assert(target != null, "Targeted non-enemy!", targetsBuffer[0]); return true; } target = null; return false; }
, , . , .
لتوجيه البرج إلى الهدف ، Tower
يحتاج الفصل إلى ارتباط بمكون Transform
البرج. إضافة حقل التكوين لهذا وتوصيله إلى البرج الجاهزة. [SerializeField] Transform turret = default;
البرج المرفق.إذا كان GameUpdate
هناك هدف حقيقي ، فعلينا إطلاق النار عليه. ضع كود الرماية بطريقة منفصلة. اجعله يدور البرج نحو الهدف ، واصفًا طريقته Transform.LookAt
بنقطة الهدف كحجة. public override void GameUpdate () { if (TrackTarget() || AcquireTarget()) {
فقط تهدف.نحن اطلاق النار ليزر
لوضع شعاع الليزر ، Tower
يحتاج الفصل أيضًا إلى رابط إليه. [SerializeField] Transform turret = default, laserBeam = default;
وصلنا شعاع الليزر.لتحويل المكعب إلى حزمة ليزر حقيقية ، تحتاج إلى اتخاذ ثلاث خطوات. أولاً ، يجب أن يتوافق اتجاهه مع اتجاه البرج. يمكن القيام بذلك عن طريق نسخ دورته. void Shoot () { Vector3 point = target.Position; turret.LookAt(point); laserBeam.localRotation = turret.localRotation; }
ثانياً ، نقيس حزمة الليزر بحيث يساوي طولها المسافة بين نقطة المنشأ المحلية للبرج ونقطة الهدف. نقوم بتوسيع نطاقه على طول المحور Z ، أي المحور المحلي الموجه نحو الهدف. للحفاظ على مقياس XY الأصلي ، نكتب المقياس الأصلي عندما نستيقظ على برج Awake. Vector3 laserBeamScale; void Awake () { laserBeamScale = laserBeam.localScale; } … void Shoot () { Vector3 point = target.Position; turret.LookAt(point); laserBeam.localRotation = turret.localRotation; float d = Vector3.Distance(turret.position, point); laserBeamScale.z = d; laserBeam.localScale = laserBeamScale; }
ثالثًا ، نضع شعاع الليزر في الوسط بين البرج ونقطة الهدف. laserBeam.localScale = laserBeamScale; laserBeam.localPosition = turret.localPosition + 0.5f * d * laserBeam.forward;
اطلاق النار بالليزر.أليس من الممكن جعل شعاع الليزر طفل برج؟, , forward. , . .
هذا يعمل بينما يتم تثبيت البرج على الهدف. لكن عندما لا يكون هناك هدف ، يظل الليزر نشطًا. يمكننا إيقاف تشغيل عرض الليزر عن طريق GameUpdate
ضبط حجمه على 0. public override void GameUpdate () { if (TrackTarget() || AcquireTarget()) { Shoot(); } else { laserBeam.localScale = Vector3.zero; } }
أبراج الخمول لا تطلق النار.صحة العدو
حتى الآن ، أشعة الليزر لدينا تلمس فقط الأعداء ولم تعد تؤثر عليهم. من الضروري التأكد من أن الليزر يلحق الضرر بالأعداء. لا نريد تدمير الأعداء على الفور ، لذلك سنمنح Enemy
خاصية الصحة. يمكنك اختيار أي قيمة كصحة ، لذلك دعونا نأخذ 100. ولكن سيكون من المنطقي أكثر للأعداء الكبار أن يتمتعوا بمزيد من الصحة ، لذلك سنقدم معاملًا لذلك. float Health { get; set; } … public void Initialize (float scale, float speed, float pathOffset) { … Health = 100f * scale; }
لإضافة دعم للتعامل الضرر ، إضافة طريقة عامة ApplyDamage
يطرح المعلمة من الصحة. سنفترض أن الضرر غير سلبي ، لذلك نضيف بيانًا حول هذا. public void ApplyDamage (float damage) { Debug.Assert(damage >= 0f, "Negative damage applied."); Health -= damage; }
لن نتخلص على الفور من العدو بمجرد وصول صحته إلى الصفر. سيتم التحقق من استنفاد الصحة وتدمير العدو في البداية GameUpdate
. public bool GameUpdate () { if (Health <= 0f) { OriginFactory.Reclaim(this); return false; } … }
وبفضل هذا ، سيتم إطلاق النار على جميع الأبراج بشكل متزامن ، وليس بدوره ، مما سيسمح لهم بالانتقال إلى أهداف أخرى إذا كان البرج السابق قد دمر العدو ، والذي كان الهدف منه أيضًا.الضرر في الثانية الواحدة
الآن نحن بحاجة إلى تحديد مقدار الضرر الذي ستحدثه الليزر. للقيام بذلك ، أضف إلى Tower
حقل التكوين. نظرًا لأن حزمة الليزر تتعرض لأضرار مستمرة ، فإننا نعبر عنها كضرر في الثانية. نحن Shoot
نطبقها على المكون Enemy
الهدف مع الضرب بحلول وقت دلتا. [SerializeField, Range(1f, 100f)] float damagePerSecond = 10f; … void Shoot () { … target.Enemy.ApplyDamage(damagePerSecond * Time.deltaTime); }
تلف كل برج 20 وحدة في الثانية الواحدة.تهدف عشوائي
نظرًا لأننا نختار دائمًا الهدف الأول المتاح ، يعتمد سلوك التصويب على الترتيب الذي يتحقق به محرك الفيزياء من المصادم المتقاطع. هذا الاعتماد ليس جيدًا للغاية ، لأننا لا نعرف التفاصيل ، لا يمكننا التحكم فيه ، علاوة على ذلك ، سيبدو غريبًا وغير متسق. غالبًا ما يؤدي هذا السلوك إلى نيران مركزة ، لكن هذا ليس هو الحال دائمًا.بدلاً من الاعتماد بالكامل على محرك الفيزياء ، دعنا نضيف بعض العشوائية. يمكن القيام بذلك عن طريق زيادة عدد التقاطعات التي تتلقاها المصادمات ، على سبيل المثال ، ما يصل إلى 100. ربما لن يكون هذا كافياً للحصول على جميع الأهداف الممكنة في حقل مليء بالأعداء ، ولكن هذا سيكون كافياً لتحسين الهدف. static Collider[] targetsBuffer = new Collider[100];
الآن ، بدلاً من اختيار الهدف المحتمل الأول ، سنختار عنصرًا عشوائيًا من الصفيف. bool AcquireTarget () { … if (hits > 0) { target = targetsBuffer[Random.Range(0, hits)].GetComponent<TargetPoint>(); … } target = null; return false; }
تهدف عشوائي.هل يمكن استخدام معايير أخرى لاختيار الأهداف؟, , . , , . . .
لذلك ، في لعبة برج الدفاع ، ظهرت الأبراج أخيرًا. في الجزء التالي ، سوف تأخذ اللعبة شكلها النهائي أكثر.