الفصل الأول: برمجة أنظمة التصويب

العب

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

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

الشكل 30: المركبة الفضائية الخاصة بتصويب المقذوفات

الشكل 30: المركبة الفضائية الخاصة بتصويب المقذوفات

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

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

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

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class Target : MonoBehaviour {
5.  
6.  //حالما تتم إصابة الهدفtrue سيقوم بريمج الطلقة بتغيير هذه القيمة إلى
7.  public bool hit = false;
8.  
9.  //Destroy()بمجرد أن تقوم باستدعاء الدّالّة true غيّر هذه القيمة إلى
10.     bool destroyed = false;
11.     
12.     void Start () {
13.         
14.     }
15.     
16.     void Update () {
17.         if(hit){
18.             //أصابت الطلقة الهدف، لذا قم بتشغيل حركة التدمير وهي عبارة عن دوران
19.             //وتصغير في الحجم
20.             transform.Rotate(0, 720 * Time.deltaTime, 0);
21.             transform.localScale -= Vector3.one * Time.deltaTime;
22.             
23.             //لنقم باستدعائها الآن Destroy() إن لم يسبق لنا استدعاء
24.             if(!destroyed){
25.                 //قم بتأجيل التدمير ثانية واحدة حتى يتسنى 
26.                 //للاعب مشاهدة الحركة
27.                 Destroy(gameObject, 1);
28.                 //destroyed قم بتعديل قيمة المتغير 
29.                 //أكثر من مرة Destroy() حتى تمنع استدعاء
30.                 destroyed = true;
31.             }
32.         }
33.     }
34. }

السرد 15: البريمج الخاص بالهدف

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

التدوير يتم باستخدام الدّالّة ()transform.Rotate والتي سبق لنا التعامل معها عدة مرات، أما تصغير الحجم فيتم بطرح قيمة بسيطة من القياس في كل إطار وتساوي متجها تساوي قيم مكوّناته Time.deltaTime، أي أن عملية التصغير تتم بسرعة تعادل مترا واحدا في الثانية مما يجعل الهدف يختفي بعد ثانية واحد حيث سيصبح حجمه صفرا. لهذا السبب نقوم في السطر 27 باستدعاء الدّالّة ()Destroy ونعطيها الكائن الذي نريد تدميره (وهو كائن الهدف نفسه في هذه الحالة ونصل إليه عن طريق المتغير gameObject). بالإضافة للكائن الذي سنقوم بتدميره يمكننا أن نزود ()Destroy بالوقت الذي عليها انتظاره قبل تنفيذ التدمير وهو هنا ثانية واحدة.

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

لنقم الآن بإضافة بريمج آخر على كائن الهدف وستكون مهمته تحريك الهدف حتى لا يظل ساكنا في مكانه. هذه الحركة ستعتمد على الوقت فقط وليس على مدخلات اللاعب كما فعلنا عدة مرات، وذلك لأن اللاعب لا يتحكم بالأهداف بل تتحرك من تلقاء نفسها. سيحرك البريمج الهدف في اتجاه محدد وبسرعة ثابتة. السرد 16 يوضح البريمج ِAutoMover والذي يقوم بتحريك الكائن تلقائيا حسب السرعة المحدد في متجه السرعة. حين يغادر الكائن مجال رؤية الكاميرا من جهة ما سنقوم بإعادته الجهة المقابلة. هذه الحركة تسمى الالتفاف wrapping وهي مشهورة في كثير من الألعاب كما في اللعبة الكلاسيكية الشهيرة Pac-Man. السرد 17 يوضح البريمج Wrapper والذي يقوم بتدوير الكائن حول الشاشة بناء على موقعه على المحورين x و z.

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class AutoMover : MonoBehaviour {
5.  
6.  //سرعة الحركة على المحاور الثلاث
7.  public Vector3 speed = new Vector3(0, 0, 0);
8.  
9.  void Start () {
10.     
11.     }
12.     
13.     void Update () {
14.         //نقوم بتحريك الكائن بناء على السرعة المحددة
15.         transform.Translate(speed * Time.deltaTime);    
16.     }
17. }

السرد 16: بريمج يقوم بتحريك الكائن بسرعة ثابتة وبشكل تلقائي

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class Wrapper : MonoBehaviour {
5. 
6.  //عندما يتجاوز الكائن هذه الحدود على المحاور المختلفة
7.  //سنقوم بتدويره حول المشهد وإدخاله من الجهة المقابلة
8.  public Vector3 limits = new Vector3(10, 0, 10);
9.  
10.     void Start () {
11.     
12.     }
13.     
14.     void Update () {
15.         //نقوم بأخذ الموقع الحالي
16.         Vector3 newPos = transform.position;
17.         
18.         if(transform.position.x > limits.x){
19.             //الكائن تجاوز الحد الأيمن بالتالي نعيده لليسار
20.             newPos.x = -limits.x;
21.         }
22.         
23.         if(transform.position.x < -limits.x){
24.             //الكائن تجاوز الحد الأيسر بالتالي نعيده لليمين
25.             newPos.x = limits.x;
26.         }
27.         
28.         if(transform.position.z > limits.z){
29.             //الكائن تجاوز الحد نحو الأمام فنعيده للخلف
30.             newPos.z = -limits.z;
31.         }
32.         
33.         //نقوم بتحديث موقع الكائن حسب القيم الجديدة
34.         transform.position = newPos;
35.     }
36. }

السرد 17: البريمج الخاص بالالتفاف حول المشهد عند خروج الكائن خارج حدود الكاميرا

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

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

الشكل 31: كيفية استخدام كائن لإنشاء قالب جديد

الشكل 31: كيفية استخدام كائن لإنشاء قالب جديد

سأتطرق الآن لموضوع مهم يتعلق بكيفية تصميم البرمجيات أثناء التعامل مع Unity. في العادة نستخدم البرمجة غرضية التوجه Object-Oriented Programming مع الوراثة Inheritance وذلك لجعل عملة إعادة الاستخدام أكثر سهولة. أما في Unity فلا نستعمل عادة هذا التوجه ونستبدله بالتركيب composition وهو عبارة عن فصل الوظائف والصفات في بريمجات مختلفة وتركيبها مع بعضها البعض بتوافق يحقق الغرض المطلوب من كل كائن. على سبيل المثال لدينا ثلاث وظائف منفصلة وهي: إصابة الهدف، والحركة التلقائية، والالتفاف. بفصل هذه الوظائف عن بعضها، يمكننا مستقبلا بسهولة بناء كائنات قابلة للإصابة لكنها لا تتحرك، وذلك عن طريق استخدام Target فقط، كما يمكننا بناء كائنات تتحرك لكنها غير قابلة للإصابة إذا استخدمت AutoMover فقط، وغيرها من التراكيب المختلفة.

لنعد الآن إلى مشهدنا لبناء كائن الطلقة ومن ثم استعماله لإنشاء قالب. الطلقة التي سنستعملها هي عبارة عن كائن من نوع كرة بقياس (0.25 ,0.25 ,0.25) وكل ما سنفعله هو أننا سنضيف لهذه الكرة بريمجين: أحدهما Projectile الموضح في السرد 18 ومهمته دفع الكائن للحركة نحو الأمام بمدى محدد، والثاني هو TargetHitDestroyer ومهمته فحص التصادم بين الطلقة وجميع الأهداف الموجودة في المشهد حتى يقوم بتدمير الهدف في حال حدث التصادم. هذا الأخير موضح في السرد 19.

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class Projectile : MonoBehaviour {
5.  
6.  //سرعة المقذوف مقدرة بمتر \ ثانية
7.  public float speed = 15;
8.  
9.  //المدى: وهو عدد الأمتار التي يمكن للمقذوف قطعها قبل أن يتم تدميره
10.     public float range = 20;
11.     
12.     //نستخدم هذا المتغير لحساب المسافة الكلية التي قطعها المقذوف منذ إطلاقه
13.     //عندما تتجاوز هذه القيمة المدى المحدد للمقذوف نقوم بتدميره
14.     float totalDistance = 0;
15.     
16.     void Start () {
17.     
18.     }
19.     
20.     void Update () {
21.         //المسافة التي سيقطعها المقذوف خلال الإطار الحالي
22.         float distance = speed * Time.deltaTime;
23.         //z نقوم بتحريك المقذوف نحو الأمام على محوره المحلي 
24.         transform.Translate(0, 0, distance);
25.         
26.         //نقوم بإضافة المسافة المقطوعة خلال الإطار الحالي إلى المسافة الكلية
27.         totalDistance += distance;
28.         
29.         //عندما تتجاوز المسافة الكلية المدى المحدد للمقذوف
30.         //يجب أن نقوم بتدمير المقذوف
31.         if(totalDistance > range){
32.             Destroy(gameObject);
33.         }
34.     }
35. }

السرد 18: البريمج الخاص بتحريك المقذوف

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class TargetHitDestroyer : MonoBehaviour {
5. 
6.  void Start () {
7.  
8.  }
9.  
10.     void Update () {
11.         //Target البحث عن جميع الكائنات التي تحتوي على البريمج 
12.         Target[] allTargets = FindObjectsOfType<Target>();
13.         
14.         //فحص التصادم بين المقذوف وجميع الأهداف
15.         foreach(Target t in allTargets){
16.             //نهتم فقط بأمر الأهداف التي لم يسبق وتمت إصابتها
17.             if(!t.hit){
18.                 //نقيس المسافة بين الهدف والمقذوف
19.                 float distance = Vector3.Distance(
20.                                 transform.position, 
21.                                 t.transform.position);
22. 
23.                 if(distance < 
24.                     t.transform.localScale.magnitude * 0.5f){
25.                     //hit المقذوف يتصادم مع الهدف، لذا سنقوم بتغيير قيمة 
26.                     //true في المقذوف إلى
27.                     t.hit = true;
28.                     
29.                     //أخيرا نقوم بتدمير المقذوف
30.                     Destroy(gameObject);
31.                 
32.                 }
33.             }
34.         }
35.     }
36. }

السرد 19: البريمج الخاص بكشف التصادم بين المقذوف والهدف

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

في السطر 12 أيضا قمنا باستدعاء الدّالّة >FindObjectsOfType<Target والتي تقوم بالبحث في المشهد عن جميع الكائنات التي تحتوي على البريمج Target وتعيدها لنا على شكل مصفوفة لنقوم بتخزينها في allTargets. الخطوة التالية في السطر 15 هي استخدام الحلقة التكرارية foreach والتي تسهل علينا التعامل مع المصفوفات وغيرها من مجموعات الكائنات. في الحلقة التكرارية قمنا بتعريف المتغير t والذي ستتغير قيمته في كل دورة حتى تمر على جميع عناصر المصفوفة. بمعنى أن الخطوات من السطر 17 إلى السطر 32 ستتكرر على جميع الأهداف في المشهد. فإذا كان لدينا عشرة أهداف ستتكرر هذه الخطوات عشر مرات كحد أقصى وإن كان لدينا عشرون فعشرون وهلم جرا.

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

في الأسطر من 19 إلى 21 قمنا بحساب المسافة بين الهدف والكائن الحالي وهو بطبيعة الحال مقذوف. بعد ذلك نقارن المسافة بين المقذوف والهدف بالقيمة t.transform.localScale.magnitude والتي تمثل طول أحد أضلاع المكعب ثم نضرب هذه القيمة في 0.5 وذلك حتى تعطينا قيمة تقريبية للمسافة بين مركز المكعب وسطحه. قلت "تقريبية" لأن المكعب ليس له مسافة ثابتة بين مركزه وجميع النقاط على سطحه، فالنقاط القريبة من منتصفات وجوه المكعب تكون أقرب إلى المركز من تلك التي على أطراف الأوجه. على أي حال ستعطينا هذه الطريقة نتيجة مقبولة للعبتنا وهذا لا ينفي أننا سنتعرف على طرق أخرى أكثر دقة. إذا تحقق الشرط يعني ذلك أن المسافة بين المقذوف والهدف قصيرة كفاية لتحتسب على أنها تصادم، بالتالي نقوم في السطر 27 بتغيير قيمة المتغير hit إلى true حتى يدخل الهدف في حركة السقوط، ومن ثم نقوم في السطر 30 بتدمير كائن الطلقة فوريا حتى لا يتعدى هذا الهدف لغيره.

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

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

الخطوة التالية هي إنشاء قالب الصاروخ، وهو مثل الطلقة لكن له القدرة على ملاحقة الهدف بخلاف الطلقة التي تسير بخط مستقيم فقط. لتمثيل الصاروخ قم بإنشاء مكعب بحجم (0.75 ,0.1 ,0.1) ومن ثم أضف له البريمجين Projectile و TargetHitDestroyer وذلك حتى يتحرك للأمام ويدمر الأهداف حين يصيبها. الوظيفة الثالثة التي يقوم بها الصاروخ هي البحث عن أقرب هدف له لحظة الانطلاق والتركيز عليه وملاحقته إلى أن يصيبه أو يقطع المدى المحدد له دون إصابته بالتالي يدمر الصاروخ تلقائيا. هذه الوظيفة سنكتبها في البريمج TargetFollower والذي يظهره السرد 20.

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class TargetFollower : MonoBehaviour {
5.  
6.  //الهدف الذي سنقوم بتتبعه
7.  Target currentTarget;
8.  
9.  void Start () {
10.         //سنقوم بالبحث في كافة الأهداف عن أقربها إلينا
11.         Target[] allTargets = FindObjectsOfType<Target>();
12.         
13.         //تحقق من وجود أهداف في المشهد
14.         if(allTargets.Length > 0){
15.             //سنفرض مبدئيا أن الهدف الأول في المصفوفة هو الأقرب إلينا
16.             Target nearest = allTargets[0];
17.             
18.             //نبدأ الآن بالبحث عن أقرب هدف
19.             foreach(Target t in allTargets){
20.                 //إن كان الهدف مصابا من قبل
21.                 //لن نهتم لأمره
22.                 if(!t.hit){
23.                     //المسافة بين المقذوف 
24.                     //t والهدف الحالي 
25.                     float distance = 
26.                         Vector3.Distance(
27.                             transform.position, 
28.                             t.transform.position);
29.                     
30.                     //نحسب المسافة بين المقذوف 
31.                     //nearest والهدف الأقرب 
32.                     float minDistance = 
33.                         Vector3.Distance(
34.                             transform.position,
35.                             nearest.transform.position);
36.                     
37.                     //نقوم بتحديث قيمة الهدف الأقرب إن لزم الأمر
38.                     if(distance < minDistance){
39.                         nearest = t;
40.                     }
41.                 }
42.             }
43.             
44.             //نحدد الهدف الأقرب على أنه الهدف الحالي
45.             currentTarget = nearest;
46.         }
47.     }
48.     
49.     void Update () {
50.         //نتأكد أولا من أن الهدف الحالي الذي نتبعه موجود أصلا ولم تتم إصابته
51.         //من قبل صاروخ أو طلقة أخرى
52.         if(currentTarget != null && !currentTarget.hit){
53.             //نقوم بتدوير المقذوف لينظر إلى الهدف الذي نتبعه
54.             transform.LookAt(currentTarget.transform.position);
55.         }
56.     }
57. }

السرد 20: البريمج الذي يسمح للصاروخ بالبحث عن أقرب هدف وتتبعه

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

بعد ذلك نقوم مبدئيا بافتراض أن أقرب الأهداف هو هو الأول في المصفوفة والذي نصل إليه عن طريق تحديد الموقع كما في [0]allTargets. وهذا يعني أنني أريد أن أستخرج العنصر الأول في المصفوفة. في معظم لغات البرمجة ومن ضمنها #C التي نستعملها، يكون الموقع الأول في المصفوفة هو صفر، فإذا كان في المصفوفة 10 عناصر مثلا، سيكون العنصر الأخير في الموقع 9. نقوم بتخزين الهدف الأول في المتغير nearest وتعني بالعربية الأقرب، بعدها ندخل في حلقة تكرارية تمر على جميع الأهداف الموجودة في المصفوفة وذلك في السطر 19. وبما أننا نهتم فقط بالأهداف التي لم تصب حتى الآن، نقوم في السطر 22 بالتأكد من عدم إصابة الهدف قبل أن نقوم بحساب أي مسافة بينه وبين المقذوف.

المتغير distance الذي نعرفه في السطر 25 نخزن فيه المسافة بين الهدف الحالي وبين المقذوف، بينما نخزن في المتغير minDistance في السطر 32 المسافة بين المقذوف وبين الهدف الذي نفترض أنه الأقرب حتى الآن. بعد ذلك نقارن هاتين المسافتين في السطر 38، فإن تبين لنا أن المسافة distance أقل من minDistance فهذا يعني أن الهدف الحالي أقرب مسافة من الهدف المخزن حاليا في nearest، بالتالي نقوم بتحديث قيمة nearest لتأخذ القيمة الجديدة، وهكذا نمر على كل الأهداف في المصفوفة، لنحصل في النهاية على الهدف الأقرب مخزنا في المتغير nearest. بعد الانتهاء من قياس المسافات ومقارنتها نقوم أخيرا في السطر 45 بتخزين قيمة nearest في currentTarget حتى يصبح هو الهدف الذي نتتبعه.

الدّالّة ()Update تقوم بتنفيذ عملية التتبع على خطوتين، الخطوة الأولى هي أن تتأكد من وجود هدف أصلا حتى تقوم بتتبعه، وذلك في السطر 52 حيث تتأكد من أن قيمة currentTarget لا تساوي null. المقصود بـ null هو عدم وجود قيمة، أي أن currentTarget عديم القيمة ولم يتم تخزين أي هدف فيه. هذا الأمر ممكن الحدوث في حال تم إطلاق الصاروخ دون وجود أي هدف في المشهد مما يجعل الهدف الأقرب غير موجود أصلا. الخطوة الثانية هي أن تتأكد من أن الهدف الذي تقوم بتتبعه لا زال موجودا ولم تتم إصابته عن طريق طلقة أخرى أو صاروخ آخر. عندما يتحقق هذان الشرطان يتم توجيه المقذوف نحو الهدف عن طريق ()transform.LookAt. هناك ملاحظة مهمة أود الإشارة إليها هنا وهي أن العملية && تسمح بتنفيذ جملة if فقط في حال تحقق الشرطان على طرفي العملية، وفي حالة لم يتحقق الشرط الأول، فإنها لا تحاول أن تفحص الشرط الثاني. مبدأ العمل هذا مهم بالنسبة لنا في هذه الحالة، حيث لا يمكننا أن نفحص قيمة currentTarget.hit في الوقت الذي تكون فيه currentTarget نفسها عديمة القيمة، وإلا فإن خطأ سيحدث أثناء التشغيل.

بعد إضافة TargetFollower إلى الصاروخ أصبح جاهزا لاستخدامه لبناء القالب، ولا تنسى حذف كائن الصاروخ المستخدم من المشهد بعد عمل القالب. بالتالي أصبح لدينا الآن ثلاث قوالب وهي الهدف Target والصاروخ Rocket والطلقة Bullet. لإكمال بناء المشهد، قم بإضافة مجموعة من الأهداف وأعطها سرعات متفاوتة. يمكن مثلا إضافة صفين من الأهداف وإعطاء عناصر أحدهما السرعة (0 ,0 ,3) في البريمج AutoMover حتى تتحرك نحو اليمين والصف الآخر السرعة (0 ,0 ,3-) حتى تتحرك نحو اليسار.

لننتقل الآن إلى بريمج التحكم بالمركبة وهو بريمج بسيط كما ترى في السرد 21. ويقوم بوظيفة واحدة هي قراءة حالة مفاتيح الأسهم وتحريك المركبة على المحورين x و z بناء عليها.

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class ShuttleControl : MonoBehaviour {
5.  
6.  //سرعة حركة المركبة
7.  public float speed = 7;
8.  
9.  void Start () {
10.     
11.     }
12.     
13.     void Update () {
14.         //قراءة حالة مفاتيح الأسهم
15.         //وتحريك المركبة بناء عليها
16.         if(Input.GetKey(KeyCode.UpArrow)){
17.             transform.Translate(0, 0, speed * Time.deltaTime);
18.         } else if(Input.GetKey(KeyCode.DownArrow)){
19.             transform.Translate(0, 0, -speed * Time.deltaTime);
20.         }
21.         
22.         if(Input.GetKey(KeyCode.RightArrow)){
23.             transform.Translate(speed * Time.deltaTime, 0, 0);
24.         } else if(Input.GetKey(KeyCode.LeftArrow)){
25.             transform.Translate(-speed * Time.deltaTime, 0, 0);
26.         }
27.     }
28. }

السرد 21: بريمج التحكم بالمركبة

بالإضافة إلى ShuttleControl سأقوم أيضا بإضافة كل من البريمجين Wrapper و TargetHitDestroyer إلى كائن المركبة، مما سيجعلها تلتف من اليمين إلى اليسار وبالعكس، بالإضافة إلى التفافها من الأمام إلى الخلف كما سبق شرحه في السرد 17. إضافة TargetHitDestroyer ستجعل المركبة تتحطم إذا لامست أحد الأهداف (أي أن كائنها سيتم تدميره وحذفه من المشهد). بالإضافة إلى هذه البريمجات الثلاثة، سنضيف كلا من BulletShooter و RocketLauncher. مهمة هذين البريمجين متشابهة، حيث أنهما يقومان بإضافة كائن جديد إلى المشهد (طلقة أو صاروخ) بناء على مدخلات اللاعب، وهناك اختلافات بينهما من حيث كيفية تكرار عملية الإطلاق كما سنرى. لنبدأ مع BulletShooter والموضح في السرد 22.

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class BulletShooter : MonoBehaviour {
5.  
6.  //القالب الذي سنستخدمه لبناء الطلقات الجديدة
7.  public GameObject bullet;
8.  
9.  //كم من الثواني يجب أن تمر بين كل طلقتين متتابعتين؟
10.     public float timeBetweenBullets = 0.2f;
11.     
12.     //متى قامت المركبة بإطلاق آخر طلقة؟
13.     float lastBulletTime = 0;
14.     
15.     void Start () {
16.     
17.     }
18.     
19.     void Update () {
20.         //الأيسر لإطلاق الطلقات control سنستخدم مفتاح
21.         if(Input.GetKey(KeyCode.LeftControl)){
22.             //هل مضى الوقت الكافي منذ آخر طلقة تم أطلاقها؟
23.             if(Time.time - lastBulletTime > timeBetweenBullets){
24.                 //نقوم بإنشاء طلقة جديدة مستخدمين القالب
25.                 //موقع الطلقة هو نفس موقع المركبة الحالي
26.                 //وستنظر الطلقة في نفس الاتجاه الذي تنظر إليه المركبة
27.                 Instantiate(bullet, //الكائن الذي سنقوم بإنشائه
28.                    transform.position, //موقع الكائن
29.                    transform.rotation);//دوران الكائن
30.                 
31.                 //نقوم بتسجيل الوقت الذي تمت فيه عملية إطلاق الطلقة
32.                 lastBulletTime = Time.time;
33.             }
34.         }
35.     }
36. }

السرد 22: بريمج إطلاق الطلقات من المركبة

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

الشكل 32: كيفية ربط القالب بمتغير داخل البريمج

الشكل 32: كيفية ربط القالب بمتغير داخل البريمج

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

بعد هذا قمنا باستدعاء دالّة جديدة هي ()Instantiate، والتي تقوم بعمل نسخة جديدة من القالب في كل مرة نستدعيها. إضافة إلى القالب bullet، قمنا بتزويد الدّالّة بكل من موقع إنشاء النسخة الجديدة ودورانها، وهي في هذه الحالة نفس موقع المركبة ونفس دورانها، مما يجعل الطلقة تبدو وكأنها خرجت من المركبة ويجعلها تنظر تلقائيا في اتجاه مقدمة المركبة. بعد بناء النسخة الجديدة أصبح لدينا كائن طلقة جديد في المشهد، وهذا الكائن يحتوي على كافّة البريمجات التي أضفناها للقالب Bullet، بالتالي سيقوم هذا الكائن بتنفيذ السلوك المطلوب حيث سيتحرك نحو الأمام ويحسب المدى المقطوع ويفحص التصادم مع الأهداف. أخيرا نقوم في السطر 32 بتسجيل الوقت الذي تم فيه إطلاق الطلقة حتى نتمكن من حساب تردد الطلقات ومنع تكرار الإطلاق بوتيرة أعلى من الحد الأقصى.

البريمج الثاني الذي سنضيفه هو RocketLanuncher والخاص بإطلاق الصواريخ. هذا البريمج شبيه بسابقه باستثناء بعض الفروقات. السرد 23 يوضح هذا البريمج.

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class RocketLauncher : MonoBehaviour {
5.  
6.  //القالب الذي سنستخدمه لإنشاء الصواريخ
7.  public GameObject rocket;
8.  
9.  void Start () {
10.     
11.     }
12.     
13.     void Update () {
14.         //نستخدم مفتاح المسافة لإطلاق الصواريخ دون السماح بالضغط المستمر عليه
15.         if(Input.GetKeyDown(KeyCode.Space)){
16.             
17.             //كم عدد الصواريخ الموجودة في المشهد؟
18.             TargetFollower[] rockets = 
19.                 FindObjectsOfType<TargetFollower>();
20.             
21.             //لا نسمح بوجود أكثر من صاروخ في المشهد في نفس الوقت
22.             if(rockets.Length == 0){
23.                 //قم بإنشاء صاروخ جديد في نفس موقع المركبة
24.                 //باستخدام نفس دورانها
25.                 Instantiate(rocket, 
26.                     transform.position, transform.rotation);
27.             }
28.         }
29.     }
30. }

السرد 23: بريمج إطلاق الصواريخ من المركبة

الملاحظة المهمة في هذا البريمج هي أنه لا يسمح بإطلاق الصاروخ طالما وُجد صاروخ آخر في المشهد. يتم التعرف على عدد الصواريخ الموجودة في المشهد عن طريق البحث عن كائنات تحتوي على البريمج TargetFollower، السبب في اختيار هذا البريمج تحديدا هو أنه لا يوجد إلّا في كائن الصاروخ، بالتالي فإن عدد الكائنات في المشهد التي تحتوي على هذا البريمج هو نفس عدد الصواريخ بالتأكيد. أما لو قمنا بالبحث عن كائنات من نوع Projectile مثلا، فإننا سنحصل على كائنات الطلقات والصواريخ معا وهو ما لا نريده هنا. الشكل 33 يعرض صورة من اللعبة بعد إتمامها، ويمكنك مشاهدة النتيجة أيضا في المشهد scene9 في المشروع المرفق.

الشكل 33: لعبة المركبة الخاصة بعرض كيفية برمجة المقذوفات والطلقات

الشكل 33: لعبة المركبة الخاصة بعرض كيفية برمجة المقذوفات والطلقات

السابقالتالي

تعليقات واستفسارات