الشبكة العربية لمطوري الألعاب

خبير مشرف أسامة السلمان مشاركة 1

بسم الله الرحمن الرحيم

أولًا : أعتذر عن التأخر عن تقديم هذا الجزء نظرًا للامتحانات وغيرها من المشاغل.

أنصح أن تراجع معلوماتك قليلًا قبل قراءة هذا الجزء فهذا الجزء تطبيقي بحت. يمكنك قراءة الجزأين السابقين من هنا :

الأول : http://goo.gl/nhTTR0
الثاني : http://goo.gl/QGJC6U

سأقوم بتطبيق اللعبة على محرك يونتي وباستعمال لغة C#. اشعر بالحرية لتطبقها
باستخدام أي محرك آخر أو تقنية أخرى فلا شيء جديد متى ماحللت اللعبة يمكنك
تطبيقها باستخدام أي تقنية مناسبة. (إذا طبقت اللعبة باستخدام تقنية أخرى
شاركها معنا في التعليقات)

نقوم بتشغيل المحرك وإنشاء مشروع جديد كما في الصور:




نشاهد الآن نوافذ محرك يونتي الأساسية وهي كالتالي :



1. نافذة المشروع : تحتوي كل ملفات المشروع
2. نافذة قائمة المشهد : قائمة بكل الكائنات في المشهد الحالي
3. نافذة المشهد: تعرض عرض نظري كل ما في المشهد
4. نافذة المتفحص : تعرض معلومات الكائن المختار سواء في نافذة القائمة أو المشهد

نقوم الآن بصنع الكائنات التي سنتعامل معها في المشروع وهي (كما ذكرناها في الأجزاء السابقة) :

1. شجرة التفاح
2. تفاحة
3. سلة

وهكذا نقوم بإضافة كائن ثنائي الأبعاد في محرك يونتي:



وهكذا نغير اسمه :



نكرر العملية حتى نصنع باقي الكائنات.



الآن دعنا نضيف الرسوميات لهذه الكائنات حتى نبدأ برؤيتها 😄

قمت برسم بعض الرسوم البسيطة (أعلم أنها مخيفة ولكنها تؤدي الواجب 😒 ) لنستخدمها في هذا الدرس. يمكنك تحميلها من هنا :

https://goo.gl/RhlKho

نقوم بإضافة الرسوميات إلى المشروع عن طريق (بعد فك الضغط عن الملف المحمل) السحب والإدراج داخل نافذة المشروع.

نقوم بتغيير رسوميات الكائن كما في الصورة :



نكرر العملية لباقي الكائنات.

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



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



كرر العملية حتى تحصل على أربع ملفات:



نقوم بإضافة مكون RigidBody2D للشجرة والتفاحة كما في الصور:



نقوم بتغيير صفة الجاذبية في مكون RigidBody2D للشجرة إلى الصفر (لأننا لا نريد للشجرة أن تسقط!)



ملاحظة: المكون أو Component في محرك يونتي هو إضافات تضيفها للكائن بحيث يحصل
من خلالها على صفات محددة، حتى الكود البرمجي يعتبر مكون لأنها يعطيه صفات
أنت تحددها بنفسك 😒

الآن قبل أن نبدأ في كتابة كود الشجرة قم بسحب التفاحة من نافذة القائمة إلى
نافذة المشروع (يفضل أن تنشأ ملف جديد اسمه Prefabs) كي يتحول الكائن إلى
نموذج :



ملاحظة: النموذج أو
Prefab  يختلف عن الكائن بأنه كمخطط يمكن من خلاله صنع كائنات كثيرة تحمل
نفس صفات النموذج وأي تعديل على النموذج يعدل على الكائنات المصنوعة منه
ولكن العكس غير صحيح.

قم بمسح التفاحة من المشهد الآن (سنتطرق إلى
السلة في الدرس القادم يمكنك إما مسحها الآن أو إبعادها عن مشهدك قليلًا
حتى لا تراها) لأننا سنقوم بإنشاء التفاحات عن طريق الكود

قم بإضافة الكود كمكون لنموذج التفاحة ولكائن الشجرة كما في الصورة :



اضغط مرتين متتاليتين على AppleTree ليفتح محرر الكود وقم بنسخ ولصق التالي:


http://pastebin.com/fCi6yqqe
(الكود كامل في الرابط أعلى. سنستعرض أجزاءه بالأسفل عند شرحها)

سأقوم في هذا الدرس شرح كود حركة الشجرة وإسقاط التفاحات وسنكمل في الدروس اللاحقة إن شاء الله. أولًا سأستعرض الكود كاملًا :

سأبدأ بشرح Update Function : (تستدعى كل فريم)

void Update()
{
   frameCounter++;
   Move();
   ChangeDirection();
   DropApple();
}

بالنظر إلى المخطط الانسيابي للشجرة نلاحظ أنه في كل فريم هناك ثلاث مهام أساسية يجب برمجتها:


(مخطط الشجرة الذي أنشأناه في الجزء الثاني)

1. الحركة
2. تغيير الاتجاه
3. إسقاط التفاحات

لذا جعلت كل مهمة في دالة منفصلة. لنأخذ نظرة على المهمة الأولى ألا وهي
التحرك في الإتجاه الحالي. ولتحديد الاتجاه الحالي قمت بعمل متغير بولياني
True/False لتعبر قيمة الصواب عن الإتجاه اليمين وقيمة الخطأ عن الإتجاه
اليسار. ومن ثم قمت بإنشاء دالة Move ونأتي الآن لشرح ما بداخل دالة Move :

void Move ()
        {
                if (GetComponent ().position.x < 9 && (GetComponent ().position.x) > -9)
                {
                        if (currentDirection == true)
                                GetComponent ().velocity = new Vector3 (speed, 0, 0);
                        else if (currentDirection == false)
                                GetComponent ().velocity = new Vector3 (-speed, 0, 0);
                }
                else if (GetComponent ().position.x > 9) {
                        currentDirection = !currentDirection;
                        GetComponent ().velocity = new Vector3 (-speed, 0, 0);
                }
                else if (GetComponent ().position.x < -9)
                {
                        currentDirection = !currentDirection;
                        GetComponent ().velocity = new Vector3 (speed, 0, 0);
                }
        }

هناك ثلاث حالات يمكن أن تقع فيها شجرتنا :

1. في حدود الشاشة (وهذا مانريد لها أن تبقى فيه)
2. خارج الحد يمينًا
3. خارج الحد يسارًا

ملاحظة: للوصول إلى أي من صفات الكائن علينا استخدام دالة GetComponent ونضع
مابين القوسين <> اسم المكون الذي يحتوي الصفة التي نريدها
فمثلًا
إذا أردنا الوصول إلى صفة السرعة للكائن نبحث عن المكون الذي يحتويها
والذي هو RigidBody2D ومن ثم نقوم باستدعاء GetComponent عليه كالتالي:
GetComponent.velocity
وكون
لغة C# محمية لا يمكننا الوصول إلى السرعة على محور محدد. لذا لتغيير
السرعة علينا تغيير متجه السرعة كاملًا (متجه له معنى فيزيائي ولكن يكفي أن
تعلم أن كائن يحمل ثلاث قيم. قيمة سينية وقيمة صادية وقيمة عينية
<x,y.z>) عن طريق صنع متجه جديد باستخدام أمر:
(new Vector3(x,y,z
ومن ثم تعيين متجه السرعة الخاص بالكائن إلى المتجه الجديد الذي قمنا بإنشائه حالًا.

1. في حدود الشاشة : بعد التجريب وجدت أن أفضل نتيجة للحدين الأيمن والأيسر
هما 9 و -9 لذا نقوم في هذا الشرط الأول بالتحقق إذا ماكان متجه الموقع
مابين الحدين ومتى ماحصل ذلك نخبره أن قيمة الصواب لمتغير currentDirection
تحركنا باتجاه اليمين و قيمة الخطأ تحركنا لليسار

ملاحظة: أي متغير
يتم إعلانه كمتغير عام أو Public يظهر لك في نافذة المتفحص عند اختيار
الكائن مما يسهل عليك تغيير قيمه دون الوصول إلى الكود ومما يؤدي إلى تجريب
أكثر سرعة

2. خارج الحد يمينًا : نعكس اتجاه الحركة يسارًا
3. خارج الحد يسارًا : نعكس اتجاه الحركة يمينًا

الآن ننتقل إلى المهمة الثانية، ألا وهي تغيير الاتجاه :

void ChangeDirection()
        {
                currentRoll = (int) Random.Range (1, 21);
                if (currentRoll == changeRoll)
                        currentDirection = !currentDirection;
        }

 قمت بعمل دالة خاصة لها ألا وهي : ChangeDirection
بداية
أقوم بإنشاء رقم عشوائي باستخدام دالة Random.Range والتي  تعطيك رقمًا
عشوائيًا ما بين أول قيمة و وثاني قيمة بين القوسين مع تضمين أو قيمة
وإقصاء الأخرى فإن كتبت كقيمة أولى 1 وكقيمة ثانية 21 ستعطيك الدالة قيمة
عشوائية من 1 إلى 20

قمت بإنشاء متغيرين الأول currentRoll والثاني
changeRoll والأول ليسجل الرقم العشوائي الخاص بهذا الفريم والثاني يحتوي
القيمة التي يحدث عنها التغيير في الاتجاه.

ملاحظة: القيم السابقة ستعطي تغيرً في الاتجاه بنسبة 1/20 وفي المخطط ذكرت نسبة 10% والتي اكتشفت بالتجريب أنها كبيرة لذا قللتها.

الآن نتجه لآخر مهمة ألا وهي إسقاط التفاحات

void DropApple()
        {
                if (frameCounter % 30 == 0)
                {
                        GameObject newApple = Instantiate (apple) as GameObject;
                        newApple.GetComponent().position = GetComponent().position;
                }

        }

 ولكي نقوم بإسقاط تفاحة كل نصف ثانية نقوم بعمل عداد يعد عدد الفريمات المنقضية فإذا كان عدد الفريمات
يقبل القسمة على 30 دون باقي يعني أن نصف ثانية مرت (% تعطي باقي القسمة
فإن كان 0 يعني أن 30 فريم أو مضاعفاته مر وهذا يحدث كل نصف ثانية لأن دالة
Update يتم استدعاؤها بالثانية 60 مرة)

نقوم باستخدام دالة
Instantiate كي نقوم بعمل كائن جديد في المشهد ونقوم بتغيير موقعه كالمعتاد
ولأن التفاحة عندها مكون RigidBody2D ستسقط تلقائيًا (القيمة الإفتراضية
للجاذبية ليست صفرًا، إذا هناك جاذبية ستجعلها تسقط. لنتمنى ألا تسقط تلك
التفاحة على رأس رجل آخر ويصنع لنا فيزياء أخرى ☺ )

ملاحظة: دالة Instantiate ستأخذ متغيرًا (apple في هذه الحالة ) ولكننا سنغير
قيمة المتغير إلى نموذج التفاحة لاحقًا في نافذة المتفحص.

بعد أن ننتهي من كتابة الكود نقوم بحفظ الملف (CTRL + S). الآن تبقى خطوة واحدة
والتي ذكرناها سابقا(تعيين المتغير apple) وذلك يتم كما في الصورة (اضغط
الدائرة الصغيرة بجانب المتغير واختر التفاحة):



الآن إذا قمنا بتشغيل اللعبة سنشاهد التالي (الشجرة تتحرك يمنة ويسرة عشوائيًا والتفاحات تتساقط!) :



قد وصلنا إلى نهاية الدرس! أعلم أنه كان درسًا طويلًا ولكن الحمدلله تعلمنا
معًا الكثير. وإذا أشكل عليكم أمر لا تترددوا في سؤالي حيث سأقوم بالإجابة
على استفساراتكم كلها بإذن الله ☺

انتظرونا في الجزء الرابع حيث سنكمل تطوير تصرفات الكائنات الأخرى والسلام عليكم ورحمة الله وبركاته.

أسامة السلمان
مطور ألعاب ومهندس برمجيات.
للتواصل: https://twitter.com/TheDorgam
بما أن الله على كل شيء قدير ؛ إذا ، ليس هناك شيء يدعى "المستحيل".

مبتدئ  عمر خ مشاركة 2

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

خبير مشرف أسامة السلمان مشاركة 3

وعليكم السلام أخ عمر.

في 11/شعبان/1436 12:25 ص، غمغم عمر خ باستغراب قائلاً:

بخصوص حدود الشاشة في الدالة Move. ما الذي يمثله الرقم 9؟

الرقم تسعة هنا يعني 9 وحدات يونتي (9Unity Units) وهي تقريبًا تماثل المتر في العالم الحقيقي.

بتاريخ 11/شعبان/1436 12:25 ص، قطب عمر خ حاجبيه بشدة وهو يقول:

هل يمكننا الحصول على عرض النافذة بطريقة ديناميكية بدلا من كتابة الرقم 9 والاضطرار لتغيير الكود في حال تغيير عرض الشاشة.

نعم يمكن ذلك. ولكن لصعوبة شرح هذه النقطة قررت الذهاب مع الحل السهل. لكن بما أنك ذكرت الموضوع سأقوم بشرحها لك.

أولًا هذا هو الكود الذي يجب عليك كتابته لتتعامل اللعبة ديناميكيًا مع حجم الشاشة:

void Move ()
  {
     if (GetComponent ().position.x < 
         Camera.main.ScreenToWorldPoint(new Vector3(Camera.main.pixelWidth,0,0)).x - 1 &&
        (GetComponent ().position.x) > Camera.main.ScreenToWorldPoint(Vector3.zero).x + 1) 
     {
        if (currentDirection == true)
	    GetComponent ().velocity = new Vector3 (speed, 0, 0);
	else if (currentDirection == false)
            GetComponent ().velocity = new Vector3 (-speed, 0, 0);
     } 
     else if (GetComponent ().position.x > 
              Camera.main.ScreenToWorldPoint(new Vector3(Camera.main.pixelWidth,0,0)).x - 1)
     {
	     currentDirection = !currentDirection;
	     GetComponent ().velocity = new Vector3 (-speed, 0, 0);
     } 
     else if (GetComponent ().position.x < Camera.main.ScreenToWorldPoint(Vector3.zero).x + 1) 
     {
	      currentDirection = !currentDirection;
	      GetComponent ().velocity = new Vector3 (speed, 0, 0);
     }
}

ثانيًا انظر لهذه الصورة :



الآن نحن نريد معرفة طرف الشاشة صحيح؟ ولكن الإحداثيات في نظام يونتي تنقسم لثلاث أنظمة :
1. نظام المختص بالشاشة وهو بالبكسل ونقطة المرجع الركن الأيسر السفلي
2. نظام إحداثيات المشهد وهذه تستخدم في عناصر GUI والتي هي نظام يحتوي أزرار وصور وكلام يطبع على الشاشة
3. إحداثيات العالم ثلاثي الأبعاد المعتادة

الآن طرف الشاشة الأيمن معروف عندنا عن طريق Camera.main.pixelWidth ولكن كيف لنا أن نحولها إلى نظام الثلاثي الأبعاد التي فيه يقاس كل شيء بوحدات يونتي السابق ذكرها؟
الجواب نستعمل دالة Camera.main.ScreenToWorldPoint والتي ستخبرنا الإحداثيات بنظام يونتي ثلاثي الأبعاد متى ما أعطيناها الإحداثيات بنظام الشاشة البكسلي.
الطرف الأيسر بكسليًا هو النقطة 0 لذا نعطيها للدالة نفسها لتخبرنا ماذا تعني صفر الشاشة في عالم يونتي؟ وعند أخذ الجواب من الدالة يمكننا عمل المقارانات كما اعتدنا.

أرجو أن أكون قد شرحتها جيدًا. أعلم هي صعبة نوعًا ما ولكن حاول أن تقرأها بتأني لتفهمها وإذا تعثرت عاود السؤال عن النقطة التي لم تفهمها.

شكرًا.

أسامة السلمان
مطور ألعاب ومهندس برمجيات.
للتواصل: https://twitter.com/TheDorgam
بما أن الله على كل شيء قدير ؛ إذا ، ليس هناك شيء يدعى "المستحيل".