الفصل الرابع: تطبيق نظام إدخال ألعاب منظور الشخص الأول

العب

ألعاب منظور الشخص الأول first person games من أكثر الألعاب ثلاثية الأبعاد شعبية، وتنتمي لهذه الفئة الكثير من الألعاب الشهيرة في عالم ألعاب الفيديو مثل Doom و Half-Life و Call of Duty. تعتمد هذه الألعاب على وضع الكاميرا مكان وجه اللاعب ورؤية عالم اللعبة من خلال عينيه مستخدمة الفأرة لتوجيه نظر اللاعب. إضافة للفأرة تستخدم عادة مفاتيح WSAD للتحرك في الاتجاهات الأربع.

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

الشكل 21: كائن الأسطوانة الخاص بنظام إدخال منظور الشخص الأول

الشكل 21: كائن الأسطوانة الخاص بنظام إدخال منظور الشخص الأول

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

أما بالنسبة لحركة اللاعب، فضغط المفتاح W سيقوم بتحريك اللاعب نحو الأمام في الاتجاه الذي ينظر إليه حاليا (أي الاتجاه الموجب للمحور z الخاص بكائن الأسطوانة)، بينما يحركه المفتاح S عكس هذا الاتجاه. كذلك الأمر بالنسبة للجانبين حيث يحركه المفتاح D في الاتجاه الموجب للمحور x الخاص بالكائن أي نحو اليمين، بينما يحركه المفتاح A نحو اليسار. الشكل 22 يوضح كيفية استجابة كائن الأسطوانة والكاميرا لمدخلات اللاعب.

الشكل 22: تأثير مدخلات اللاعب على حركة ودوران كائن الأسطوانة والكاميرا

الشكل 22: تأثير مدخلات اللاعب على حركة ودوران كائن الأسطوانة والكاميرا

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

بقي أن نذكر أن دوران الكاميرا على محورها المحلي x (أي الاستدارة العمودية) يجب أن يكون محددا بين قيمتين عظميين للأعلى والأسفل، فلا يجب أن نسمح للكاميرا بالاستدارة إلى أن تصبح مقلوبة رأسا على عقب. لذلك علينا أن نحدد زاوية بين الكاميرا والأفق، ولتكن مثلا 60، بحيث لا نسمح لها بالنظر لأعلى أو لأسفل بمقدار يزيد عن هذه الزاوية.

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

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class FirstPersonControl : MonoBehaviour {
5.  
6.  //السرعة العمودية عند بداية القفز
7.  public float jumpSpeed = 0.25f;
8.  
9.  //سرعة السقوط
10.     public float gravity = 0.5f;
11.     
12.     //سرعة الحركة الأفقية على الأرض
13.     public float movementSpeed = 15;
14.     
15.     //سرعة النظر بالفأرة على المحورين الأفقي والعمودي
16.     public float horizontalMouseSpeed = 0.9f;
17.     public float verticalMouseSpeed = 0.5f;
18.     
19.     //أكبر قياس مسموح به لزاوية الدوران العمودي للكاميرا
20.     public float maxVerticalAngle = 60;
21.     
22.     //تخزين سرعة اللاعب على المحاور الثلاث لتنفيذ الحركة
23.     private Vector3 speed;
24.     
25.     //موفع الفأرة خلال الإطار السابق 
26.     //ضروري لحساب الإزاحة
27.     private Vector3 lastMousePosition;
28.     
29.     //متغير لتخزين كائن الكاميرا
30.     private Transform camera;
31.     
32.     void Start () {
33.         lastMousePosition = Input.mousePosition;
34.         //البحث عن كائن الكاميرا في قائمة الأبناء
35.         camera = transform.FindChild("Main Camera");
36.     }
37.     
38.     void Update () {    
39.         //y الخطوة الأولى هي إدارة الأسطوانة عموديا على المحور 
40.         //بناء على الإزاحة الأفقية للفأرة يمينا أو يسارا
41.         Vector3 mouseDelta = Input.mousePosition - lastMousePosition;
42.         
43.         transform.RotateAround(
44.                 Vector3.up, //محور الدوران
45.                 mouseDelta.x * 
46.                 horizontalMouseSpeed * 
47.                 Time.deltaTime);//زاوية الدوران
48.         
49.         //الدوران العمودي الحالي لكائن الكاميرا
50.         float currentRotation = camera.localRotation.eulerAngles.x;
51.         
52.         //تحويل دوران الكاميرا العمودي من المدى بين 0 و 360
53.         //إلى المدى بين 180- و 180
54.         if(currentRotation > 180){
55.             currentRotation = currentRotation - 360;
56.         }
57.         
58.         //حساب الدوران خلال الإطار الحالي
59.         float ang = 
60.             -mouseDelta.y * verticalMouseSpeed * Time.deltaTime;
61.         
62.         //x الخطوة الثانية هي الدوران العمودي للكاميرا على محورها المحلي 
63.         //بناء على الإزاحة العمودية للفأرة لأعلى أو لأسفل
64.         //قبل ذلك يجب أن نتأكد من حدود الدوران العمودية المسموحة
65.         if((ang < 0 && ang + currentRotation > -maxVerticalAngle) ||
66.             (ang > 0 && ang + currentRotation < maxVerticalAngle)){
67.             camera.RotateAround(camera.right, ang);
68.         }
69.         
70.         //تحديث موقع الفأرة الأخير استعدادا للإطار المقبل
71.         lastMousePosition = Input.mousePosition;
72.         
73.         //الخطوة الثالثة هي تحديث الحركة
74.         if(Input.GetKey(KeyCode.A)){
75.             //تحرك لليسار
76.             speed.x = -movementSpeed * Time.deltaTime;
77.         } else if(Input.GetKey(KeyCode.D)){
78.             //تحرك لليمين
79.             speed.x = movementSpeed * Time.deltaTime;
80.         } else {
81.             speed.x = 0;
82.         }
83.         
84.         if(Input.GetKey(KeyCode.W)){
85.             //تحرك للأمام
86.             speed.z = movementSpeed * Time.deltaTime;
87.         } else if(Input.GetKey(KeyCode.S)){
88.             //تحرك للخلف
89.             speed.z = -movementSpeed * Time.deltaTime;
90.         } else {
91.             speed.z = 0;
92.         }
93.         
94.         //قراءة مدخل القفز
95.         if(Input.GetKeyDown(KeyCode.Space)){
96.             //تنفيذ القفز فقط في حال كون اللاعب يقف على الأرض
97.             if(transform.position.y == 2.0f){
98.                 speed.y = jumpSpeed;
99.             }
100.        }
101.        
102.        //تحريك الأسطوانة حسب الاتجاهات المحسوبة
103.        transform.Translate(speed);
104.        
105.        //تنفيذ الجاذبية في حال القفز
106.        if(transform.position.y > 2.0f){
107.            speed.y = speed.y - gravity * Time.deltaTime;
108.        } else {
109.            speed.y = 0;
110.            Vector3 newPosition = transform.position;
111.            newPosition.y = 2.0f;
112.            transform.position = newPosition;
113.        }
114.    }
115. }

السرد 9: تطبيق نظام تحكم منظور الشخص الأول

قد تبدو لك بعض الأجزاء من هذا البريمج مألوفة كونك سبق ورأيتها في السرد 6 عندما قمنا ببناء نظام تحكم ألعاب المنصات (إن لم تكن قرأت ذلك الفصل يمكنك العودة له). لنناقش الآن الأجزاء الجديدة التي تخص نظام تحكم منظور الشخص الأول.

بعد أن قمنا بعريف بعض المتغيرات للتحكم بالحركة، استدعينا الدّالّة ()transform.FindChild في السطر 35 وذلك لنبحث في أبناء الكائن الحالي عن كائن محدد بالاسم. الاسم الذي قمنا بالبحث عنه في هذه الحالة هو Main Camera وهو الاسم الافتراضي المعطى من قبل Unity لكائن الكاميرا. وبما أننا سبق وأضفنا الكاميرا كابن للأسطوانة التي تحمل هذا البريمج، فإن الدّالّة المذكورة ستقوم بإيجاد الكاميرا وإرجاعها لنا ليتم تخزينها في المتغير camera مما يمكّننا من التعامل معها لاحقا.

في الخطوة الأولى في الأسطر من 43 إلى 47 قمنا باستدعاء الدّالّة ()transform.RotateAround ومهمتها تدوير الكائن حول محور محدد. فقمنا بتزويدها بكل من المحور الذي نريد أن ندير الكائن حوله، إضافة إلى زاوية الاستدارة. بالنسبة للمحور كان Vector3.up أي المحور المتجه من أسفل لأعلى مما يجعل الاستدارة أفقية حول المحور العمودي. أما زاوية الاستدارة فهي حاصل ضرب ثلاث قيم: الأولى هي mouseDelta.x والتي تمثل مقدار الإزاحة الأفقية للفأرة منذ الإطار السابق. هذه القيمة تكون موجبة إذا تحركت الفأرة من اليسار لليمين، مما يعطينا دورانا في اتجاه عقارب الساعة كما في الجزء (ب) من الشكل 22، ودورانا بعكس عقارب الساعة لو تحركت الفأرة في الاتجاه الآخر. القيمة الثانية horizontalMouseSpeed تمثل سرعة استدارة الكائن (الأسطوانة) أفقيا. في معظم الألعاب تكون هذه القيمة قابلة للتغيير من قبل اللاعب لتتناسب مع السرعة التي اعتاد عليها لتحريك الفأرة. أما القيمة الأخيرة فهي Time.deltaTime والتي اعتدنا على رؤيتها عندما يتعلق الأمر بالحركة، حيث أننا نتعامل مع سرعة يجب أن نقوم بتحويلها إلى مسافة إزاحة أو زاوية دوران.

في السطر 50 قمنا بحساب الاستدارة الحالية للكاميرا على محورها المحلي x، وهي قيمة محصورة بين 0 و 360، وتزيد بدوران الكاميرا باتجاه عقارب الساعة (أي أنها تزيد إذا نظرت الكاميرا نحو الأسفل) كما في الجزء (ت) من الشكل 22. في الأسطر 54 إلى 56 نقوم بتحويل هذه القيمة إلى زاوية بين 180 و 180- وذلك عن طريق تحويل قياس الزاوية التي تزيد عن 180 إلى قيمة سالبة (مثلا 190 تصبح 170- وهكذا). سنقوم بتخزين هذه القيمة في المتغير currentRotation لنستفيد منها لاحقا لحساب الحدود الدوران العمودي المسموح بها للكاميرا.

بعد ذلك في السطرين 59 و 60 نقوم بحساب قيمة الدوران التي سننفذها خلال الإطار الحالي وهي تحسب باستخدام القيم الثلاث mouseDelta.y- و verticalMouseSpeed و Time.deltaTime بطريقة مشابهة لحساب الدوران الأفقي باستثناء استخدامنا للقيمة السالبة للإزاحة العمودية الفأرة وهي mouseDelta.y-. السبب في ذلك هو أن الإزاحة من أسفل لأعلى تعطينا قيمة موجبة للمتغير mouseDelta.y، وهي التي نريد تحويلها لاستدارة نحو الأعلى (بعكس عقارب الساعة) مما يجعلنا في حاجة إلى قيمة سالبة، والعكس صحيح عند الإزاحة من أعلى لأسفل. بعد حساب قيمة الدوران نقوم بتخزينها في المتغير ang.

بعد أن قمنا بحساب كمية الدوران المطلوبة، بقي علينا إتمام الخطوة الثانية بتدوير الكاميرا حول محورها الأفقي. عند حساب قيمة ang لدينا ثلاث احتمالات: الاحتمال الأول أن الفأرة لم تتحرك عموديا على الإطلاق وبالتالي تكون قيمة ang تساوي صفرا، بالتالي لا نحتاج لفعل أي شيء. الاحتمال الثاني أن تكون الفأرة تحركت نحو الأعلى، أي أن قيمة ang سالبة وتعطينا دورانا للأعلى بعكس عقارب الساعة. هذه الحالة نقوم بفحصها في السطر 65 للتأكد من أن الزاوية الجديدة نحو الأعلى، والتي ستنتح من جمع ang مع currentRotation ستكون أكبر من الحد الأدنى المسموح به وهو maxVerticalAngle-. الاحتمال الثالث والأخير هو أن تكون الفأرة تحركت نحو الأسفل، مما سيعطينا قيمة موجبة للمتغير ang بالتالي علينا أن نتأكد من أن جمع قيمتها مع زاوية الدوران الحالية currentRotation لن تزيد عن القيمة القصوى المسموح بها وهي maxVerticalAngle وهذا ما نفعله في السطر 66. في حال تحقق أحد هذين الشرطين فإننا نقوم بتنفيذ الدوران حول المحور x المحلي للكاميرا بنفس المقدار ang الذي سبق وحسبناه، هذا الدوران يتم عن طريق استدعاء ()transform.RotateAround وذلك في السطر 67.

في الأسطر من 74 إلى 92 نقوم بفحص مدخلات الجهات الأربع وإضافة الاتجاه المناسب إلى السرعة النهائية: أماما أو خلفا، يمينا أو يسارا. باقي الأسطر لن أتطرق لشرحها لأنها تؤدي نفس الوظائف التي سبق وتم شرحها في نظام إدخال ألعاب المنصات. يمكنك أيضا مشاهدة النتيجة النهائية في المشهد scene5 في المشروع المرفق.

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

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