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

العب

تعتبر ألعاب المنصات من أكثر أنواع الألعاب ثنائية الأبعاد شهرة، بل وتعدت ذلك إلى الألعاب ثلاثية الأبعاد، فألعاب مثل Super Mario و Castlevania تعتبر من أشهر الألعاب القديمة التي تنتمي لهذا النوع، بالإضافة لعدد من الألعاب الحديثة الناجحة مثل Braid, FEZ, Super Meat Boy. تعتمد هذه الألعاب بشكل أساسي على ميكانيكية القفز للتحرك بين المنصات في اللعبة والتغلب على الخصوم وحل الألغاز، بالإضافة لبعض الميكانيكيات الأخرى مثل التصويب.

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

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

الشكل 17: المشهد الخاص بتطبيق نظام إدخال ألعاب المنصات

الشكل 17: المشهد الخاص بتطبيق نظام إدخال ألعاب المنصات

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

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

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class PlatformerControl : MonoBehaviour {
5.  
6.  //السرعة العمودية عند بداية القفز
7.  public float jumpSpeed = 7;
8.  
9.  //سرعة السقوط
10.     public float gravity = 9.8f;
11.     
12.     //سرعة الحركة الأفقية
13.     public float movementSpeed = 5;
14.     
15.     //لتخزين السرعة الأفقية والعمودية قبل تحريك الكائن
16.     private Vector2 speed;
17.     
18.     // Use this for initialization
19.     void Start () {
20.     
21.     }
22.     
23.     // Update is called once per frame
24.     void Update () {    
25.         //قراءة مدخلات اتجاه الحركة
26.         if(Input.GetKey(KeyCode.RightArrow)){
27.             speed.x = movementSpeed;
28.         } else if(Input.GetKey(KeyCode.LeftArrow)){
29.             speed.x = -movementSpeed;
30.         } else {
31.             speed.x = 0;
32.         }
33.         
34.         //قراءة مدخل القفز
35.         if(Input.GetKeyDown(KeyCode.Space)){
36.             //تطبيق القفز فقط في حالة كون الكائن يقف على الأرض
37.             if(transform.position.y == 0.5f){
38.                 speed.y = jumpSpeed;
39.             }
40.         }
41.         
42.         //تحريك الكائن
43.         transform.Translate(speed.x * Time.deltaTime, 
44.                       speed.y * Time.deltaTime, 
45.                       0);
46.         
47.         //تطبيق الجاذبية الأرضية في حال القفز
48.         if(transform.position.y > 0.5f){
49.             speed.y = speed.y - gravity * Time.deltaTime;
50.         } else {
51.             speed.y = 0;
52.             Vector3 newPosition = transform.position;
53.             newPosition.y = 0.5f;
54.             transform.position = newPosition;
55.         }
56.     }
57. }

السرد 6: بريمج تطبيق نظام إدخال المنصات

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

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

السرعة الثالثة وهي السرعة الأفقية movementSpeed وهي سرعة الحركة يمينا أو يسارا كما رأيناها في مثال في الفصل السابق. عندما نجمع هذه السرعات الثلاث تعطينا محصلة بسرعة متجهة لها قيمتان على المحورين x و y، لذا نقوم باستخدام المتغير speed من نوع Vector2 وهو متجه ثنائي الأبعاد لتخزين هاتين القيمتين واستخدامهما في كل دورة تصيير، خاصة أننا نحتاج لأن نحتفظ بقيمة السرعة العمودية التي تتغير بين تصيير كل إطارين وينبغي أن نحافظ على القيمة لاستخدامها في تصيير الإطار المقبل.

الخطوة الأولى في دورة التحديث (الأسطر 26 إلى 32) معروفة لدينا وهي قراءة مدخلات لوحة المفاتيح الخاصة بالحركة الأفقية، فالسهم الأيمن يعطينا قيمة للسرعة تساوي المتغير movementSpeed، بينما السهم الأيسر يعطينا القيمة السالبة لنفس القيمة. أما في حالة عدم ضغط اللاعب لأي من مفتاحي السهمين فإن السرعة الأفقية تساوي صفرا. لاحظ أننا نخزن قيمة السرعة الأفقية في العضو x داخل المتغير speed وذلك لنستخدمه لاحقا في إزاحة الكائن في الاتجاه المطلوب.

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

الخطوة الثالثة (الأسطر 43 إلى 45) هي أن نقوم بتنفيذ الإزاحة حسب قيم السرعة التي سبق وقمنا بحسابها اعتمادا على مدخلات اللاعب. وذلك عن طريق الدّالّة ()transform.Translate كما تعودنا. الحركة ستكون على المحورين x و y حسب القيم المخزنة داخل المتغيرspeed ونضربها بالزمن المنقضي كما سبق وشرحنا ذلك.

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

القسم الآخر من الخطوة الأخيرة (الأسطر 50 إلى 55) يتعلق بكون شخصية اللاعب في وضع غير وضع القفز، وهنا نحن أمام احتمالين: إما أن تكون قيمة الموقع على المحور y تساوي 0.5 وهي القيمة الأولية، وإما أن تكون قد نقصت لما دون تلك القيمة نتيجة للإزاحة في الخطوة الثالثة. وبما أن الإزاحة تعتمد على الزمن المنقضي والذي لا يمكننا التحكم فيه، فلا يمكننا أن نضمن عدم الحصول على قيم أقل من 0.5؛ لأننا – كما ذكرت في مقدمة هذا الفصل – لا نتعامل حتى اللحظة مع اكتشاف التصادمات بين الكائنات، بالتالي فإن الأرضية لن تمنع إزاحة الكائن لأسفل. وحتى نكون في الجانب الآمن، فإننا نقوم بإرجاع السرعة العمودية إلى الصفر حتى نضمن بقاء الجسم على الأرض حتى القفزة القادمة، إضافة إلى التأكد من وضع الكائن في موقعه الأصلي على المحور y وهو 0.5.

لا يسمح لك Unity3D بتغيير عضو واحد من أعضاء موقع الكائن transform.position بشكل مباشر، فلن يقبل مثلا الأمر ;transform.position.y = 0.5f. لذا عليك أولا أن تقوم بتخزين قيمة الموقع كاملة في متغير من نوع Vector3 ومن ثم تعدل على الأعضاء داخل هذا المتغير بما يناسبك، ثم بعد ذلك تعيد قيمة المتغير إلى transform.position كما في الأسطر 52 إلى 54 في السرد 6

بعد أن تقوم ببناء المشهد كما في الشكل 17 (أو أي مشهد مماثل، بشرط وجود كائنات في الخلفية لتمييز حركة اللاعب والكاميرا) وتضيف البريمج PlatformerControl على كائن شخصية اللاعب، قم بتشغيل اللعبة وتجربة التحرك والقفز. الشكل 18 يوضح لقطة من اللعبة أثناء القفز.

الشكل 18: تطبيق نظام تحكم المنصات وتنفيذ القفز

الشكل 18: تطبيق نظام تحكم المنصات وتنفيذ القفز

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

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

1. using UnityEngine;
2. using System.Collections;
3. 
4. public class PlayerTracking : MonoBehaviour {
5.  //نحتاج مرجعا لأحد مكوّنات كائن شخصية اللاعب حتى نتمكن من معرفة موقعه
6.  public Transform playerCharacter;
7.  //أقصى مسافة للحركة نحو اليمين قبل أن تبدأ الكاميرا بالتتبع
8.  public float maxDistanceRight = 1.5f;
9.  //أقصى مسافة للحركة نحو اليسار قبل أن تبدأ الكاميرا بالتتبع
10.     public float maxDistanceLeft = 1.5f;
11.     //أقصى مسافة للحركة نحو الأعلى قبل أن تبدأ الكاميرا بالتتبع
12.     public float maxDistanceUp = 1.0f;
13.     //أقصى مسافة للحركة نحو الأسفل قبل أن تبدأ الكاميرا بالتتبع
14.     public float maxDistanceDown = 1.0f;
15.     
16.     // Use this for initialization
17.     void Start () {
18.     
19.     }
20.     
21.     // LateUpdate هذه المرة قمنا باستخدام الدّالّة 
22.     void LateUpdate () {
23.         //موقع الكاميرا الحالي
24.         Vector3 camPos = transform.position;
25.         //موقع اللاعب الحالي
26.         Vector3 playerPos = playerCharacter.position;
27.         
28.         //هل اللاعب إلى يمين الكاميرا بمسافة تتجاوز الحد الأقصى؟
29.         if(playerPos.x - camPos.x > maxDistanceRight){
30.             camPos.x = playerPos.x - maxDistanceRight;
31.         } 
32.         //هل اللاعب إلى يسار الكاميرا بمسافة تتجاوز الحد الأقصى؟
33.         else if(camPos.x - playerPos.x > maxDistanceLeft){
34.             camPos.x = playerPos.x + maxDistanceLeft;
35.         }
36.         
37.         //هل اللاعب أعلى الكاميرا بمسافة تتجاوز الحد الأقصى؟
38.         if(playerPos.y - camPos.y > maxDistanceUp){
39.             camPos.y = playerPos.y - maxDistanceUp;
40.         } 
41.         //هل اللاعب أسفل الكاميرا بمسافة تتجاوز الحد الأقصى؟
42.         else if(camPos.y - playerPos.y > maxDistanceDown){
43.             camPos.y = playerPos.y + maxDistanceDown;
44.         }
45.         //تحديث موقع الكاميرا بعد إجراء التعديلات
46.         transform.position = camPos;
47.     }
48. }

السرد 7: آلية تتبع الكاميرا لشخصية اللاعب

هناك الكثير من الأمور الجديدة في هذا البريمج والتي ينبغي أن نتحدث عنها ببعض التفصيل. أول هذه الأمور هو المتغير playerCharacter من نوع Transform والذي يختلف عن المتغيرات التي تعاملنا معها حتى الآن وكان معظمها رقميا، بحيث يمكننا أن ندخل قيمتها باستخدام لوحة المفاتيح.

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

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

الشكل 19: كيفية ربط الكائن مع المتغير داخل البريمج

الشكل 19: كيفية ربط الكائن مع المتغير داخل البريمج

إضافة إلى المتغير playerCharacter، قمنا بتعريف أربع متغيرات لتحديد الإطار الذي يمكن للاعب التحرك في داخله دون أن تتبعه الكاميرا. هذه المتغيرات تحمل أسماء تدل على موقع اللاعب بالنسبة للكاميرا على المحورين x و y. فلدينا maxDistanceRight و maxDistanceLeft اللذين يحددان أقصى مسافة إلى يمين ويسار الكاميرا، فإذا كانت شخصية اللاعب على يمين الكاميرا بمسافة تساوي maxDistanceRight، فإن الكاميرا تتحرك باتجاه اليمين حتى لا تسمح للمسافة بالزيادة عن هذا الحد. كذلك الأمر بالنسبة للمتغيرين maxDistanceUp و maxDistanceDown مع حركة اللاعب على المحور y. الشكل 20 يوضح كيف سيبدو شكل هذه المحددات لو رسمناها على شكل مستطيل حول موقع الكاميرا.

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

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

بعد أن قمنا بتحديد هذه المسافات علينا أن نتتبع موقع اللاعب في كل دورة تصيير حتى نعرف إن كان ينبغي أن نحرك الكاميرا في اتجاه ما. بداية لاحظ أننا في السطر 22 استخدمنا الدّالّة ()LateUpdate بدلا من ()Update التي اعتدنا استخدامها. وجه الشبه بينهما أنهما تستدعيان في كل دورة تحديث، أما الفرق بينهما أن Unity يقوم باستدعاء ()Update أولا من جميع البريمجات الموجودة في المشهد، ومن ثم يعود ليستدعي ()LateUpdate.

تذكر أن المشهد الحالي يحتوي على بريمجين هما PlatformerControl الذي يقوم بقراءة مدخلات اللاعب وتحريك الشخصية، و PlayerTracking الذي يساعد الكاميرا على تتبع موقع اللاعب. ما نريد أن نضمنه هو أن تحريك شخصية اللاعب يتم أولا ثم يتم بعد ذلك تحريك الكاميرا. الطريقة الأسهل لذلك هي أن نقوم بتحديث بريمج الكاميرا عن طريق ()LateUpdate مما يضمن لنا أنه سيتم تحديثه بعد أن تكون شخصية اللاعب قد تحركت وأصبحت في موقعها الجديد الذي نريد أن نقارنه بموقع الكاميرا. ربما لن تلاحظ فرقا إن قمت باستخدام ()Update بدلا من ()LateUpdate لأنك قد تكون محظوظا ويقوم Unity بتنفيذ PlatformerControl قبل PlayerTracking، لكن في عالم البرمجة يجب ألا تترك شيئا للصدفة وعليك دائما أن تحسب الاحتمال الأسوأ وتحتاط منه.

كما ترى في السطرين 24 و 26، فإننا نبدأ بتخزين موقعي كل من اللاعب والكاميرا حتى نقوم بعمل الحسابات المطلوبة بينهما. نبدأ بعد ذلك في فحص الاحتمالات الممكنة على المحور x وذلك في الأسطر 30 إلى 36. آخذين في الحسبان أن الاتجاه الموجب للمحور x هو إلى جهة اليمين والاتجاه السالب إلى جهة اليسار، نقوم بطرح الإحداثي x لموقع الكاميرا من الإحداثي x لموقع اللاعب، فإذا كان الناتج أكبر من المسافة المحددة من جهة اليمين، فإننا نقوم بتغيير موقع الكاميرا لتتبع اللاعب.

المهم هو كيف نحسب الموقع الجديد للكاميرا على المحور x. المثال التالي يوضح الطريقة: لنفرض أن أقصى مسافة مسموح بها من جهة اليمين هي 1.5 كما في السطر 8، ولنفرض أن شخصية اللاعب تحركت لليمين في دورة التحديث الحالية ووصل للموقع 1.6 بينما لا تزال الكاميرا في الموقع 0. لو حسبنا الفرق بين الموقعين سيكون 1.6 = 0 – 1.6، وهي قيمة أكبر من القيمة المسموح بها وهي 1.5 لذا يجب أن نحرك الكاميرا مع مراعاة أن تبقى شخصية اللاعب على مسافة 1.5 إلى يمين الكاميرا، وذلك لضمان استمرار حركة الكاميرا مع حركة الشخصية نحو اليمين. لذا نقوم بطرح المسافة القصوى المسموحة من موقع الشخصية الحالي 0.1 = 1.5 – 1.6 وهو الموقع الجديد الذي يجب أن ننقل الكاميرا إليه، وهذا بالضبط ما نفعله في السطر 30.

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

بعد الانتهاء من حساب الموقع الجديد للكاميرا بناء على موقع اللاعب وحدود الحركة التي قمنا بضبطها، تبقى الخطوة الأخيرة وهي اعتماد الموقع الجديد للكاميرا بوضع قيمته في transform.position كما في السطر 46. يمكنك مشاهدة النتيجة النهائية لهذا العمل في المشهد scene3 في المشروع المرفق.

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

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

أضف تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *