اذهب إلى المحتوى

بناء واجهة المستخدم وأوضاع اللعبة (modes) في Unity3d


ياسر جفال

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

ملاحظة: يمكن تحميل الملفات المصدرية لكامل هذه السلسلة عبر حساب أكاديمية حسوب على Github، يمكن أيضا تحميل ملف APK لتجريب اللعبة على أجهزة Android.

بناء الشاشة الرئيسية والواجهة

لإضافة مشهد جديد اضغط Control+N ومن ثم قم بحفظ هذا المشهد باسم MenuScene. آلية التنقل بين المشاهد في محرك Unity تعتمد على رقم كل مشهد في ترتيب بناء اللعبة. هذا الترتيب يمكن الوصول إليه عن طريق القائمة:

 File > Build Settings 

النافذة التي ستظهر هي المسؤولة عن ترتيب المراحل في اللعبة وتصديرها. ما يهمنا الآن هو الترتيب. قم بسحب المشهدين MenuScene و GameScene من مستعرض المشروع إلى القائمة Scenes in Build كما ترى في الصورة التالية. من المهم هنا مراعاة الترتيب حيث يأخذ المشهد MenuScene الرقم 0 والمشهد GameScene الرقم 1.

image00.thumb.png.40b179f3490dd4f8ef84cd

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

عناصر واجهة المستخدم في محرك Unity تندرج تحت القائمة:

 Game Object > UI

لنبدأ أولا بصورة خلفية للشاشة الرئيسية. هذه الخلفية تتم إضافتها عن طريق لوح:

Game Object > UI > Panel 

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

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

image01.thumb.png.d9ec46ca2888ce97062130

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

image02.thumb.png.3fcde25e0704816a5435a7

أول ما يمكن ملاحظته هو إمكانية تحديد نوع الارتكاز بشكل مختلف أفقيا وعموديا، حيث يمكننا استخدام التمدد stretch والذي يجعل العنصر مربوطا من زواياه الأربع وبالتالي يجب أن تبقى هذه الزوايا دائما في مواقعها بغض النظر عن حجم الشاشة. أما الخيارات الأخرى مثل left ،center ،right أفقيا أو top ،middle ،bottom عموديا، فهي تحدد نقطة ارتكاز العنصر بالنسبة للكائن الأب، فإذا حددت مثلا نقطة الارتكاز بأنها middle center، فإن العنصر سيحافظ على مسافة ثابتة من وسط الكائن الأب بغض النظر عن حجم هذا الأخير وحجم الشاشة. سنستخدم هذا الخيار عند إضافة أزرار القائمة الرئيسية كأبناء لكائن اللوح، وبذلك نضمن وجودها دائما في وسط الشاشة.

بالعودة لخلفية القائمة، قم بسحب إحدى صور الخلفية المتوفرة إلى الخانة Source Image لتظهر هذه الصورة في خلفية الشاشة الرئيسية. افتراضيا سيقوم Unity بجعل كائن اللوح شفافا نسبيا مما يظهره بشكل معتم وهذا ما لا نريده. لنقم بإزالة هذه الشفافية عن طريق فتح لوح الألوان بالضغط على المستطيل الأبيض في الخانة Color، ومن ثم تحريك منزلق الشفافية A إلى أقصى اليمين كما ترى هنا:

image03.thumb.png.e0653f833bfa19a2b6e969

بعد ذلك قم بتغيير اسم الكائن من Panel إلى MainMenu. سنضيف لهذا اللوح ثلاثة أبناء:

  • الأول هو ظل العنوان GameTitleShadow.
  • الثاني والثالث هما الزران "ابدأ اللعب" NewGame و "خروج" Exit.

لنبدأ أولا مع ظل العنوان: لماذا أضفت ظل العنوان قبل العنوان نفسه؟ السبب هو أن ترتيب تصيير عناصر واجهة المستخدم يقدّم تصيير الآباء على الأبناء. لذا سنضيف العنوان نفسه GameTitle كابن لظل العنوان GameTitleShadow. بعد ذلك قم بكتابة عنوان اللعبة "الحيوانات الغاضبة" داخل الخانة Text لكل من الظل والابن، ومن ثم غير ألوانهما للأخضر بحيث يبدو الظل أفتح من العنوان نفسه. وأخيرا قم بتصغير العنوان قليلا حتى يظهر الظل من خلاله. (استخدمت خطا يسمى HACEN PROMOTER LT ويمكن تحميله مجانا من الموقع hacen.net). الشكل التالي يوضح الإعدادات الكاملة لنص العنوان وظله:

image04.thumb.png.ca7c68bca5b4b49fa37301

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

image05.thumb.png.78b41b950eb530de1f13b0

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

طبعا لن نحتاج لجميع محتويات المجموعتين، لكن سنكتفي بالعناصر الموضحة في الصورة التالية:

image06.thumb.png.401997196225ab79b00a83

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

image07.thumb.png.2714509abf071c110dee71

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

احظ أن لدينا 3 أزرار من كل لون: الأول مرتفع والثاني عادي والثالث مضغوط للأسفل. الأزرار التي سنضيفها ستعمل كالآتي. في الحالة الافتراضية سنعرض صورة الزر العادي، وحين تمرير الفأرة على الزر سنعرض صورة الزر المرتفع، وحين الضغط عليه سنعرض صورة الزر المنخفض. يمكنك تحقيق هذا السلوك عن طريق ضبط إعدادات المكوّنين Image و Button في كائن الزر وفق القيم التي تراها في الصورة التالية. تذكر أن نقطة الارتكاز للأزرار وللعنوان هي المنتصف أفقيا وعموديا middle center.

image08.thumb.png.d9c7d281c3d223209f5f7a

لاحظ أننا حددنا الصورة الافتراضية للزر عبر المكوّن Image، حيث تلاحظ أن نوع الصورة هو Sliced أي صورة مقسمة إلى 9 أجزاء كما سبق ورأينا. بعدها قمنا بتغيير نوع الانتقال في الزر Transition إلى Sprite Swap بحيث نقوم بتبديل الصورة حين الانتقال بين الحالات. حددنا هنا صورتين أخريين إحداهما لحالة التحديد أو المرور بالفأرة Highlighted Sprite والثانية لحالة الضغط Pressed Sprite. أخيرا ستلاحظ وجود كان ابن لكل زر وهو عبارة عن نص يعرض الكتابة التي نرغب بظهورها على الزر. بالتالي يمكننا استخدام هذه الكائنات للكتابة والحصول في النهاية على النتيجة التالية:

image09.thumb.png.057c843ad0427ca989e04f

حل مشكلة اللغة العربية

مشكلة اللغة العربية في محرك Unity قديمة وقد عانيت معها منذ أول إصدار استخدمته وهو 3.5، وحتى الإصدار الخامس لم تحل هذه المشكلة من قبل الشركة. إلا أن الحل موجود، حيث قمت قبل عدة سنوات بكتابة بريمج يسمى ArabicText، ومبدأ عمله يقوم على عكس ترتيب الأحرف بحيث تصبح من اليمين لليسار كما يفترض، إضافة إلى استبدال رموز الحروف المتقطعة بالمتصلة حسب موقع الحرف في الكلمة. هذا البريمج موجود في المشروع داخل المجلد Assets\Scripts\UI وكل ما عليك هو إضافته لكل كائن من نوع Text (أي العنوان وظل العنوان وكائنات الأبناء النصيّة للأزرار). عند تشغيل اللعبة سيقوم البريمج بعمله في تصحيح النص كما ترى في الصورة التالية. هذا البريمج مجرد أداة مساعدة لا علاقة لها بموضوع الدرس لذا لن أقوم بشرحه هنا، لكن يمكنك الاطلاع عليه في المجلد المذكور إن كنت ترغب بذلك.

image10.thumb.png.a378404b4ca8c1c475e0b1

التفاعل مع عناصر واجهة المستخدم

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

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

using UnityEngine;
using System.Collections;

public class ExitGameCommand : MonoBehaviour {

       //يتم استدعاؤها مرة واحدة عند بداية التشغيل
       void Start () {
    
       }
    
       //يتم استدعاؤها مرة عند تصيير كل إطار
       void Update () {
    
       }

   //تخرج من اللعبة
   public void ExitGame()
   {
       if (Application.loadedLevel == 0)
       {
           //نحن الآن في القائمة الرئيسية، بالتالي أغلق التطبيق نهائيا
           Application.Quit();
       }
       else if (Application.loadedLevel == 1)
       {
           //نحن في مشهد اللعبة، بالتالي عد للقائمة الرئيسية
           Application.LoadLevel(0);
       }
   }
}

لاحظ أن البريمج يستخدم أوامر مباشرة يتيحها محرك Unity من أجل تحميل المشاهد حسب أرقامها. حيث نعرف المشهد الحالي عن طريق المتغير Application.loadedLevel ونقوم بتحميل المشهد الذي نريد عن طريق الدّالة ()Application.LoadLevel. تذكر أن مشهد القائمة الرئيسية يحمل الرقم 0 ومشهد اللعبة يحمل الرقم 1. أخيرا يمكنك ملاحظة أن الدّالة ()Application.Quit تستخدم لإغلاق التطبيق.

الخطوة التالية هي ربط الضغط على الزر "خروج" باستدعاء الدّالة ()ExitGame. الخطوة الأولى هي أن نضيف هذا البريمج إلى المشهد على أي كائن، وليكن كائن الزر نفسه. بعدها نعود للمكوّن Button حيث سنجد في أسفله قائمة بالأوامر التي نريد استدعاءها حين الضغط على هذا الزر وهي مرتبة في القائمة ()OnClick كما في ترى في الصورة التالية. يمكنك إضافة أمر جديد بالضغط على الزر + في أسفل القائمة:

image11.thumb.png.505097fed512fca7a0245d

عند إضافة أمر جديد للقائمة، عليك تحديد الكائن الذي سينفذ الأمر، ومن ثم تحدد البريمج، وأخيرا تحدد الدّالة داخل هذا البريمج. بما أننا أضفنا البريمج ExitGameCommand على كائن الزر نفسه، علينا أن نسحب الزر "خروج" Exit من الهرمية إلى داخل الخانة الخاصّة بالكائن، ومن ثم نفتح قائمة الأوامر والتي تكون قيمتها الافتراضية No Function حيث نختار البريمج ExitGameCommand ومن ثم الدّالة ()ExitGame كما في الصورة التالية:

image12.thumb.png.753cdbd1393c5a033126dc

بالتالي عند الضغط على الزر "خروج" فإن التطبيق سيتم إيقاف تشغيله (ملاحظة: لا يعمل الأمر ()Application.Quit من داخل محرر المحرك، عليك بناء التطبيق وتشغيله منفردا لتجربته).

لننتقل الآن للزر الثاني وهو زر بداية اللعب. عند الضغط عليه سيعرض نافذة جديدة تحتوي على المراحل الموجودة بحيث يتسنى للاعب اختيار مرحلة منها. من أجل ذلك علينا أولا بناء نافذة مستقلة تحتوي على أزرار المراحل. مبدئيا سنكتفي بزرين يحملان الأرقام 1 و 2 حيث سيكون لدينا مرحلتان فقط في هذا المثال. إضافة لذلك سيكون هناك زر ثالث يحمل الرمز X يقوم بإغلاق النافذة والعودة للقائمة الرئيسية مرة أخرى. لعمل النافذة المذكورة قم بإضافة لوح جديد Panel واحرص على أن يكون ترتيبه بين أبناء الكائن Canvas تحت اللوح الأول الخاص بالقائمة الرئيسية MainMenu. هذا الترتيب مهم حيث أن الكائنات في أسفل الهرمية تظهر على الشاشة أمام الكائنات التي في الأعلى، وهذا ما نريده: أن تظهر هذه النافذة حين عرضها أمام القائمة الرئيسية. قم بتغيير لون اللوح للأسود مع الإبقاء على الشفافية، ومن ثم غير اسمه إلى LevelSelector وحجمه على المحورين الأفقي والعمودي إلى 0.9. هذا سيجعل شكله يبدو كالآتي (لاحظ أيضا ترتيب العناصر في الهرمية على اليسار).

image13.thumb.png.059cc2782b7be5a58f7425

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

image14.thumb.png.30be885f3a1357ca34853f

المتغيرات الأربع تحت البند Padding تحدد حجم الهوامش من الجهات الأربعة بالبكسل، وهنا اخترت القيمة 32. بعدها نقوم بتحديد حجم كل عنصر أفقيا وعموديا عن طريق المتغير Cell Size وأخيرا نحدد المسافات بين العناصر وهي هنا 16. باقي المتغيرات يمكن تركها على قيمها الافتراضية. بعد هذا علينا أن نصنع قالبا لزر المرحلة بحيث يمكننا إضافته عدة مرات مع تغيير المرحلة التي سيتم تحميلها بتغير الزر. الزر المذكور شبيه بالأزرار التي أضفناها حتى الآن من حيث آلية العرض بتغيير الصور، لكن الذي سيتغير هو النص حيث سيحمل كل زر رقم المرحلة (1، 2، 3، …) إضافة لتغيير البريمج الذي سيستقبل الأمر. بعد صنع قالب الزر حسب الوصف السابق، سنضيف عليه البريمج LevelButton، وهو موضح في السرد التالي:

using UnityEngine;
using System.Collections;

public class LevelButton : MonoBehaviour {

       //تستدعى مرة واحدة عند بداية التشغيل
       void Start () {
    
       }
    
       //تستدعى مرة عند تصيير كل إطار
       void Update () {
    
       }

   //تقوم ببدء اللعبة مستخدمة رقم المرحلة المزود
   public void StartLevel(int levelIndex)
   {
       //SelectedLevelخزن رقم المرحلة في الإعدادات في الخانة
       PlayerPrefs.SetInt("SelectedLevel", levelIndex);
       //قم بتحميل المشهد رقم 1 وهو مشهد اللعبة
       Application.LoadLevel(1);
   }
}

يعمل هذا البريمج عند استدعاء الدّالة ()StartLevel على تحميل مشهد اللعبة (المشهد رقم 1)، لكن قبل ذلك يقوم بتخزين رقم المرحلة التي يجب تحميلها في إعدادات اللاعب. هذه الإعدادات يتم تخزينها على القرص الصلب أي أنها دائمة وليست في الذاكرة. بالتالي فإن القيمة التي تخزن فيها ستبقى محفوظة حين الانتقال بين مشهدي القائمة واللعبة. يمكن الوصول لإعدادات اللاعب عبر PlayerPrefs والتي تحتوي على دوالّ لتخزين وقراءة بيانات بأنواع مختلفة. هنا مثلا استخدمنا الدّالة ()SetInt والتي تقوم بتخزين عدد صحيح، واخترنا اسما للقيمة التي قمنا بتخزينها وهي SelectedLevel. سنرى لاحقا كيف يمكننا قراءة هذه القيمة وتحميل المرحلة بناء عليها.

ما علينا فعله الآن هو إضافة عنصر جديد للقائمة ()OnClick ومن ثم سحب الزر نفسه لخانة الكائن تماما كما فعلنا مع زر الخروج من اللعبة. بعدها سنختار البريمج LevelButton من القائمة ومن ثم نختار الدّالة ()StartLevel. الشيء الجديد الذي ستلاحظه هذه المرة هو وجود خانة لإدخال قيمة المتغير levelIndex، حيث أن هذه الدّالة تأخذ متغيرا بخلاف ()ExitGame التي تعاملنا معها في زر الخروج من اللعبة. لاحظ الصورة التالية:

image15.thumb.png.c9e511058ae9b6e70554ad

سنقوم بعمل زرين من هذا القالب. أحدهما سيحمل النص 1 والقيمة 0 للدّالة ()StartLevel، والآخر سيحمل النص 2 والقيمة 1 للدّالة ()StartLevel. هذان الزران سنقوم بإضافتهما كأبناء للكائن LevelSelector. بعد إضافة الأزرار بالترتيب الصحيح سيتكفل المكوّن Grid Layout Group بوضع كل منهما في الموقع الصحيح وبالحجم الصحيح. قبل الانتقال للحديث عن الزر الثالث وهو زر إغلاق النافذة، لنقم بكتابة البريمج الذي سيحول هذا اللوح إلى نافذة يمكن فتحها وإغلاقها. هذا البريمج هو UIDialog وهو موضح في السرد التالي:

using UnityEngine;
using System.Collections;

public class UIDialog : MonoBehaviour {

    //قياس النافذة حين تكون مفتوحة
    public Vector2 maxSize = Vector2.one;

    //هل تتمدد النافذة حاليا؟
   private bool expanding = false;

   //هل تتقلص النافذة حاليا؟
   private bool shrinking = false;

       //تستدعى مرة واحدة عند بداية التشغيل
       void Start () {
       //أخف النافذة مبدئيا
       transform.localScale = Vector2.zero;
       }
    
       //تستدعى مرة عند تصيير كل إطار
       void Update () {
       //اجلب القياس الحالي للنافذة
       Vector2 scale = transform.localScale;

       //احسب القياس الجديد بناء على حالة النافذة
       if (expanding)
       {
           scale = Vector2.Lerp(scale, maxSize, Time.deltaTime * 10);
           //تحقق من الوصول للمنطقة الميتة
           if (Vector2.Distance(scale, maxSize) < 0.01f)
           {
               scale = maxSize;
               expanding = false;
           }
       }
       else if (shrinking)
       {
           scale = Vector2.Lerp(scale, Vector2.zero, Time.deltaTime * 10);
           //تحقق من الوصول للمنطقة الميتة
           if (Vector2.Distance(scale, Vector2.zero) < 0.01f)
           {
               scale = Vector2.zero;
               shrinking = false;
           }
       }

       //قم بتغيير القياس للقيمة الجديدة
       transform.localScale = scale;
       }

   //تقوم بفتح النافذة
   public void Show()
   {
       expanding = true;
       shrinking = false;
   }

   //تقوم بإغلاق النافذة
   public void Hide()
   {
       shrinking = true;
       expanding = false;
   }
}

يعتمد مبدأ عمل هذا البريمج على قيمة المتغيرين expanding و shrinking، حيث يحددان ما إذا كانت النافذة تتقلص إلى أن تصل لمرحلة الإغلاق أو إن كانت تتمدد حتى تصل لمرحلة الفتح. حين فتح النافذة يتم إيصال قياسها لقيمة الحد الأعلى المحددة في maxSize، أما حين الإغلاق يجب أن تصل قيمة القياس لصفر حتى تختفي النافذة تماما. لاحظ أن تمدد وتقلص النافذة يتم بشكل سلس عبر ()Vector2.Lerp والتي تستخدم الاستيفاء الذي شرحناه سابقا. افتراضيا يتم ضبط قياس النافذة على Vector2.zero مما يؤدي لأن تبدأ مختفية (مغلقة)، وسيتم إظهارها (فتحها) حين استدعاء الدّالة ()Show، كما يمكن استدعاء ()Hide لإخفائها (أو إغلاقها) ثانية. بعد إضافة هذا البريمج للكائن LevelSelector ستلاحظ أنه يختفي تلقائيا عند تشغيل اللعبة. بقي علينا أن نظهره حين يضغط اللاعب على "ابدأ اللعب" ونخفيه حين يضغط اللاعب على الزر X الذي سنضيفه لهذه النافذة.

لنبدأ بعمل زر إغلاق النافذة مستخدمين الصور الثلاث للزر الأحمر. بعد إضافة هذا الزر سنقوم بحذف مكوّن النص من الكائن الإبن Text ونضيف بدلا منه كائن صورة Image كما هو موضح في الصورة التالية:

image16.thumb.png.3a95e86a12f7585134e32d

بعدها سنقوم بسحب الصورة cross والموجودة في مجموعة رسوم واجهة المستخدم في المجلد Assets\Kenney.nl\UI Pack إلى الخانة Source Image مما يجعل الشكل النهائي لنافذة اختيار المرحلة يبدو كالتالي:

image17.thumb.png.553891ee26a2a9c5fe93dc

بالنسبة لزر الإغلاق الأحمر سيكون الكائن الهدف لتنفيذ الأمر هو النافذة LevelSelector بالتالي سنسحبها لعنصر جديد نضيفه للقائمة ()OnClick، ومن ثم سنقوم بتحديد البريمج UIDialog والدّالة ()Hide كأمر يتم تنفيذه حين الضغط على هذا الزر. بعدها سنعود للزر الأول في القائمة الرئيسية وهو "ابدأ اللعب" ونضيف له النافذة LevelSelector كهدف للأمر والبريمج UIDialog أيضا، إلا أن الدّالة هذه المرة ستكون ()Show حيث أن الضغط على هذا الزر سيظهر النافذة. الشكل النهائي للقائمة ()OnClick في هذين الزرين سيبدو كالآتي:

image18.thumb.png.d59076d954016e4551019c

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

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

image19.thumb.png.47ed446e44aea6b5371f18

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

image20.thumb.png.e253d75177abc0a8ffdf10

الزر الذي يحمل علامة ✓ سيعود باللاعب للقائمة الرئيسية، بينما الآخر سيقوم بإغلاق نافذة الحوار هذه مع البقاء في مشهد اللعبة• من أجل العودة للقائمة الرئيسية سنحتاج مرة أخرى للبريمج ExitGameCommand ولكننا سنضيفه هذه المرة على الكائن الجذري للواجهة Canvas حيث سنحتاج لاستدعائه من أكثر من نافذة كما سنرى. بعد إضافة البريمج ستكون الدّالة ()ExitGame  المعرفة فيه هي الأمر الذي سينفذ حين الضغط على الزر ✓، بينما سيكون Canvas هو الكائن الهدف حيث أنه من يحمل هذا البريمج. بالتالي سيكون شكل القائمة ()OnClick كما يلي:

image21.thumb.png.3d373eeb2b96bb380121fc

تذكر أن الدّالة ()ExitGame تعود للقائمة الرئيسية في حال تم استدعاؤها من مشهد اللعبة. بعد ذلك علينا أن نضيف لهذه النافذة الصغيرة البريمج UIDialog بحيث تبدأ مختفية وتظهر حين استدعاء ()Show. بطبيعة الحال سيكون زر الخروج الموجود أعلى يمين الشاشة هو من يستدعي الدّالة ()Show عند الضغط عليه، بينما سيكون زر الإغلاق X الموجود على النافذة نفسها هو من يستدعي لها الدّالة ()Hide. أعتقد أنه إلى هنا أصبحت فكرة ربط الضغط على الزر بكائن معين واستدعاء دالّة من بريمج عليه واضحة، لذا لن أعاود شرحها بالصور وسأكتفي بذكر الارتباطات اللازمة.

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

إضافة المراحل والتنقل بينها

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

image22.thumb.png.568561d5fd70a9f360a5d1

لاحظ أن المرحلة تتكون من 3 عناصر: القاذف، والعناصر البنائية والوحوش والتي تندرج تحت الكائن الفارغ Elements، وأخيرا المقذوفات والتي تندرج تحت الكائن الفارغ Projectiles. لاحظ أيضا أن جميع العناصر على مستوى أفقي واحد وهو المستوى الذي ستظهر فيه الأرضية. عند بناء المرحلة، راعي أن يكون الكائن الفارغ الأب للمرحلة (Level1 في الصورة السابقة) في منتصف المشهد، وأن يكون أسفل المرحلة على مستوى الأرضية التي ستظهر. بعد بناء المرحلة يجب أن نحولها كاملة إلى قالب واحد كبير، لكن قبل ذلك علينا إضافة بريمج بسيط لكائن المرحلة الجذري. هذا البريمج مهمته تحديد رقم الخلفية التي ستظهر حين تحميل المرحلة ويسمى GameLevel وهو موضح في السرد التالي:

using UnityEngine;
using System.Collections;

public class GameLevel : MonoBehaviour {

    //صورة الخلفية التي ستظهر عند تحميل المرحلة
    public int backgroundIndex;

       //تستدعى مرة واحدة عند بداية التشغيل
       void Start () {
    
       }
    
       //يتم استدعاؤها مرة عند تصيير كل إطار
       void Update () {
    
       }
}

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

using UnityEngine;
using System.Collections;

public class GameLevelLoader : MonoBehaviour {

    //تقوم بتخزين القوالب الخاصة بجميع المراحل
    public GameLevel[] allLevels;

    //العنصر الخاص بالمرحلة المحملة حاليا
   private int currentLevel = -1;

   //مرجع لبريمج التحكم بالخلفية
   private BackgroundManager bgManager;

   //مرجع لبريمج أمر الخروج من اللعبة
   ExitGameCommand egc;

       //يتم استدعاؤها مرة عند بداية التشغيل
       void Start () {
       bgManager = GetComponent<BackgroundManager>();
       egc = FindObjectOfType<ExitGameCommand>();
       }
    
       //يتم استدعاءها مرة عند تصيير كل إطار لكن في وقت متأخر
       void LateUpdate () {
       if (currentLevel == -1)
       {
           //قم بتحميل المرحلة التي تم اختيارها من القائمة الرئيسية
           //إذا لم تكن هناك أي مرحلة، قم تلقائيا بتحميل المرحلة الأولى
           int selectedLevel = PlayerPrefs.GetInt("SelectedLevel", 0);
           LoadLevel(selectedLevel);
       }
       }

   //تقوم بتحميل المرحلة المحددة
   public void LoadLevel(int index)
   {
       //قم بالتأكد من وجود المرحلة المطلوبة في المصفوفة
       //إن لم تكن موجودة فعد للقائمة الرئيسية
       if (index < 0 || index >= allLevels.Length)
       {
           egc.ExitGame();
           return;
       }

       //قم بالبحث عن المرحلة المحملة حاليا وتدميرها إن وجدت
       GameLevel current = FindObjectOfType<GameLevel>();
       if (current != null)
       {
           Destroy(current.gameObject);
       }

       //قم بإنشاء المرحلة الجديدة مستخدما قالبها
       GameObject newLevelObject = (GameObject)Instantiate(allLevels[index].gameObject);
       GameLevel newLevelScript = newLevelObject.GetComponent<GameLevel>();

       //قم بتحديد الكائن الأب والموقع للقائمة الجديدة
       newLevelObject.transform.parent = transform;
       newLevelObject.transform.position = Vector2.zero;

       //قم بتغيير رقم العنصر الخاص بالمرحلة الحالية
       currentLevel = index;

       //قم بتغيير الخلفية للصورة المحددة في المرحلة الجديدة
       bgManager.ChangeBackground(newLevelScript.backgroundIndex);

       //قم بإبلاغ البريمجات الأخرى بان هناك مرحلة جديدة تم تحميلها للتو
       SendMessage("NewLevelLoaded");
   }

   //تقوم بإعادة لعب المرحلة الحالية
   public void RestartCurrentLevel()
   {
       if (currentLevel != -1)
       {
           LoadLevel(currentLevel);
       }
   }

   //تقوم بتحميل المرحلة التالية في المصفوفة
   public void LoadNextLevel()
   {
       LoadLevel(currentLevel + 1);
   }
}

هذا البريمج يحتوي على متغير عام واحد فقط وهو المصفوفة allLevels التي تحتوي على قوالب جميع مراحل اللعبة. لاحظ أن تحميل المرحلة يتم تأخيره باستخدام الدّالة ()LateUpdate وذلك حتى نضمن أن جميع البريمجات الأخرى قد بدأت العمل ويمكننا الاعتماد عليها مثل البريمج BackgroundManager والذي سنستخدمه لتغيير صورة الخلفية للصورة المحددة في البريمج GameLevel الخاص بالمرحلة الحالية. يبحث البريمج مبدئيا عن متغير مخزن في إعدادات اللاعب ويحمل الاسم SelectedLevel. كما تذكر فإن هذا المتغير يفترض أن يتم تخزينه من قبل البريمج LevelButton والخاص بأزرار المراحل في القائمة الرئيسية. إذا لم يوجد هذا المتغير يتم إرجاع القيمة الافتراضية وهي 0 بالتالي يتم تحميل المرحلة الأولى في المصفوفة.

الدّالة ()LoadLevel هي الأساسية في هذا البريمج حيث نعطيها رقم المرحلة التي نرغب بتحميلها من المصفوفة، فإذا لم يكن رقم المرحلة المحددة صالحا ستعود للقائمة الرئيسية للعبة. أما إذا كان الرقم صحيحا فإنها تتأكد من عدم وجود مرحلة محملة حاليا عن طريق البحث عن بريمج من نوع GameLevel، إذا وجدت هذا البريمج فإنها تقوم بتدمير الكائن الذي يحمله، وبما أن الكائن هو جذر عناصر المرحلة جميعها، سيتم تدمير جميع هذه العناصر أيضا، مما يجعل المشهد فارغا وجاهزا لاستقبال المرحلة الجديدة. هذه المرحلة يتم إنشاؤها عن طريق استخراج القالب الموجود في الموقع المحدد index ومن ثم بناء كائن منه. هذا الكائن تتم إضافته كابن للكائن الجذري لمشهد اللعبة كما يتم وضعه في منتصف المشهد. بعدها يتم تحديث قيمة currentLevel إلى المرحلة الجديدة وتغيير الخلفية باستخدام المتغير backgroundIndex الخاص ببريمج المرحلة الجديدة. أخيرا تقوم الدّالة بإرسال الرسالة NewLevelLoaded حتى تخبر البريمجات الأخرى بأن المرحلة الجديدة تم تحميلها.

إضافة لذلك لدينا الدّالتان ()RestartCurrentLevel والتي تقوم بإعادة تحميل المرحلة الحالية، و ()LoadNextLevel والتي تقوم بتحميل المرحلة التالية في الترتيب في المصفوفة.

حالة اللعبة وشروط الفوز والخسارة

بعد أن أصبحت جميع محتويات اللعبة مكتملة بما فيها المراحل، بقي علينا أن نضيف شروط الفوز والخسارة وما يترتب عليها من تغيير على حالة اللعبة. في ألعاب من هذا النوع يفوز اللاعب إذا قام بتدمير جميع الخصوم وهي الوحوش في لعبتنا هذه، ويخسر إذا استنفد جميع ما لديه من مقذوفات دون تدمير الخصوم. تذكر أننا سابقا أضفنا البريمج Enemy لكائنات الوحوش والذي يرسل للكائن الجذري الرسالة EnemyDestroyed حين يتم تدمير الكائن، كما أن بريمج المقلاع Launcher يقوم بإرسال الرسالة ProjectilesConsumed عندما يتم إطلاق كافة المقذوفات التي كانت بحوزة اللاعب. ما يتوجب علينا هو كتابة بريمج يستقبل هاتين الرسالتين وبناء عليهما يقوم بفحص حالة اللعبة والتأكد من فوز أو خسارة اللاعب. هذا البريمج هو GameStateManager وهو موضح في السرد التالي:

using UnityEngine;
using System.Collections;

public class GameStateManager : MonoBehaviour {
    //متغير لمعرفة ما إذا فاز اللاعب بالمرحلة
    private bool playerWon;
       //تستدعى مرة واحدة عند بداية التشغيل
       void Start () {
       playerWon = false;
       }
    
       //تستدعى مرة عند تصيير كل إطار
       void Update () {
    
       }

   //والتي ترسلها EnemyDestroyed تستقبل الرسالة
   //كائنات الوحوش عند تدميرها
   void EnemyDestroyed()
   {
       //قم بعد الوحوش المتبقية في المشهد
       Enemy[] enemies = FindObjectsOfType<Enemy>();
       if (enemies.Length <= 1)
       {
           //تم تدمير جميع الوحوش، أي فاز اللاعب بالمرحلة
           SendMessage("PlayerWon");
           playerWon = true;
       }
   }

   //تقوم باستقبال رسالة استنفاد اللاعب
   //لجميع المقذوفات التي كانت بحوزته
   void ProjectilesConsumed()
   {
       if (!playerWon)
       {
           SendMessage("PlayerLost");
       }
   }

   //التي تعني NewLevelLoaded تستقبل الرسالة
   //false إلى  playerWon أنه تم تحميل مرحلة جديدة، وبناء عليها تعيد
   void NewLevelLoaded()
   {
       playerWon = false;
   }
}

في كل مرة يتم فيها تدمير أحد الوحوش يستقبل هذا البريمج الرسالة EnemyDestroyed ومن ثم يقوم بعد الوحوش المتبقية في المشهد عن طريق البحث عن البريمج Enemy. لاحظ أن وجود بريمج واحد في المشهد يعني أن اللاعب قد فاز فكيف هذا؟ الجواب هو أن تدمير الكائن لا يتم مباشرة عند استدعاء ()Destroy، وإنما يتم تأخيره حتى نهاية الإطار الحالي. لذا فمن المحتمل أن تصل الرسالة ويتم بعدها البحث عن بريمج من نوع Enemy وإيجاده. فإن كان العدد واحدا فهذا يعني أنه الأخير المتبقي في المشهد واستقبال الرسالة يعني أنه تم تدميره. لذا نعرف هنا بأن اللاعب قد فاز في المرحلة ويتم إرسال الرسالة PlayerWon وتغيير قيمة playerWon إلى true.

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

السؤال الآن هو ماذا سيحدث عندما يتم إرسال PlayerWon أو PlayerLost؟ الجواب هو أن كل رسالة ستقوم بإظهار نافذة حوار مختلفة. ففي حال فوز اللاعب ستظهر له نافذة تخيره بين إعادة اللعب وبين التقدم للمرحلة التالية واسمها WinDialog، وفي حالة الخسارة تظهر نافذة أخرى تخيره بين إعادة المرحلة والخروج للقائمة الرئيسية واسمها LoseDialog. هاتان النافذتان ستكونان كالنوافذ السابقة عبارة عن كائنات Panel مضاف عليها البريمج UIDialog وتحتوي كل منهما على نص وزرين تماما كنافذة تأكيد العودة للقائمة الرئيسية.

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

image23.thumb.png.a5b8ebdc19f205593d6d5d

المشترك في الزرين الموجودين على النافذة هو أن الهدف لأوامرهما هو الكائن الجذري لمشهد اللعبة SceneRoot وتحديدا بريمج تحميل المراحل GameLevelLoader. أما الفرق فهو أن الزر الأيمن سيقوم باستدعاء الدّالة ()LoadNextLevel عند الضغط عليه مما ينقل اللاعب للمرحلة التالية، بينما الزر الأيسر يستدعي عند الضغط عليه الدّالة ()RestartCurrentLevel مما يؤدي لإعادة تحميل المرحلة الحالية. علاوة على ذلك، يجب أن يقوم كلا هذين الزرين أيضا بإخفاء النافذة حتى يتمكن اللاعب من متابعة اللعب سواء في المرحلة التالية أو الحالية. من أجل ذلك يجب أن نضيف لكل منهما هدفا آخر وهو النافذة نفسها، حيث سيقوم كلاهما باستدعاء الدّالة ()Hide من البريمج UIDialog. لاحظ أن ()OnClick هي عبارة عن قائمة كما ذكرنا سابقا، بالتالي يمكنها أن تستدعي أكثر من أمر من أكثر من كائن وبريمج كما هو مبين في الصورة التالية:

image24.thumb.png.65d91939874420ba7d637e

النافذة الأخرى وهي التي تظهر في حال الخسارة تبدو بهذا الشكل:

image25.thumb.png.02921cb8d9337d75a9cd8e

الزر الأيمن وهو زر العودة للقائمة الرئيسية سيكون الكائن الهدف بالنسبة له هو الكائن الجذري للواجهة Canvas وتحديدا البريمج ExitGameCommand والدّالة ()ExitGame. أما الزر الأيسر فتماما كما في نافذة الفوز يستدعي الدّالة ()RestartCurrentLevel ويقوم أيضا باستدعاء ()Hide من البريمج UIDialog الموجود على كائن نافذة الخسارة LoseDialog.

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

using UnityEngine;
using System.Collections;

public class GameStateDialogs : MonoBehaviour {

    //متغير لتخزين نافذة فوز اللاعب
    public UIDialog winDialog;

    //متغير لتخزين نافذة خسارة اللاعب
   public UIDialog loseDialog;

       //تستدعى مرة واحدة عن بداية التشغيل
       void Start () {
    
       }
    
       //تستدعى مرة عند تصيير كل إطار
       void Update () {
    
       }

   //تعرض نافذة فوز اللاعب
   public void ShowWinDialog()
   {
       winDialog.Show();
   }

   //تعرض نافذة خسارة اللاعب
   public void ShowLoseDialog()
   {
       loseDialog.Show();
   }
}

كل ما يفعله هذا البريمج البسيط هو تخزين نافذتي الفوز والخسارة في مراجع ومن ثم عرضها عند استدعاء الدّالة ()ShowWindDialog أو الدّالة ()ShowLoseDialog. بعد إضافة هذا البريمج للجذر Canvas عليك أن تسحب كلا من نافذة الفوز ونافذة الخسارة لخانة المتغير المناسب لها كما ترى في الصورة التالية:

image26.thumb.png.b2bc1383ceaf5c17b1b6c4

لننتقل الآن للطرف الآخر وهو جذر المشهد SceneRoot والذي سنضيف عليه البريمج GameStateReporter الموضح في السرد التالي:

using UnityEngine;
using System.Collections;

public class GameStateReporter : MonoBehaviour {

    //مرجع لبريمج عرض نافذتي الفوز والخسارة
    GameStateDialogs gsDialogs;

       //تستدعى مرة عند بداية التشغيل
       void Start () {
       gsDialogs = FindObjectOfType<GameStateDialogs>();
       }
    
       //تستدعى مرة عند تصيير كل إطار
       void Update () {
    
       }

   //تستقبل رسالة فوز اللاعب
   void PlayerWon()
   {
       gsDialogs.ShowWinDialog();
   }

   //تستقبل رسالة عرض اللاعب
   void PlayerLost()
   {
       gsDialogs.ShowLoseDialog();
   }
}

كل ما يفعله هذا البريمج هو استقبال رسائل الفوز والخسارة ومن ثم استدعاء الدّالة التي تعرض النافذة المناسبة من البريمج GameStateDialog.

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


تفاعل الأعضاء

أفضل التعليقات



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...