الفصل الرابع: الأسلحة والذخيرة وإعادة التعبئة

العب

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

الشكل 88: مشهد خاص بتجربة تبديل الأسلحة وإعادة تعبئتها

الشكل 88: مشهد خاص بتجربة تبديل الأسلحة وإعادة تعبئتها

جدير بالذكر أن الجدران التي تراها في الشكل 88 تم إنشاؤها باستخدام وحدات بنائية قابلة للهدم شبيهة بتلك التي استخدمناها في الفصل السادس من الوحدة الرابعة. رغم ذلك فمن الأفضل استخدام قالب جديد خاص بهذه الوحدات البنائية يختلف عن ذلك الذي استخدمنا سابقا؛ والسبب أننا سنقوم بإجراء بعض التعديلات عليه لناسب احتياجاتنا الحالية. من ميزات الوحدات البنائية هي إمكانية تعديل عدد كبير من الكائنات من مكان واحد، لذا ما سنقوم به هو نسخ القالب الحالي المسمى ReturnableBrick وتسمية النسخة الجديدة ShootableBreak ومن ثم استخدامها لبناء المشهد الجديد. سنعود لاحقا لهذا القالب لإجراء التعديلات اللازمة، لكن علينا الآن أن نضيف كائن اللاعب ولنسمّه player. سنضيف هذا الكائن في المشهد في نفس موقع الكاميرا التي ننظر من خلالها وسنقوم باستخدامه كموقع لإطلاق النار. هذا يعني بطبيعة الحال أنّ كائن اللاعب يجب أن ينظر للأمام باتجاه الجدران، أي أن محور z الخاص به يجب أن يشير باتجاهها. أخيرا سنضيف ثلاثة أبناء هي عبارة عن كائنات فارغة سنستخدمها ككائنات للأسلحة بالتالي سنسميها بأسماء تمثلها: Rifle, RPG, Sniper.

تمتلك الأسلحة المذكورة خصائص متشابهة، مثل إمكانية أطلاق النار من خلالها بطبيعة الحال، إضافة إلى حاجتها للذخيرة وهكذا. على الجانب الآخر فإن كل واحد من هذه الأسلحة لديه طريقته الخاصة في إطلاق النار: فالقناص (sniper) يطلق في كل مرة طلقة واحدة عالية الدقة في الإصابة، بينما الرشاش
(rifle) فيقوم بإطلاق عدد أكبر من الطلقات في وقت قصير، وأخيرا فإنّ قاذفة RPG تطلق قذيفة واحدة في كل مرة. هذا يعني أن علينا فصل الوظائف الأساسية المشتركة عن الوظائف الخاصة بكل سلاح وكيفية إطلاق النار فعليا من خلاله. بداية سنحتاج للبريمج GeneralWeapon والذي يحتوي على الوظائف الأساسية المشتركة بين الأسلحة، وهو موضح في السرد 82.

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class GeneralWeapon : MonoBehaviour {
5. 	
6. 	//عدد مخازن الذخيرة المتبقية
7. 	public int magazineCount = 3;
8. 	
9. 	//السعة التخزينية من الطلقات لكل مخزن ذخيرة
10. 	public int magazineCapacity = 30;
11. 	
12. 	//عدد الطلقات المتبقية في المخزن الحالي
13. 	public int magazineSize = 30;
14. 	
15. 	//كم من الوقت نحتاج لإعادة التعبئة؟
16. 	public float reloadTime = 3;
17. 	
18. 	//كم عدد الطلقات التي يستطيع السلاح إطلاقها في الثانية الواحدة؟
19. 	public float fireRate = 3;
20. 	
21. 	//true إذا كانت قيمة هذا المتغير هي
22. 	//فذلك يعني أن لا حاجة لرفع الإصبع عن الزناد بين عمليتي الإطلاق المتتاليتين
23. 	public bool automatic = false;
24. 	
25. 	//كمية الطلقات التي تنقص من مخزن الذخيرة الحالي مع كل عملية إطلاق للنار
26. 	//يجب ألا يتجاوز هذا الرقم السعة التخزينية لمخزن الذخيرة
27. 	public int ammoPerFiring = 1;
28. 	
29. 	//هل هذا السلاح هو المستخدم حاليا من قبل اللاعب؟
30. 	public bool inHand = false;
31. 	
32. 	//مؤقت داخلي لحساب وقت الانتظار بين عمليات الإطلاق
33. 	float lastFiringTime = 0;
34. 	
35. 	//متغير داخلي لتقدم عملية إعادة التعبئة
36. 	float reloadProgress = 0;
37. 	
38. 	//متغير داخلي لمعرفة ما إذا كان اللاعب حاليا يضغط على الزناد
39. 	bool triggerPulled = false;
40. 	
41. 	void Start () {
42. 		
43. 	}
44. 	
45. 	void Update () {
46. 		//إذا كان اللاعب يستخدم هذا السلاح حاليا وعملية إعادة التعبئة لم تتم بعد
47. 		//علينا في هذه الحالة أن نقوم بزيادة مقدار تقدم العملية
48. 		if(inHand && reloadProgress > 0){
49. 			reloadProgress += Time.deltaTime;
50. 			if(reloadProgress >= reloadTime){
51. 				//اكتملت إعادة التعبئة
52. 				//قم بالتخلص من المخزن الحالي
53. 				//وتركيب واحد جديد
54. 				magazineSize = magazineCapacity;
55. 				magazineCount--;
56. 				reloadProgress = 0;
57. 				SendMessage("OnReloadComplete",
58. 					SendMessageOptions.DontRequireReceiver);
59. 			}
60. 		}
61. 	}
62. 	
63. 	public void Fire(){
64. 		//هناك عدو شروط قبل إطلاق النار وهي أن يكون السلاح حاليا بيد اللاعب
65. 		//وألا يكون قيد إعادة التعبئة، كما ينبغي التأكد من مرور الفترة الزمنية المحددة
66. 		//بين عمليتي الإطلاق المتتاليتين وأن يكون السلاح إمّا أوتوماتيكيا أو أنّ اللاعب
67. 		//قد قام برفع إصبعه عن الزناد بعد آخر عملية إطلاق نار تمت
68. 		if(inHand && reloadProgress == 0 && 
69. 			(automatic || !triggerPulled) &&
70. 			Time.time - lastFiringTime > 1 / fireRate){
71. 			//هل يحتوي المخزن الحالي على عدد كاف من الطلقات؟
72. 			if(magazineSize >= ammoPerFiring){
73. 				//نعم، بالتالي تتم عملية الإطلاق عبر إنقاص الذخيرة
74. 				//وتحديد وقت الإطلاق الأخير، إضافة إلى
75. 				//OnWeaponFire إرسال الرسالة
76. 				magazineSize -= ammoPerFiring;
77. 				lastFiringTime = Time.time;
78. 				triggerPulled = true;
79. 				SendMessage("OnWeaponFire", 
80. 					SendMessageOptions.DontRequireReceiver);
81. 				
82. 				//إن كانت الذخيرة المتبقية في المخزن غير كافية يجب إعادة التعبئة
83. 				if(magazineSize < ammoPerFiring){
84. 					Reload();
85. 				}
86. 				
87. 			} else {
88. 				//لا توجد ذخيرة كافية للإطلاق، يجب إعادة التعبئة
89. 				Reload();
90. 			}
91. 		}
92. 	}
93. 	
94. 	public void ReleaseTrigger(){
95. 		triggerPulled = false;
96. 	}
97. 	
98. 	public void Reload(){
99. 		//قم بالتأكد من عدم وجود عملية تعبئة قيد التنفيذ
100. 		if(reloadProgress == 0){
101. 			//قم بالتأكد من وجود عدد كاف من المخازن وأنّ
102. 			//المخزن الحالي ليس ممتلئا
103. 			if(magazineCount > 0 && 
104. 				magazineSize < magazineCapacity){
105. 				//قم ببدء عملية إعادة التعبئة
106. 				reloadProgress = Time.deltaTime;
107. 				SendMessage("OnReloadStart",
108. 					SendMessageOptions.DontRequireReceiver);
109. 			}
110. 		}
111. 	}
112. 	
113. 	//تعيد هذه الدّالة مقدار التقدم الحاصل حاليا في إعادة التعبئة
114. 	public float GetReloadProgress(){
115. 		return reloadProgress / reloadTime;
116. 	}
117. }

السرد 82: البريمج الخاص بالوظائف الأساسية المشتركة بين جميع الأسلحة

تقوم المتغيرات الثلاث الأولى بإدارة كمية الذخيرة المتوفرة في السلاح. الفرق بين magazineCapacity و magazineSize هو أن الأول رقم ثابت يعبر عن الحد الأقصى من الطلقات الذي يمكن أن يسعه مخزن واحد، بينما يمثل الآخر عدد الطلقات المتبقية في المخزن المثبت بالسلاح حاليا. هذا العدد ينقص مع كل عملية إطلاق نار بمقدار يساوي ammoPerFiring أي عدد الطلقات التي تخرج من السلاح حين استخدامه لمرة واحدة. بالإضافة لمكية الذخيرة هناك متغيرات للتحكم بسرعة الإطلاق مثل reloadTime والذي يحدد الوقت اللازم لإتمام عملية إعادة تعبئة السلاح (أي استبدال المخزن الحالي بآخر جديد). إضافة لذلك لدينا المتغير fireRate والذي يحدد كم مرة يمكن إطلاق النار من السلاح في الثانية الواحدة. المتغيران الداخليان lastFiringTime و reloadProgress يعملان جنبا إلى جنب مع fireRate و reloadTime من أجل حساب التوقيتات بشكل صحيح. الأمر الآخر المهم هو تحديد ما إذا كان السلاح أوتوماتيكيا، والذي يعني قدرة السلاح على الاستمرار في إطلاق النار طالما لم يرفع اللاعب إصبعه عن الزناد. هذه العملية تتم إدارتها عبر المتغيرين automatic و triggerPulled. أخيرا لدينا المتغير inHand والذي يحدد ما إذا كان هذا السلاح هو الموجود حاليا بين يدي اللاعب، وهذا المتغير هو بمثابة المفتاح الرئيسي لجميع الوظائف الأخرى؛ حيث لا يمكن أن يتم إطلاق للنار أو إعادة للتعبئة طالما لم يكن السلاح بين يدي اللاعب ابتداءا.

الدّالتان ()Fire و ()ReleaseTrigger مرتبطتان ببعضهما؛ حيث إنّه في حالة السلاح غير الأوتوماتيكي فإنّه لا بد من استدعاء ()ReleaseTrigger مرة على الأقل بعد كل مرة يتم فيها استدعاء ()Fire وذلك حتى تصبح عملية الإطلاق التالية ممكنة الحدوث. عند إطلاق النار عبر استدعاء الدّالة ()Fire، فإنّ هذه الأخيرة عليها أن تتأكد من عدة أمور وهي:

  • أنّ اللاعب يحمل السلاح الحالي بين يديه (قيمة inHand هي true)
  • أنّ السلاح ليس قيد إعادة التعبئة حاليا (قيمة reloadProgress هي صفر)
  • أنّ السلاح أوتوماتيكي أو أنّه تم رفع الإصبع عن الزناد بعد آخر عملية إطلاق (قيمة automatic هي true أو قيمة triggerPulled هي false)
  • وجود ذخيرة كافية في السلاح (قيمة magazineSize أكبر من أو تساوي قيمة ammoPerFiring)

في حال تحقق جميع الشروط أعلاه، فإنّ البريمج يقوم بإرسال الرسالة OnWeaponFire بالإضافة إلى تغيير قيمة triggerPulled إلى true، ومن ثم يقوم أخيرا بإنقاص القيمة ammoPerFiring من مجموع الطلقات في المخزن الحالي magazineSize. إذا أصبح العدد الجديد للطلقات في المخزن الحالي أقل من عدد الطلقات اللازمة لإطلاق النار فإنّ البريمج يقوم تلقائيا باستبدال المخزن عن طريق استدعاء الدّالة ()Reload. مهمة هذه الدّالة هو أن تقوم ببدء عملية إعادة التعبئة وليس أن تقوم بها مباشرة ومرة واحدة، حيث يمث المتغير reloadProgress الوقت المنقضي منذ بدأت عملية إعادة التعبئة. تقوم الدّالة ()Reload أولا بفحص قيمة المتغير reloadProgress حيث يفترض أن تكون قيمتها صفرا إذا لم تكن عملية إعادة التعبئة بدأت بعد. بعد ذلك ينبغي التأكد من وجود مخزن إضافي واحد على الأقل غير ذلك الموجود على السلاح حاليا؛ لذا تقوم ()Reload بفحص قيمة magazineCount. إضافة إلى ذلك تقوم الدّالة أيضا بمقارنة قيمتي magazineSize و magazineCapacity، حيث أنّ تساوي هاتين القيمتين يعني أنّ المخزن الحالي ممتلئ بالتالي لا حاجة لاستبداله من الأساس. أخيرا بعد التحقق من كافة الشروط يتم تغيير قيمة المتغير reloadProgress إلى Time.deltaTime مما يؤدي إلى تجميع قيم Time.deltaTime أثناء تصيير الإطارات اللاحقة عن طريق الدّالة ()Update. تجميع هذه القيم يتم داخل المتغير reloadProgress، حتى إذا وصلت قيمته لقيمة أكبر من أو تساوي الوقت اللازم لإعادة التعبئة reloadTime عنى هذا أنّ عملية إعادة التعبئة قد اكتملت بالتالي يتم إنقاص عدد المخازن المتبقية بمقدار واحد وتغيير كمية الذخيرة في المخزن الحالي magazineSize إلى السعة الكاملة magazineCapacity.

آخر دالة سنتناولها من هذا البريمج هي ()GetReloadProgress. إذا كان السلاح الحالي قيد إعادة التعبئة فإنّ هذه الدّالة يفترض أن تعيد مقدار التقدم في التعبئة على شكل قيمة عدد كسري بين صفر وواحد. إرجاع القيمة صفر يعني أن السلاح ليس قيد إعادة التعبئة حاليا. هذه القيمة قد تكون مهمة لاستخدامها في مؤشرات تقدم أو لتحريك نموذج معين حسب تقدم إعادة التعبئة. الشكل 89 يظهر البريمج GeneralWeapon كما يظهر في شاشة الخصائص الخاصة بكل سلاح. من المهم هنا ذكر حقيقة أن القيمة fireRate لا تؤثر كثيرا في الأسلحة غير الأوتوماتيكية. لذا فبالرغم من حقيقة أنّ كلا من سلاح القناص Sniper وقاذفة RPG تم ضبطهما على قيم عالية إلّا أنّ هذا لن يكون ملاحظا لأن على اللاعب أن يفلت الزناد (زر الفأرة الأيسر) ويعيد الضغط عليه وهي عملية تأخذ وقتا، إضافة لأن قاذفة RPG لا تمتلك مخزنا بل يجب تركيب كل قذيفة لوحدها قبل إطلاقها.

الشكل 89: إعداد خصائص البريمج GeneralWeapon للأسلحة الثلاث المختلفة

الشكل 89: إعداد خصائص البريمج GeneralWeapon للأسلحة الثلاث المختلفة

لو اعتبرنا أنّ البريمج GeneralWeapon هو وحدة معالجة عملية إطلاق النار، فإنّ هذه الوحدة ستحتاج لمدخلات ومخرجات حتى يكتمل نظام الأسلحة الذي نسعى لبنائه. المدخلات ستكون عبارة عن التصويب نحو الهدف ومن ثم استدعاء الدّالة ()Fire بناء على مدخلات اللاعب عبر الفأرة. على الجانب الآخر فإنّ المخرجات ستكون عبارة عن بريمجات تستقبل الرسالة OnWeaponFire وتقوم بترجمتها لعملية إطلاق نار فعلية ذات تأثير على المشهد. لنبدأ أولا مع نظام المدخلات كونه مشتركا بين الأسلحة الثلاث بخلاف نظام المخرجات. للتذكير فإننا سنستخدم الفأرة للتصويب وإطلاق النار وبالتالي سنحتاج لبريمج ينظر مباشرة لموقع مؤشر الفأرة في المشهد. هذا البريمج هو MousePointerFollower الموضح في السرد 83 والذي سنقوم بإضافته لكائن اللاعب.

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class MousePointerFollower : MonoBehaviour {
5. 	
6. 	//كائن يستخدم كعلامة لموقع الهدف داخل المشهد
7. 	public Transform targetMarker;
8. 	
9. 	void Start () {
10. 		//قم بإخفاء العلامة خلف اللاعب
11. 		targetMarker.position = 
12. 			transform.position - Vector3.forward;
13. 	}
14. 	
15. 	void Update () {
16. 		//حاول العثور على ما يشير إليه مؤشر الفأرة داخل المشهد
17. 		Ray camToMouse = 
18. 			Camera.main.ScreenPointToRay (Input.mousePosition);
19. 
20. 		RaycastHit hit;
21. 		if(Physics.Raycast(camToMouse, out hit, 500)){
22. 			//تم العثور على الكائن، قم بالنظر لموقع المؤشر فوقه
23. 			transform.LookAt(hit.point);
24. 			//قم بتحريك العلامة إلى موقع المؤشر
25. 			targetMarker.position = hit.point;
26. 			//قم بتحريك العلامة قليلا باتجاه اللاعب حتى تصبح مرئية
27. 			targetMarker.LookAt(transform.position);
28. 			targetMarker.Translate(0, 0, 0.1f);
29. 		} else {
30. 			//لا يوجد هدف تحت مؤشر الفأرة، قم باختيار نقطة بعيدة
31. 			//والنظر إليها
32. 			transform.LookAt(camToMouse.GetPoint(500));
33. 			//قم بإخفاء العلامة
34. 			targetMarker.position = 
35. 				transform.position - Vector3.forward;
36. 		}
37. 	}
38. 	
39. }

السرد 83: بريمج بجعل الكائن ينظر بشكل دائم لموقع مؤشر الفأرة داخل المشهد

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

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

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class WeaponController : MonoBehaviour {
5. 	
6. 	//مصفوفة تحتوي على الأسلحة المتوفرة
7. 	public GeneralWeapon[] weapons;
8. 	
9. 	//الموقع الخاص بالسلاح الذي يحمله اللاعب ابتداء
10. 	public int initialWeapon = -1;
11. 	
12. 	//الموقع الخاص بالسلاح الذي يحمله اللاعب حاليا
13. 	int currentWeapon;
14. 	
15. 	void Start () {
16. 		//قم بضبط قيمة السلاح الحالي إلى السلاح الأولي
17. 		//الذي تم اختياره عبر نافذة الخصائص
18. 		currentWeapon = initialWeapon;
19. 		//لجميع الأسلحة inHand قم بتحديث قيم المتغير 
20. 		RefreshInHandValues();
21. 	}
22. 	
23. 	void Update () {
24. 		UpdateSwitching();
25. 		UpdateShooting();
26. 	}
27. 	
28. 	void UpdateSwitching(){
29. 		//قم بتحويل قيمة المفتاح "1” من لوحة المفاتيح إلى قيمة رقمية 
30. 		//نتحدث هنا عن المفتاح الذي يقع فوق الأحرف وليس على لوحة الأرقام اليمنى
31. 		int keyCode = (int)KeyCode.Alpha1;
32. 		for(int i = 0; i < weapons.Length; i++){
33. 			//لاختيار أي سلاح نحتاج لقيمة المفتاح "1” مضافا إليها موقع السلاح في المصفوفة
34. 			if(Input.GetKeyDown((KeyCode) keyCode + i)){
35. 				currentWeapon = i;
36. 				RefreshInHandValues();
37. 			}
38. 		}
39. 	}
40. 	
41. 	void UpdateShooting(){
42. 		//تم الضغط على زر الفأرة: قم بالضغط على الزناد
43. 		if(Input.GetMouseButton(0)){
44. 			weapons[currentWeapon].Fire();
45. 		}
46. 		//تم رفع الضغط عن زر الفأرة: أزل الإصبع عن الزناد
47. 		if(Input.GetMouseButtonUp(0)){
48. 			weapons[currentWeapon].ReleaseTrigger();
49. 		}
50. 		//تم الضغط على الزر الفأرة الأيمن: قم بإعادة تعبئة السلاح
51. 		if(Input.GetMouseButtonDown(1)){
52. 			weapons[currentWeapon].Reload();
53. 		}
54. 	}
55. 	
56. 	//Change weapon
57. 	public void SetCurrentWeapon(int newIndex){
58. 		weapons[currentWeapon].ReleaseTrigger();
59. 		currentWeapon = newIndex;
60. 		RefreshInHandValues();
61. 	}
62. 	
63. 	void RefreshInHandValues(){
64. 		foreach(GeneralWeapon gw in weapons){
65. 			//في حالة true يجب أن تكون inHand قيمة المتغير
66. 			//السلاح الذي يحمله اللاعب حاليا فقط
67. 			gw.inHand = weapons[currentWeapon] == gw;
68. 		}
69. 	}
70. }

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

يجب أن تتم إضافة كافة الأسلحة التي قمنا بعملها إلى المصفوفة weapons وذلك حتى يتمكن البريمج من الوصول إليها وبالتالي تمكين اللاعب من التبديل بينها واستخدامها. القيمة المبدئية للمتغير initialWeapon هي 1-، مما يعني أنّ اللاعب لا يحمل أي سلاح بيده. بعد أن نقوم بإضافة الأسلحة الثلاث يمكننا تغيير هذه القيمة إلى 0 أو 1 أو 2 بحسب السلاح الذي نريده. المتغير الذي يحدد السلاح الحالي بيد اللاعب هو currentWeapon وهو متغير داخلي يمكن تغيير قيمته فقط عبر الدّالة
()SetCurrentWeapon. هذا الأمر ضروري لضمان استدعاء الدّالة ()RefreshInHandValues بعد كل عملية تبديل سلاح. أهمية الدّالة ()RefreshInHandValues هو أنها تضمن وجود سلاح واحد فقط تكون فيه قيمة inHand هي true وبالتالي يمكنه الاستجابة للمدخلات. هذا السلاح الوحيد هو بطبيعة الحال الموجود داخل الموقع currentWeapon في مصفوفة الأسلحة. لقراءة المدخلات يتم استدعاء كل من ()UpdateShooting و ()UpdateSwitching في كل دورة تصيير.

بالحديث بالتفصيل عن الدّالة ()UpdateSwitching نلاحظ أنها تقوم بفحص حالة مفاتيح الأرقام ابتداء من KeyCode.Alpha1 وهو المفتاح رقم 1 الذي تجده فوق الحرف Q أعلى يسار لوحة المفاتيح (لا ينطبق هذا على مفتاح 1 الأيمن الموجود بجوار الأسهم على لوح الأرقام). بعدها يتم تحويل قيمته من من المعدّد KeyCode إلى عدد صحيح int. هذا العدد لو أضفنا إليه 1 ومن ثم أعدنا تحويله إلى نوع المعدّد KeyCode فإننا نحصل على قيمة مساوية لـ KeyCode.Alpha2. هذه الحقيقة تساعدنا على فحص قيمة جميع الأرقام الممثلة بالأسلحة بشكل متسلسل عبر حلقة تكرارية مما يختصر علينا كتابة عدد كبير من جمل if-else لكل المفاتيح من KeyCode.Alpha1 إلى KeyCode.Alpha9. بالتالي فإنّ ضغط المفتاح 1 على لوحة المفاتيح سيحولنا للسلاح الموجود في الخانة صفر في المصفوفة weapons أي السلاح الأول والمفتاح 2 للسلاح الثاني وهكذا. على الجانب الآخر تقوم الدّالة
()UpdateShooting بقراءة مدخلات أزرار الفأرة، حيث تستدعي الدّالة ()Fire من السلاح الحالي عند الضغط على الزر الأيسر، كما تقوم باستدعاء ()ReleaseTrigger حين إفلات هذا الزر. إضافة إلى ذلك فإنّ الضغط على زر الفأرة الأيمن سيؤدي لاستدعاء الدّالة ()Reload من السلاح الحالي والتي تعمل على إعادة تعبئة السلاح. عند إعداد كائن اللاعب بشكل صحيح يجب أن يظهر في نافذة الخصائص كما في الشكل 90.

الشكل 90: كائن اللاعب حين إعداده بشكله النهائي

الشكل 90: كائن اللاعب حين إعداده بشكله النهائي

عند الانتهاء من إعداد كائن اللاعب بهذه الصورة تكون عملية الحصول على مدخلات الأسلحة قد اكتملت ويمكننا الانتقال للمخرجات. بما أنّ كلا من الرشاش والقناص سيعتمد على بث الأشعة في عملية التصويب، فمن المنطقي إعادة استخدام البريمج RaycastShooter (السرد 49) وكل ما علينا فعله بعد إضافته لكل من السلاحين هو ضبط قيمه مثل المدى range وعدم الدقة inaccuracy و قوة الطلقة power. بعدها علينا أن نقوم بكتابة بريمج صغير هو عبارة عن "جسر" بين البريمجات الأخرى ومهمته استقبال الرسالة OnWeaponFire من البريمج GeneralWeapon واستدعاء الدّالة ()Shoot من البريمج RaycastShooter بناء عليها. البريمج WeaponToRaycast الذي يقوم بهذه المهمة موضح في السرد 85.

1. using UnityEngine;
2. using System.Collections;
3. 
4. [RequireComponent(typeof(GeneralWeapon))]
5. [RequireComponent(typeof(RaycastShooter))]
6. public class WeaponToRaycast : MonoBehaviour {
7. 	
8. 	RaycastShooter shooter;
9. 	
10. 	void Start () {
11. 		shooter = GetComponent<RaycastShooter>();
12. 	}
13. 	
14. 	void Update () {
15. 	}
16. 	
17. 	void OnWeaponFire(){
18. 		shooter.Shoot();
19. 	}
20. }

السرد 85: بريمج بسيط يعمل كجسر بين GeneralWeapon و RaycastShooter

من البديهي أن يعتمد هذا البريمج على كل من RaycastShooter و GeneralWeapon كونه يعمل على الربط بينهما لا أكثر. عند إعداد كل من القناص والرشاش سيظهر البريمج RaycastShooter بشكله النهائي في نافذة الخصائص كما في الشكل 91.

الشكل 91: الإعدادان المختلفان للبريمج RaycastShooter على القناص (يمينا) وعلى الرشاش (يسارا)

الشكل 91: الإعدادان المختلفان للبريمج RaycastShooter على القناص (يمينا) وعلى الرشاش (يسارا)

يمكننا الآن العودة إلى قالب الوحدة البنائية الذي قمنا بنسخه وإجراء التعديلات اللازمة عليه. بالإضافة لكون هذه الوحدات البنائية قابلة للهدم، عليها أن تتأثر بطلقات البث الإشعاعي التي يطلقها RaycastShooter. علينا أولا أن نتأكد من أن الطلقات تحدث ثقوبا في هذه الوحدات البنائية وذلك عن طريق إضافة البريمج BulletHoleMaker (السرد 52 ) إلى القالب الجديد الذي أسميناهShootableBrick ونحدد قالب الثقب الذي سبق واستخدمناه. علينا أيضا أن نقوم بإزالة البريمج MouseExploder لتجنب حدوث انفجار عند كل نقرة بالفأرة على أي وحدة بنائية.

لإزالة مكوّن أو بريمج من كائن ما قم بالضغط بالفأرة على رمز الترس في أعلى يسار المكوّن ومن ثم اختر Remove Component

البريمج التالي الذي ينبغي أن نضيفه للقالب هو BulletForceReceiver (السرد 53) والذي يسمح لطلقات كل من الرشاش والقناص أن يطبقا قوة دفع فيزيائية على الأجسام التي تصيبها. من المهم ملاحظة أن هذا التأثير على الوحدة البنائية غير ممكن بشكلها الحالي؛ والسبب هو أنها تحتوي على البريمج Destructible والذي يقوم بتجميد الجسم الصلب الخاص بها ويمع تأثير القوى الخارجية عليها، مما يعني أن البريمج BulletForceReceiver لن يكون ذو تأثير قبل أن يتم هدم الوحدة البنائية. لحل هذه المشكلة سنحتاج لبريمج يقوم بهدم الوحدة البنائية حين إصابتها بإطلاق نار ببث شعاعي (أي عند استقبال الرسالة OnRaycastHit) ذو قوة كافية لهدمها. هذا البريمج هو DestructOnHitDamage وهو موضح في السرد 86.

1. using UnityEngine;
2. using System.Collections;
3. 
4. [RequireComponent(typeof(Destructible))]
5. public class DestructOnHitDamage : MonoBehaviour {
6. 	
7. 	//الحد الأدنى لقوة الإصابة القادرة على هدم الوحدة البنائية
8. 	public float destructionDamage = 250;
9. 	
10. 	Destructible dest;
11. 	
12. 	void Start () {
13. 		dest = GetComponent<Destructible>();
14. 	}
15. 	
16. 	void Update () {
17. 	
18. 	}
19. 	
20. 	void OnRaycastHit(RaycastHit hit){
21. 		//distance تذكر أن قوة الإصابة مخزنة داخل المتغير
22. 		//destructionDamage إذا كانت قيمتها أكبر من
23. 		//نقوم بهدم الوحدة البنائية
24. 		if(hit.distance > destructionDamage){
25. 			dest.Destruct();
26. 		}
27. 	}
28. }

السرد 86: البريمج الخاص باستقبال إصابة بث الأشعة وهدم الوحدة البنائية المصابة بناء على قوة الإصابة

كل ما علينا فعله الآن هو تحديد مقدار القوة اللازمة لهدم الوحدة البنائية. عند استدعاء الدّالة
()Destruct فإننا نقوم بإزالة كافة القيود عن حركة الوحدة البنائية مما يسمح لقوة دفع الرصاصة التي يقوم البريمج BulletForceReceiver بالتأثير على موقع الوحدة البنائية ودورانها.

آخر ما علينا التعامل معه في موضوع مخرجات السلاح هو قذائف RPG. سنحتاج في هذه الحالة إلى قالب للقذيفة والتي سيتم إطلاقها عند استخدام سلاح القاذفة. في حال اصطدام هذه القذيفة بأحد الجدران فستقوم بإحداث انفجار وتدميره كليا أو جزئيا. سنحتاج لجزأين في عملية إطلاق القذيفة: القذيفة نفسها بالإضافة إلى بريمج يستجيب للرسالة OnWeaponFire عن طريق إطلاق هذه القذيفة في اتجاه التصويب الحالي. لنبدأ مع البريمج RPG والذي يعمل على إطلاق القذيفة والذي يجب أن نضيفه لكائن سلاح RPG. هذا البريمج موضح في السرد 87.

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class RPG : MonoBehaviour {
5. 	
6. 	//قالب القذيفة التي سيتم إطلاقها
7. 	public GameObject rocketPrefab;
8. 	
9. 	void Start () {
10. 	
11. 	}
12. 	
13. 	void Update () {
14. 	
15. 	}
16. 	
17. 	//قم باستقبال الرسالة وإطلاق القذيفة بناء على ذلك
18. 	//سنعطي القذيفة موقع ودورانا أوليين مساويان للموقع والدوران الخاصين
19. 	//بكائن القاذفة نفسها
20. 	void OnWeaponFire(){
21. 		GameObject rocket = (GameObject)Instantiate(rocketPrefab);
22. 		rocket.transform.position = transform.position;
23. 		rocket.transform.rotation = transform.rotation;
24. 	}
25. }

السرد 87: بريمج قاذفة RPG

سنستخدم لعمل القذيفة كرة سنقوم بتمديدها على محورها المحلي z بحيث تصبح إهليجية الشكل (أشبه بكرة القدم الأمريكية). يمكننا مثلا استخدام الأبعاد (0.75 ,0.2 ,0.2) ومن ثم نعطي الكائن أي إكساء مناسب. بعد ذلك سنقوم ببناء قالب من هذا الكائن ونربط هذا القالب بالمتغير rocketPrefab في البريمج RPG. سيحتاج هذا القالب بطبيعة الحال لعدد من المكونات والبريمجات حتى يصبح سلوكه كقذيفة صحيحا. بداية سنحتاج لمكوّن الجسم الصلب rigid body وذلك لنكون قادرين على تحريكه بقوة دافعة واكتشاف تصادمه مع الكائنات الأخرى. قبل الخوض في تفاصيل المكوّنات الأخرى علينا أن نتأمل ما تقوم به القذيفة: أولا يتم إطلاقها بقوة دافعة من القاذفة، ثانيا تمتلك القدرة على الانفجار، ثالثا تقوم بتفجير الهدف الذي تصيبه، رابعا تُدمر عند التصادم التصادم. لكل واحد من هذه الخصائص الأربعة سنحتاج بريمجا مختلفا، ولتكن البداية مع الإطلاق والحركة والتي يتولاها البريمج RPGRocket الموضح في السرد 88.

1. using UnityEngine;
2. using System.Collections;
3. 
4. [RequireComponent(typeof(Rigidbody))]
5. public class RPGRocket : MonoBehaviour {
6. 	
7. 	//القوة الدافعة عند الإطلاق
8. 	public float launchForce = 100;
9. 	
10. 	//عدد الثواني التي تبقى خلالها القذيفة موجودة في المشهد
11. 	//في حال لم تصب أي هدف
12. 	public float lifeTime = 7;
13. 	
14. 	//متغير داخلي لحساب وقت الإبقاء على القذيفة
15. 	float launchTime;
16. 	
17. 	//متغير داخلي لتتبع حالة القذيفة
18. 	//هذا المتغير مهم لمنع اكتشاف التصادم بين كائن القذيفة بعد اصطدامها
19. 	//والأجزاء التي ستنتج عنها عند تحطمها 
20. 	bool destroyed = false;
21. 	
22. 	void Start () {
23. 		rigidbody.AddForce(transform.forward * launchForce,
24. 							ForceMode.VelocityChange);
25. 		
26. 		launchTime = Time.time;
27. 	}
28. 	
29. 	void Update () {
30. 		if(!destroyed && Time.time - launchTime > lifeTime){
31. 			Destroy(gameObject);
32. 		}
33. 	}
34. 	
35. 	void OnCollisionEnter(Collision col){
36. 		if(!destroyed){
37. 			destroyed = true;
38. 			//قم بإعلام البريمجات الأخرى بحدوث التصادم مع كائن آخر
39. 			//وأرفق مع الرسالة مرجعا لهذا الكائن
40. 			SendMessage("OnRocketHit", 
41. 				col.collider, 
42. 				SendMessageOptions.DontRequireReceiver);
43. 			
44. 			//قم بتدمير كائن الصاروخ
45. 			Destroy(gameObject);
46. 		}
47. 	}
48. }

السرد 88: البريمج الخاص بإطلاق قذيفة RPG واكتشاف تصادمها مع الكائنات الأخرى

يعطي هذا البريمج عند بدايته قوة دافعة لإطلاق القذيفة، بحيث تكون قوة الإطلاق نحو الأمام. بعد ذلك يبدأ البريمج في حساب عمر القذيفة الذي تم تحديده قبل تدميرها. على أي حال فإن اصطدام القذيفة بهدف ما سيؤدي إلى تدميرها حتى لو لم ينقضي العمر المحدد لها، لكن هذا التدمير لن يتم قبل إرسال الرسالة OnRocketHit والتي تخبر البريمجات الأخرى بحدوث التصادم وتحمل إليها مرجعا للجسم الذي تم التصادم معه. لاحظ أنّه من الممكن أن تصطدم القذيفة بأكثر من جسم في نفس الإطار، كما يمكنها أن تتصادم مع حطامها الذي سنقوم بإنشائه كما سنرى بعد قليل. من أجل ذلك نحتاج لاستخدام متغير الحالة الداخلي destroyed حتى نضمن إرسال الرسالة OnRocketHit مرة واحدة فقط. بالعودة لموضوع الحطام، يمكننا إعادة استخدام البريمج Breakable الذي سبق وكتبناه (السرد 59) وسنستخدم قالبا خاصا بالقذيفة ليمثل قطع حطامها. فيما يتعلق بالمتغير explosionPower، علينا هذه المرة أن نستخدم قيمة عالية نسبيا (1000 مثلا) بحيث تمثل انفجارا قويا تحدثه القذيفة حين اصطدامها ويؤثر إلى تناثر قطع الحطام بعيدا عن بعضها البعض.

الخطوة التالية هي الربط بين تصادم القذيفة مع الهدف وتدميرها، من أجل هذا نحتاج لبريمج صغير هدفه ربط البريمج RPGRocket مع البريمج Breakable. ما سيقوم به هذا البريمج هو استقبال الرسالة OnRocketHit من RPGRocket وإرسال الرسالة Break (أو استدعاء الدّالة ()Break مباشرة) إلى البريمج Breakable. البريمج هو BreakOnRocketHit وهو موضح في السرد 89.

1. using UnityEngine;
2. using System.Collections;
3. 
4. [RequireComponent(typeof(RPGRocket))]
5. [RequireComponent(typeof(Breakable))]
6. public class BreakOnRocketHit : MonoBehaviour {
7. 
8. 	void Start () {
9. 	
10. 	}
11. 	
12. 	void Update () {
13. 	
14. 	}
15. 	
16. 	void OnRocketHit(Collider hitObject){
17. 		GetComponent<Breakable>().Break();
18. 	}
19. }

السرد 89: بريمج يربط RPGRocket و Breakable بحيث يؤدي لتدمير القذيفة مباشرة عند اصطدامها مع الهدف

البريمج الأخير الذي سنضيفه لقالب القذيفة هو المادة المتفجرة التي ستقوم بتدمير الهدف، عند استقبال الرسالة OnRocketHit من البريمج RPGRocket علينا أن نقوم بالبحث عن الوحدات البنائية الموجودة ضمن مدى الانفجار وهدمها بحيث تصبح تحت تأثير قوة الانفجار التي سنضيفها لها. هاتان الخطوات يقوم بهما البريمج DestructOnRocketHit والذي يوضحه السرد 90.

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class DestructOnRocketHit : MonoBehaviour {
5. 	
6. 	//نصف قطر محيط تأثير الانفجار
7. 	public float explosionRadius = 3;
8. 	
9. 	//قوة الانفجار
10. 	public float explosionForce = 50000;
11. 	
12. 	void Start () {
13. 	
14. 	}
15. 	
16. 	void Update () {
17. 	
18. 	}
19. 	
20. 	void OnRocketHit(Collider target){
21. 		//قم بهدم جميع الوحدات البنائية الواقعة ضمن محيط الانفجار
22. 		//وإضافة قوة الانفجار لها بعد ذلك
23. 		Destructible[] all = FindObjectsOfType<Destructible>();
24. 		
25. 		Vector3 explosionPos = transform.position;
26. 		
27. 		foreach(Destructible dest in all){
28. 			if(Vector3.Distance
29. 				(explosionPos, dest.transform.position) 
30. 								< explosionRadius){
31. 				
32. 				dest.Destruct();
33. 				
34. 				dest.rigidbody.
35. 					AddExplosionForce(explosionForce, 
36. 								explosionPos, 
37. 								explosionRadius);
38. 			}
39. 		}
40. 	}
41. }

السرد 90: بريمج لهدم وتفجير الوحدات البنائية المحيطة بمكان اصطدام قذيفة RPG

لعلك لاحظت الشبه الكبير بين البريمجين MouseExploder (السرد 57) و هذا البريمج، الفرق هو أنّ MouseExploder يعتمد على موقع مؤشر الفأرة كموقع للانفجار بينما DestructOnRocketHit يعتمد على موقع اصطدام القذيفة.

بهذا تكون جميع الأسلحة جاهزة للاستخدام ويمكنك تجربتها في المشهد. يمكن إطلاق النار من الأسلحة عن طريق زر الفأرة الأيسر كما يمكن التبديل بينها باستخدام مفاتيح الأرقام 1 و2 و 3. آخر ما يتوجب علينا فعله الآن هو تفعيل آلية عرض كمية الذخيرة ومقدار التقدم في عملية إعادة التعبئة للأسلحة الثلاث. بالعودة إلى الشكل 88 يمكنك ملاحظة وجود كائن نص يحمل القيمة (amm) إلى جانب كل اسم من أسماء الأسلحة الثلاث. سنستخدم هذه النصوص لعرض معلومات عن كل سلاح، فمثلا سنعرض النص (XXX) أمام كل سلاح لا يحمله اللاعب حاليا للدلالة على أنه ليس قيد الاستخدام في هذه اللحظة، بالتالي يكون اللاعب دوما على علم بالسلاح الذي يحمله. أمّأ إذا كان السلاح بيد اللاعب فإننا سنعرض كمية الذخيرة المتبقية مستخدمين التنسيق A/B حيث أن A هو كمية الذخيرة المتبقية في المخزن الحالي و B هو عدد المخازن المتبقية. أمّا إذا كان السلاح قيد إعادة التعبئة فإنّ تقدم العملية سيظهر على شكل نسبة مئوية.

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

1. using UnityEngine;
2. using System.Collections;
3. 
4. [RequireComponent(typeof(GeneralWeapon))]
5. public class AmmoDisplay : MonoBehaviour {
6. 	
7. 	//كائن النص الذي سيتم عرض المعلومات عليه
8. 	public TextMesh display;
9. 	
10. 	//مرجع لبريمج السلاح الذي سنستخدمه كمصدر للمعلومات المعروضة
11. 	GeneralWeapon weapon;
12. 	
13. 	void Start () {
14. 		weapon = GetComponent<GeneralWeapon>();
15. 	}
16. 	
17. 	void LateUpdate () {
18. 		//لا تعرض أي معلومات ما لم يكن اللاعب يحمل السلاح
19. 		if(!weapon.inHand){
20. 			display.text = "XXX";
21. 			return;
22. 		}
23. 		
24. 		float reloadProgress = weapon.GetReloadProgress();
25. 		//قم بعرض كمية الذخيرة المتبقية في المخزن وعدد المخازن المتوفرة
26. 		if(reloadProgress == 0){
27. 			display.text = weapon.magazineSize + "/" +
28. 						weapon.magazineCapacity + " (x" + 
29. 							weapon.magazineCount + ")";
30. 		} else {
31. 			//في حالة إعادة التعبئة قم بعرض التقدم في هذه العملية
32. 			int progress = (int)(reloadProgress * 100);
33. 			display.text = "RLD " + progress + "%";
34. 		}
35. 	}
36. }

السرد 91: البريمج الخاص بعرض حالة السلاح وكمية الذخيرة المتبقية على شكل نص

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

الشكل 92: الشكل النهائي لمشهد تجربة تبديل الأسلحة

الشكل 92: الشكل النهائي لمشهد تجربة تبديل الأسلحة

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

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