الفصل السادس: الانفجارات وهدم المنشآت

العب

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

الشكل 68: مكعب مع إكساء على شكل لبنات سنستخدمه كوحدة بنائية للمبنى

الشكل 68: مكعب مع إكساء على شكل لبنات سنستخدمه كوحدة بنائية للمبنى

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

1. using UnityEngine;
2. //سنحتاح لاستيراد هذه المكتبة
3. using System.Collections.Generic;
4. 
5. public class Destructible : MonoBehaviour {
6.  
7.  //سنقوم بإجراء مسح عن طريق شعاع ابتداء من مركز هذه الوحدة
8.  //وانطلاقا في الاتجاه المحدد عبر هذا المتجه
9.  public Vector3 scanDirection;
10.     
11.     //قائمة بالوحدات البنائية التي تعتمد في ثباتها على هذه الوحدة
12.     List<Destructible> dependents = new List<Destructible>();
13.     
14.     //متغير لتخزين قيم حرية الحركة الأصلية الخاصّة بالجسم الصلب قبل تجميده
15.     RigidbodyConstraints original;
16.     
17.     void Start () {
18.         //قم بالبحث عن وحدة بنائية أخرى مستخدما اتجاه المسح المحدد سلفا
19.         //إذا ما تم العثور على وحدة ما فإننا نضيف هذه الوحدة إلى قائمة الوحدات المعتمدة عليها
20.         RaycastHit hit;
21.         Ray scanRay = new Ray(transform.position, scanDirection);
22.         
23.         if(Physics.Raycast(scanRay, out hit, scanDirection.magnitude)){
24.             
25.             Destructible dependency = 
26.                 hit.transform.GetComponent<Destructible>();
27.             
28.             if(dependency != null){
29.                 dependency.dependents.Add(this);
30.             }
31.         }
32.         
33.         //قم بتخزين قيم حرية الحركة والدوران الأصلية ومن ثم قم بتجميد الكائن
34.         original = rigidbody.constraints;
35.         rigidbody.constraints = RigidbodyConstraints.FreezeAll;
36.     }
37.     
38.     void Update () {
39.         
40.     }
41.     
42.     //قم بهدم هذه الوحدة البنائية وذلك باستعادة قيم حرية الحركة الأصلية الخاصّة بها
43.     public void Destruct(){
44.         rigidbody.constraints = original;
45.         //من جميع الوحدات التي تعتمد على هذه الوحدة Destruct() قم باستدعاء مُؤجّل للدّالة 
46.         foreach(Destructible dependent in dependents){
47.             if(dependent != null){
48.                 float time = Random.Range(0.0f, 0.01f);
49.                 dependent.Invoke("Destruct", time);
50.             }
51.         }
52.         //قم بإعلام البريمجات الأخرى بحدوث عملية الهدم لهذه الوحدة
53.         SendMessage("OnDestruction", 
54.                     SendMessageOptions.DontRequireReceiver);
55.     }
56. }

السرد 56: البريمج الخاص بالوحدات البنائية للمبنى القابل للتدمير

قبل الخوض في تفاصيل البريمج لابد من التطرق لكلمة using التي استخدمناها في السطر الثالث لإضافة مكتبة جديدة لهذا البريمج وهي مكتبة System.Collections.Generic. ماهية المكتبة ليست مهمة بقدر أهمية معرفتنا لكيفية استخدامها. بعد استيراد هذه المكتبة يصبح من الممكن أن نقوم بتعريف قائمة List تحتوي على عناصر من نوع Distructible وذلك باستخدام النوع
>List<Distructible. القوائم تشبه المصفوفات من حيث أنها تحتوي على مجموعة من العناصر ذات نفس النوع، إلا أنها أكثر مرونة ويمكننا بسهولة أن نضيف ونحذف العناصر منها ولا يلزمنا تحديد عدد العناصر مسبقا.

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

الشكل 69: عمود تم إنشاؤه باستخدام الوحدات البنائية

الشكل 69: عمود تم إنشاؤه باستخدام الوحدات البنائية

تبعا للعلاقة المنطقية بين أجزاء العمود، فإننا نتوقع أنهياره بشكل تام عند تهدم الوحدة البنائية في الأسفل (يستثنى من ذلك المكعبات الطائرة في الهواء كالتي في لعبة Super Mario). لأجل ذلك علينا إعلام المحاكي الفيزيائي بأن كل وحدة بنائية تعتمد على تلك التي أسفل منها وبالتالي تتهدم إذا تهدم ما تحتها. من أجل هذا الغرض نستخدم المتجه scanDirection والذي يبدأ من مركز الوحدة البنائية متجها في اتجاه معين. في حالة العمود مثلا يجب أن يتجه نحو الأسفل ويكون بطول يزيد عن 0.5 وهي المسافة بين مركز المكعب وسطحه الأسفل. قبل أن ننتقل للحديث عن آلية عمل scanDirection من المهم أن نفهم طبيعة العلاقات بين الوحدات البنائية. لكل وحدة من هذه الوحدات قائمة تحتوي على مجموعة من الوحدات الأخرى التي تبني عليها، فعندما يتم تهديم الوحدة الأساس فإن كافّة الوحدات التي تعتمد عليها والموجودة في قائمة dependents يجب أن تتهدم كذلك.

عندما يتم استدعاء الدّالّة ()Start حين بداية تشغيل اللعبة، تقوم كل وحدة بنائية ببث شعاع بالاتجاه والطول المحددين عبر scanDirection، في محاولة لإيجاد وحدة بنائية أخرى تعتمد عليها. بطبيعة الحال فإنّ الوحدة البنائية يجب أيضا أن تحتوي على البريمج Destructible وهذا ما يتم التأكد منه في السطر 28. إذا وُجدت الوحدة المطلوبة فإنّ الوحدة الحالية (أي التي قامت ببث الشعاع) تضيف نفسها إلى قائمة dependents في الوحدة الأخرى التي اصطدم بها الشعاع. في حالو العمود الموضح في الشكل 69 فإنّ scanDirection يجب أن يحمل القيمة (0 ,0.6- ,0) مما يجعل شعاع المسح يتجه نحو الأسفل. نتيجة لذلك فإنّ كل وحدة بنائية في العمود ستضيف نفسها إلى قائمة dependents في الوحدة الأسفل منها، بينما لن تجد الوحدة الواقعة في قاعدة العمود أي وحدة أخرى تعتمد عليها.

بعد الانتهاء من بناء العلاقات بين الوحدات البنائية يجب أن يتم تجميد هذه الوحدات حتى لا تتأثر بالقوى الفيزيائية من حولها. تنفيذ التجميد يتم عبر تخزين القيم الأصلية للمتغير rigidbody.constraints في المتغير original ومن ثم تغيير قيمته إلى RigidbodyConstraints.FreezeAll. معنى ذلك أن كلا من الموقع والدوران الخاصين بالوحدة البنائية لن يتغيرا إلى أن نسمح بذلك مرة أخرى. بالتالي ستبقى الوحدة البنائية جامدة في مكانها إلى أن يتم استدعاء الدّالّة ()Destruct أو استقبال رسالة تحمل الاسم Destruct. عند استدعاء هذه الدّالّة نقوم بالسماح للوحدة بالتحرك تحت تأثير المحاكي الفيزيائي وذلك باسترجاع القيم الأصلية الخاصة بـ rigidbody.constraints والتي سبق تخزينها في original. بعد ذلك لا بد من نشر عملية الهدم إلى كل الوحدات البنائية التي تعتمد على هذه الوحدة وذلك عن طريق استدعاء الدّالّة ()Destruct من جميع الوحدات البنائية الموجودة في القائمة dependents. من الأفضل عند استدعاء هذه الدّالّة أن نضيف تأخيرا زمنيا بسيطا يعطي انطباعا عن طبيعة العلاقة بين الوحدات البنائية، بحيث يرى اللاعب الوحدة الأصلية تهدم أولا ثم تتبعها الوحدات الوحدات المعتمدة عليها على شكل انهيار متدرج للبناء. من المفيد أيضا إعلام أية بريمجات أخرى بحصول الهدم وذلك عن طريق إرسال الرسالة OnDestruction مما يمكننا من تنفيذ أية تأثيرات أخرى تتعلق بالهدم مثل تشغيل صوت أو إحداث غبار. الشكل 70 يظهر مبنى أكثر تعقيدا تم بناؤه باستخدام عدد أكبر من الوحدات البنائية.

الشكل 70: مبنى تم إنشاؤه بشكل كامل من وحدات بنائية قابلة للهدم

الشكل 70: مبنى تم إنشاؤه بشكل كامل من وحدات بنائية قابلة للهدم

تحديد قيمة scanDirection الخاصّة بكل وحدة بنائية يعتمد بشكل كامل على الوحدات المجاورة لها. الشكل 71 يظهر نفس المبنى مع إضافة أسهم تبين متجهات scanDirection. كل واحد من هذه الأسهم يبدأ من منتصف الوحدة ويؤشر رأسه إلى اتجاه المسح حيث الوحدة التي يعتمد عليها.

الشكل 71: تغيير اتجاه المسح تبعا لتغير ترتيب الوحدات البنائية. يسارا: يتم المسح للبحث عن وحدات يعتمد عليها في اتجاه أفقي لليمن أو اليسار أو في اتجاه عمودي نحو الأسفل. يمينا: يتم المسح باتجاه واحد فقط هو أسفل يسار الوحدة

الشكل 71: تغيير اتجاه المسح تبعا لتغير ترتيب الوحدات البنائية. يسارا: يتم المسح للبحث عن وحدات يعتمد عليها في اتجاه أفقي لليمن أو اليسار أو في اتجاه عمودي نحو الأسفل. يمينا: يتم المسح باتجاه واحد فقط هو أسفل يسار الوحدة

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

الشكل 72: أشجار العلاقات بين الوحدات البنائية. عند تهدم الجذر فإنّ كافّة الفروع المتصلة به يجب أن تتهدم كذلك. الجذور في الصورة تظهر على شكل مربعات صغيرة في الأسفل

الشكل 72: أشجار العلاقات بين الوحدات البنائية. عند تهدم الجذر فإنّ كافّة الفروع المتصلة به يجب أن تتهدم كذلك. الجذور في الصورة تظهر على شكل مربعات صغيرة في الأسفل

نحتاج الآن لتنفيذ بعض الانفجارات حتى نقوم بتجربة نظام التهدم الذي بنيناه للتو. سنستعمل لتحقيق ذلك بريمجا يقوم بتوليد انفجار في أي نقطة نضغط عليها بزر الفأرة الأيسر. البريمج MouseExploder والموضح في السرد 57 هو البريمج الثاني الذي نحتاج لإضافته لقالب الوحدة البنائية.

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class MouseExploder : MonoBehaviour {
5.  
6.  //مقدار قوة الانفجار
7.  public float explosionForce = 40000;
8.  
9.  //نصف قطر محيط تأثير الانفجار
10.     public float explosionRadius = 5;
11.     
12.     //موقع الانفجار
13.     //هذا الموقع نسبي إلى موقع الجسم أو الوحدة البنائية التي تتعرض للانفجار
14.     //ممثلا بإحداثيات فضاء المشهد
15.     public Vector3 explosionPosition = new Vector3(-1, 0, -1);
16.     
17.     void Start () {
18.     
19.     }
20.     
21.     void Update () {
22.         
23.     }
24.     
25.     //تنفيذ الانفجار بمجرد الضغط على زر الفأرة
26.     void OnMouseDown(){
27.         //ابحث عن كافّة الأجسام الصلبة في المشهد
28.         Rigidbody[] allBodies = FindObjectsOfType<Rigidbody>();
29.         
30.         //قم بحساب موقع الانفجار
31.         Vector3 explosionPos = transform.position;
32.         explosionPos += explosionPosition;
33.         
34.         //قم بإيجاد كافّة الأجسام الصلبة ضمن محيط الانفجار ومن ثم
35.         //لتنفيذ التدمير في حال وجود وحدة بنائية Destruct أرسل الرسالة 
36.         //أخيرا قم بإضافة قوة الانفجار للجسم الصلب
37.         foreach(Rigidbody body in allBodies){
38.             float dist = 
39.                 Vector3.Distance(
40.                     body.transform.position, 
41.                     explosionPos);
42.             
43.             if(dist < explosionRadius){
44.                 body.SendMessage("Destruct", 
45.                     SendMessageOptions.DontRequireReceiver);
46.                 
47.                 body.AddExplosionForce(
48.                     explosionForce, //قوة الانفجار
49.                     explosionPos, //موقع الانفجار
50.                     explosionRadius);//نصف قطر التأثير
51.             }
52.         }
53.     }
54. }

السرد 57: بريمج يقوم بإنشاء انفجار في النقطة التي يتم الضغط عليها بزر الفأرة الأيسر

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

تقوم الدّالّة ()OnMouseClick باستقبال الضغط بزر الفأرة على الوحدة البنائية ومن ثم تقوم بإضافة قوة انفجار لجميع الأجسام الصلبة المحيطة بموقع الانفجار. كخطوة أولى يتم البحث عن كافّة الأجسام الصلبة الموجودة في المشهد وتخزينها في المصفوفة allBodies. بعد ذلك يتم حساب موقع الانفجار explosionPos عن طريق إضافة الإزاحة explosionPosition إلى موقع الوحدة التي تم الضغط عليها. تقوم الحلقة for الموجودة في السطر 37 بالمرور على جميع الأجسام الصلبة وحساب المسافة بينها وبين موقع الانفجار، فإذا كانت المسافة أقل من نصف قطر محيط التأثير explosionRadius يتم إرسال الرسالة Destruct للجسم الصلب وذلك بهدف فك التجميد في حال وجوده، حيث أنّ أي قوة انفجار لن يكون لها أي تأثير ما لم يتم فك تجميد الجسم الصلب أولا. الشكل 73 يظهر تأثير أحد الانفجارات على المبنى. لاحظ أن الانفجار ذو تأثير فيزيائي فقط حيث لم نقم بإضافة أي مؤثرات رسومية له كالنار والدخان.

الشكل 73: تأثير الانفجار على المبنى القابل للهدم. لاحظ اندفاع الوحدات البنائية بعيدا عن مركز الانفجار المشار إليه ببقعة بيضاء

الشكل 73: تأثير الانفجار على المبنى القابل للهدم. لاحظ اندفاع الوحدات البنائية بعيدا عن مركز الانفجار المشار إليه ببقعة بيضاء

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

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class Returner : MonoBehaviour {
5.  
6.  //الموقع الأصلي للوحدة البنائية
7.  Vector3 originalPos;
8.  //الدوران الأصلي للوحدة البنائية
9.  Quaternion originalRot;
10.     //هل تتحرك الوحدة حاليا نحو موقعها الأصلي؟
11.     bool returning = false;
12.     
13.     void Start () {
14.         //قم بحفظ الموقع والدوران الأصليين
15.         originalPos = transform.position;
16.         originalRot = transform.rotation;
17.     }
18.     
19.     void Update () {
20.         
21.         //قم بتنفيذ العودة عند الضغط على مفتاح المسافة
22.         if(Input.GetKeyDown(KeyCode.Space)){
23.             Return();
24.         }
25.         
26.         if(returning){
27.             //علينا أثناء عملية إعادة الوحدة لموقعها الأصلي أن نقوم بتجميد
28.             //كافّة الحركات الممكنة حتى نمنع أي تأثير فيزيائي خارجي من إعاقة عملية الإرجاع
29.             if(rigidbody.constraints != 
30.                 RigidbodyConstraints.FreezeAll){
31.                 //قم بتصفير أي سرعات خطية أو دورانية
32.                 //حتى يتوقف الكائن تماما عن الحركة الفيزيائية
33.                 rigidbody.velocity = Vector3.zero;
34.                 rigidbody.angularVelocity = Vector3.zero;
35.                 //قم الآن بتجميد الحركة والدوران
36.                 rigidbody.constraints = 
37.                     RigidbodyConstraints.FreezeAll;
38.             }
39.             
40.             //قم بشكل سلس بإعادة كل من الموقع والدوران
41.             //إلى قيمهما الأصلية
42.             transform.position = 
43.                 Vector3.Lerp(transform.position, //من
44.                         originalPos, //إلى
45.                         Time.deltaTime * 3);//مقدار الحركة
46.             
47.             transform.rotation = 
48.                 Quaternion.Lerp(
49.                         transform.rotation, //من
50.                         originalRot, //إلى
51.                         Time.deltaTime * 3);//مقدار الحركة
52.             
53.              //إذا كان الجسم قريبا جدا من موقعه الأصلي قم بإعادته إلى القيمة الأصلية مباشرة
54.             //false إلى returning ومن ثم غيّر قيمة
55.             float remaining = 
56.                  Vector3.Distance(transform.position, originalPos);
57.             if(remaining < 0.01f){
58.                 transform.position = originalPos;
59.                 transform.rotation = originalRot;
60.                 returning = false;
61.             }
62.         }
63.     }
64.     
65.     //قم بإعادة الوحدة البنائية لموقعها الأصلي
66.     public void Return(){
67.         returning = true;
68.     }
69. }

السرد 58: البريمج الخاص بإرجاع الوحدة البنائية لموقعها الأصلي قبل الهدم

ما يقوم به هذا البريمج ابتداء هو تخزين كل من الموقع والدوران الأصليين للكائن وذلك في المتغيرين originalPos و originalRot، وهذا الأخير يحمل النوع Quaternion الخاص بتخزين ومعالجة قيم الدوران. أثناء عملية التحديث نقوم في الدّالّة ()Update بفحص حالة مفتاح المسافة، فإذا كان المفتاح مضغوطا فإننا نقوم باستدعاء الدّالّة ()Return والتي تقوم بدورها بتغيير قيمة متغير الحالة returning إلى true وذلك حتى يسمح للدّالّة ()Update بتحريك الكائن بشكل تدريجي نحو كل من originalPos و originalRot. قبل تنفيذ التحريك والتدوير علينا أن نقوم بتصفير كافّة السرعات الخطية أو الدورانية على الجسم الصلب ومن ثم تجميده حتى لا يتأثر بأي قوى فيزيائية (الأسطر 29 إلى 38). بعد ذلك نستخدم الدّالّتين ()Vector3.Lerp و ()Quaternion.Lerp لإرجاع الوحدة البنائية بشكل تدريجي وسلس. بضربنا لقيمة Time.deltaTime في 3 نضمن سرعة أكبر في إعادة الوحدة لمكانها. بعد كل عملية تحريك نفحص المسافة بين موقع الوحدة الحالي وموقعها الأصلي، فإذا كانت هذه المسافة أقل من 0.01 فيمكننا حينها أن نعيد الموقع والدوران مباشرة لقيمهما الأصلية. يمكن تجربة النتيجة النهائية في المشهد scene19 في المشروع المرفق.

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

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