الفصل الثاني: برمجة الأشياء القابلة للجمع

العب

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

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

بمجرد أن يلمس أحد المجمعين أحد الكائنات القابلة للجمع، فإنه يحاول أن "يأخذ" هذا الكائن أو "يجمعه"، هذه المحاولة قد تنجح وقد تفشل اعتمادا على عدة عوامل كما سنرى. لشرح فكرة المجمّع وما يمكن أن يجمعه، سأستخدم كرة لتعبر عن اللاعب. هذه الكرة ستتدحرج فوق أرضية خشبية ويمكن للاعب أن يراها من منظور علوي ويحركها باستخدام الأسهم على المحورين x و z. تمتلك هذه الكرة القدرة على جمع القطع النقدية بمجرد لمسها، وتقوم بإضافة هذه القطع إلى حقيبة محتوياتها. بالإضافة للقطع النقدية، يمكن للكرة أن تجمع نوعين من "الطعام". هذا الطعام يعمل على زيادة حجم الكرة لفترة زمنية محددة، مما يجعل عملية تجميع القطع أسهل وأسرع. لنبدأ إذن بكرة ذات قياس أولي (1 ,1 ,1) وإكساء زجاجي، وأرضية عبارة عن لوح بقياس (10 ,1 ,10) ذات إكساء خشبي. بعد بناء المشهد كما في الشكل 34، قم بوضع الكاميرا فوق الكرة تماما وبارتفاع 15 ثم قم بتدويرها لتنظر نحو الأسفل.

الشكل 34: الكرة الزجاجية التي سنستعملها كشخصية للاعب

الشكل 34: الكرة الزجاجية التي سنستعملها كشخصية للاعب

لنتحكم بالكرة سنقوم بكتابة البريمج BallRoller والموضح في السرد 24. إضافة لذلك سنحتاج للبريمج ObjectTracker والذي سنقوم بإضافته للكاميرا وذلك حتى تتمكن من تتبع حركة الكرة واللحاق بها. السرد 25 يوضح البريمج ObjectTracker. أخيرا، لا تنسى أن تقوم بإسناد كائن الكرة للمتغير objectToTrack في ObjectTracker، وذلك حتى تعرف الكاميرا ما هو الكائن الذي عليها أن تتبعه.

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class BallRoller : MonoBehaviour {
5.  
6.  //z و x سرعة الحركة على المحورين 
7.  public float moveSpeed = 5;
8.  
9.  //سرعة الدحرجة
10.     public float rollSpeed = 360;
11.     
12.     void Start () {
13.     
14.     }
15.     
16.     void Update () {
17.         //z وتدحرج على محور الفضاء x تحرك على محور الفضاء 
18.         if(Input.GetKey(KeyCode.UpArrow)){
19.             transform.Translate(0, 0, 
20.                 moveSpeed * Time.deltaTime, Space.World);
21.             
22.             transform.Rotate(rollSpeed * Time.deltaTime, 
23.                             0, 0, Space.World);
24.         } else if(Input.GetKey(KeyCode.DownArrow)){
25.             transform.Translate(0, 0, 
26.                 -moveSpeed * Time.deltaTime, Space.World);
27.             
28.             transform.Rotate(-rollSpeed * Time.deltaTime, 
29.                             0, 0, Space.World);
30.         }
31.         
32.         //x وتدحرج على محور الفضاء z تحرك على محور الفضاء 
33.         if(Input.GetKey(KeyCode.RightArrow)){
34.             transform.Translate(moveSpeed * Time.deltaTime,
35.                                 0, 0, Space.World);
36.             
37.             transform.Rotate(0, 0, 
38.                 -rollSpeed * Time.deltaTime, Space.World);
39. 
40.         } else if(Input.GetKey(KeyCode.LeftArrow)){
41.             transform.Translate(-moveSpeed * Time.deltaTime,
42.                                 0, 0, Space.World);
43. 
44.             transform.Rotate(0, 0, 
45.                 rollSpeed * Time.deltaTime, Space.World);
46.         }
47.     }
48. }

السرد 24: بريمج لتحريك الكرة ودحرجتها باستخدام مفاتيح الأسهم

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class ObjectTracker : MonoBehaviour {
5.  
6.  //الهدف المراد تتبعه
7.  public Transform objectToTrack;
8.  
9.  void Start () {
10.     
11.     }
12.     
13.     void Update () {
14.         //للكائن الذي (x, z) لهذا الكائن إلى الموقع (x, z) غير الموقع
15.         //نقوم بتتبعه
16.         Vector3 newPos = transform.position;
17.         newPos.x = objectToTrack.position.x;
18.         newPos.z = objectToTrack.position.z;
19.         transform.position = newPos;
20.     }
21. }

السرد 25: البريمج الذي يسمح للكاميرا بتتبع اللاعب من الأعلى

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

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class Collectable : MonoBehaviour {
5.  
6.  //المسافة بين مركز الكائن
7.  //وسطح التصادم الخارجي
8.  public float radius = 0.5f;
9.  
10.     void Start () {
11.     
12.     }
13.     
14.     void Update () {
15.     
16.     }
17.     
18.     //باستدعاء هذه الدالّة Collector سيقوم المجمّع صاحب البريمج 
19.     //عندما يلامس هذا الكائن
20.     public void Collect(Collector owner){
21.         //قم بتبليغ البريمجات الأخرى المضافة على هذا الكائن
22.         //بحدوث التلامس مع المجمّع وذلك حتى تنفذ منطق عملية الجمع
23.         SendMessage("Collected", 
24.             owner, SendMessageOptions.RequireReceiver);
25.     }
26. }

السرد 26: البريمج الخاص بالأشياء القابلة للجمع

كما تلاحظ فإن الدالتين ()Start و ()Update خاليتان من أي أوامر، مما يعني أن هذا الكائن سيكون خاملا ولن يقوم بأي شيء دون تدخل من كائن آخر. فكل ما يفعله Collectable هو الانتظار حتى يقوم أحد ما باستدعاء الدّالة ()Collect. فائدة المتغير radius هو أن المجمّع Collector سيقوم باستخدامه لفحص التصادم مع الكائن القابل للجمع. عندما يلمس اللاعب أو المجمّع بشكل عام الكائن المحتوي على البريمج Collectable فإنه يقوم باستدعاء الدالّة ()Collect ويقوم بتزويد نفسه عبر المتغير owner وذلك حتى يعرف البريمج من هو الذي يحاول أن يجمع هذا الكائن. كل ما تقوم به ()Collect هو أنها ترسل الرسالة Collected مرفقة معها owner. إرفاق owner مع الرسالة مهم لمعرفة هوية المجمّع؛ فمثلا إذا كان ما نقوم بجمعه هو قطعة نقدية، فإننا نعرف عن طريق owner من هو الذي قام بجمعها وبالتالي من المجمّع الذي ينبغي أن نزيد رصيده المالي.

السؤال الذي ينبغي أن نفكر بإجابته الآن هو: عندما يتم إرسال الرسالة Collected، من الذي سيقوم باستقبالها؟ الإجابة ببساطة هي: كافة البريمجات الأخرى المضافة على الكائن القابل للجمع والمحتوي على البريمج Collectable. سنرى بعد قليل كيف يمكن أن نستقبل هذه الرسالة ونربط ذلك بمنطق مخصص لعملية الجمع. معنى ذلك أن البريمج Collectable لا يقوم عمليا بأي شيء سوى إخبار البريمجات الأخرى أن owner يحاول أن يقوم بجمع هذا الكائن. من المهم أن أشير هنا إلى استخدام الخيار SendMessageOptions.RequireReceiver عند إرسال الرسالة. هذا الخيار يؤكد على أنه ينبغي استقبال الرسالة من بريمج واحد على الأقل، ويقوم بالإبلاغ عن خطأ في البرنامج إذا لم يتم هذا الأمر. الهدف من استخدام هذا الخيار هو التأكيد على كون عملية الجمع ذات معنى منطقي، وعدم استقبال هذه الرسالة يجعلها بخلاف ذلك.

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

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class Collector : MonoBehaviour {
5.  
6.  //نصف القطر المستخدم لفحص التصادمات
7.  public float radius = 0.5f;
8.  
9.  void Start () {
10. 
11.     }
12.     
13.     void Update () {
14.         //ابحث عن كافة الأشياء القابلة للجمع في المشهد
15.         Collectable[] allCollectables = 
16.                         FindObjectsOfType<Collectable>();
17.         
18.         //افحص عملية التصادم مع الأشياء القابلة للجمع
19.         //يتم هذا الفحص بمقارنة المسافة بين المراكز مع مجموع أنصاف الأقطار
20.         foreach(Collectable col in allCollectables){
21.             float distance = 
22.                 Vector3.Distance(transform.position, 
23.                                 col.transform.position);
24.             
25.             //كون المسافة بين المركزين أقل من مجموع أنصاف الأقطار يعني وجود تصادم
26.             //لذا نحاول أن نقوم بجمع هذا الكائن
27.             if(distance < col.radius + radius){
28.                 //أخبر الكائن القابل للجمع بأن هذا المجمّع
29.                 //يحاول أن يجمعه
30.                 col.Collect (this);
31.             }
32.         }
33.     }
34. }

السرد 27: البريمج الخاص بتجميع الكائنات القابلة للجمع

بقراءة هذا البريمج يمكننا الاستنتاج بأنه ذو وظيفة مجرّدة، ولا تُعنى بأي تفاصيل خاصة بعملية الجمع نفسها وكيف تتم. فكل ما يقوم به البريمج هو اكتشاف التصادمات مع الأشياء القابلة للجمع استدعاء الدّالة ()Collect من هذه الأشياء. اكتشاف التصادمات يتم عن طريق حساب المسافة بين المجمّع والكائن المراد جمعه، ومن ثم مقارنة هذه المسافة مع أنصاف الأقطار المفترضة للكائنين كما في السطر 27. ذكرت كلمة "مفترضة" لأن الكائنات ليست بالضرورة دائرية أو كروية الشكل حتى يكون لها نصف قطر. لكن هذه الطريقة تعطي فحص تصادم بدقة كافية لتحقيق مبتغانا في هذا المثال. إذا تم اكتشاف التصادم فإن المجمّع يقوم باستدعاء الدّالة ()Collect من الكائن أو الكائنات التي حدث التصادم معها. لاحظ أن المجمّع يقوم بتزويد نفسه عبر المتغير owner وذلك باستخدام الكلمة this في السطر 30. هذه الكلمة تستخدم كمتغير خاص يمكّن البريمج من قراءة قيمة نفسه داخليا، وهذا ما يحتاجه Collector ليخبر الدّالة ()Collect أنه هو نفسه owner الذي يحاول أن يقوم بعملية الجمع.

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

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class YRotator : MonoBehaviour {
5.  
6.  //سرعة التدوير مقدرة بدرجة \ ثانية
7.  public float speed = 180;
8.  
9.  //هل يجب أن يبدأ التدوير بزاوية عشوائية؟
10.     public bool randomStartAngle = true;
11.     
12.     void Start () {
13.         if(randomStartAngle){
14.             //قم بالتدوير بزاوية عشوائية بين 0 و 180 درجة
15.             transform.Rotate(
16.                 0, Random.Range(0, 180), 0, Space.World);
17.         }
18.     }
19.     
20.     void Update () {
21.         //بمرور الوقت y قم بالتدوير حول المحور 
22.         transform.Rotate(
23.             0, speed * Time.deltaTime, 0, Space.World);
24.     }
25. }

السرد 28: بريمج لتدوير الكائن حول محور الفضاء y اعتمادا على الوقت

بمقدورنا أن نحدد randomStartAngle لتجنب الحصول على دوران موحد لجميع القطع النقدية، مما يعطي المشهد عشوائية جميلة الشكل. لاحظ أننا استخدمنا لهذا الغرض الدّالة ()Random.Range والتي يمكنها أن تعطينا قيما عشوائية بين الحدين الأدنى والأعلى الذين نقوم بتزويدهما. ففي هذه الحالة تعطينا قيمة عشوائية للزاوية المبدئية بين 0 و 180 درجة.

ما يتوجب علينا القيام به الآن هو إضافة البريمجين Collectable و YRotator إلى كافّة الكائنات التي نرغب بجعلها قابلة للجمع، حيث يمكن القول أننا سنستخدم التدوير كعلامة مميزة تدل اللاعب على ما يمكنه جمعه من عناصر المشهد. لنبدأ مع القطع النقدية والتي هي عبارة عن أسطوانة بقياس (1 ,0.02 ,1) مضافا إليها إكساء بلون الذهب لتعطي المظهر المطلوب. كما يمكننا أن نجعلها تشع بإضافة ضوء نقطي كابن لهذه الأسطوانة وإعطائه لونا أصفر. بعد بناء هذه الأسطوانة كما في الشكل 35، علينا أن نضيف إليها البريمجين Collectable و YRotator ومن ثم نقوم ببناء قالب منها، ذلك لأننا سنضيف عددا لا بأس به من القطع النقدية للمشهد.

الشكل 35: القطعة النقدية التي سنستخدمها في المثال

الشكل 35: القطعة النقدية التي سنستخدمها في المثال

الشكل 35: القطعة النقدية التي سنستخدمها في المثال
الشكل 35: القطعة النقدية التي سنستخدمها في المثال

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

1. using UnityEngine;
2. using System.Collections.Generic;
3. 
4. public class InventoryBox : MonoBehaviour {
5.  
6.  //كم هو المبلغ الذي يملكه اللاعب حاليا
7.  public int money = 0;
8.  
9.  void Start () {
10.     
11.     }
12.     
13.     void Update () {
14.     
15.     }
16. }

السرد 29: حقيبة اللاعب

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

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class Coin : MonoBehaviour {
5.  
6.  //هذا المبلغ ستتم إضافته لرصيد حامل الحقيبة عندما
7.  //يقوم بجمع هذه القطعة النقدية
8.  public int coinValue = 1;
9.  
10.     void Start () {
11.     
12.     }
13.     
14.     void Update () {
15.     
16.     }
17.     
18.     //Collected هذه الدّالة تختص باستقبال الرسالة 
19.     //Collectable والتي يتم إرسالها من قبل البريمج 
20.     public void Collected(Collector owner){
21.         //علينا أولا التحقق من أن المجمّع يمتلك حقيبة لجمع النقود
22.         InventoryBox box = owner.GetComponent<InventoryBox>();
23.         if(box != null){
24.             //الحقيبة موجودة، لذا يمكننا أن نضيف
25.             //قيمة القطعة النقدية إلى الرصيد
26.             box.money += coinValue;
27.             
28.             //نقوم أخيرا بحذف القطعة من المشهد
29.             Destroy(gameObject);
30.         }
31.     }
32. }

السرد 30: البريمج الخاص بالقطع النقدية القابلة للجمع

كما تلاحظ فإن هذا البريمج أيضا لا يقوم بأي عمل بشكل منفرد (الدّالتان ()Start و ()Update خاليتان). المتغير الوحيد الذي فمنا بتعريفه هو coinValue والذي يسمح لنا بتحديد قيمة مختلفة لكل قطعة نقدية. الجزء الأهم في هذا البريمج هو الدّالة ()Collected والذي تأخذ المتغير owner من نوع Collector. بالعودة إلى السطر 23 في البريمج Collectable (السرد 26) نتذكر أن هذا البريمج يرسل الرسالة Collected ويضمنها المرفق owner وهو الذي يقوم بمحاولة الجمع. هذه الدّالة تحمل نفس الاسم وتأخذ متغيرا من نفس نوع المرفق الذي يصاحب الرسالة وهو Collector، وهذه المميزات تجعلها قادرة على استقبال الرسالة Collected. باختصار، يمكن القول أنه من أجل أن تستقبل رسالة مرسلة عن طريق الدّالة ()SendMessae فما عليك سوى أن تعرف داّلة عند المستقبِل تحمل نفس اسم الرسالة، وإذا كانت الرسالة تحمل مرفقا، يجب أن تأخذ دالّة المستقبِل متغيرا بنفس نوع المرفق. أخيرا، عندما تصل الرسالة للمستقبِل فإنّ الدالّة التي تحمل اسم الرسالة سيتم تنفيذها.

لننتقل الآن إلى منطق الدّالة ()Collected وهو عمليا إضافة المبلغ coinValue إلى رصيد المجمّع owner. كما سبق وذكرت فإن جمع المال يعتمد على امتلاك المجمّع للحقيبة InventoryBox وهذا ما تقوم الدّالة بالتأكد منه. باستدعاء الدّالة ()

الشكل 36 يلخص بمخطط سهمي تفاصيل التفاعلات بين البريمجات Collector و Collectable و Coin و CategoryBox لإتمام عملية جمع النقود وإضافتها للحقيبة.

الشكل 36: تفاعلات عملية جمع قطعة النقود بين جميع البريمجات المعنية

الشكل 36: تفاعلات عملية جمع قطعة النقود بين جميع البريمجات المعنية

يلخص المخطط في الشكل 36 التفاعلات بين البريمجات المختلفة الداخلة في عملية الجمع والتي تتم بعد اكتشاف التصادم بين المجمّع وقطعة النقود. في الخطوة أ يقوم المجمّع أي اللاعب باستدعاء الدّالة ()Collect من البريمج Collectable والموجود في قطعة النقود ممررا نفسه عبر المتغير this كمالك لما سيتم جمعه. في الخطوة ب يقوم البريمج Collectable بإرسال الرسالة Collected إلى جميع البريمجات الأخرى في قطعة النقود. هذه الرسالة يتم استقبالها في الخطوة ج من قبل البريمج Coin والذي يقوم بدوره في الخطوة د بفحص وجود الحقيبة (بريمج InventoryBox) لدى المجمّع المزوّد عبر المتغير owner. إذا وُجِد هذا البريمج ينقلنا Coin إلى الخطوة هـ والتي يتم خلالها زيادة الرصيد المالي في حقيبة اللاعب بمقدار قيمة القطعة النقدية الموجود في المتغير coinValue ومن ثم حذف قطعة النقود من المشهد.

نفس الخطوات المذكورة سيتم اتباعها فيما يتعلق بجمع الطعام، حيث سيكون الاختلاف الوحيد هو في التأثير على اللاعب. سيكون لدينا نوعان من الطعام: أخضر وأحمر. الاختلاف بينهما سيكون في مدة ومقدار التأثير على اللاعب حيث سيعمل كل منهما على زيادة حجم الكرة بمقدار مختلف ولمدة مختلفة. لأجل ذلك من الأفضل أن نقوم بعمل قالب خاص بكل نوع منهما. لنبدأ أولا مع البريمجات: كما أن بريمج النقود Coin يحتاج إلى بريمج الحقيبة InventoryBox لتتم عملية الجمع، فإن بريمج الطعام يحتاج إلى بريمج مقابل عند اللاعب لتتم عملية تناول الطعام وإحداث تأثيره. البريمج الأول الذي سنتناوله هو البريمج المستقبِل للطعام من جهة اللاعب وهو SizeChanger الموضح في السرد 31 والذي يعمل على تغيير قياس الكرة بمقدار محدد ولمدة محددة. بعد ذلك سنتناول البريمج Food الموضح في السرد 32 الخاص بالطعام والذي سنضيفه إلى قالب الطعام الأحمر والأخضر جنبا إلى جنب مع Collectable و YRotator.

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class SizeChanger : MonoBehaviour {
5.  
6.  //القياس الحالي للكائن
7.  float currentSize = 0;
8.  
9.  //المضاف إلى الكائن Collector مرجع للبريمج 
10.     Collector col;
11.     
12.     void Start () {
13.         col = GetComponent<Collector>();
14.     }
15.     
16.     void Update () {
17.     
18.     }
19.     
20.     //باستدعاء هذه الدّالة محاولا إعطاء الطعام للمجمّع Food يقوم البريمج
21.     public bool IncreaseSize(float amount, float duration){
22.         //لا يمكن تغيير الحجم الحالي إلى لو كان صفرا
23.         if(currentSize == 0){
24.             //amount نقوم بتغيير الحجم الحالي إلى قيمة المتغير 
25.             currentSize = amount;
26.             transform.localScale = Vector3.one * currentSize;
27.               //durationبعد انقضاء الوقت المحدد بـ DecreaseSize()نقوم باستدعاء 
28.             Invoke("DecreaseSize", duration);
29.             
30.             //radius قم بزيادة قيمة Collector إذا وُجد البريمج
31.             if(col != null){
32.                 col.radius = col.radius * currentSize;
33.             }
34.             //يعني أن الطعام تم تناوله true إرجاع القيمة
35.             return true;
36.         }
37.         //يعني أن الطعام لم يتم تناوله false إرجاع القيمة
38.         return false;
39.     }
40.     
41.     //تقوم هذه الدّالة بإرجاع الكائن لحجمه الأصلي
42.     public void DecreaseSize(){
43.         transform.localScale = Vector3.one;
44.         //Collector في حال وجد البريمج radiusنقوم باسترجاع القيمة الأصلية لـ
45.         if(col != null){
46.             col.radius = col.radius / currentSize;
47.         }
48.         //إلى صفر مرة أخرى currentSize قم بتغيير قيمة
49.         currentSize = 0;
50.     }
51. }

السرد 31: البريمج الخاص بتناول الطعام وتغيير الحجم بشكل مؤقت

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class Food : MonoBehaviour {
5.  
6.  //مقدار زيادة الحجم التي يسببها هذا الطعام
7.  public float sizeIncrementAmount = 2;
8.  
9.  //المدة الزمنية لتأثير الطعام مقدرة بالثواني
10.     public float incrementDuration = 5;
11.     
12.     void Start () {
13.     
14.     }
15.     
16.     void Update () {
17.     
18.     }
19.     
20.     //Collected الهدف من تعريف هذه الدّالة هو استقبال الرسالة
21.     //وذلك حتى ننفذ عملية التجميع Collectable التي يرسلها 
22.     public void Collected(Collector owner){
23.         //حتى يتأثر بالطعام SizeChanger يجب أن يحتوي المجمّع على البريمج 
24.         SizeChanger changer = owner.GetComponent<SizeChanger>();
25.         if(changer != null){
26.             //البريمج موجود، فنحاول أن نعطي الطعام للمجمّع
27.             bool canTake = 
28.                 changer.IncreaseSize(
29.                     sizeIncrementAmount, incrementDuration);
30.             //هل قام المجمّع بإخذ الطعام؟
31.             if(canTake){
32.                 //نعم، إذن علينا في هذه الحالة أن نحذف الطعام من المشهد
33.                 Destroy(gameObject);
34.             }
35.         }
36.     }
37. }

السرد 32: البريمج الخاص بالطعام القابل للجمع

البريمج الأول SizeChanger يحتوي على دالّتين رئيسيتين هما ()IncreaseSize و ()DecreaseSize. بالإضافة لذلك يحتوي على المتغير col والذي نحاول في بداية تشغيل البريمج أن نخزن فيه مرجعا للبريمج الخاص بالجمع Collector إن وجد. عندما يلمس المجمّع كائنا قابلا للجمع من نوع Food أي طعام يتم استدعاء الدّالة ()IncreaseSize وتزويدها بمتغيرين هما مقدار الزيادة في الحجم amount بالإضافة إلى مدة التأثير duration. ما تقوم به هذه الدّالة هو التحقق أولا من أن currentSize تساوي صفرا، مما يعني أن الكائن حاليا في حجمه الطبيعي وليس تحت تأثير طعام سابق تم جمعه. بعد التحقق من هذا الشرط يتم تغيير currentSize إلى قيمة amount وتغيير قياس الكائن بمقدار amount أيضا. بعد ذلك نستخدم الدّالة ()Invoke لاستدعاء الدّالة ()DecreaseSize لإعادة الحجم الأصلي. الميزة التي تعطينا إياها ()Invoke هي إمكانية تأجيل استدعاء الدّالة التي نرغب بها لمدة زمنية محددة. في هذه الحالة نريد أن يعود الكائن لحجمه الأصلي بعد زوال مدة تأثير الطعام والمحددة عن طريق المتغير duration. أي أننا عندما نستدعي ()Invoke كما في السطر 28 فإننا نخبر البريمج SizeChange بأن يقوم باستدعاء ()DecreaseSize لكن بعد مرور duration من الثواني.

كل ما تم من تغييرات حتى الآن لا يؤثر في إمكانية البريمج Collector على الجمع وذلك لأن عملية اكتشاف التصادمات تعتمد على المتغير radius الذي لا زال على حاله. من أجل ذلك نقوم في السطر 32 بتغيير قيمة radius عن طريق ضربها بقيمة amount بالتالي نزيد من نصف قطر التصادم الخاص بالمجمّع مما يجعله قادرا على التقاط الأشياء من مسافة أبعد بالتالي تسهيل وتسريع عملية الجمع. من المهم هنا الإشارة إلى أن استدعاء ()IncreaseSize لا يعني بالضرورة أن الطعام تم أخذه وأن تأثيره حدث، فالبريمج قد يكون أصلا تحت تأثير طعام آخر مما يمنعه من أخذ غيره وبالتالي فإن القيمة التي سترجعها ()IncreaseSize هي false، أما إذا تمت عملية تناول الطعام وتغيير الحجم فإن القيمة التي سترجع هي true.

بعد انقضاء المدة المحددة لتأثير الطعام يتم تنفيذ ()DecreaseSize والتي تعمل على استرجاع القياس الأصلي للكائن بالإضافة إلى إعادة currentSize إلى القيمة صفر. طبعا يجب ألا ننسى قبل ذلك إعادة قيمة نصف القطر radius الخاصة بـ Collector إلى مقدارها الأصلي عن طريق قسمتها على currentSize.

على الطرف الآخر من عملية تناول الطعام يوجد البريمج Food والذي يشبه البريمج Coin في كنه يستقبل الرسالة Collected ويقوم بترجمتها لشيء ذي معنى. هنا نتذكر أن البريمج Coin اعتمد على وجود الحقيبة InventoryBox على الطرف الآخر، وبالمثل فإن Food يعتمد على وجود SizeChanger. لذا فالخطوة الأولى التي يقوم بها البريمج Food عند استقبال الرسالة Collected هي التأكد من وجود SizeChanger لدى المجمّع المفترض owner. إذا وُجد هذا البريمج فإنه يتم استدعاء الدّالة ()IncreaseSize الخاصة به وتزويدها بقيمة كل من sizeIncrementAmount و incrementDuration. لاحظ أن هذين المتغيرين هما public مما يمكننا لاحقا من تحديد قيمتهما من خلال نافذة الخصائص. عند استدعاء ()IncreaseSize فإنها سترجع قيمة من نوع bool لذا نقوم بتخزينها في المتغير canTake. إذا كانت قيمة canTake هي true فهذا يعني أن الطعام قد تم أخذه وبالتالي يمكننا أن نقوم بحذفه من المشهد. أما إذا كانت القيمة false فهذا يعني أن المجمّع لم يتمكن من أخذ الطعام في هذه المحاولة، بالتالي يبقى الطعام في المشهد ولا شيء يتغير.

أخيرا يمكننا أن نقوم بعمل قوالب خاصة بكل من الطعام الأخضر والأحمر وذلك باستخدام مكعبات كما في الشكل 37. بعد ذلك نضيف البريمجات YRotator و Collectable و Food إلى كل من هذه القوالب. لتغيير تأثير أنواع الطعام المختلفة يمكننا ببساطة أن نغير كلا من القيم sizeIncrementAmount و incrementDuration. فمثلا يمكننا إعداد الطعام الأخضر ليزيد الحجم بمقدار 2 لمدة سبع ثوان ونصف والطعام الأحمر ليزيد الحجم بمقدار 3.5 لمدة 5 ثوان. يمكنك الاطلاع على النتيجة النهائية في المشهد scene10 في المشروع المرفق.

الشكل 37: قوالب الطعام المستخدمة. الأحمر إلى اليمين والأخضر إلى اليسار

الشكل 37: قوالب الطعام المستخدمة. الأحمر إلى اليمين والأخضر إلى اليسار

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

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