الفصل الثالث: شخصية اللاعب الفيزيائية

العب

سنقوم في هذا الفصل ببناء شخصية يمكن للاعب التحكم بها وتمتلك في الوقت ذاته خصائص فيزيائية. هذه الشخصية يمكن استخدامها لاحقا في بناء أنظمة تحكم لمنظور الشخص الأول أو الثالث، أو حتى أنظمة ألعاب المنصات. تتمحور الفكرة حول استعمال كائن كبسولة مضاف إليها جسم صلب، وسنقوم بالتحكم بهذه الكبسولة عن طريق إضافة القوى المناسبة كمّاً واتجاهاً. سأقوم لتوضيح الفكرة بإنشاء نظام تحكم لمنظور الشخص الأول، مما يعني أن علينا إضافة الكاميرا كابن لهذه الكبسولة في موقع الرأس (أعلى الكبسولة). بعد إضافة مكوّن الجسم الصلب Rigid Body للكبسولة، قم بضبط الكتلة (المتغير Mass) على 70 أي أن وزن الشخصية التي سنستعملها هي 70 كيلوجرام، كما علينا أن نقوم بإيقاف الدوران الفيزيائي على المحاور الثلاث، وذلك باختيار القيم الثلاث x و y و z من خانة Freeze Rotation. وهذا يعني أن المحاكي الفيزيائي والقوى المختلفة التي نطبقها على الشخصية من خلاله ستكون قادرة فقط على تحريكها ولن يكون لها أي تأثير على الدوران.

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

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class PhysicsCharacter : MonoBehaviour {
5.  
6.  //أقصى ارتفاع للقفز مقدرا بالأمتار
7.  public float jumpHeight = 2;
8.  
9.  //سرعة الحركة الأفقية
10.     public float movementSpeed = 8;
11.         
12.     //موقع أقدام اللاعب
13.     public Transform feet;
14.     
15.     //المتغيرات الخاصة بمدخلات الحركة
16.     bool walkForward, walkBackwards, 
17.          strafeRight, strafeLeft, jump;
18.     
19.     void Start () {
20.         
21.     }
22.     
23.     public void WalkForward(){
24.         walkForward = true;
25.         walkBackwards = false;
26.     }
27.     
28.     public void WalkBackwards(){
29.         walkBackwards = true;
30.         walkForward = false;
31.     }
32.     
33.     public void StrafeRight(){
34.         strafeRight = true;
35.         strafeLeft = false;
36.     }
37.     
38.     public void StrafeLeft(){
39.         strafeLeft = true;
40.         strafeRight = false;
41.     }
42.     
43.     public void Jump(){
44.         jump = true;
45.     }
46.     
47.     public void Turn(float amount){
48.         transform.RotateAround(Vector3.up, amount);
49.     }
50.     
51.     //عند تحديث ما يتعلق بالفيزياء FixedUpdate()كما عرفنا نستخدم الدّالة 
52.     void FixedUpdate () {   
53.         //يمكن للاعب توجيه الجسم في اتجاه معين
54.         //فقط عندما يكون واقفا بقدميه على الأرض
55.         //أو عالقا في مكان ما بسرعة عمودية تكاد تساوي صفرا
56.         Vector3 velocity = rigidbody.velocity;
57.         if(OnGround() || Mathf.Abs(velocity.y) < 0.1f)){
58.             //إلى صفر zو x أعد ضبط السرعة على المحورين 
59.             velocity.x = velocity.z = 0;
60.             
61.             //قم بتحديث الحركة الأفقية
62.             if(strafeLeft){
63.                 //تحرك لليسار
64.                 velocity += -transform.right * movementSpeed;
65.                 strafeLeft = false;
66.             } else if(strafeRight){
67.                 //تحرك لليمين
68.                 velocity += transform.right * movementSpeed;
69.                 strafeRight = false;
70.             }
71.         
72.             if(walkForward){
73.                 //تحرك للأمام
74.                 velocity += transform.forward * movementSpeed;
75.                 walkForward = false;
76.             } else if(walkBackwards){
77.                 //تحرك للخلف
78.                 velocity += -transform.forward * movementSpeed;
79.                 walkBackwards = false;
80.             }
81.         }
82.         
83.         rigidbody.velocity = velocity;
84.         
85.         //قم بقراءة مدخل القفز
86.         if(jump && OnGround()){
87.             //v2^2 - v1^2 = 2as نستعمل هنا معادلات المقذوفات لحساب قوة القفز
88.             //v2 = 0 (عند أقصى ارتفاع)
89.             //v1^2 = -2as
90.             //v1 = sqrt(-2as) (تعني الجذر التربيعي sqrt)
91.             float v1 = 
92.                 Mathf.Sqrt(-2 * Physics.gravity.y * jumpHeight);
93.             
94.             //مستخدما الاتجاه العلوي كاتجاه للسرعة p=mv قم باستخدام معادلة الاندفاع
95.             rigidbody.AddForce(
96.                 Vector3.up * v1 * rigidbody.mass, 
97.                 ForceMode.Impulse);
98.             
99.             jump = false;
100.        }
101.        
102.    }
103.    
104.    //تقوم بحص ما إذا كانت الشخصية تقف على الأرض
105.    public bool OnGround(){
106.        //قم ببث أشعة من موقع أقدام اللاعب نحو الخلف
107.        //سيكون طول هذه الأشعة هو 10 سنتيمترات
108.        //إذا اصطدمت هذه الأشعة بالأرض أو أي كائن آخر
109.        //فإن ذلك يعني أن الشخصية تقف على الأرض فعلا
110.        if(Physics.Raycast(
111.            new Ray(feet.position, -Vector3.up), 0.1f)){
112.            return true;
113.        }
114.        return false;
115.    }
116. }

السرد 47: بريمج الشخصية الفيزيائية القابلة للتحكم

بطبيعة الحال يمكننا عند إضافة هذا البريمج لكائن الكبسولة ضبط عدد من القيم لتناسب احتياجاتنا، مثال على ذلك قيمة ارتفاع القفز jumpHeight و سرعة الحركة الأفقية (المشي) عبر المتغير movementSpeed. بالإضافة إلى ذلك علينا أن نضيف كائنا فارغا كابن للكبسولة ونضعه في أسفلها أي في موقع أقدام اللاعب ومن ثم نربطه بالمتغير feet ليمثل موقع القدمين الذي سنحتاجه في الدّالة ()OnGround في السطر 105 والتي سأبدأ بها قبل الانتقال لباقي البريمج. تخبرنا هذه الدّالة فيما إذا كانت الشخصية تقف على الأرض أم لا وذلك عن طريق استخدام بث الأشعة. بث الأشعة الذي يتم تنفيذه عن طريق الدّالة ()Physics.Raycast والتي تحتاج إلى شعاع Ray ويمكن أيضا اختياريا تزويدها بمسافة أقصى لعملية البث. تقوم هذه الدّالة ببث الأشعة في الاتجاه المحدد ومن ثم تخبرنا إذا ما اصطدمت هذه الأشعة بكائن ما. ما قمنا به هو أننا في السطر 111 قمنا بإنشاء شعاع جديد يبدأ من موقع الأقدام الذي حددناه عبر المتغير feet ويتجه في الاتجاه السالب للمتجه Vector3.up أي نحو الأسفل. قمنا أيضا بتحديد مسافة بث الأشعة بعشر سنتيمترات فقط، أي أنه بعد عشر سنتيمترات ستتوقف الأشعة سواء اصطدمت بكائن أم لا. إذا كانت القيمة التي تعيدها الدّالة ()Raycast هي true فيعني ذلك أن الأشعة اصطدمت بشيء ما، وهذا الشيء لا بد وأنه تحت أقدام اللاعب مما يعني أن اللاعب ولا شك يقف على شيء ما وبالتالي نعيد true، وما عدا ذلك نعيد القيمة false.

بالعودة لبداية البريمج، قمنا على غرار ما فعلناه في بريمج التحكم بالسيارة الفيزيائية بتحديد بعض المتغيرات الخاصة بالتحكم بحالة الشخصية. وقمنا بكتابة دالّة خاصة بكل واحد من هذه المتغيرات لتغيير قيمتها مثل ()WalkForward و ()WalkBackwards. الدّالة التي تختلف قليلا عن باقي دوال التحكم هي ()Turn والتي لا تتعامل مع متغير محدد وإنما تأخذ قيمة زاوية محددة وتدير بناء عليها كائن الكبسولة حول محوره المحلي y بنفس المقدار.

لنأتي الآن على آخر الدوال التي سنناقشها – وأكثرها أهمية بالطبع – وهي ()FixedUpdate والتي تقوم بقراءة متغيرات التحكم المختلفة وتحريك الشخصية بناء عليها. الخطوة الأولى في السطر 56 حيث نقوم بقراءة قيمة السرعة المتجهة الحالية للكائن وتخزينها في المتغير velocity. بعد ذلك نقوم بقراءة مدخلات الحركة الأفقية للاعب على المحورين x و z (أي الحركة أماما وخلفا ويمينا ويسارا). بداية فإننا نفترض أن اللاعب لن يكون قادرا على توجيه نفسه في الاتجاهات المختلفة إذا كان يطير في الهواء (أي أثناء القفز أو السقوط) ونعرف ذلك بإحدى طريقتين: الطريقة الأولى هي أن تعيد لنا الدّالة ()OnGround القيمة false أي أنه لا شيء تحت أقدام اللاعب، والطريقة الأخرى هي فحص السرعة العمودية للكائن لنرى إذا ما كانت تقترب من الصفر كثيرا. الطريقة الثانية مفيدة في بعض الحالات التي قد تعلق فيها الكبسولة فوق فجوة كما في الشكل 60 مما يجعلها تبدو للدّالة ()OnGround كأنها في الهواء ولكنها في الحقيقة ليست كذلك. فإذا لم نسمح للاعب بالتحكم في الحركة الأفقية في حالة كهذه فإن الشخصية تعلق في مكانها للأبد. انتبه هنا أننا استخدمنا الدّالة ()Mathf.Abs من أجل الحصول على القيمة المطلقة للسرعة حيث تهمنا القيمة بغض النظر عن الاتجاه الموجب نحو الأعلى أو السالب نحو الأسفل.

الشكل 60: مثال على حالة لا تقف فيها الشخصية على الأرض، لكننا برغم ذلك يجب أن نكون قادرين على تحريكها أفقيا

الشكل 60: مثال على حالة لا تقف فيها الشخصية على الأرض، لكننا برغم ذلك يجب أن نكون قادرين على تحريكها أفقيا

إذا تبين لنا بعد التحقق من هذه الشروط أنه يمكن تحريك الشخصية، علينا أولا أن نقوم بتصفير سرعتها الأفقية عن طريق ضبط قيم الأعضاء x و z في متغير السرعة velocity إلى صفر. بعد أن نمر على مجموعة من الخطوات في الأسطر 62 إلى 80 نكون قد قرأنا قيم السرعة الأفقية الجديدة اعتمادا على قيم متغيرات الإدخال، ومن ثم نقوم أخيرا في السطر 83 بتخزين قيمة السرعة الجديدة في الجسم الصلب الخاص بالكبسولة. جدير بالذكر أن كل هذه الخطوات التي قمنا بها لم تمس قيمة العضو y في المتغير velocity مما يعني أن السرعة العمودية لم يحدث عليها أي تغيير حتى الآن.

الخطوة التالية هي في السطر 86، حيث نقوم بقراءة مدخل القفز جنبا إلى جنب مع عملية التحقق من كون الشخصية تقف على الأرض. إذا تحققنا من هذين الشرطين يمكننا أن نطبق القفز عن طريق إضافة قوة دافعة نحو الأعلى للشخصية ولمرة واحدة فقط، أي أشبه بضربة قوية من الأسفل تقذف الشخصية نحو الأعلى. هذه القوة يجب أن تكون بالقدر الذي يوصل الكبسولة إلى الارتفاع المحدد في المتغير jumpHeight قبل أن تبدأ بالسقوط مرة أخرى. إذن نحن نتحدث – فيزيائيا – عن مقذوف سيتم رميه للأعلى بسرعة أولية، ومن ثم تتناقص سرعته إلى الصفر عند أقصى ارتفاع للقفز. بعد ذلك يبدأ المقذوف بالسقوط نحو الأسفل بفعل الجاذبية. إذن كل ما علينا هو أن نحسب المقدار الصحيح للقوة التي سنطبقها على الكائن حتى يصل إلى الارتفاع المطلوب، وهذه العملية تتطلب الخوض في بعض التفاصيل الفيزيائية لقوانين المقذوفات والتي سأناقشها في الفقرة التالية. إذا لم تكن تحب الخوض في هذه التفاصيل يمكنك تخطي تلك الفقرة. هذه الفقرة تشرح الأسطر 91 إلى 99 من البريمج.

لحساب قوة القفز نستخدم معادلة المقذوفات 4b، حيث v2 هي سرعة الجسم عند وصوله للارتفاع الأقصى، بينما v1 هي السرعة الأولية للجسم في اللحظة التي يرتفع فيها عن سطح الأرض (سرعة الإطلاق). المتغيران الآخران في المعادلة هما a وتمثل التسارع، و s هي أقصى ارتفاع يمكن للجسم أن يصله. بتطبيق هذه المعادلة على حالة القفز التي بين أيدينا، يتبين لدينا أن المجهول الوحيد في المعادلة هو v1. أمّا باقي المتغيرات فهي كالتالي: عند أقصى ارتفاع للمقذوف يتوقف عن الحركة قبل أن يبدأ بالسقوط، ذلك يعني أنّ سرعته عند تلك النقطة هي صفر، بالتالي v2=0. بالنسبة للارتفاع الأقصى للمقذوف، فهو نفسه أقصى ارتفاع للقفز jumpHeight وهو معروف لدينا أيضا. أخيرا فإن المقذوف يفقد من سرعته أثناء ارتفاعه نحو الأعلى بسبب تسارع وزنه نحو الأسفل، وهو بطبيعة الحال تسارع الجاذبية الأرضية وهو كما نعرف 9.8 متر\ثانية2. بحل المعادلة لـv1 نحصل على التعبير 4a وهو موجود في الأسطر 91 و92. بما أنّ قوة القفز الدافعة تطبق لحظيا لمرة واحدة على شكل ضربة فإنها تسمى فيزيائيا قوة الاندفاع ومعادلتها هي p=mv حيث m هي كتلة الجسم و v هي السرعة الأولية التي حسبناها للتو. هذه المعادلة مطبقة في السطر 95 حيث نستدعي الدّالة ()rigibbody.AddForce مزودين الخيار ForceMode.Impulse. بعد تطبيق القفز نعيد قيمة jump إلى false.

لنقم الآن بكتابة البريمج الذي سيقرأ مدخلات اللاعب ويقوم بناء عليها باستدعاء دوال التحكم من البريمج PhysicsCharacter. هذا البريمج هو FPSInput وهو موضح في السرد 48.

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class FPSInput : MonoBehaviour {
5.  
6.  //سرعة النظر بالفأرة على المحورين الأفقي والعمودي
7.  public float horizontalMouseSpeed = 0.9f;
8.  public float verticalMouseSpeed = 0.5f;
9.  
10.     //الحد الأقصى لزاوية النظر نحو الأعلى والأسفل
11.     public float maxVerticalAngle = 60;
12.     
13.     //موقع مؤشر الفأرة في الإطار السابق
14.     //ويستخدم لحساب الإزاحة
15.     private Vector3 lastMousePosition;
16.     
17.     //متغير لتخزين الكاميرا
18.     private Transform camera;
19.     
20.     //بريمج الشخصية الفيزيائية الذي سنتحكم به
21.     PhysicsCharacter character;
22.     
23.     void Start () {
24.         character = GetComponent<PhysicsCharacter>();
25.         lastMousePosition = Input.mousePosition;
26.         //نقوم بإيجاد كائن الكاميرا في الأبناء
27.         camera = transform.FindChild("Main Camera");
28.     }
29.     
30.     void Update () {
31.         //y الخطوة الأولى هي إدارة الكبسولة حول محورها المحلي 
32.         //بناء على الإزاحة الأفقية للفأرة
33.         Vector3 mouseDelta = Input.mousePosition - lastMousePosition;
34.         
35.         character.Turn(mouseDelta.x * 
36.                 horizontalMouseSpeed * 
37.                 Time.deltaTime);
38.         
39.         //قم بتخزين الدوران الحالي للكاميرا
40.         float currentRotation = camera.localRotation.eulerAngles.x;
41.         
42.         //قم بتحويل الدوران من المجال بين 0 و 360
43.         //إلى المجال بين -180 و 180
44.         if(currentRotation > 180){
45.             currentRotation = currentRotation - 360;
46.         }
47.         
48.         //قم بحسبا كمية الدوران للإطار الحالي
49.         float ang = 
50.             -mouseDelta.y * verticalMouseSpeed * Time.deltaTime;
51.         
52.         //x الخطوة الثانية هي تدوير الكاميرا حول محورها المحلي
53.         //بناء على الإزاحة العمودية للفأرة 
54.         //لكن قبل ذلك علينا فحص حدود زاوية الدوران
55.         if((ang < 0 && ang + currentRotation > -maxVerticalAngle) ||
56.             (ang > 0 && ang + currentRotation < maxVerticalAngle)){
57.             camera.RotateAround(camera.right, ang);
58.         }
59.         
60.         //قم بتحديث موقع الفأرة الأخير استعدادا للإطار المقبل
61.         lastMousePosition = Input.mousePosition;
62.         
63.         if(Input.GetKey(KeyCode.A)){
64.             character.StrafeLeft();
65.         } else if(Input.GetKey(KeyCode.D)){
66.             character.StrafeRight();
67.         }
68.         
69.         if(Input.GetKey(KeyCode.W)){
70.             character.WalkForward();
71.         } else if(Input.GetKey(KeyCode.S)){
72.             character.WalkBackwards();
73.         }
74.         
75.         if(Input.GetKeyDown(KeyCode.Space)){
76.             character.Jump();
77.         }
78.     }
79. }

السرد 48: بريمج لقراءة مدخلات اللاعب واستخدامها للتحكم بالشخصية الفيزيائية

لعلك تذكر أننا قمنا بنقاش بريمج مماثل وهو FirstPersonControl في السرد 9 حيث يتشابه البريمجان في الطريقة التي يقومان فيها بتدوير الكاميرا. الفرق بينهما هو أن FPSInput لا يملك القدرة على تحريك شخصية اللاعب مباشرة وإنما يقوم باستدعاء وظائف التحريك من البريمج PhysicsCharacter والذي يشترط وجوده على نفس الكائن، وهذا الأمر واضح في الأسطر 61 إلى 77. يمكنك الآن أن تقوم ببناء بيئة صغيرة لتجرب التحكم بهذه الشخصية الفيزيائية، ومن الأفضل أيضا أن تقوم بتعطيل مكوّن التصيير Renderer الخاص بالكبسولة. من الجدير بالذكر أنّ هذه الشخصية قادرة على دفع الكائنات الأخرى التي تحتوي على مكوّن الجسم الصلب إذا كانت ذات كتلة مناسبة، ويمكنك تجربة كل ذلك في المشهد scene17 في المشروع المرفق.

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

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