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

العب

أنظمة الإدخال الخاصة بألعاب سباق السيارات شبيهة إلى حد بعيد بأنظمة تحكم منظور الشخص الثالث من ناحية موقع الكاميرا في المشهد، مع عدم إغفال بعض الاختلافات خاصة في طريقة الحركة ودوران الكاميرا كما سنرى حالا. لتكن البداية مع مجسم يمثل السيارة، وسأستخدم هنا كائن مكعب بأبعاد (3.5 ,1 ,2) ليمثل السيارة. ولنقم أيضا بإضافة أرضية بمساحة كافية وهي عبارة عن كائن لوح بطول وعرض يساوي 100 (تذكر أن المساحة الافتراضية للوح هي 10 * 10، لذا فإن المحصلة النهائية عند الضرب ب100 ستكون 1000 * 1000 أي كيلومترا مربعا واحدا). وسأستعمل إكساءً يمثل أسفلت الشارع إضافة إلى ضوء اتجاهي للمشهد. أخيرا سنقوم بإضافة كائنين فارغين كأبناء لمكعب السيارة، وذلك لاستخدامهما كمحاور للدوران. المحور الأول هو RotationAxisR وموقعه المحلي بالنسبة لمكعب السيارة هو (0 ,0 , 1) أي يبعد مترا واحدا إلى يمين السيارة، والآخر هو RotationAxisL ويبعد مترا واحدا إلى يسار السيارة أي في الموقع المحلي (0 ,0 ,1-). الشكل 25 يوضح المشهد الذي سنتعامل معه لبناء نظام تحكم ألعاب السيارات.

الشكل 25: المشهد المستخدم لتطبيق نظام تحكم ألعاب سباق السيارات

الشكل 25: المشهد المستخدم لتطبيق نظام تحكم ألعاب سباق السيارات

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

لنقم الآن بكتابة البريمج الذي سيسمح لنا بتحويل هذا المكعب الساكن إلى سيارة (ليس بالشكل طبعا لكن بالحركة!) يمكننا أن نتحكم بها بزيادة وإنقاص السرعة والانعطاف يمينا ويسارا. السرد 12 يوضح البريمج CarController والذي سنضيفه إلى المكعب الذي يمثل السيارة.

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class CarController : MonoBehaviour {
5.  
6.  //السرعة القصوى للسيارة كم \ ساعة
7.  public float maxSpeed = 200;
8.  
9.  //كمية الزيادة في السرعة كم \ ساعة
10.     public float acceleration = 20;
11.     
12.     //كمية التناقص في السرعة كم \ ساعة
13.     public float deceleration = 16;
14.     
15.     //كمية التناقص في السرعة أثناء استخدام المكابح كم \ ساعة
16.     public float braking = 60;
17.     
18.     //كمية التناقص في السرعة أثناء الانعطاف نحو اليمين واليسار كم \ ساعة
19.     public float turnDeceleration = 30;
20.     
21.     //سرعة الدوران أثناء الانعطاف نحو اليمين واليسار درجة \ ثانية
22.     public float steeringSpeed = 70;
23.     
24.     //متغيرات لحفظ محوري الدوران الأيمن والأيسر
25.     Transform rightAxis, leftAxis;
26.     
27.     //سرعة السيارة الحالية متر \ ثانية
28.     float currentSpeed = 0;
29.     
30.     //قم بالضرب بهذه القيمة لتحويل السرعة من 
31.     // كم \ ساعة إلى متر \ ثانية
32.     const float CONVERSION_FACTOR = 0.2778f;
33.     
34.     void Start () {
35.         //إيجاد محوري الدوران الأيمن والأيسر في قائمة الأبناء
36.         rightAxis = transform.FindChild("RotationAxisR");
37.         leftAxis = transform.FindChild("RotationAxisL");
38.     }
39.     
40.     void Update () {
41.         
42.         if(Input.GetKey(KeyCode.UpArrow)){
43.             //اللاعب يضغط على زر التسريع
44.             //قم بزيادة السرعة بالاعتماد على متغير التسارع
45.             AdjustSpeed(
46.                acceleration * CONVERSION_FACTOR * Time.deltaTime);
47.         } else {
48.             //اللاعب لا يضغط على زر التسريع
49.             //قم بتخفيض السرعة بالاعتماد على متغير تناقص السرعة
50.             AdjustSpeed(
51.               -deceleration * CONVERSION_FACTOR * Time.deltaTime);
52.         }
53.         
54.         if(Input.GetKey(KeyCode.DownArrow)){
55.             //Braking pressed
56.             //Decrease speed by braking amount
57.             AdjustSpeed(
58.                 -braking * CONVERSION_FACTOR * Time.deltaTime);
59.         }
60.         
61.         //الانعطاف نحو اليمين، وينفذ فقط في حال كانت سرعة السيارة أكبر من 5 كم \ ساعة
62.         if(Input.GetKey(KeyCode.RightArrow) && 
63.             currentSpeed > 5 * CONVERSION_FACTOR){
64.             AdjustSteering(steeringSpeed, rightAxis.position);
65.         }
66.         
67.         //الانعطاف نحو اليسار، وينفذ فقط في حال كانت سرعة السيارة أكبر من 5 كم \ ساعة
68.         if(Input.GetKey(KeyCode.LeftArrow) && 
69.             currentSpeed > 5 * CONVERSION_FACTOR){
70.             AdjustSteering(-steeringSpeed, leftAxis.position);
71.         }
72.         
73.         //z قم بتنفيذ الحركة للأمام على المحور المحلي 
74.         transform.Translate(0, 0, currentSpeed * Time.deltaTime);
75.         
76.     }
77.     
78.     //الدّالّة الخاصة بإضافة قيمة جديدة إلى السرعة الحالية
79.     //تقوم بفحص الحد الأعلى للسرعة بالإضافة إلى التأكد من منع القيمة السالبة
80.     //القيمة المضافة يجب أن تكون بوحدة متر \ ثانية
81.     void AdjustSpeed(float newValue){
82.         currentSpeed += newValue;
83.         if(currentSpeed > maxSpeed * CONVERSION_FACTOR){
84.             currentSpeed = maxSpeed * CONVERSION_FACTOR;
85.         }
86.         
87.         if(currentSpeed < 0){
88.             currentSpeed = 0;
89.         }
90.     }
91.     
92.     //الدّالّة الخاصة بتدوير السيارة بشكل أفقي حول النقطة المزودة
93.     // بناء على السرعة المزودة بوحدة درجة \ ثانية
94.     void AdjustSteering(float speed, Vector3 rotationAxis){
95.         //تنفيذ الدوران
96.         transform.RotateAround(
97.             rotationAxis, Vector3.up, speed * Time.deltaTime);
98.         //في حال كون السرعة أكبر من 30 كم \ ساعة
99.         //نقوم بإنقاص السرعة بمقدار متغير تناقص الانعطاف
100.        if(currentSpeed > 30 * CONVERSION_FACTOR){
101.            AdjustSpeed(
102.                -turnDeceleration * CONVERSION_FACTOR * 
103.                Time.deltaTime);
104.        }
105.    }
106. }

السرد 12: بريمج التحكم بالسيارة

في الأسطر 6 إلى 19 نقوم بتعريف عدد من المتغيرات الخاصة بسرعة حركة السيارة وهي maxSpeed والتي تمثل أقصى سرعة يمكن أن تصلها السيارة، و acceleration وهي مقدار الزيادة في السرعة مع مرور الوقت، و deceleration وهي مقدار التناقص في السرعة مع مرور الوقت، و braking والتي تمثل التناقص في السرعة مع مرور الوقت أثناء ضغط المكابح، وأخيرا turnDeceleration والتي تمثل مقدار التناقص في السرعة مع مرور الوقت أثناء الانعطاف نحو اليمين أو اليسار. جميع هذه القيم ممثلة بوحدة كم \ ساعة كونها أسهل للتعامل معها بالنسبة لنا.

في السطر 22 قمنا بتعريف المتغير steeringSpeed والممثل بوحدة درجة \ ثانية. المتغيران rightAxis و leftAxis في السطر 25 سنستخدمهما للوصول إلى الكائنين RotationAxisR و RotationAxisL الذين سبق وأضفناهما كأبناء للسيارة على يمينها ويسارها لنقوم بإدارة جسم السيارة حولهما أثناء الانعطاف؛ ففي الواقع تحتاج السيارة إلى مساحة للانعطاف ولا تدور في مكانها، لذا نستخدم هذين الكائنين لتحديد هذه المساحة، بحيث تزيد المساحة اللازمة كلما أبعدنا الكائنين عن جسم السيارة.

في السطر 28 قمنا بتعريف متغير خاص بتخزين السرعة الحالية للسيارة والتي سنقوم بحسابها بناء على مدخلات اللاعب من تسرع ومكابح وانعطاف. لاحظ أن هذا المتغير خلافا لمتغيرات السرعة الأخرى يستخدم وحدة متر \ ثانية، ذلك لأن الوقت المعتمد في Unity هو الثانية، والوحدة الواحدة تساوي مترا واحدا مما يجعل هذه الوحدة أسهل للاستخدام مع Unity من وحدة كم \ ساعة. للتحويل من كم \ ساعة إلى متر \ ثانية قمنا في السطر 32 بتعريف متغير ذو قيمة ثابتة وهو CONVERSION_FACTOR وقيمته الثابتة هي 0.2778، وكل ما علينا هو أن نضرب أي قيمة ممثلة بـ كم \ ساعة بهذا المتغير لتحويلها إلى متر \ ثانية. بعد ذلك قمنا في الدّالّة ()Start بالبحث عن الكائنين RotationAxisR و RotationAxisL وتخزينهما في المتغيرين rightAxis و leftAxis من أجل استخدامهما لاحقا عند تطبيق الانعطاف.

قبل الشروع في شرح محتويات الدّالّة ()Update سأنتقل للأسطر 81 إلى 90، حيث قمنا هذه المرة بتعريف دالّة خاصة بنا للاستفادة منها وهي ()AdjustSpeed. إن لم تكن لك خبرة كبيرة بالبرمجة، فيمكن القول ببساطة أن تعريف الدّوال مفيد عندما تحتاج لتكرار عملية معينة عدّة مرات لكن بقيم مختلفة، وهذه العملية تتطلب منك كتابة عدّة أسطر مما يجعل أمر تكرار كتابتها متعبا. هذا تحديدا هو الحال مع عملية تغيير سرعة السيارة: حيث علينا أولا أن نضيف القيمة الجديدة إلى السرعة الحالية، ومن ثم نتأكد من أن السرعة الجديدة أقل من السرعة القصوى وتعديلها إن كانت أكبر، كما أن علينا أيضا أن نتأكد أن السرعة ليست أقل من صفر، وتغييرها إلى صفر إن كانت أقل. كل ما علينا الآن هو استدعاء هذه الدّالّة في كل مرة نرغب فيها بتغيير السرعة، وتزويدها بالقيمة newValue التي نرغب بإضافتها أو طرحها من السرعة. وقبل ذلك علينا أن نحولها للوحدة المناسبة، كون الدّالّة ()AdjustSpeed تتعامل مع الوحدة متر \ ثانية.

قمنا أيضا بتعريف دالّة أخرى في الأسطر من 94 إلى 105 وهي ()AdjustSteering، والتي سنستعملها للانعطاف يمينا أويسارا. عند استدعاء هذه الدّالّة علينا أن نقوم بتزويدها بالنقطة التي نريد تنفيذ الدوران حولها، بالإضافة إلى سرعة الانعطاف. عند استدعائها تقوم ()AdjustSteering بإدارة الكائن حول النقطة التي زودناها بها وهي rotationAxis مستخدمة متغير السرعة speed. بعد ذلك تقوم الدّالّة بفحص سرعة السيارة الحالية، بحيث تقوم بإنقاصها بمقدار turnDeceleration في حال كون السرعة أكبر من 30 كم \ في الساعة. لاحظ أن ()AdjustSteering تقوم بإنقاص السرعة عن طريق استدعاء ()AdjustSpeed، وهذا ممكن حيث أن لغة البرمجة تسمح لنا باستدعاء دالّة من داخل دالّة أخرى.

بالعودة الآن إلى ()Update وتحديدا الأسطر من 42 إلى 52، حيث نقوم بفحص مفتاح السهم العلوي للوحة المفاتيح، وزيادة سرعة السيارة بمقدار التسارع acceleration في حال كان اللاعب يضغط على هذا المفتاح. لاحظ أننا قمنا باستدعاء ()AdjustSpeed حتى تقوم بكل ما يلزم بخصوص تغيير السرعة، وقمنا عند استدعائها بضرب التسارع بمتغير التحويل CONVERSION_FACTOR لتحويل قيمة التسارع إلى متر \ ثانية، حيث أن Time.deltaTime تعطينا الوقت المنقضي بالثواني كما أن ()AdjustSpeed تتعامل فقط مع هذه الوحدة، واستدعاؤها دون إجراء التحويل سيعطينا قيما خاطئة. في حال كون اللاعب لا يضغط على مفتاح السهم العلوي، نقوم بإنقاص سرعة السيارة بمقدار التناقص deceleration مع مراعاة استخدام القيمة السالبة هذه المرة، كوننا نريد طرح القيمة من السرعة الحالية وليس زيادتها عليها.

في الأسطر من 54 إلى 59 نقوم بفحص مفتاح السهم الأسفل، وتشغيل المكابح في حال كون اللاعب يضغط عليه. المكابح هنا عبارة عن إنقاص للسرعة ولكن بمقدار أكبر من deceleration وهو braking، مما يعني أن السيارة مع المكابح ستحتاج إلى وقت أقل للتوقف لأن التناقص في السرعة يكون أكبر في كل ثانية.

تنفيذ الانعطاف يتم في الأسطر 62 إلى 71، حيث نقوم بفحص حالة كل من السهم الأيمن وسرعة السيارة الحالية، حيث لا ننعطف إذا كانت سرعة السيارة تقل عن 5 كم \ في الساعة. الهدف من هذا هو ألا نسمح لمفاتيح الانعطاف بأن تحرك السيارة إذا كانت متوقفة. لاحظ أننا في السطر 64 قمنا باستخدام النقطة rightAxis كمركز للدوران مع القيمة الموجبة للمتغير steeringSpeed وذلك لأن الدوران لليمين يكون مع عقارب الساعة عندما يكون محور الدوران هو الاتجاه الموجب للمحور y (السطر 97). أما في السطر 70 فإننا نستخدم القيمة السالبة للمتغير steeringSpeed لإحداث دوران بعكس عقارب الساعة حول النقطة leftAxis.

أخيرا وبعد حساب السرعة و تنفيذ الانعطاف، بقي أن نقوم بتحريك السيارة نحو الأمام على محورها المحلي z باستخدام قيمة currentSpeed مضروبة في الوقت المنقضي. تذكر أن currentSpeed تم حسابها بوحدة متر \ ثانية لذا يمكن ضربها في Time.deltaTime مباشرة، كما في السطر 74. يمكنك الآن تجربة نظام التحكم بالسيارة كونه أصبح جاهزا. وذلك قبل الانتقال للخطوة التالية وهي كتابة البريمج الخاص بالكاميرا.

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

الشكل 26: دوران السيارة أمام الكاميرا أثناء الانعطاف وظهور جانب السيارة للاعب

الشكل 26: دوران السيارة أمام الكاميرا أثناء الانعطاف وظهور جانب السيارة للاعب

البريمج الذي سنضيفه للكاميرا هو CarCamera الموضح في السرد 13.

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class CarCamera : MonoBehaviour {
5.  
6.  //متغير لتخزين كائن السيارة
7.  public Transform car;
8.  
9.  //ارتفاع الكاميرا عموديا فوق ارتفاع السيارة
10.     public float height = 4;
11.     
12.     //بعد الكاميرا من الخلف عن السيارة
13.     //بغض النظر عن الارتفاع
14.     public float zDistance = 10;
15.     
16.     //عدد ثواني الانتظار قبل البدء بتدوير الكاميرا
17.     //بعد أن تقوم السيارة بالانعطاف
18.     public float turnTimeout = 0.25f;
19.     
20.     //سرعة دوران الكاميرا
21.     //بوحدة درجة \ ثانية
22.     public float turnSpeed = 50;
23.     
24.     //الوقت المنقضي منذ تغير الزاوية بين الكاميرا والسيارة
25.     //إلى قيمة مرتفعة
26.     float angleChangeTime = -1;
27.     
28.     void Start () {
29.         //يتم وضع الكاميرا مبدئيا في نفس
30.         //موقع السيارة وإدارتها بنفس دورانها
31.         transform.position = car.position;
32.         transform.rotation = car.rotation;
33.     }
34.     
35.     //لنضمن تحرك السيارة LateUpdate نقوم باستخدام
36.     //قبل تحريك الكاميرا
37.     void LateUpdate () {
38.         //نبدأ أولا بوضع الكاميرا في نفس موقع السيارة
39.         //في فضاء المشهد
40.         transform.position = car.position;
41.         
42.         //نقوم الآن بتحريك الكاميرا نحو الخلف
43.         // z على محورها المحلي
44.         //و لأعلى بناء على متغيري المسافة والارتفاع
45.         transform.Translate(0, height, -zDistance);
46.         
47.         //حساب الزاوية الأفقية بين اتجاه نظر الكاميرا واتجاه مقدمة السيارة 
48.         float angle = Vector3.Angle(car.forward, transform.forward);
49.         
50.         //تأكد من كون قياس الزاوية أكبر من المنطقة الميتة
51.         //قياس الزاوية سيكون دائما موجبا بغض النظر 
52.         //عن اتجاه انعطاف السيارة
53.         if(angle > 1){
54.             //الفرق كبير كفاية لتدوير الكاميرا
55.             //لنتأكد أولا إن كان الوقت قد حان للبدء بتدوير الكاميرا
56.             if(angleChangeTime == -1){
57.                 angleChangeTime = 0;
58.             }
59.             
60.             //نضيف الوقت المنقضي من الإطار السابق
61.             //إلى مجموع الوقت منذ بداية انعطاف السيارة
62.             angleChangeTime += Time.deltaTime;
63.             
64.             if(angleChangeTime > turnTimeout){
65.                 //حان الوقت للبدء بتدوير الكاميرا
66.                 //نقوم أولا بعملية الضرب الاتجاهي لكل من اتجاه نظر الكاميرا
67.                 //واتجاه مقدمة السيارة
68.                 float resultDirection = 
69.                     Vector3.Cross(car.forward, 
70.                             transform.forward).y;
71.                 
72.                 //y إشارة المتغير  
73.                 //في المتجه الناتج تحدد اتجاه الدوران المطلوب
74.                 float rotationDirection;
75.                 if(resultDirection > 0){
76.                     rotationDirection = -1;
77.                 } else {
78.                     rotationDirection = 1;
79.                 }
80.                 
81.                 //نقوم الآن بإدارة الكاميرا حول السيارة مستخدمين اتجاه 
82.                 //الدوران وقيمة سرعة الدوران
83.                 transform.RotateAround(car.position, 
84.                                 Vector3.up, 
85.                   rotationDirection * turnSpeed * Time.deltaTime);
86.             }
87.         } else {
88.             //الفرق في الزاوية صغير جدا
89.             //نقوم بإعادة الوقت المنقضي إلى القيمة الافتراضية
90.             angleChangeTime = -1;
91.         }
92.     }
93. }

السرد 13: الكاميرا الخاصة بنظام تحكم ألعاب السيارات

تذكر أننا هنا نتعامل مع كائن كاميرا مستقل عن كائن السيارة، بخلاف ما كان عليه الحال مع كاميرا منظور الشخص الأول ومنظور الشخص الثالث. من أجل ذلك قمنا بتعريف المتغير car والذي سنقوم بربطه مع كائن السيارة كما سبق وفعلنا في نظام تحكم ألعاب المنصات (انظر الشكل 19)، بالإضافة إلى كل من المتغير height الذي يحدد ارتفاع الكاميرا عن مستوى ارتفاع السيارة والمتغير zDistance لتحديد المسافة الأفقية بين السيارة والكاميرا (أي بعد الكاميرا عن السيارة من الخلف).

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

الخطوة الأولى مع الدّالّة ()Start في السطر 28، وهي أن نتأكد من أن الكاميرا موجودة في نفس موقع السيارة، وأنها تنظر في الاتجاه الذي تشير إليه مقدمة السيارة. لذا قمنا بنسخ قيم الموقع والدوران من كائن السيارة إلى كائن الكاميرا. سألفت الانتباه هنا إلى قضية برمجية بسيطة وهي الفرق بين نسخ القيمة ونسخ المؤشر. في حال نسخ القيمة كما فعلنا هنا تبقى المتغيرات مستقلة عن بعضها البعض بعد إتمام عملية النسخ، بمعنى أن أي تغير مستقبلي على قيمة car.position أو car.rotation لن يؤثر على متغيرات الكاميرا transform.position و transform.rotation، بينما يختلف الموضوع في حالة نسخ المؤشر، لكنني سأؤجل الحديث عنها حتى الوقت المناسب لذلك.

لاحظ في السطر 37 أننا استخدمنا ()LateUpdate وليس ()Update؛ وذلك حتى نضمن قيام Unity بتنفيذ CarCotroller أولا لتحديث موقع السيارة قبل تنفيذ CarCamera لتحريك الكاميرا (يمكنك العودة لشرح ()LateUpdate). في السطرين 40 و 45 قمنا بوضع الكاميرا في المكان المناسب بالنسبة للسيارة، وذلك على خطوتين: الخطوة الأولى هي تحريك الكاميرا إلى نفس موقع السيارة (السطر 40) ومن ثم استخدام ()transform.Translate لتحريكها نحو الخلف والأعلى على محاور فضائها المحلي وفقا للقيم height و zDistance. استخدام القيمة السالبة لـ zDistance الهدف منه أن تتحرك الكاميرا نحو الخلف وليس الأمام حسب قاعدة اليد اليسرى.

بعد تحديث موقع الكاميرا حان الوقت لتحديث الدوران. الخطوة الأولى لذلك هي أن نقوم بحساب الزاوية بين الاتجاه الذي تشير إليه مقدمة السيارة car.forward والاتجاه الذي تنظر إليه الكاميرا transform.forward، المقصود بالمتجه forward هنا هو الاتجاه الموجب لمحور الفضاء المحلي z لكل من السيارة والكاميرا. هذا الحساب قمنا بتنفيذه في السطر 48 وتخزين قيمة الزاوية في المتغير angle. من المهم الإشارة هنا إلى أن الدّالّة ()Vector3.Angle تقوم بحساب أصغر زاوية ممكنة بين المتجهين الذين نقوم بتمريرهما لها وإرجاع القيمة على شكل قياس زاوية موجب، بغض النظر عن كون المتجه الأول إلى يمين أو يسار المتجه الثاني.

جميع الخطوات اللاحقة من السطر 53 إلى السطر 85 والمتعلقة بتدوير الكاميرا تعتمد على كون قياس الزاوية angle أكبر من درجة واحدة، وهو الشرط الذي نقوم بفحصه في السطر 53 قبل الانتقال إلى الخطوات اللاحقة، والتي تبدأ بالتأكد من أن قيمة متغير الوقت المنقضي منذ تغير الزاوية angleChangeTime لا تساوي 1-، وإعادتها إلى 0 في تلك الحالة (الأسطر 56 إلى 58). بعد ذلك نقوم بإضافة الوقت المنقضي منذ الإطار السابق إلى مجموع الوقت المنقضي منذ تغير الزاوية، وذلك في السطر 62. في السطر 64 نقوم بالتأكد من أن مجموع الوقت المنقضي منذ تغير الزاوية بين السيارة والكاميرا قد تجاوز فترة الانتظار المحددة في المتغير turnTimeout، ونبدأ بتدوير الكاميرا إذا ما تحقق هذا الأمر.

لتدوير الكاميرا نحتاج لثلاث معلومات كما تعلمنا سابقا: محور الدوران، واتجاه الدوران مع أو عكس عقارب الساعة، وأخيرا سرعة الدوران. محور الدوران هنا هو المحور العمودي، وموقعه هو نفس موقع السيارة، حيث أن الكاميرا ستدور حول السيارة، أمّا السرعة فهي محددة سلفا في المتغير turnSpeed. بقي علينا الآن أن نعرف اتجاه الدوران، وهو يتحدد تبعا لاتجاه انعطاف السيارة؛ فإذا انعطفت لليمين، سندير الكاميرا مع عقارب الساعة لتصبح خلف السيارة، أما لو انعطفت لليسار فسيكون الدوران بعكس عقارب الساعة.

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

الشكل 27: فرق الزاوية بين متجه مقدمة السيارة (أ) ومتجه اتجاه نظر الكاميرا (ب)

الشكل 27: فرق الزاوية بين متجه مقدمة السيارة (أ) ومتجه اتجاه نظر الكاميرا (ب)

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

هذه القيمة نقوم بتخزينها في المتغير resultDirection ومن ثم نستفيد منها لمعرفة اتجاه الدوران rotationDirection. هذا الأمر يتم في الأسطر 74 إلى 79. فإذا كانت قيمة resultDirection سالبة، نستنتج من قاعدة اليد اليسرى أن الاتجاه الأقصر هو أن ندير السيارة بعكس عقارب الساعة، وبما أننا هنا لا نملك التحكم بالسيارة وإنما بالكاميرا، فإن اتجاه دوران الكاميرا يجب أن يكون في الاتجاه المعاكس أي مع عقارب الساعة حتى تلحق بالسيارة. لهذا السبب ترى أن إشارة rotationDirection تكون عكس إشارة resultDirection. أخيرا نقوم في الأسطر من 83 إلى 85 بتنفيذ الدوران المطلوب للكاميرا تبعا للحسابات التي قمنا بها. يمكنك مشاهدة النتيجة وتجربتها في المشهد scene7 في المشروع المرفق.

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

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