الفصل الرابع: المحفّزات ومفاتيح التحكم

العب

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

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

الشكل 38: المشهد الذي سنستعمله لشرح استخدام المحفّزات ومفاتيح التحكم

الشكل 38: المشهد الذي سنستعمله لشرح استخدام المحفّزات ومفاتيح التحكم

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

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

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class SwitchableTrigger : MonoBehaviour {
5.  
6.  //يتنقل المحفّز بين هذه الحالات المختلفة مع كل استخدام
7.  public TriggerState[] states;
8.  
9.  //الموقع الخاص بالحالة التي عليها المحفّز في الوقت الراهن
10.     public int currentState = 0;
11.     
12.     //أبعد مسافة يمكن ابتداء منها التعامل مع هذا المحفّز
13.     public float activationDistance = 3;
14.     
15.     //آخر وقت تم فيه تغيير حالة المحفّز
16.     float lastSwitchTime = 0;
17.     
18.     void Start () {
19.     
20.     }
21.     
22.     void Update () {
23.     
24.     }
25.     
26.     //تحاول هذه الدّالة تغيير حالة المحفّز والانتقال إلى الحالة التالية
27.     //true إذا نجحت محاولة الانتقال فإنها تعيد القيمة
28.     public bool SwitchState(){
29.         //إذا كانت مصفوفة الحالات فارغة فليس هناك ما يمكن فعله
30.         if(states.Length == 0){
31.             return false;
32.         }
33.         
34.         //قم باستدعاء الحالة التي نحن عليها في الوقت الراهن
35.         TriggerState current = states[currentState];
36.         
37.         //تأكد من أن وقت الانتظار الخاص بهذه الحالة قد انقضى
38.         if(Time.time - lastSwitchTime > current.restTime){
39.             //إذا انقضى ذلك الوقت فعلا، حينها يمكننا التبديل إلى حالة جديدة
40.             currentState += 1;
41.             
42.             //إذا كنا في الحالة الأخيرة فإننا نرجع إلى الأولى
43.             if(currentState == states.Length){
44.                 currentState = 0;
45.             }
46.             
47.             //احصل على الحالة الجديدة التي نرغب بالتحول إليها
48.             TriggerState newState = states[currentState];
49.             
50.             //قم بإرسال جميع الرسائل المخزنة في الحالة الجديدة
51.             foreach(TriggerMessage message 
52.                                 in newState.messagesToSend){
53.                 //احصل على مستقبل الرسالة
54.                 GameObject sendTo = message.messageReceiver;
55.                 //احصل على اسم الرسالة
56.                 string messageName = message.messageName;
57.                 //قم بإرسال الرسالة إلى مستقبلها
58.                 sendTo.SendMessage(messageName);
59.             }
60.             
61.             //نقوم أخيرا بتسجيل وقت التبديل إلى الحالة الجديدة
62.             lastSwitchTime = Time.time;
63.             return true;
64.         } else {
65.             return false;
66.         }
67.     }
68. }
69. 
70. //بنية صغيرة تستخدم لتخزين الرسائل
71. [System.Serializable]
72. public class TriggerState{
73.     public float restTime;
74.     public TriggerMessage[] messagesToSend;
75. }
76. 
77. //بنية أخرى تمثل الرسائل التي يمكننا إرسالها
78. [System.Serializable]
79. public class TriggerMessage{
80.     public GameObject messageReceiver;
81.     public string messageName;
82. }

السرد 35: البريمج الخاص بالمحفّزات التي يمكن للاعب تفعيلها يدويا

قبل الدخول في تفاصيل البريمج نفسه، أود الانتقال أولا إلى آخر السرد تحديدا الأسطر 72 إلى 76 و 79 إلى 83. تلاحظ في هذه الأسطر أننا قمنا بتعريف وحدات برمجية تختلف عن البريمجات التي عهدناها، هذه الوحدات تعرف بالبٌنى البرمجية structures. بالرغم من أن تعريفها شيبيه جدا بتعريف البريمجات إلى أنها تختلف من ناحية أنها لا ترث من MonoBehavior، كما أن تعريفها مسبوق بالأمر
[System.Serializable]. هذه الوحدات البرمجية بسيطة التركيب سنستخدمها كأنها متغيرات نخزن فيها بعض القيم لا أكثر، فهي بالتالي لا تحتوي على أية دوالّ برمجية يمكن استدعاؤها ولا تملك بنفسها أي منطق برمجي قابل للتنفيذ. فإذا عرفنا – مثلا – متغيرا من نوع TriggerState فإن هذا المتغير يحتوي في داخله على متغيرين آخرين هما restTime والمصفوفة messagesToSend. أما أهمية الأمر
[System.Serializable] فهي جعل هذا المتغير ظاهرا وقابلا للتعديل والتحرير في نافذة الخصائص داخل Unity.

سنستخدم النوع TriggerState لتعريف الحالات المختلفة التي يمكن للمحفّز أن يكون عليها، فمثلا بالنسبة لمفتاح المصباح الكهربائي هناك حالتان فقط هما "مشغّل" و"مطفأ". لكن هذا لا يمنع وجود أكثر من حالة في أمثلة أخرى. لكل حالة من الحالات هناك وقت انتظار خاص بها وهو restTime والذي يجب أن ينقضى كاملا قبل أن نسمح للمحفز بالانتقال من حالته التي هو عليها إلى حالة أخرى. هذا الوقت قد يكون مفيدا في الحالات التي تتطلب وقتا مثل تحريك مصعد أو فتح باب كهربائي، مما يلزمنا بقفل المحفّز مؤقتا ريثما تتم العملية المطلوبة. لكل حالة من الحالات هناك مصفوفة تسمى messagesToSend وتحتوي على عناصر من نوع TriggerMessage. بالاطلاع على TriggerMessage نجد أن كل متغير من هذا النوع سيحتوي على كائن messageReceiver وهو الذي سيقوم باستقبال الرسالة و messageName وهو اسم الرسالة التي سنرسلها له. بكلمات أخرى فإن messageReceiver يجب أن يحتوي على الأقل على بريمج واحد يمكنه استقبال رسالة تحمل الاسم المخزن في messageName. لا تقلق إذا بدا الأمر إلى الآن معقدا، فبالمثال ستتضح الصورة أكثر إن شاء الله.

في كل مرة يحاول فيها اللاعب استعمال المحفّز، فإنه عمليا يقوم باستدعاء الدّالة ()SwitchState والتي بدورها تقوم بإبلاغ اللاعب بنجاح أو فشل محاولة التفعيل التي قام بها وذلك عن طريق إرجاع true أو false. أحد الأسباب التي قد تؤدي لفشل محاولة التفعيل هو عدم انقضاء وقت الانتظار المطلوب للحالة التي عليها المحفّز الآن. في حال نجاح التفعيل فإن قيمة currentState تزداد بمقدار واحد، أو تعود للقيمة صفر (الحالة الأولى) في حال كانت أصلا على آخر حالة ممكنة. بعد التغيير إلى الحالة الجديدة يقوم المحفز بالمرور على كافّة عناصر المصفوفة messagesToSend والمخزنة في الحالة الجديدة، ومن ثم يقوم بإرسال كل رسالة إلى مستقبلها كما في الأسطر 51 إلى 59. قبل أن نبلغ اللاعب بنجاح التفعيل ونعيد له القيمة true علينا أولا أن نقوم بتسجيل الوقت الذي تمت فيه عملية التفعيل بحيث نتمكن من حساب وقت الانتظار في محاولة التفعيل المقبلة. الشكل 39 يوضح الآلية التي يتم فيها إحداث عدد من التغييرات المتزامنة في المشهد من خلال تفعيل محفّز واحد.

الشكل 39: آلية التحفيز: عندما تتغير حالة المحفّز يتم إرسال مجموعة من الرسائل المخزنة في المصفوفة messagesToSend الخاصة بالحالة الجديدة

الشكل 39: آلية التحفيز: عندما تتغير حالة المحفّز يتم إرسال مجموعة من الرسائل المخزنة في المصفوفة messagesToSend الخاصة بالحالة الجديدة

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

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class LightControl : MonoBehaviour {
5.  
6.  //مكوّن الضوء الموجود على الكائن والذي سنتحكم به
7.  Light toControl;
8.  
9.  void Start () {
10.         toControl = GetComponent<Light>();
11.     }
12.     
13.     void Update () {
14. 
15.     }
16.     
17.     //"SwitchOn" دالّة خاصة باستقبال الرسالة
18.     public void SwitchOn(){
19.         toControl.enabled = true;
20.     }
21.     
22.     //"SwitchOff" دالّة خاصة باستقبال الرسالة
23.     public void SwitchOff(){
24.         toControl.enabled = false;
25.     }
26. }

السرد 36: بريمج يتحكم بالضوء عن طريق استقبال رسائل من المحفّز

هذا البريمج مصمم لتتم إضافته لكائن الضوء النقطي الذي سنستخدمه كمصباح كهربائي، ذلك أنه عند بداية التشغيل يقوم بالبحث عن مكوّن من نوع Light وهو لا يوجد إلّا على كائنات الأضواء، ومن ثم يقوم بتخزين هذا المكوّن في المتغير toControl. يقوم هذا البريمج ببساطة باستقبال الرسالة SwitchOn وتفعيل مكوّن الضوء بناء عليها، كما أنّه يقوم بتعطيل مكوّن الضوء بناء على استقبال الرسالة SwitchOff. البريمج الآخر والذي سنضيفه على كائن المفتاح هو ZFlipper الموضح في السرد 37، هذا البريمج يقوم بتدوير الكائن حول محوره المحلي z بمقدار 180 درجة عند استقباله للرسالة Flip. الهدف من هذا البريمج هو محاكاة حركة المفتاح نحو الأعلى الأسفل عن طريق تدويره مما يغير اتجاه الإكساء. تأمل الشكل 40 والذي يمثل صورة مقربة للمفتاح، لو تم تدوير هذا الكائن بمقدار 180 درجة سيبدو وكأن المفتاح قد تم تحريكه نحو الأسفل.

الشكل 40: كائن المفتاح المستخدم للتحكم بالمصباح الكهربائي

الشكل 40: كائن المفتاح المستخدم للتحكم بالمصباح الكهربائي

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class ZFlipper : MonoBehaviour {
5. 
6.  void Start () {
7.  
8.  }
9.  
10.     void Update () {
11.     
12.     }
13.     //بمقدار 180 درجة z تقوم هذه الدّالة بتدوير الكائن حول محوره المحلي 
14.     public void Flip(){
15.         transform.Rotate(0, 0, 180);
16.     }
17. }

السرد 37: بريمج تدوير المفتاح الكهربائي

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

الشكل 41: بريمج المحفّز بعد إعداده للتحكم في المصباح. الإطاران الأسودان يمثلان الحالتين اللتين ينتقل بينهما المحفّز، بينما يمثل الإطاران الأبيضان مجموعتي الرسائل التي يتم إرسالها إبّان تفعيل كل حالة

الشكل 41: بريمج المحفّز بعد إعداده للتحكم في المصباح. الإطاران الأسودان يمثلان الحالتين اللتين ينتقل بينهما المحفّز، بينما يمثل الإطاران الأبيضان مجموعتي الرسائل التي يتم إرسالها إبّان تفعيل كل حالة

بدراسة الشكل 41 نلاحظ أن المحفّز ينتقل بين حالتين، حيث تقوم الحالة الأولى بإرسال الرسالة SwitchOn إلى كائن الضوء وهو هنا يحمل الاسم ControllableLight، بينما تقوم الحالة الثانية بإرسال الرسالة SwitchOff إلى نفس الكائن. لاحظ أيضا أن كلا الحالتين ترسلان الرسالة Flip إلى الكائن LightSwitch وهو نفس كائن المفتاح المضاف إليه البريمج SwithcableTrigger. بكلمات أخرى فإن البريمج يرسل الرسالة Flip إلى نفسه عن طريق SwitchableTrigger ومن ثم يقوم باستقبالها عن طريق ZFlipper. بقي أن نعطي اللاعب القدرة على تفعيل المحفّزات عن طريق الضغط على المفتاح E، لذا علينا أن نضيف البريمج TriggerSwitcher إلى الأسطوانة التي تمثل اللاعب. هذا البريمج موضح في السرد 38.

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class TriggerSwitcher : MonoBehaviour {
5. 
6.  void Start () {
7.  
8.  }
9.  
10.     void Update () {
11.         if(Input.GetKeyDown(KeyCode.E)){
12.             //ابحث عن جميع المحفّزات الموجودة في المشهد
13.             SwitchableTrigger[] allST = 
14.                 FindObjectsOfType<SwitchableTrigger>();
15.             
16.             //البحث عن محفّز ملائم لتفعيله
17.             //المحفّز الملائم يجب أن يكون قريبا ومواجها للاعب
18.             foreach(SwitchableTrigger st in allST){
19.                 float dist = 
20.                     Vector3.Distance(transform.position,
21.                                 st.transform.position);
22.                 
23.                 //activationDistanceإذا كانت المسافة أقل من
24.                 //فهذا يعني أن المحفّز قريب من اللاعبة بشكل كاف لتفعيله
25.                 if(dist < st.activationDistance){
26.                     Vector3 distVector = 
27.                         st.transform.position 
28.                             - transform.position;
29.                     
30.                     float angle = 
31.                         Vector3.Angle(distVector, 
32.                                   transform.forward);
33.                     //إذا كانت الزاوية أقل من 90، فهذا يعني أن المحفّز مواجه للاعب
34.                     if(angle < 90){
35.                         //بما أنه مواجه يمكننا أن نحاول تفعيله
36.                         st.SwitchState();
37.                     }
38.                 }
39.             }
40.         }
41.     }
42. }

السرد 38: البريمج الذي يمكن اللاعب من تفعيل المحفّزات باستخدام المفتاح E

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

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

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class ControllableFan : MonoBehaviour {
5.  
6.  public float speed1 = 20;
7.  public float speed2 = 40;
8.  public float speed3 = 60;
9.  
10.     float currentSpeed = 0;
11.     
12.     void Start () {
13.     
14.     }
15.     
16.     void Update () {
17.         transform.Rotate(0, currentSpeed * Time.deltaTime, 0);
18.     }
19.     
20.     public void SetSpeed1(){
21.         currentSpeed = speed1;
22.     }
23.     
24.     public void SetSpeed2(){
25.         currentSpeed = speed2;
26.     }
27.     
28.     public void SetSpeed3(){
29.         currentSpeed = speed3;
30.     }
31.     
32.     public void SwitchOff(){
33.         currentSpeed = 0;
34.     }
35. }

السرد 39: بريمج المروحة

هذا البريمج يعمل ببساطة شديدة على التحكم بقيمة المتغير currentSpeed وبالتالي سرعة دوران المروحة وذلك عن طريق استقبال الرسائل المختلفة مثل SetSpeed1 و SetSpeed2. إضافة إلى ذلك يعمل البريمج على إيقاف المروحة وذلك بتغيير السرعة إلى صفر عند استقبال الرسالة SwitchOff. بعد إضافة هذا البريمج للمروحة علينا أن نقوم بإضافة SwitchableTrigger إلى لوحة التحكم الموجودة في منتصف الغرفة وإعداده وفقا الشكل 42.

الشكل 42: تهيئة المحفّز الخاص بالتحكم بالمروحة

الشكل 42: تهيئة المحفّز الخاص بالتحكم بالمروحة

هذه المرة لدينا 4 حالات مختلفة: 3 سرعات وحالة الإيقاف. لاحظ أن الحالات الثلاث الأولى ترسل الرسائل SetSpeed1 و SetSpeed2 و SetSpeed3 على التوالي، بينما ترسل الحالة الأخيرة الرسالة SwitchOff لإيقاف المروحة. لاحظ أننا قمنا بإعطاء القيمة 3 للمتغير currentState وذلك لأنه المروحة متوقفة في الأصل مما يعني أنها في الحالة الرابعة (تذكر أن تعداد الحالات يبدأ من 0 لأنها مصفوفة وليس من 1). فإذا قام اللاعب بتفعيل هذا المحفز سيعود إلى الحالة الأولى وبالتالي يرسل الرسالة SetSpeed1 إلى المروحة. يمكنك الاطلاع على النتيجة النهائية أيضا في المشهد scene12 في المشروع المرفق.

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

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